Skip to content

Commit 53b7de6

Browse files
committed
feat: fs.watchAsync and fs.unwatchAsync working in nodeTauriWS initial draft
1 parent af07eaa commit 53b7de6

16 files changed

Lines changed: 891 additions & 231 deletions

.github/workflows/test-on-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
platform: [macos-latest, ubuntu-latest, windows-latest]
1010

1111
runs-on: ${{ matrix.platform }}
12-
timeout-minutes: 30
12+
timeout-minutes: 60
1313
steps:
1414
- uses: actions/checkout@v3
1515
- name: setup node

README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ By adopting Phoenix VFS, you're not just leveraging a file system; you're integr
5858
* [Use `fs.BYTE_ARRAY_ENCODING` for binary files](#use-fsbytearrayencoding-for-binary-files)
5959
* [`fs.Buffer`](#fsbuffer)
6060
* [Buffer Encodings support](#buffer-encodings-support)
61+
* [`EventEmitter`](#eventemitter)
6162
* [`fs.mountNativeFolder(optionalDirHandle?, callback)` Function](#fsmountnativefolderoptionaldirhandle-callback-function)
6263
* [Parameters:](#parameters)
6364
* [Example Usage:](#example-usage)
@@ -313,20 +314,26 @@ any of the below APIs if there are some errors.
313314
## Supported file encodings
314315
When using file read and write apis, use `fs.SUPPORTED_ENCODINGS.*` to get a supported encoding.
315316

316-
The [iconv](https://www.npmjs.com/package/iconv-lite) library can also be directly accessed under `fs.iconv` variable for advanced uses.
317+
The [iconv](https://www.npmjs.com/package/iconv-lite) library can also be directly accessed under `fs.utils.iconv` variable for advanced uses.
317318

318319
```js
319320
// Examples:
320321
// Convert from an encoded buffer to a js string.
321-
str = fs.iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
322+
str = fs.utils.iconv.decode(Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]), 'win1251');
322323

323324
// Convert from a js string to an encoded buffer.
324-
buf = fs.iconv.encode("Sample input string", 'win1251');
325+
buf = fs.utils.iconv.encode("Sample input string", 'win1251');
325326

326327
// Check if encoding is supported
327-
fs.iconv.encodingExists("us-ascii")
328+
fs.utils.iconv.encodingExists("us-ascii")
328329
```
329330

331+
## `fs.utils`
332+
`fs.utils` houses several file related utilities.
333+
1. [`fs.utils.iconv`](https://www.npmjs.com/package/iconv-lite) - iconv-lite: Pure JS character encoding conversion library. See API docs here: https://www.npmjs.com/package/iconv-lite
334+
2. [`fs.utils.anymatch`](https://github.com/micromatch/anymatch) - Javascript module to match a string against a regular expression, glob, string, or function that takes the string as an argument and returns a truthy or falsy value. The matcher can also be an array of any or all of these. Useful for allowing a very flexible user-defined config to define things like file paths. Docs: https://github.com/micromatch/anymatch
335+
3. [`fs.utils.ignore`](https://www.npmjs.com/package/ignore) - To filter filenames according to a .gitignore file. https://www.npmjs.com/package/ignore
336+
330337
### Usage of encoding in `fs.readFile` API
331338

332339
```js
@@ -366,13 +373,20 @@ to use iconv to work with custom file encodings and then use the buffer.
366373
```js
367374
// to get a buffer from string, instead of doing Buffer.from, use iconv.encode
368375
buf = Buffer.from("Sample input string", 'win1251'); // not supported/recommended and wont work even if it works for some cases
369-
buf = fs.iconv.encode("Sample input string", 'win1251'); // recommended way to create buffer for encoding
376+
buf = fs.utils.iconv.encode("Sample input string", 'win1251'); // recommended way to create buffer for encoding
370377

371378
// to convert buffer to string, use iconv as well
372379
str = buf.toString('win1251'); // not supported/recommended and wont work even if it works for some cases
373-
str = fs.iconv.decode(buf, 'win1251'); // recommended way
380+
str = fs.utils.iconv.decode(buf, 'win1251'); // recommended way
374381
```
375382

383+
## `EventEmitter`
384+
This library provides a global utility, `EventEmitter`, which is accessible via `window.EventEmitter`,
385+
`self.EventEmitter`, or simply `EventEmitter`, depending on the context. This utility replicates the
386+
functionality of the Node.js event emitter API, offering a handy tool for incorporating familiar
387+
Node.js-style event handling in your browser environment.
388+
389+
For a quick introduction on using the event emitter, refer to: [Node.js EventEmitter Guide](https://nodejs.dev/en/learn/the-nodejs-event-emitter/).
376390

377391
## `fs.mountNativeFolder(optionalDirHandle?, callback)` Function
378392

dist/phoenix-fs.js

Lines changed: 128 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ const fs = require("fs/promises");
33
const path = require("path");
44
const os = require('os');
55
const { exec } = require('child_process');
6+
const chokidar = require('chokidar');
7+
const anymatch = require('anymatch');
8+
const ignore = require('ignore');
9+
const crypto = require('crypto');
610

711
const IS_MACOS = os.platform() === 'darwin';
812
let debugMode = false;
@@ -22,6 +26,10 @@ function toArrayBuffer(buf) {
2226
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length);
2327
}
2428

29+
function generateRandomId(length = 20) {
30+
return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length);
31+
}
32+
2533
function getWindowsDrives(callback) {
2634
exec('wmic logicaldisk get name', (error, stdout) => {
2735
if (error) {
@@ -101,6 +109,7 @@ function splitMetadataAndBuffer(concatenatedBuffer) {
101109
const WS_COMMAND = {
102110
PING: "ping",
103111
RESPONSE: "response",
112+
EVENT: "event",
104113
LARGE_DATA_SOCKET_ANNOUNCE: "largeDataSock",
105114
CONTROL_SOCKET_ANNOUNCE: "controlSock",
106115
GET_WINDOWS_DRIVES: "getWinDrives",
@@ -110,15 +119,18 @@ const WS_COMMAND = {
110119
WRITE_BIN_FILE: "writeBinFile",
111120
MKDIR: "mkdir",
112121
RENAME: "rename",
113-
UNLINK: "unlink"
122+
UNLINK: "unlink",
123+
WATCH: "watch",
124+
UNWATCH: "unwatch"
114125
};
115126

116127
const LARGE_DATA_THRESHOLD = 2*1024*1024; // 2MB
117-
// A map from dataSocketID to the actual data socket that is used for transporting large data only.
128+
// A map from socketGroupID to the actual data socket that is used for transporting large data only.
118129
// binary data larger than 2MB is considered large data and we will try to send it through a large data web socket if present.
119130
// a client typically makes 2 websockets, one for small data and another for large data transport.
120131
// so large file transfers wont put pressure on the websocket.
121-
const largeDataSocketMap = {};
132+
const largeDataSocketMap = {},
133+
controlSocketMap = {};
122134

123135
function _getResponse(originalMetadata, data = null) {
124136
return {
@@ -148,6 +160,24 @@ function _sendResponse(ws, metadata, dataObjectToSend = null, dataBuffer = new A
148160
socketToUse.send(mergeMetadataAndArrayBuffer(response, dataBuffer));
149161
}
150162

163+
function _sendEvent(defaultWS, socketGroupID, eventEmitterID, eventName, dataObjectToSend = null, dataBuffer = new ArrayBuffer(0)) {
164+
const response = {
165+
eventEmitterID,
166+
eventName,
167+
commandCode: WS_COMMAND.EVENT,
168+
socketGroupID,
169+
data: dataObjectToSend
170+
};
171+
let socketToUse = controlSocketMap[socketGroupID], largeDataSocket = largeDataSocketMap[socketGroupID];
172+
if(dataBuffer && dataBuffer.byteLength > LARGE_DATA_THRESHOLD && largeDataSocket) {
173+
socketToUse = largeDataSocket;
174+
}
175+
if(!socketToUse){
176+
socketToUse = defaultWS;
177+
}
178+
socketToUse.send(mergeMetadataAndArrayBuffer(response, dataBuffer));
179+
}
180+
151181
function _getStat(fullPath) {
152182
return new Promise((resolve, reject) => {
153183
fs.stat(fullPath)
@@ -173,7 +203,7 @@ function _getStat(fullPath) {
173203
});
174204
}
175205

176-
function _reportError(ws, metadata, err, defaultMessage = "Operation failed! ") {
206+
function _reportError(ws, metadata, err= { }, defaultMessage = "Operation failed! ") {
177207
metadata.error = {
178208
message: err.message || defaultMessage,
179209
code: err.code || "EIO",
@@ -269,6 +299,86 @@ function _unlink(ws, metadata) {
269299
}).catch((err)=>_reportError(ws, metadata, err, `Failed to unlink path ${fullPath}`));
270300
}
271301

302+
// eventEmitterID to watcher
303+
const watchersMap = {};
304+
function _watch(ws, metadata) {
305+
const fullPath = metadata.data.path,
306+
// array of anymatch compatible path definition. Eg. ["**/{node_modules,bower_components}/**"]. full path is checked
307+
ignoredPaths = metadata.data.ignoredPaths,
308+
// contents of a gitIgnore file as text. The given path is used as the base path for gitIgnore
309+
gitIgnorePaths = metadata.data.gitIgnorePaths,
310+
persistent = metadata.data.persistent || true,
311+
ignoreInitial = metadata.data.ignoreInitial || true,
312+
socketGroupID = ws.socketGroupID;
313+
try{
314+
const gitignore = ignore().add(gitIgnorePaths);
315+
316+
// Filter function to integrate with chokidar
317+
function isIgnored(pathToFilter) {
318+
if(anymatch(ignoredPaths, pathToFilter)){
319+
debugMode && console.log("ignored watch path: ", pathToFilter, "rel: ",relativePath);
320+
return true;
321+
}
322+
const relativePath = path.relative(fullPath, pathToFilter);
323+
if(relativePath && gitignore.ignores(relativePath)){
324+
debugMode && console.log("ignored watch gitIgnore path: ", pathToFilter, "rel: ",relativePath);
325+
return true;
326+
} else {
327+
return false;
328+
}
329+
}
330+
331+
let readySent = false;
332+
const watcher = chokidar.watch(fullPath, {
333+
persistent,
334+
ignoreInitial,
335+
ignored: path => isIgnored(path)
336+
});
337+
const eventEmitterID = generateRandomId();
338+
watcher.eventEmitterID = eventEmitterID;
339+
watchersMap[watcher.eventEmitterID] = watcher;
340+
watcher.on('ready', () => {
341+
if(readySent){
342+
return;
343+
}
344+
readySent = true;
345+
_sendResponse(ws, metadata, {eventEmitterID});
346+
});
347+
watcher.on('error', (err) => {
348+
if(readySent){
349+
console.error(err);
350+
return;
351+
}
352+
readySent = true;
353+
_reportError(ws, metadata, err, `Error while watching path ${fullPath}`);
354+
});
355+
let watchEvents = ['add', 'change', 'unlink', 'addDir', 'unlinkDir'];
356+
for(let watchEvent of watchEvents){
357+
watcher.on(watchEvent, (path) => {
358+
_sendEvent(ws, socketGroupID, eventEmitterID,watchEvent, {path});
359+
});
360+
}
361+
} catch (err) {
362+
_reportError(ws, metadata, err, `Failed to watch path ${fullPath}`);
363+
}
364+
}
365+
366+
function _unwatch(ws, metadata) {
367+
const eventEmitterID = metadata.data.eventEmitterID;
368+
const watcher = watchersMap[eventEmitterID];
369+
if(!watcher) {
370+
_reportError(ws, metadata, new Error("Couldnt unwatch, no such watcher for eventEmitterID: "+ eventEmitterID));
371+
return;
372+
}
373+
delete watchersMap[eventEmitterID];
374+
watcher.close()
375+
.then(() => {
376+
_sendResponse(ws, metadata, {eventEmitterID});
377+
}).catch(err=>{
378+
_reportError(ws, metadata, err, `Failed to unwatch watcher for eventEmitterID ${eventEmitterID}`);
379+
});
380+
}
381+
272382
function processWSCommand(ws, metadata, dataBuffer) {
273383
try{
274384
switch (metadata.commandCode) {
@@ -302,15 +412,24 @@ function processWSCommand(ws, metadata, dataBuffer) {
302412
case WS_COMMAND.UNLINK:
303413
_unlink(ws, metadata);
304414
return;
415+
case WS_COMMAND.WATCH:
416+
_watch(ws, metadata);
417+
return;
418+
case WS_COMMAND.UNWATCH:
419+
_unwatch(ws, metadata);
420+
return;
305421
case WS_COMMAND.LARGE_DATA_SOCKET_ANNOUNCE:
306422
console.log("Large Data Transfer Socket established, socket Group: ", metadata.socketGroupID);
307423
ws.isLargeData = true;
308-
ws.LargeDataSocketGroupID = metadata.socketGroupID;
424+
ws.socketGroupID = metadata.socketGroupID;
309425
largeDataSocketMap[metadata.socketGroupID] = ws;
310426
_sendResponse(ws, metadata, {}, dataBuffer);
311427
return;
312428
case WS_COMMAND.CONTROL_SOCKET_ANNOUNCE:
313429
console.log("Control Socket established, socket Group:", metadata.socketGroupID);
430+
ws.isLargeData = false;
431+
ws.socketGroupID = metadata.socketGroupID;
432+
controlSocketMap[metadata.socketGroupID] = ws;
314433
_sendResponse(ws, metadata, {}, dataBuffer);
315434
return;
316435
default: console.error("unknown command: "+ metadata);
@@ -361,8 +480,11 @@ function CreatePhoenixFsServer(server, wssPath = "/phoenixFS") {
361480
ws.on('close', () => {
362481
if(ws.isLargeData && ws.socketGroupID && largeDataSocketMap[ws.socketGroupID] === ws){
363482
delete largeDataSocketMap[ws.socketGroupID];
483+
console.log('Websocket Client disconnected: Large data Socket');
484+
} else if(!ws.isLargeData && ws.socketGroupID && controlSocketMap[ws.socketGroupID] === ws){
485+
delete controlSocketMap[ws.socketGroupID];
486+
console.log('Websocket Client disconnected: control Socket');
364487
}
365-
console.log('Websocket Client disconnected');
366488
});
367489
});
368490
}

0 commit comments

Comments
 (0)