@kirill.konshin/utils 0.0.14 → 0.0.15
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.
- package/.ctirc +2 -1
- package/.turbo/turbo-build.log +18 -13
- package/.turbo/turbo-test.log +78 -25
- package/CHANGELOG.md +6 -0
- package/README.md +1 -1
- package/demo/{cache-demo-clean.ts → worker-demo.ts} +1 -1
- package/dist/bootstrap/controls.d.ts +5 -2
- package/dist/bootstrap/controls.d.ts.map +1 -1
- package/dist/bootstrap/controls.js +1 -0
- package/dist/bootstrap/controls.js.map +1 -1
- package/dist/bootstrap/controls.stories.d.ts +9 -0
- package/dist/bootstrap/controls.stories.d.ts.map +1 -0
- package/dist/bootstrap/error.stories.d.ts +8 -0
- package/dist/bootstrap/error.stories.d.ts.map +1 -0
- package/dist/bootstrap/field.stories.d.ts +8 -0
- package/dist/bootstrap/field.stories.d.ts.map +1 -0
- package/dist/bootstrap/globalLoading.stories.d.ts +7 -0
- package/dist/bootstrap/globalLoading.stories.d.ts.map +1 -0
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/loading.stories.d.ts +9 -0
- package/dist/bootstrap/loading.stories.d.ts.map +1 -0
- package/dist/bootstrap/screen.stories.d.ts +14 -0
- package/dist/bootstrap/screen.stories.d.ts.map +1 -0
- package/dist/bootstrap/toaster.stories.d.ts +8 -0
- package/dist/bootstrap/toaster.stories.d.ts.map +1 -0
- package/dist/bootstrap/useModal.stories.d.ts +9 -0
- package/dist/bootstrap/useModal.stories.d.ts.map +1 -0
- package/dist/core/cache.d.ts +7 -8
- package/dist/core/cache.d.ts.map +1 -1
- package/dist/core/cache.js +7 -4
- package/dist/core/cache.js.map +1 -1
- package/dist/core/cache.test.d.ts +2 -0
- package/dist/core/cache.test.d.ts.map +1 -0
- package/dist/core/files.d.ts +3 -0
- package/dist/core/files.d.ts.map +1 -0
- package/dist/core/files.js +27 -0
- package/dist/core/files.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +14 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/ls.d.ts +6 -0
- package/dist/core/ls.d.ts.map +1 -0
- package/dist/core/ls.js +12 -0
- package/dist/core/ls.js.map +1 -0
- package/dist/core/worker.d.ts +23 -9
- package/dist/core/worker.d.ts.map +1 -1
- package/dist/core/worker.js +93 -90
- package/dist/core/worker.js.map +1 -1
- package/dist/core/worker.test.d.ts +2 -0
- package/dist/core/worker.test.d.ts.map +1 -0
- package/dist/core/workerUtils.d.ts +3 -0
- package/dist/core/workerUtils.d.ts.map +1 -0
- package/dist/core/workerUtils.js +31 -0
- package/dist/core/workerUtils.js.map +1 -0
- package/dist/core/wrpc.d.ts +45 -0
- package/dist/core/wrpc.d.ts.map +1 -0
- package/dist/core/wrpc.fixture.d.ts +29 -0
- package/dist/core/wrpc.fixture.d.ts.map +1 -0
- package/dist/core/wrpc.js +242 -0
- package/dist/core/wrpc.js.map +1 -0
- package/dist/core/wrpc.test.d.ts +1 -0
- package/dist/core/wrpc.test.d.ts.map +1 -0
- package/dist/mui/formControlFieldset.stories.d.ts +7 -0
- package/dist/mui/formControlFieldset.stories.d.ts.map +1 -0
- package/dist/react/form/form.d.ts +7 -6
- package/dist/react/form/form.d.ts.map +1 -1
- package/dist/react/form/form.js +14 -20
- package/dist/react/form/form.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +8 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/keyboard.d.ts +11 -0
- package/dist/react/keyboard.d.ts.map +1 -0
- package/dist/react/keyboard.js +38 -0
- package/dist/react/keyboard.js.map +1 -0
- package/package.json +5 -2
- package/src/bootstrap/controls.stories.tsx +2 -2
- package/src/bootstrap/controls.tsx +3 -3
- package/src/bootstrap/error.stories.tsx +3 -3
- package/src/bootstrap/field.stories.tsx +2 -2
- package/src/bootstrap/globalLoading.stories.tsx +2 -2
- package/src/bootstrap/loading.stories.tsx +2 -2
- package/src/bootstrap/screen.stories.tsx +2 -2
- package/src/bootstrap/toaster.stories.tsx +2 -2
- package/src/bootstrap/useModal.stories.tsx +3 -3
- package/src/core/README.md +91 -0
- package/src/core/cache.ts +12 -12
- package/src/core/files.ts +24 -0
- package/src/core/index.ts +4 -0
- package/src/core/ls.ts +11 -0
- package/src/core/worker.test.ts +197 -0
- package/src/core/worker.ts +106 -93
- package/src/core/workerUtils.ts +32 -0
- package/src/core/wrpc.fixture.ts +72 -0
- package/src/core/wrpc.test.ts +225 -0
- package/src/core/wrpc.ts +359 -0
- package/src/mui/formControlFieldset.stories.tsx +1 -1
- package/src/react/form/form.tsx +53 -33
- package/src/react/index.ts +1 -0
- package/src/react/keyboard.tsx +49 -0
- package/src/tailwind/README.md +8 -0
- package/tsconfig.json +2 -1
- package/vite.config.ts +10 -3
- package/vite.exports.ts +3 -3
- package/demo/cache-demo.ts +0 -343
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import '@vitest/web-worker';
|
|
2
|
+
import { expect, describe, test, vi } from 'vitest';
|
|
3
|
+
import { wrpc } from './wrpc';
|
|
4
|
+
import type { responder } from './wrpc.fixture'; // DO NOT IMPORT ANYTHING OTHER THAN TYPES!!!
|
|
5
|
+
|
|
6
|
+
// Prepare
|
|
7
|
+
|
|
8
|
+
const createWorker = () => {
|
|
9
|
+
const mainWorker = new Worker(new URL('./wrpc.fixture', import.meta.url), { type: 'module' });
|
|
10
|
+
mainWorker.addEventListener('error', (e) => console.error('Worker Error', e));
|
|
11
|
+
|
|
12
|
+
const caller = wrpc().createCaller(
|
|
13
|
+
mainWorker,
|
|
14
|
+
{} as typeof responder.responders, // only types, object will be proxied
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
return { mainWorker, caller };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getWorkerInfo = async (worker: Worker, msg: string): Promise<any> =>
|
|
21
|
+
await new Promise((res) => {
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
worker.addEventListener(
|
|
24
|
+
'message',
|
|
25
|
+
(e) => {
|
|
26
|
+
controller.abort();
|
|
27
|
+
res(e.data);
|
|
28
|
+
},
|
|
29
|
+
controller,
|
|
30
|
+
);
|
|
31
|
+
worker.postMessage({ msg });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Tests
|
|
35
|
+
|
|
36
|
+
describe(
|
|
37
|
+
'WRPC',
|
|
38
|
+
async () => {
|
|
39
|
+
test('for await ... of return yield', async () => {
|
|
40
|
+
const { caller } = createWorker();
|
|
41
|
+
|
|
42
|
+
const fn = vi.fn();
|
|
43
|
+
|
|
44
|
+
for await (const data of caller.returnYield()) {
|
|
45
|
+
fn(data);
|
|
46
|
+
console.log('DATA', data);
|
|
47
|
+
//TODO Test early return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
expect(fn).nthCalledWith(1, 1);
|
|
51
|
+
expect(fn).nthCalledWith(2, 2);
|
|
52
|
+
expect(fn).nthCalledWith(3, 3);
|
|
53
|
+
expect(fn).toBeCalledTimes(3);
|
|
54
|
+
|
|
55
|
+
let res;
|
|
56
|
+
const it = caller.returnYield();
|
|
57
|
+
do {
|
|
58
|
+
res = await it.next();
|
|
59
|
+
console.log('DATA', res);
|
|
60
|
+
fn(res);
|
|
61
|
+
} while (!res.done);
|
|
62
|
+
|
|
63
|
+
expect(fn).nthCalledWith(4, { value: 1, done: false });
|
|
64
|
+
expect(fn).nthCalledWith(5, { value: 2, done: false });
|
|
65
|
+
expect(fn).nthCalledWith(6, { value: 3, done: false });
|
|
66
|
+
expect(fn).nthCalledWith(7, { value: undefined, done: true });
|
|
67
|
+
expect(fn).toBeCalledTimes(7);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('for await ... of separate return & yield', async () => {
|
|
71
|
+
const { caller } = createWorker();
|
|
72
|
+
|
|
73
|
+
const fn = vi.fn();
|
|
74
|
+
|
|
75
|
+
for await (const data of caller.test()) {
|
|
76
|
+
fn(data);
|
|
77
|
+
console.log('DATA', data);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(fn).nthCalledWith(1, { progress: 0 });
|
|
81
|
+
expect(fn).nthCalledWith(2, { progress: 0.5 });
|
|
82
|
+
expect(fn).nthCalledWith(3, { progress: 1, aborted: false });
|
|
83
|
+
expect(fn).toBeCalledTimes(3);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('do ... while', async () => {
|
|
87
|
+
const { caller } = createWorker();
|
|
88
|
+
|
|
89
|
+
const fn = vi.fn();
|
|
90
|
+
|
|
91
|
+
let res;
|
|
92
|
+
const it = caller.test();
|
|
93
|
+
do {
|
|
94
|
+
res = await it.next();
|
|
95
|
+
console.log('DATA', res);
|
|
96
|
+
fn(res);
|
|
97
|
+
} while (!res.done);
|
|
98
|
+
|
|
99
|
+
expect(fn).nthCalledWith(1, { value: { progress: 0 }, done: false });
|
|
100
|
+
expect(fn).nthCalledWith(2, { value: { progress: 0.5 }, done: false });
|
|
101
|
+
expect(fn).nthCalledWith(3, { value: { progress: 1, aborted: false }, done: false });
|
|
102
|
+
expect(fn).nthCalledWith(4, { value: 'foo', done: true });
|
|
103
|
+
expect(fn).toBeCalledTimes(4);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('break', async () => {
|
|
107
|
+
const { caller } = createWorker();
|
|
108
|
+
|
|
109
|
+
const fn = vi.fn();
|
|
110
|
+
|
|
111
|
+
for await (const data of caller.test()) {
|
|
112
|
+
fn(data);
|
|
113
|
+
console.log('DATA', data);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
expect(fn).nthCalledWith(1, { progress: 0 });
|
|
118
|
+
expect(fn).toBeCalledTimes(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('abort', async () => {
|
|
122
|
+
const { caller } = createWorker();
|
|
123
|
+
|
|
124
|
+
const controller = new AbortController();
|
|
125
|
+
|
|
126
|
+
const fn = vi.fn();
|
|
127
|
+
|
|
128
|
+
for await (const data of caller.test({ signal: controller.signal })) {
|
|
129
|
+
fn(data);
|
|
130
|
+
console.log('DATA', data);
|
|
131
|
+
controller.abort('Test');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expect(fn).nthCalledWith(1, { progress: 0 });
|
|
135
|
+
expect(fn).toBeCalledTimes(1); // not guaranteed without explicit break, caller generator may receive multiple events and enqueue them while reader only reads one, so second will also be read
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('error', async () => {
|
|
139
|
+
const { caller } = createWorker();
|
|
140
|
+
|
|
141
|
+
await expect(async () => {
|
|
142
|
+
for await (const data of caller.error()) {
|
|
143
|
+
console.log('DATA', data);
|
|
144
|
+
}
|
|
145
|
+
}).rejects.toThrowError('Test');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('unknown responder', async () => {
|
|
149
|
+
const { caller } = createWorker();
|
|
150
|
+
|
|
151
|
+
await expect(caller['unknown']()).rejects.toThrowError('Unknown responder');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('promise', async () => {
|
|
155
|
+
const { caller } = createWorker();
|
|
156
|
+
const promise = caller.promise();
|
|
157
|
+
await new Promise((res) => setTimeout(res, 100)); // introduce delay
|
|
158
|
+
await expect(promise).resolves.toEqual({ test: 'test' });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('promise error', async () => {
|
|
162
|
+
const { caller } = createWorker();
|
|
163
|
+
await expect(caller.promiseError()).rejects.toThrowError('Test');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('promise as generator', async () => {
|
|
167
|
+
const { caller } = createWorker();
|
|
168
|
+
await expect(async () => {
|
|
169
|
+
// @ts-ignore
|
|
170
|
+
for await (const data of caller.promise()) {
|
|
171
|
+
console.log('DATA', data);
|
|
172
|
+
}
|
|
173
|
+
}).rejects.toThrowError('Unexpected promise in generator');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('stop', async () => {
|
|
177
|
+
const { caller, mainWorker } = createWorker();
|
|
178
|
+
|
|
179
|
+
await getWorkerInfo(mainWorker, 'stop');
|
|
180
|
+
|
|
181
|
+
const fn = vi.fn();
|
|
182
|
+
|
|
183
|
+
await Promise.race([
|
|
184
|
+
(async () => {
|
|
185
|
+
for await (const data of caller.test()) {
|
|
186
|
+
fn(data);
|
|
187
|
+
console.log('DATA', data);
|
|
188
|
+
}
|
|
189
|
+
})(),
|
|
190
|
+
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
expect(fn).toBeCalledTimes(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('bidirectional', async () => {
|
|
197
|
+
const { caller } = createWorker();
|
|
198
|
+
|
|
199
|
+
const it = caller.bidirectional(1);
|
|
200
|
+
|
|
201
|
+
//TODO Readme
|
|
202
|
+
// > No log at this step: the first value sent through `next` is lost
|
|
203
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next#sending_values_to_the_generator
|
|
204
|
+
await expect(it.next(9)).resolves.toEqual({ value: '1.0', done: false }); // 9 is ignored as yield is not yet reached
|
|
205
|
+
await expect(it.next(2)).resolves.toEqual({ value: '2.1', done: false });
|
|
206
|
+
await expect(it.next(3)).resolves.toEqual({ value: '3.2', done: false });
|
|
207
|
+
await expect(it.next(9)).resolves.toEqual({ value: 'done', done: true }); // 9 is ignored as generator has returned and is done
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test.skip('setTimeout & setInterval', async () => {
|
|
211
|
+
const { caller } = createWorker();
|
|
212
|
+
|
|
213
|
+
await caller.setTimeout({ timeout: 10 });
|
|
214
|
+
caller.setTimeout({ timeout: 10 }).then(() => {});
|
|
215
|
+
|
|
216
|
+
let i = 0;
|
|
217
|
+
for await (const data of caller.setInterval({ timeout: 10 })) {
|
|
218
|
+
console.log('DATA', data);
|
|
219
|
+
i++;
|
|
220
|
+
if (i >= 3) break;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
{ timeout: 1000 },
|
|
225
|
+
);
|
package/src/core/wrpc.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { getTransferrable } from './workerUtils';
|
|
2
|
+
|
|
3
|
+
// Types
|
|
4
|
+
|
|
5
|
+
export type Responders = { [key: string]: (arg?: any) => Generator<any> | AsyncGenerator<any> | Promise<any> | any };
|
|
6
|
+
|
|
7
|
+
export type Context = {
|
|
8
|
+
message: string;
|
|
9
|
+
id: string;
|
|
10
|
+
signalName?: string;
|
|
11
|
+
done?: boolean;
|
|
12
|
+
abort?: string;
|
|
13
|
+
error?: boolean;
|
|
14
|
+
ack?: boolean;
|
|
15
|
+
promise?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type Event = MessageEvent<{ ctx: Context; payload: any | Error }>;
|
|
19
|
+
|
|
20
|
+
export type WorkerLike = {
|
|
21
|
+
postMessage: (payload: any, transferable: any[]) => void;
|
|
22
|
+
addEventListener: (
|
|
23
|
+
event: string,
|
|
24
|
+
listener: (e: any) => void,
|
|
25
|
+
options?: { signal?: AbortSignal; once?: boolean },
|
|
26
|
+
) => void;
|
|
27
|
+
removeEventListener: (event: string, listener: (e: any) => void) => void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export enum LogLevel {
|
|
31
|
+
disabled = 0,
|
|
32
|
+
error = 1,
|
|
33
|
+
lifecycle = 2,
|
|
34
|
+
detail = 3,
|
|
35
|
+
transport = 4,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Helpers
|
|
39
|
+
|
|
40
|
+
export function deriveController(signal?: AbortSignal): [AbortController, AbortSignal] {
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
return [controller, AbortSignal.any([controller.signal, signal].filter(Boolean) as AbortSignal[])];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function rejectOnSignal(promise: Promise<any>, signal: AbortSignal) {
|
|
46
|
+
return Promise.race([
|
|
47
|
+
promise,
|
|
48
|
+
new Promise<Event['data']>((resolve, reject) => {
|
|
49
|
+
signal.addEventListener('abort', () => reject('RaceAborted:' + signal.reason), { once: true, signal });
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Lib
|
|
55
|
+
|
|
56
|
+
export function wrpc({
|
|
57
|
+
debug = LogLevel.disabled,
|
|
58
|
+
name = (globalThis as any).importScripts ? 'RESPONDER' : 'CALLER',
|
|
59
|
+
} = {}) {
|
|
60
|
+
// Logger
|
|
61
|
+
|
|
62
|
+
function logger(type: string) {
|
|
63
|
+
return (level: number, ctx: Context | null, ...args: any[]) => {
|
|
64
|
+
if (!debug || debug < level) return;
|
|
65
|
+
const { id, message, ...restCtx } = ctx || {};
|
|
66
|
+
console[type](name, message || '*', id || '*', ...args, restCtx);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const log = logger('log');
|
|
71
|
+
const logWarn = logger('warn');
|
|
72
|
+
const logError = logger('error');
|
|
73
|
+
|
|
74
|
+
// Service
|
|
75
|
+
|
|
76
|
+
function send(worker: WorkerLike, ctx: Context, payload?: any) {
|
|
77
|
+
log(LogLevel.transport, ctx, 'SEND', payload, typeof payload);
|
|
78
|
+
worker.postMessage({ ctx, payload }, getTransferrable(payload));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//TODO Listener error handling
|
|
82
|
+
function listen(
|
|
83
|
+
worker: WorkerLike,
|
|
84
|
+
context: Context | null,
|
|
85
|
+
listener: (event: Event['data']) => void,
|
|
86
|
+
signal: AbortSignal,
|
|
87
|
+
once: boolean = false,
|
|
88
|
+
) {
|
|
89
|
+
signal.addEventListener(
|
|
90
|
+
'abort',
|
|
91
|
+
() => logWarn(LogLevel.detail, context, 'LISTEN SIGNAL ABORT', signal.reason),
|
|
92
|
+
{
|
|
93
|
+
signal,
|
|
94
|
+
once: true,
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
worker.addEventListener(
|
|
98
|
+
'message',
|
|
99
|
+
({ data }: Event) => {
|
|
100
|
+
log(LogLevel.transport, context, 'LISTEN EVENT', data);
|
|
101
|
+
// no context = listen all, or matching context
|
|
102
|
+
if (!context || (data?.ctx?.message === context.message && data.ctx?.id === context.id)) listener(data);
|
|
103
|
+
},
|
|
104
|
+
{ signal, once },
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function waitFor(
|
|
109
|
+
worker: WorkerLike,
|
|
110
|
+
ctx: Context,
|
|
111
|
+
condition: (data: Event['data']) => boolean = (data) => true,
|
|
112
|
+
signal: AbortSignal,
|
|
113
|
+
) {
|
|
114
|
+
const [waitController, waitSignal] = deriveController(signal);
|
|
115
|
+
|
|
116
|
+
return new Promise<Event['data']>((res) =>
|
|
117
|
+
listen(
|
|
118
|
+
worker,
|
|
119
|
+
ctx,
|
|
120
|
+
(data) => {
|
|
121
|
+
if (!condition(data)) return;
|
|
122
|
+
waitController.abort('WaitFinished');
|
|
123
|
+
res(data);
|
|
124
|
+
},
|
|
125
|
+
waitSignal,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Responder
|
|
131
|
+
|
|
132
|
+
//TODO Name arg
|
|
133
|
+
function createResponder<T extends Responders>(worker: WorkerLike, responders: T) {
|
|
134
|
+
const mainController = new AbortController();
|
|
135
|
+
|
|
136
|
+
worker.addEventListener('messageerror', (e) => logError(LogLevel.error, null, 'MESSAGE ERROR', e), {
|
|
137
|
+
signal: mainController.signal,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
listen(
|
|
141
|
+
worker,
|
|
142
|
+
null,
|
|
143
|
+
async ({ ctx, payload = {} }) => {
|
|
144
|
+
if (!ctx || !ctx.message || !ctx.id || ctx.abort || ctx.ack) return; // ignore invalid and service messages
|
|
145
|
+
try {
|
|
146
|
+
const responder = responders[ctx.message];
|
|
147
|
+
|
|
148
|
+
if (!responder) throw new Error('Unknown responder', { cause: ctx });
|
|
149
|
+
|
|
150
|
+
const [subController, subSignal] = deriveController(mainController.signal);
|
|
151
|
+
|
|
152
|
+
// always recover signal so that promises also can abort
|
|
153
|
+
if (ctx.signalName) payload[ctx.signalName] = subSignal;
|
|
154
|
+
|
|
155
|
+
const iterator = responder(payload); // no await!
|
|
156
|
+
|
|
157
|
+
log(LogLevel.lifecycle, ctx, 'START', iterator);
|
|
158
|
+
|
|
159
|
+
// PROMISE
|
|
160
|
+
|
|
161
|
+
// https://stackoverflow.com/a/19660350/5125659 the only right way is to call function and see what it returned
|
|
162
|
+
if (!iterator[Symbol.asyncIterator] && !iterator[Symbol.iterator]) {
|
|
163
|
+
// Errors are caught in generic handler
|
|
164
|
+
send(worker, { ...ctx, promise: true }, await rejectOnSignal(iterator, subSignal));
|
|
165
|
+
log(LogLevel.detail, ctx, 'PROMISE RESOLVED');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// GENERATOR
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Listen outside the loop to react immediately
|
|
173
|
+
// Could be a wait for since abort is a one-off, but listeners are disposed by signal anyway, and we don't need to block the async thread
|
|
174
|
+
listen(
|
|
175
|
+
worker,
|
|
176
|
+
ctx,
|
|
177
|
+
(data) => {
|
|
178
|
+
if (!data.ctx.abort) return;
|
|
179
|
+
subController.abort(data.ctx.abort);
|
|
180
|
+
logWarn(LogLevel.lifecycle, ctx, 'GOT ABORT');
|
|
181
|
+
},
|
|
182
|
+
subSignal,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
let res: any;
|
|
186
|
+
let nextPayload: any = payload; // does not matter, will be ignored anyway
|
|
187
|
+
do {
|
|
188
|
+
res = await rejectOnSignal(iterator.next(nextPayload), subSignal);
|
|
189
|
+
|
|
190
|
+
send(worker, { ...ctx, done: res.done }, res.value);
|
|
191
|
+
|
|
192
|
+
if (res.done) {
|
|
193
|
+
subController.abort('Done');
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const nextData = await rejectOnSignal(
|
|
198
|
+
waitFor(worker, ctx, (data) => !!data.ctx.ack, subSignal),
|
|
199
|
+
subSignal,
|
|
200
|
+
);
|
|
201
|
+
nextPayload = nextData.payload;
|
|
202
|
+
|
|
203
|
+
log(LogLevel.detail, ctx, 'LOOP ACK');
|
|
204
|
+
} while (!res.done && !subSignal.aborted);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
if (!subSignal.aborted && !e.message.includes('RaceAborted')) {
|
|
207
|
+
logError(LogLevel.error, ctx, 'LOOP ERROR', e);
|
|
208
|
+
send(worker, { ...ctx, error: true, done: true }, e);
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
log(LogLevel.detail, ctx, 'LOOP DONE');
|
|
212
|
+
}
|
|
213
|
+
} catch (e) {
|
|
214
|
+
// Generator-specific error handling
|
|
215
|
+
logError(LogLevel.error, ctx, 'ERROR', e);
|
|
216
|
+
send(worker, { ...ctx, error: true, done: true }, e); // Generic error handling
|
|
217
|
+
} finally {
|
|
218
|
+
log(LogLevel.detail, ctx, 'DONE');
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
mainController.signal,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
function stop() {
|
|
225
|
+
mainController.abort('Stop');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { responders, stop }; // responders needed for typing
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Caller
|
|
232
|
+
|
|
233
|
+
//TODO Name arg
|
|
234
|
+
function createCaller<T extends Responders>(worker: WorkerLike, responders: T): T {
|
|
235
|
+
worker.addEventListener('messageerror', (e) => logError(LogLevel.error, null, 'MESSAGE ERROR', e));
|
|
236
|
+
|
|
237
|
+
//TODO Stop
|
|
238
|
+
|
|
239
|
+
return new Proxy<T>(responders as any, {
|
|
240
|
+
get(target, prop, receiver) {
|
|
241
|
+
function gen(payload: any) {
|
|
242
|
+
const ctx: Context = { message: prop.toString(), id: crypto.randomUUID() };
|
|
243
|
+
|
|
244
|
+
let signal: AbortSignal = null as any;
|
|
245
|
+
|
|
246
|
+
if (payload && Object.is(payload, Object(payload))) {
|
|
247
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
248
|
+
if (value instanceof AbortSignal) {
|
|
249
|
+
log(LogLevel.detail, ctx, 'FOUND SIGNAL', key);
|
|
250
|
+
ctx.signalName = key;
|
|
251
|
+
signal = value;
|
|
252
|
+
payload[key] = null; // will be recovered in responder
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const [mainController, mainSignal] = deriveController(signal);
|
|
258
|
+
|
|
259
|
+
const cancel = (notify: boolean, reason: string = '') => {
|
|
260
|
+
if (mainController.signal.aborted) return;
|
|
261
|
+
logWarn(LogLevel.lifecycle, ctx, 'CANCEL', reason, notify);
|
|
262
|
+
if (reason) mainController.abort(reason); // stop listening
|
|
263
|
+
if (notify) send(worker, { ...ctx, abort: reason }); // worker will break & acknowledge, but it's not important
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Generator
|
|
267
|
+
|
|
268
|
+
const [promiseController, promiseSignal] = deriveController(mainSignal);
|
|
269
|
+
|
|
270
|
+
const promise = waitFor(
|
|
271
|
+
worker,
|
|
272
|
+
ctx,
|
|
273
|
+
(data) => !!data.ctx.promise || !!data.ctx.error,
|
|
274
|
+
promiseSignal,
|
|
275
|
+
).then(({ ctx, payload }) => {
|
|
276
|
+
if (ctx.error) {
|
|
277
|
+
logError(LogLevel.error, ctx, 'PROMISE ERROR', payload);
|
|
278
|
+
throw payload;
|
|
279
|
+
}
|
|
280
|
+
log(LogLevel.lifecycle, ctx, 'PROMISE RESOLVED', payload);
|
|
281
|
+
return payload;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
async function* generator() {
|
|
285
|
+
let done = false;
|
|
286
|
+
try {
|
|
287
|
+
// React immediately
|
|
288
|
+
mainSignal.addEventListener('abort', () => cancel(true, mainSignal.reason), {
|
|
289
|
+
once: true,
|
|
290
|
+
signal: mainSignal,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
log(LogLevel.detail, ctx, 'GENERATOR START');
|
|
294
|
+
promiseController.abort('GeneratorUsed');
|
|
295
|
+
|
|
296
|
+
do {
|
|
297
|
+
const data = await rejectOnSignal(
|
|
298
|
+
waitFor(worker, ctx, undefined, mainSignal),
|
|
299
|
+
mainSignal,
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const { ctx: ctxIn, payload: value } = data;
|
|
303
|
+
|
|
304
|
+
log(LogLevel.lifecycle, ctx, 'EVENT', mainSignal.aborted, ctxIn, value);
|
|
305
|
+
|
|
306
|
+
if (ctxIn.promise) {
|
|
307
|
+
// && !ctxIn.error
|
|
308
|
+
// cancel(false, 'Resolved');
|
|
309
|
+
// return payloadIn;
|
|
310
|
+
logError(LogLevel.error, ctx, 'ERROR', 'Unexpected promise in generator', data);
|
|
311
|
+
throw new Error('Unexpected promise in generator', { cause: data });
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (ctxIn.error) {
|
|
315
|
+
logError(LogLevel.error, ctx, 'ERROR', data);
|
|
316
|
+
cancel(false, 'Error');
|
|
317
|
+
throw value;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (ctxIn.done) {
|
|
321
|
+
done = true;
|
|
322
|
+
return value;
|
|
323
|
+
} else {
|
|
324
|
+
const nextPayload = yield value;
|
|
325
|
+
if (!mainSignal.aborted) send(worker, { ...ctx, ack: true }, nextPayload);
|
|
326
|
+
}
|
|
327
|
+
} while (!mainSignal.aborted);
|
|
328
|
+
} finally {
|
|
329
|
+
// prevent further cycles in worker once main is closed/cancelled/aborted
|
|
330
|
+
log(LogLevel.detail, ctx, 'DONE');
|
|
331
|
+
cancel(true, mainSignal.aborted ? mainSignal.reason : done ? 'Done' : 'Break');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const iterator = generator();
|
|
336
|
+
|
|
337
|
+
send(worker, ctx, payload);
|
|
338
|
+
|
|
339
|
+
// FIXME Nasty hack, return both iterator and promise
|
|
340
|
+
// Consuming code will get both, but IDE should show only proper type
|
|
341
|
+
// Consuming code will likely use only one except someone calls `for await ... of await` which is not useful in any case
|
|
342
|
+
iterator['then'] = promise.then.bind(promise);
|
|
343
|
+
iterator['catch'] = promise.catch.bind(promise);
|
|
344
|
+
iterator['finally'] = promise.finally.bind(promise);
|
|
345
|
+
return iterator;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
//TODO mimic-function(gen, fn); but no access to original
|
|
349
|
+
Object.defineProperty(gen, 'name', { value: prop.toString() });
|
|
350
|
+
|
|
351
|
+
(target as any)[prop] = gen; // so that fn is visible after use, maybe not needed
|
|
352
|
+
|
|
353
|
+
return gen;
|
|
354
|
+
},
|
|
355
|
+
}) as T;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return { createResponder, createCaller };
|
|
359
|
+
}
|
|
@@ -9,7 +9,7 @@ import { FormControlFieldset } from './formControlFieldset';
|
|
|
9
9
|
import { FormLabelLegend } from './formLabelLegend';
|
|
10
10
|
|
|
11
11
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
|
12
|
-
const meta = {
|
|
12
|
+
const meta: Meta<typeof GenericControl> = {
|
|
13
13
|
title: 'MUI / FormControlFieldset',
|
|
14
14
|
parameters: {
|
|
15
15
|
layout: 'centered',
|