Skip to content

Commit 91d9365

Browse files
authored
Merge pull request #1180 from streamich/fsa-observable
Reactivity improvements
2 parents 02ca41c + 10e65a2 commit 91d9365

File tree

11 files changed

+236
-77
lines changed

11 files changed

+236
-77
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"typescript.tsdk": "node_modules/typescript/lib"
3+
}

src/core/Link.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { EventEmitter } from 'events';
21
import { constants, PATH } from '../constants';
2+
import { FanOut } from 'thingies/lib/fanout';
33
import type { Node } from './Node';
44
import type { Superblock } from './Superblock';
55

6+
export type LinkEventChildAdd = [type: 'child:add', link: Link, parent: Link];
7+
8+
export type LinkEventChildDelete = [type: 'child:del', link: Link, parent: Link];
9+
10+
export type LinkEvent = LinkEventChildAdd | LinkEventChildDelete;
11+
612
const { S_IFREG } = constants;
713

814
/**
915
* Represents a hard link that points to an i-node `node`.
1016
*/
11-
export class Link extends EventEmitter {
17+
export class Link {
18+
public readonly changes = new FanOut<LinkEvent>();
19+
1220
vol: Superblock;
1321

1422
parent: Link | undefined;
@@ -45,7 +53,6 @@ export class Link extends EventEmitter {
4553
}
4654

4755
constructor(vol: Superblock, parent: Link | undefined, name: string) {
48-
super();
4956
this.vol = vol;
5057
this.parent = parent;
5158
this.name = name;
@@ -87,7 +94,7 @@ export class Link extends EventEmitter {
8794
}
8895

8996
this.getNode().mtime = new Date();
90-
this.emit('child:add', link, this);
97+
this.changes.emit(['child:add', link, this]);
9198

9299
return link;
93100
}
@@ -102,7 +109,7 @@ export class Link extends EventEmitter {
102109
this.length--;
103110

104111
this.getNode().mtime = new Date();
105-
this.emit('child:delete', link, this);
112+
this.changes.emit(['child:del', link, this]);
106113
}
107114

108115
getChild(name: string): Link | undefined {

src/core/Node.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import { FanOut } from 'thingies/lib/fanout';
12
import process from '../process';
23
import { Buffer, bufferAllocUnsafe, bufferFrom } from '../internal/buffer';
34
import { constants, S } from '../constants';
4-
import { EventEmitter } from 'events';
5+
6+
export type NodeEventModify = [type: 'modify'];
7+
8+
export type NodeEventDelete = [type: 'delete'];
9+
10+
export type NodeEvent = NodeEventModify | NodeEventDelete;
511

612
const { S_IFMT, S_IFDIR, S_IFREG, S_IFLNK, S_IFCHR } = constants;
713
const getuid = (): number => process.getuid?.() ?? 0;
@@ -10,7 +16,9 @@ const getgid = (): number => process.getgid?.() ?? 0;
1016
/**
1117
* Node in a file system (like i-node, v-node).
1218
*/
13-
export class Node extends EventEmitter {
19+
export class Node {
20+
public readonly changes = new FanOut<NodeEvent>();
21+
1422
// i-node number.
1523
ino: number;
1624

@@ -35,7 +43,6 @@ export class Node extends EventEmitter {
3543
symlink: string;
3644

3745
constructor(ino: number, mode: number = 0o666) {
38-
super();
3946
this.mode = mode;
4047
this.ino = ino;
4148
}
@@ -221,7 +228,7 @@ export class Node extends EventEmitter {
221228

222229
touch() {
223230
this.mtime = new Date();
224-
this.emit('change', this);
231+
this.changes.emit(['modify']);
225232
}
226233

227234
canRead(uid: number = getuid(), gid: number = getgid()): boolean {
@@ -285,7 +292,7 @@ export class Node extends EventEmitter {
285292
}
286293

287294
del() {
288-
this.emit('delete', this);
295+
this.changes.emit(['delete']);
289296
}
290297

291298
toJSON() {

src/fsa/CoreFileSystemFileHandle.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import type {
99
IFileSystemSyncAccessHandle,
1010
} from './types';
1111
import type { Superblock } from '../core/Superblock';
12-
import { Buffer } from '../internal/buffer';
1312
import { ERROR_CODE } from '../core/constants';
1413

1514
export class CoreFileSystemFileHandle extends CoreFileSystemHandle implements IFileSystemFileHandle {

src/fsa/CoreFileSystemObserver.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type {
2+
IFileSystemChangeRecord,
3+
IFileSystemDirectoryHandle,
4+
IFileSystemFileHandle,
5+
IFileSystemObserver,
6+
IFileSystemObserverObserveOptions,
7+
IFileSystemSyncAccessHandle,
8+
} from './types';
9+
import type { Superblock } from '../core';
10+
11+
export class CoreFileSystemObserver implements IFileSystemObserver {
12+
constructor(
13+
protected readonly _core: Superblock,
14+
protected readonly callback: (records: IFileSystemChangeRecord[], observer: IFileSystemObserver) => void,
15+
) {}
16+
17+
public async observe(
18+
handle: IFileSystemFileHandle | IFileSystemDirectoryHandle | IFileSystemSyncAccessHandle,
19+
options?: IFileSystemObserverObserveOptions,
20+
): Promise<void> {
21+
throw new Error('Method not implemented.');
22+
}
23+
24+
/** Disconnect and stop all observations. */
25+
public disconnect(): void {
26+
throw new Error('Method not implemented.');
27+
}
28+
}

src/fsa/__tests__/CoreFileSystemHandle.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Superblock } from '../../core/Superblock';
22
import { CoreFileSystemDirectoryHandle } from '../CoreFileSystemDirectoryHandle';
3-
import { CoreFileSystemFileHandle } from '../CoreFileSystemFileHandle';
43
import { onlyOnNode20 } from '../../__tests__/util';
54
import { DirectoryJSON } from '../../core/json';
65

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { fsa, IFileSystemChangeRecord } from '..';
2+
import { onlyOnNode20 } from '../../__tests__/util';
3+
4+
onlyOnNode20('CoreFileSystemObserver', () => {
5+
test.skip('can listen to file writes', async () => {
6+
const { dir, FileSystemObserver } = fsa({ mode: 'readwrite' });
7+
const changes: IFileSystemChangeRecord[] = [];
8+
const observer = new FileSystemObserver(records => {
9+
changes.push(...records);
10+
});
11+
const file = await dir.getFileHandle('file.txt', { create: true });
12+
await observer.observe(file);
13+
expect(changes).toEqual([]);
14+
const writable = await file.createWritable();
15+
await writable.write('Hello, world!');
16+
await writable.close();
17+
expect(changes.length).toBe(1);
18+
expect(changes).toMatchObject([
19+
{
20+
type: 'modified',
21+
},
22+
]);
23+
observer.disconnect();
24+
});
25+
});

src/fsa/__tests__/scenarios.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import { Superblock } from '../../core/Superblock';
2-
import { coreToFsa, fsa } from '../index';
2+
import { CoreFsaContext, fsa } from '../index';
33
import { CoreFileSystemDirectoryHandle } from '../CoreFileSystemDirectoryHandle';
44
import { onlyOnNode20 } from '../../__tests__/util';
5-
import { fileURLToPath } from 'url';
5+
6+
const coreToFsa = (
7+
core: Superblock,
8+
dirPath: string = '/',
9+
ctx?: Partial<CoreFsaContext>,
10+
): CoreFileSystemDirectoryHandle => {
11+
const { dir } = fsa(ctx, core, dirPath);
12+
return dir;
13+
};
614

715
onlyOnNode20('coreToFsa scenarios', () => {
816
test('can create FSA from empty Superblock', () => {

src/fsa/index.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,44 @@
11
import { CoreFileSystemDirectoryHandle } from './CoreFileSystemDirectoryHandle';
2-
import { CoreFsaContext } from './types';
2+
import {
3+
CoreFsaContext,
4+
IFileSystemChangeRecord,
5+
IFileSystemObserver,
6+
IFileSystemObserverConstructable,
7+
} from './types';
38
import { Superblock } from '../core/Superblock';
9+
import { CoreFileSystemObserver } from './CoreFileSystemObserver';
410

511
export * from './types';
612
export * from './CoreFileSystemHandle';
713
export * from './CoreFileSystemDirectoryHandle';
814
export * from './CoreFileSystemFileHandle';
915
export * from './CoreFileSystemSyncAccessHandle';
1016
export * from './CoreFileSystemWritableFileStream';
17+
export * from './CoreFileSystemObserver';
1118
export * from './CorePermissionStatus';
1219

13-
/**
14-
* Creates a File System Access API implementation on top of a Superblock.
15-
*/
16-
export const coreToFsa = (
17-
core: Superblock,
18-
dirPath: string = '/',
19-
ctx?: Partial<CoreFsaContext>,
20-
): CoreFileSystemDirectoryHandle => {
21-
return new CoreFileSystemDirectoryHandle(core, dirPath, ctx);
22-
};
23-
2420
/**
2521
* Create a new instance of an in-memory File System Access API
2622
* implementation rooted at the root directory of the filesystem.
2723
*
2824
* @param ctx Optional context for the File System Access API.
25+
* @param core Optional low-level file system implementation to
26+
* back the File System Access API. If not provided, a new empty
27+
* Superblock instance will be created.
28+
* @param dirPath Optional path within the filesystem to use as the root
29+
* directory of the File System Access API. Defaults to `/`.
2930
* @returns A File System Access API implementation `dir` rooted at
30-
* the root directory of the filesystem, as well as the `core`
31-
* file system itself.
31+
* the root directory of the filesystem, as well as the `core`,
32+
* a low-level file system implementation itself. Also, returns
33+
* `FileSystemObserver`, a class that can be used to create
34+
* observers that watch for changes to files and directories.
3235
*/
33-
export const fsa = (ctx?: Partial<CoreFsaContext>) => {
34-
const core = new Superblock();
35-
const dir = new CoreFileSystemDirectoryHandle(core, '/', ctx);
36-
return { dir, core };
36+
export const fsa = (ctx?: Partial<CoreFsaContext>, core = new Superblock(), dirPath: string = '/') => {
37+
const dir = new CoreFileSystemDirectoryHandle(core, dirPath, ctx);
38+
const FileSystemObserver: IFileSystemObserverConstructable = class FileSystemObserver extends CoreFileSystemObserver {
39+
constructor(callback: (records: IFileSystemChangeRecord[], observer: IFileSystemObserver) => void) {
40+
super(core, callback);
41+
}
42+
};
43+
return { core, dir, FileSystemObserver };
3744
};

src/fsa/types.ts

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { Superblock } from '../core/Superblock';
2-
31
export interface IPermissionStatus {
42
name: string;
53
state: 'granted' | 'denied' | 'prompt';
@@ -126,3 +124,105 @@ export type Data =
126124
| DataView
127125
| Blob
128126
| string;
127+
128+
/**
129+
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemChangeRecord
130+
*/
131+
export interface IFileSystemChangeRecord {
132+
/**
133+
* A reference to the file system handle that the change was observed on. This
134+
* property will be null for records with a "disappeared", "errored", or
135+
* "unknown" type.
136+
*/
137+
changedHandle: IFileSystemHandle | IFileSystemSyncAccessHandle | IFileSystemDirectoryHandle | null;
138+
139+
/**
140+
* An array containing the path components that make up the relative file path
141+
* from the `root` to the `changedHandle`, including the `changedHandle`
142+
* filename.
143+
*/
144+
relativePathComponents: string[];
145+
146+
/**
147+
* An array containing the path components that make up the relative file path
148+
* from the `root` to the `changedHandle`'s former location, in the case of
149+
* observations with a `"moved"` type. If the type is not `"moved"`, this
150+
* property will be `null`.
151+
*/
152+
relativePathMovedFrom: string[] | null;
153+
154+
/**
155+
* A reference to the root file system handle, that is, the one passed to the
156+
* `observe()` call that started the observation.
157+
*/
158+
root: IFileSystemHandle | IFileSystemSyncAccessHandle | IFileSystemDirectoryHandle;
159+
160+
/**
161+
* The type of change that occurred.
162+
*/
163+
type: /** The file or directory was created or moved into the `root` file structure. */
164+
| 'appeared'
165+
166+
/**
167+
* The file or directory was deleted or moved out of the root file structure.
168+
* To find out which file or directory `disappeared`, you can query the
169+
* `relativePathComponents` property.
170+
*/
171+
| 'disappeared'
172+
173+
/** An error state occurred in the observed directory. */
174+
| 'errored'
175+
176+
/** The file or directory was modified. */
177+
| 'modified'
178+
179+
/**
180+
* The file or directory was moved within the root file structure.
181+
*
182+
* Chrome note: On Windows, "moved" observations aren't supported between
183+
* directories. They are reported as a "disappeared" observation in the
184+
* source directory and an "appeared" observation in the destination directory.
185+
*/
186+
| 'moved'
187+
188+
/**
189+
* Indicates that some observations were missed. If you wish to find out
190+
* information on what changed in the missed observations, you could fall
191+
* back to polling the observed directory.
192+
*/
193+
| 'unknown';
194+
}
195+
196+
export interface IFileSystemObserverObserveOptions {
197+
/** Whether to observe changes recursively in subdirectories. */
198+
recursive?: boolean;
199+
}
200+
201+
/**
202+
* @see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemObserver
203+
*/
204+
export interface IFileSystemObserver {
205+
/**
206+
* Start observing changes to a given file or directory.
207+
*
208+
* @param handle The file or directory handle to observe.
209+
* @param options Optional settings for the observation.
210+
*/
211+
observe(
212+
handle: IFileSystemFileHandle | IFileSystemDirectoryHandle | IFileSystemSyncAccessHandle,
213+
options?: IFileSystemObserverObserveOptions,
214+
): Promise<void>;
215+
216+
/** Disconnect and stop all observations. */
217+
disconnect(): void;
218+
}
219+
220+
export interface IFileSystemObserverConstructable {
221+
/**
222+
* Constructor for creating a FileSystemObserver.
223+
*
224+
* @param callback Function called with file system change records and the
225+
* observer instance
226+
*/
227+
new (callback: (records: IFileSystemChangeRecord[], observer: IFileSystemObserver) => void): IFileSystemObserver;
228+
}

0 commit comments

Comments
 (0)