@jpmorganchase/mosaic-core 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,308 @@
1
+ import { EventEmitter } from 'stream';
2
+ import { Worker } from 'worker_threads';
3
+
4
+ import WorkerSubscription, { EVENT } from '../WorkerSubscription';
5
+
6
+ jest.mock('worker_threads');
7
+
8
+ const utf8Encoder = new TextEncoder();
9
+
10
+ describe('GIVEN WorkerSubscription', () => {
11
+ let subscription: WorkerSubscription;
12
+
13
+ beforeEach(() => {
14
+ subscription = new WorkerSubscription({
15
+ modulePath: 'module',
16
+ plugins: [],
17
+ options: { primary: true }
18
+ });
19
+ });
20
+
21
+ describe('WHEN calling `stop`', () => {
22
+ beforeEach(() => {
23
+ jest.spyOn(Worker.prototype, 'terminate');
24
+ subscription = new WorkerSubscription({
25
+ modulePath: 'module',
26
+ options: {},
27
+ plugins: []
28
+ });
29
+ });
30
+ afterEach(() => {
31
+ (Worker.prototype.terminate as jest.MockedFunction<any>).mockRestore();
32
+ });
33
+ test('THEN the worker should throw if the source is already stopped', async () => {
34
+ subscription.stop();
35
+ expect(() => subscription.stop()).toThrow(
36
+ new Error('Cannot stop source - subscription is already closed')
37
+ );
38
+ });
39
+ test('THEN the worker should be closed', () => {
40
+ subscription.stop();
41
+ expect(Worker.prototype.terminate).toHaveBeenCalledTimes(1);
42
+ });
43
+ });
44
+
45
+ describe('WHEN events fire', () => {
46
+ const workerHandlers: {
47
+ exit: (code: number) => void;
48
+ message: ({ type, data }: { type: string; data: string }) => void;
49
+ error: (error: Error) => void;
50
+ } = {
51
+ exit: null,
52
+ message: null,
53
+ error: null
54
+ };
55
+
56
+ afterEach(() => {
57
+ (Worker.prototype.on as jest.MockedFunction<any>).mockRestore();
58
+ (Worker.prototype.once as jest.MockedFunction<any>).mockRestore();
59
+ (Worker.prototype.terminate as jest.MockedFunction<any>).mockRestore();
60
+ });
61
+ beforeEach(() => {
62
+ jest.spyOn(Worker.prototype, 'terminate');
63
+ jest.spyOn(Worker.prototype, 'on').mockImplementation(function on(event: EVENT, handler) {
64
+ workerHandlers[event] = handler;
65
+ return this as EventEmitter;
66
+ } as any);
67
+ jest.spyOn(Worker.prototype, 'once').mockImplementation(function on(event: EVENT, handler) {
68
+ workerHandlers[event] = handler;
69
+ return this as EventEmitter;
70
+ } as any);
71
+ subscription = new WorkerSubscription({
72
+ modulePath: 'module',
73
+ options: {},
74
+ plugins: []
75
+ });
76
+ });
77
+
78
+ describe('AND exceptions occur', () => {
79
+ beforeEach(() => {
80
+ (Worker.prototype.terminate as jest.MockedFunction<any>).mockClear();
81
+ });
82
+ test('THEN the worker should terminate on exit event', () => {
83
+ workerHandlers.exit(0);
84
+ expect(Worker.prototype.terminate).toHaveBeenCalled();
85
+ });
86
+ test('THEN the worker should terminate on errors', () => {
87
+ workerHandlers.error(new Error('something broke'));
88
+ expect(Worker.prototype.terminate).toHaveBeenCalled();
89
+ });
90
+ test('THEN start handler should fire', () => {
91
+ const startSpy = jest.fn();
92
+ subscription.on(EVENT.START, startSpy);
93
+ workerHandlers.message({ data: utf8Encoder.encode('{ "key": "value"}'), type: 'init' });
94
+ workerHandlers.message({ data: utf8Encoder.encode('{ "key": "value"}'), type: 'init' });
95
+ expect(startSpy).toHaveBeenCalledTimes(2);
96
+ });
97
+ test('THEN exit handler should only fire once', () => {
98
+ const exitSpy = jest.fn();
99
+ subscription.on(EVENT.EXIT, exitSpy);
100
+ workerHandlers.exit(0);
101
+ workerHandlers.exit(0);
102
+ expect(exitSpy).toHaveBeenCalledTimes(1);
103
+ });
104
+ test('THEN error handler should only fire once', () => {
105
+ const errorSpy = jest.fn();
106
+ subscription.on(EVENT.ERROR, errorSpy);
107
+ workerHandlers.error(new Error('an exception that should only trigger an event once'));
108
+ workerHandlers.error(new Error('an exception that should only trigger an event once'));
109
+ expect(errorSpy).toHaveBeenCalledTimes(1);
110
+ });
111
+ });
112
+
113
+ describe('AND event handlers are attached', () => {
114
+ test('THEN the update handler should return a cleanup function', () => {
115
+ const updateSpy = jest.fn();
116
+ const cleanup = subscription.on(EVENT.UPDATE, updateSpy);
117
+ cleanup();
118
+ workerHandlers.message({ data: utf8Encoder.encode('{ "key": "value"}'), type: 'message' });
119
+ expect(updateSpy).not.toHaveBeenCalled();
120
+ });
121
+ test('THEN the error handler should return a cleanup function', () => {
122
+ const errorSpy = jest.fn();
123
+ const cleanup = subscription.on(EVENT.ERROR, errorSpy);
124
+ cleanup();
125
+ workerHandlers.error(new Error('something broke'));
126
+ expect(errorSpy).not.toHaveBeenCalled();
127
+ });
128
+ test('THEN the update handler should be fired ONCE for a once message handler', () => {
129
+ const updateSpy = jest.fn();
130
+ subscription.once(EVENT.UPDATE, updateSpy);
131
+ workerHandlers.message({ type: 'message', data: utf8Encoder.encode('{ "key": "value"}') });
132
+ workerHandlers.message({ type: 'message', data: utf8Encoder.encode('{ "key": "value"}') });
133
+ expect(updateSpy).toHaveBeenCalledTimes(1);
134
+ });
135
+ test('THEN the update handler should be fired on a message', () => {
136
+ const updateSpy = jest.fn();
137
+ subscription.on(EVENT.UPDATE, updateSpy);
138
+ workerHandlers.message({ type: 'message', data: utf8Encoder.encode('{ "key": "value"}') });
139
+ expect(updateSpy).toHaveBeenCalledWith({ data: { key: 'value' } });
140
+ });
141
+
142
+ test('THEN `stop` should unsubscribe all handlers', () => {
143
+ const exitSpy = jest.fn();
144
+ const errorSpy = jest.fn();
145
+ const updateSpy = jest.fn();
146
+ const startSpy = jest.fn();
147
+ subscription.on(EVENT.ERROR, errorSpy);
148
+ subscription.on(EVENT.EXIT, exitSpy);
149
+ subscription.on(EVENT.START, startSpy);
150
+ subscription.on(EVENT.UPDATE, updateSpy);
151
+ subscription.stop();
152
+ workerHandlers.error(new Error('i should never be handled'));
153
+ workerHandlers.message({ type: 'init', data: utf8Encoder.encode('{ "key": "value"}') });
154
+ workerHandlers.message({ type: 'message', data: utf8Encoder.encode('{ "key": "value"}') });
155
+ workerHandlers.exit(0);
156
+ expect(exitSpy).toHaveBeenCalled();
157
+ expect(errorSpy).not.toHaveBeenCalled();
158
+ expect(updateSpy).not.toHaveBeenCalled();
159
+ expect(startSpy).not.toHaveBeenCalled();
160
+ });
161
+
162
+ test('THEN the error handler should unsubscribe all other handlers and invoke exit', async () => {
163
+ const exitSpy = jest.fn();
164
+ const errorSpy = jest.fn();
165
+ subscription.on(EVENT.ERROR, errorSpy);
166
+ subscription.on(EVENT.EXIT, exitSpy);
167
+ workerHandlers.error(new Error('some error'));
168
+ workerHandlers.exit(0);
169
+ workerHandlers.error(new Error('i should never be handled'));
170
+ expect(exitSpy).toHaveBeenCalledTimes(1);
171
+ expect(errorSpy).toHaveBeenCalledTimes(1);
172
+ });
173
+
174
+ test('THEN the exit handler should unsubscribe all other handlers', async () => {
175
+ const exitSpy = jest.fn();
176
+ const errorSpy = jest.fn();
177
+ const updateSpy = jest.fn();
178
+ const startSpy = jest.fn();
179
+ subscription.on(EVENT.ERROR, errorSpy);
180
+ subscription.on(EVENT.EXIT, exitSpy);
181
+ subscription.on(EVENT.START, startSpy);
182
+ subscription.on(EVENT.UPDATE, updateSpy);
183
+ workerHandlers.exit(0);
184
+ workerHandlers.error(new Error('i should never be handled'));
185
+ workerHandlers.message({ type: 'message', data: 'i should be ignored' });
186
+ workerHandlers.message({ data: utf8Encoder.encode('{ "key": "value"}'), type: 'init' });
187
+ expect(exitSpy).toHaveBeenCalledTimes(1);
188
+ expect(errorSpy).not.toHaveBeenCalled();
189
+ expect(startSpy).not.toHaveBeenCalled();
190
+ expect(updateSpy).not.toHaveBeenCalled();
191
+ });
192
+
193
+ test('THEN an exception should throw for an invalid signal', async () => {
194
+ const errorSpy = jest.fn();
195
+ subscription.on(EVENT.ERROR, errorSpy);
196
+ const updateSpy = jest.fn();
197
+ subscription.on(EVENT.UPDATE, updateSpy);
198
+ workerHandlers.message({ data: 'something', type: 'invalid-signal' });
199
+ workerHandlers.message({ data: 'should-not-be-received', type: 'message' });
200
+ expect(errorSpy).toHaveBeenCalledWith(
201
+ new Error(
202
+ "Unsupported type 'invalid-signal' with value of 'something' received from source worker."
203
+ )
204
+ );
205
+ expect(updateSpy).not.toHaveBeenCalled();
206
+ });
207
+
208
+ test('THEN the start handler should be fired when a worker received an init signal', () => {
209
+ const startSpy = jest.fn();
210
+ subscription.on(EVENT.START, startSpy);
211
+ workerHandlers.message({ data: utf8Encoder.encode('{ "key": "value"}'), type: 'init' });
212
+ expect(startSpy).toHaveBeenCalledTimes(1);
213
+ });
214
+
215
+ test('THEN the exit handler should be fired when a worker emits an error', () => {
216
+ const exitSpy = jest.fn();
217
+ subscription.on(EVENT.EXIT, exitSpy);
218
+ workerHandlers.error(new Error('an exception that should cause an exit'));
219
+ expect(exitSpy).toHaveBeenCalledTimes(1);
220
+ });
221
+
222
+ test('THEN the exit handler should be fired when a worker emits an error (exit code > 0)', () => {
223
+ const exitSpy = jest.fn();
224
+ subscription.on(EVENT.EXIT, exitSpy);
225
+ const errorSpy = jest.fn();
226
+ subscription.on(EVENT.ERROR, errorSpy);
227
+ workerHandlers.exit(1);
228
+ expect(errorSpy).toHaveBeenCalledWith(new Error('mosaic source stopped with exit code 1'));
229
+ expect(exitSpy).toHaveBeenCalledTimes(1);
230
+ });
231
+
232
+ test('THEN the exit handler should be fired when a worker is complete', () => {
233
+ const exitSpy = jest.fn();
234
+ subscription.on(EVENT.EXIT, exitSpy);
235
+ workerHandlers.exit(0);
236
+ expect(exitSpy).toHaveBeenCalledTimes(1);
237
+ });
238
+
239
+ test('THEN the error handler should be fired on an error', () => {
240
+ const error = new Error('something broke');
241
+ const errorSpy = jest.fn();
242
+ subscription.on(EVENT.ERROR, errorSpy);
243
+ workerHandlers.error(error);
244
+ expect(errorSpy).toHaveBeenCalledWith(error);
245
+ });
246
+ });
247
+ });
248
+
249
+ test('THEN it should instantiate correctly', () => {
250
+ expect(
251
+ new WorkerSubscription({
252
+ modulePath: 'module',
253
+ options: {},
254
+ plugins: []
255
+ })
256
+ ).toBeDefined();
257
+ });
258
+
259
+ test('THEN it should have a once method', () => {
260
+ expect(
261
+ new WorkerSubscription({
262
+ modulePath: 'module',
263
+ options: {},
264
+ plugins: []
265
+ })
266
+ ).toHaveProperty('once');
267
+ });
268
+
269
+ test('THEN it should have a on method', () => {
270
+ expect(
271
+ new WorkerSubscription({
272
+ modulePath: 'module',
273
+ options: {},
274
+ plugins: []
275
+ })
276
+ ).toHaveProperty('on');
277
+ });
278
+
279
+ test('THEN it should have a close getter', () => {
280
+ expect(
281
+ new WorkerSubscription({
282
+ modulePath: 'module',
283
+ options: {},
284
+ plugins: []
285
+ })
286
+ ).toHaveProperty('closed');
287
+ });
288
+
289
+ test('THEN it should have a stop method', () => {
290
+ expect(
291
+ new WorkerSubscription({
292
+ modulePath: 'module',
293
+ options: {},
294
+ plugins: []
295
+ })
296
+ ).toHaveProperty('stop');
297
+ });
298
+
299
+ test('THEN the child worker should be created', () => {
300
+ expect(Worker).toHaveBeenCalledWith(expect.any(String), {
301
+ workerData: {
302
+ plugins: [],
303
+ modulePath: 'module',
304
+ options: { primary: true }
305
+ }
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,256 @@
1
+ import path from 'path';
2
+ import glob from 'fast-glob';
3
+ import { PathLike } from 'fs';
4
+ import { createFsFromVolume } from 'memfs';
5
+ import type { DirectoryJSON, IRealpathOptions, Volume } from 'memfs/lib/volume';
6
+ import type { TDataOut } from 'memfs/lib/encoding';
7
+
8
+ import type IFileAccess from '@jpmorganchase/mosaic-types/dist/IFileAccess';
9
+
10
+ export default class FileAccess implements IFileAccess {
11
+ #adapter: Volume;
12
+ #hooks: ((filepath: PathLike, fileData: TDataOut) => Promise<TDataOut>)[] = [];
13
+ #rawReadonlyFs;
14
+ #symlinks: { [key: string]: { target: string; type: string }[] } = {};
15
+ #frozen = false;
16
+ #cachedPages = new Map();
17
+
18
+ constructor(filesystemAdapter: Volume) {
19
+ this.#adapter = filesystemAdapter;
20
+ }
21
+
22
+ async readFile(file) {
23
+ const resolvedPath = await this.#resolvePath(file);
24
+ return await this.#getFileFromCache(resolvedPath);
25
+ //return this.#adapter.promises.readFile(resolvedPath);
26
+ }
27
+
28
+ async glob(pattern, options) {
29
+ // We need a created filesystem, not a Volume, for glob
30
+ this.#rawReadonlyFs = this.#rawReadonlyFs || createFsFromVolume(this.#adapter as any);
31
+ const paths = Array.from(
32
+ new Set(
33
+ await Promise.all(
34
+ (
35
+ await glob(pattern, {
36
+ ...options,
37
+ absolute: true,
38
+ fs: this.#rawReadonlyFs
39
+ })
40
+ ).map(filepath => this.#resolvePath(filepath))
41
+ )
42
+ )
43
+ );
44
+
45
+ return paths;
46
+ }
47
+
48
+ $$addReadFileHook(hook) {
49
+ // if (this.#frozen) {
50
+ // throw new Error('This file system has been frozen. Mutations are not allowed.');
51
+ // }
52
+ this.#hooks.push(hook);
53
+ }
54
+
55
+ readdir(dir, options) {
56
+ return this.#adapter.promises.readdir(dir, options);
57
+ }
58
+
59
+ async stat(file, options) {
60
+ return this.#adapter.promises.stat(file, options);
61
+ }
62
+
63
+ get $$frozen() {
64
+ return this.#frozen;
65
+ }
66
+
67
+ realpath(target, options?: string | IRealpathOptions) {
68
+ return this.#adapter.promises.realpath(target, options);
69
+ }
70
+
71
+ async $$symlinksFromJSON(symlinks) {
72
+ this.#symlinks = symlinks;
73
+ if (this.#frozen) {
74
+ throw new Error('This file system has been frozen. Mutations are not allowed.');
75
+ }
76
+ for (const alias in symlinks) {
77
+ for (const { target, type = 'file' } of symlinks[alias]) {
78
+ if (!(await this.exists(alias))) {
79
+ if (!(await this.exists(path.dirname(alias)))) {
80
+ // TODO: This works... But why are the subdirs not being passed from the child process as `null`s?
81
+ await this.#adapter.promises.mkdir(path.dirname(alias), { recursive: true });
82
+ }
83
+ await this.#adapter.promises.symlink(target, alias, type);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ async unlink(target) {
90
+ if (this.#frozen) {
91
+ throw new Error('This file system has been frozen. Mutations are not allowed.');
92
+ }
93
+ await this.#adapter.promises.unlink(target);
94
+ // If path refers to a symbolic link, then the link is removed without affecting the file or directory to which that link refers.
95
+ this.#cachedPages.delete(target);
96
+ }
97
+
98
+ symlink(target, alias, type) {
99
+ if (this.#frozen) {
100
+ throw new Error('This file system has been frozen. Mutations are not allowed.');
101
+ }
102
+ this.#symlinks[alias] = this.#symlinks[alias] || [];
103
+ this.#symlinks[alias].push({ target, type });
104
+ // console.debug('[PullDocs]', target, 'aliased to', alias);
105
+ return this.#adapter.promises.symlink(target, alias, type);
106
+ }
107
+
108
+ async writeFile(file, data) {
109
+ if (this.#frozen) {
110
+ throw new Error('This file system has been frozen. Mutations are not allowed.');
111
+ }
112
+ await this.#adapter.promises.writeFile(file, data);
113
+
114
+ this.#cachedPages.delete(file);
115
+ // TODO: Do we need?
116
+ this.#cachedPages.delete(await this.#resolvePath(file));
117
+ }
118
+
119
+ async mkdir(dir, options) {
120
+ if (this.#frozen) {
121
+ throw new Error('This file system has been frozen. Mutations are not allowed.');
122
+ }
123
+ return this.#adapter.promises.mkdir(dir, options);
124
+ }
125
+
126
+ async exists(file) {
127
+ try {
128
+ return !!(await this.#adapter.promises.realpath(file));
129
+ } catch (e) {
130
+ if (e.code !== 'ENOENT') {
131
+ throw e;
132
+ }
133
+ }
134
+ return false;
135
+ }
136
+
137
+ toJSON() {
138
+ return this.#adapter.toJSON();
139
+ }
140
+
141
+ symlinksToJSON() {
142
+ return this.#symlinks;
143
+ }
144
+
145
+ async #resolvePath(file) {
146
+ const realPath = await this.#adapter.promises.realpath(file);
147
+
148
+ if (
149
+ realPath !== file &&
150
+ !(await this.#adapter.promises.stat(realPath, { throwIfNoEntry: true }))
151
+ ) {
152
+ throw new Error(`ENOENT: no such file or directory, stat '${file}'`);
153
+ }
154
+
155
+ const stat = await this.#adapter.promises.stat(realPath, { throwIfNoEntry: true });
156
+ // Enable this for /index directories ... but should a filesystem really rewrite URLs like that?
157
+ // The downside is that relative links can break when /index is missing from the end of a URL
158
+ // e.g. if /home/dir is actually /home/dir/index any relative links will be from ./home not ./home/dir
159
+ // if (stat.isDirectory()) {
160
+ // try {
161
+ // const resolved = await this.#resolvePath(path.join(String(realPath), 'index'));
162
+
163
+ // return resolved;
164
+ // } catch {}
165
+ // }
166
+ if (!stat.isFile()) {
167
+ return file;
168
+ //throw new Error(`EISDIR: illegal operation on a directory, open '${file}'`);
169
+ }
170
+ return realPath;
171
+ }
172
+
173
+ async #getFileFromCache(file) {
174
+ const stat = await this.#adapter.promises.stat(file, { throwIfNoEntry: true });
175
+
176
+ if (!stat.isFile()) {
177
+ return null;
178
+ }
179
+
180
+ if (await this.#isNonHiddenPageCached(file)) {
181
+ const cachedItem = this.#cachedPages.get(file);
182
+ return cachedItem;
183
+ }
184
+
185
+ // Bit of parallel programming needed here, to avoid 2+ requests triggering a file load at the same time and making a race condition
186
+ const loadPagePromise = new Promise(async resolve => {
187
+ const rawFile = await this.#adapter.promises.readFile(file);
188
+
189
+ // if (!this.#pageTest(file)) {
190
+ // return resolve(rawFile);
191
+ // }
192
+ let fileData = rawFile;
193
+
194
+ for (const hook of this.#hooks) {
195
+ fileData = await hook(file, fileData);
196
+ }
197
+ return resolve(fileData);
198
+ });
199
+
200
+ this.#cachedPages.set(file, loadPagePromise);
201
+
202
+ const result = await loadPagePromise;
203
+
204
+ if (this.#frozen) {
205
+ this.#cachedPages.set(file, result);
206
+ } else {
207
+ // Clear the `currentlyResolving` entry
208
+ this.#cachedPages.delete(file);
209
+ }
210
+
211
+ return result;
212
+
213
+ // this.#cachedPages.add(file);
214
+ //return this.#adapter.promises.writeFile(file, JSON.stringify(currentPage));
215
+ }
216
+
217
+ async #isNonHiddenPageCached(fileArg) {
218
+ const file = await this.#adapter.promises.realpath(fileArg);
219
+
220
+ return this.#cachedPages.has(file);
221
+ }
222
+
223
+ $$freeze() {
224
+ if (this.#frozen) {
225
+ throw new Error('This file system has already been frozen.');
226
+ }
227
+ this.#frozen = true;
228
+ }
229
+
230
+ $$unfreeze() {
231
+ if (!this.#frozen) {
232
+ throw new Error('This file system is not frozen.');
233
+ }
234
+ this.#frozen = false;
235
+ }
236
+
237
+ $$clearCache() {
238
+ this.#cachedPages.clear();
239
+ }
240
+
241
+ async $$reset() {
242
+ this.#rawReadonlyFs = null;
243
+ this.#frozen = false;
244
+ this.#symlinks = {};
245
+ this.#hooks = [];
246
+ this.#adapter.reset();
247
+ this.#cachedPages.clear();
248
+ }
249
+
250
+ fromJSON(json: DirectoryJSON) {
251
+ if (this.#frozen) {
252
+ throw new Error('This file system has been frozen. Mutations are not allowed.');
253
+ }
254
+ this.#adapter.fromJSON(json);
255
+ }
256
+ }
@@ -0,0 +1,45 @@
1
+ import type { IVolumeImmutable } from '@jpmorganchase/mosaic-types/dist/Volume';
2
+ import type IFileAccess from '@jpmorganchase/mosaic-types/dist/IFileAccess';
3
+
4
+ export default class ImmutableVolume implements IVolumeImmutable {
5
+ #vfs: IFileAccess;
6
+ namespace: string;
7
+
8
+ constructor(vfs, namespace) {
9
+ this.#vfs = vfs;
10
+ this.namespace = namespace;
11
+ }
12
+
13
+ promises: IVolumeImmutable['promises'] = {
14
+ readFile: file => {
15
+ return this.#vfs.readFile(file);
16
+ },
17
+ readdir: (dir, options) => {
18
+ return this.#vfs.readdir(dir, options);
19
+ },
20
+ stat: (file, options) => {
21
+ return this.#vfs.stat(file, options);
22
+ },
23
+ realpath: (target, options) => {
24
+ return this.#vfs.realpath(target, options);
25
+ },
26
+ exists: file => {
27
+ return this.#vfs.exists(file);
28
+ },
29
+ glob: (pattern, options) => {
30
+ return this.#vfs.glob(pattern, options);
31
+ }
32
+ };
33
+
34
+ get frozen() {
35
+ return this.#vfs.$$frozen;
36
+ }
37
+
38
+ symlinksToJSON(): { [key: string]: { target: string; type: string }[] } {
39
+ return this.#vfs.symlinksToJSON();
40
+ }
41
+
42
+ toJSON() {
43
+ return this.#vfs.toJSON();
44
+ }
45
+ }
@@ -0,0 +1,51 @@
1
+ import type { DirectoryJSON } from 'memfs/lib/volume';
2
+
3
+ import type { IVolumeMutable } from '@jpmorganchase/mosaic-types/dist/Volume';
4
+ import type IFileAccess from '@jpmorganchase/mosaic-types/dist/IFileAccess';
5
+
6
+ import FileSystemRestricted from './RestrictedVolume';
7
+
8
+ export default class MutableVolume extends FileSystemRestricted implements IVolumeMutable {
9
+ #vfs: IFileAccess;
10
+
11
+ constructor(vfs, namespace) {
12
+ super(vfs, namespace);
13
+ this.#vfs = vfs;
14
+ }
15
+
16
+ asRestricted() {
17
+ return new FileSystemRestricted(this.#vfs, super.namespace);
18
+ }
19
+
20
+ freeze() {
21
+ this.#vfs.$$freeze();
22
+ }
23
+
24
+ clearCache() {
25
+ this.#vfs.$$clearCache();
26
+ }
27
+
28
+ unfreeze() {
29
+ this.#vfs.$$unfreeze();
30
+ }
31
+
32
+ __internal_do_not_use_addReadFileHook(hook) {
33
+ this.#vfs.$$addReadFileHook(hook);
34
+ }
35
+
36
+ symlinksToJSON(): { [key: string]: { target: string; type: string }[] } {
37
+ return this.#vfs.symlinksToJSON();
38
+ }
39
+
40
+ async symlinksFromJSON(json: { [key: string]: { target: string; type: string }[] }) {
41
+ return await this.#vfs.$$symlinksFromJSON(json);
42
+ }
43
+
44
+ fromJSON(json: DirectoryJSON) {
45
+ return this.#vfs.fromJSON(json);
46
+ }
47
+
48
+ reset() {
49
+ this.#vfs.$$reset();
50
+ }
51
+ }