@kirill.konshin/utils 0.0.14 → 0.0.16

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.
Files changed (107) hide show
  1. package/.ctirc +2 -1
  2. package/.turbo/turbo-build.log +18 -13
  3. package/.turbo/turbo-test.log +76 -23
  4. package/CHANGELOG.md +12 -0
  5. package/README.md +1 -1
  6. package/demo/{cache-demo-clean.ts → worker-demo.ts} +1 -1
  7. package/dist/bootstrap/controls.d.ts +5 -2
  8. package/dist/bootstrap/controls.d.ts.map +1 -1
  9. package/dist/bootstrap/controls.js +1 -0
  10. package/dist/bootstrap/controls.js.map +1 -1
  11. package/dist/bootstrap/controls.stories.d.ts +9 -0
  12. package/dist/bootstrap/controls.stories.d.ts.map +1 -0
  13. package/dist/bootstrap/error.stories.d.ts +8 -0
  14. package/dist/bootstrap/error.stories.d.ts.map +1 -0
  15. package/dist/bootstrap/field.stories.d.ts +8 -0
  16. package/dist/bootstrap/field.stories.d.ts.map +1 -0
  17. package/dist/bootstrap/globalLoading.stories.d.ts +7 -0
  18. package/dist/bootstrap/globalLoading.stories.d.ts.map +1 -0
  19. package/dist/bootstrap/index.js +2 -1
  20. package/dist/bootstrap/loading.stories.d.ts +9 -0
  21. package/dist/bootstrap/loading.stories.d.ts.map +1 -0
  22. package/dist/bootstrap/screen.stories.d.ts +14 -0
  23. package/dist/bootstrap/screen.stories.d.ts.map +1 -0
  24. package/dist/bootstrap/toaster.stories.d.ts +8 -0
  25. package/dist/bootstrap/toaster.stories.d.ts.map +1 -0
  26. package/dist/bootstrap/useModal.stories.d.ts +9 -0
  27. package/dist/bootstrap/useModal.stories.d.ts.map +1 -0
  28. package/dist/core/cache.d.ts +7 -8
  29. package/dist/core/cache.d.ts.map +1 -1
  30. package/dist/core/cache.js +7 -4
  31. package/dist/core/cache.js.map +1 -1
  32. package/dist/core/cache.test.d.ts +2 -0
  33. package/dist/core/cache.test.d.ts.map +1 -0
  34. package/dist/core/files.d.ts +3 -0
  35. package/dist/core/files.d.ts.map +1 -0
  36. package/dist/core/files.js +27 -0
  37. package/dist/core/files.js.map +1 -0
  38. package/dist/core/index.d.ts +4 -0
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +14 -2
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/ls.d.ts +6 -0
  43. package/dist/core/ls.d.ts.map +1 -0
  44. package/dist/core/ls.js +12 -0
  45. package/dist/core/ls.js.map +1 -0
  46. package/dist/core/worker.d.ts +23 -9
  47. package/dist/core/worker.d.ts.map +1 -1
  48. package/dist/core/worker.js +93 -90
  49. package/dist/core/worker.js.map +1 -1
  50. package/dist/core/worker.test.d.ts +2 -0
  51. package/dist/core/worker.test.d.ts.map +1 -0
  52. package/dist/core/workerUtils.d.ts +3 -0
  53. package/dist/core/workerUtils.d.ts.map +1 -0
  54. package/dist/core/workerUtils.js +31 -0
  55. package/dist/core/workerUtils.js.map +1 -0
  56. package/dist/core/wrpc.d.ts +45 -0
  57. package/dist/core/wrpc.d.ts.map +1 -0
  58. package/dist/core/wrpc.fixture.d.ts +29 -0
  59. package/dist/core/wrpc.fixture.d.ts.map +1 -0
  60. package/dist/core/wrpc.js +242 -0
  61. package/dist/core/wrpc.js.map +1 -0
  62. package/dist/core/wrpc.test.d.ts +1 -0
  63. package/dist/core/wrpc.test.d.ts.map +1 -0
  64. package/dist/mui/formControlFieldset.stories.d.ts +7 -0
  65. package/dist/mui/formControlFieldset.stories.d.ts.map +1 -0
  66. package/dist/react/form/form.d.ts +7 -6
  67. package/dist/react/form/form.d.ts.map +1 -1
  68. package/dist/react/form/form.js +14 -20
  69. package/dist/react/form/form.js.map +1 -1
  70. package/dist/react/index.d.ts +1 -0
  71. package/dist/react/index.d.ts.map +1 -1
  72. package/dist/react/index.js +8 -2
  73. package/dist/react/index.js.map +1 -1
  74. package/dist/react/keyboard.d.ts +11 -0
  75. package/dist/react/keyboard.d.ts.map +1 -0
  76. package/dist/react/keyboard.js +38 -0
  77. package/dist/react/keyboard.js.map +1 -0
  78. package/package.json +5 -2
  79. package/src/bootstrap/controls.stories.tsx +2 -2
  80. package/src/bootstrap/controls.tsx +3 -3
  81. package/src/bootstrap/error.stories.tsx +3 -3
  82. package/src/bootstrap/field.stories.tsx +2 -2
  83. package/src/bootstrap/globalLoading.stories.tsx +2 -2
  84. package/src/bootstrap/loading.stories.tsx +2 -2
  85. package/src/bootstrap/screen.stories.tsx +2 -2
  86. package/src/bootstrap/toaster.stories.tsx +2 -2
  87. package/src/bootstrap/useModal.stories.tsx +3 -3
  88. package/src/core/README.md +91 -0
  89. package/src/core/cache.ts +12 -12
  90. package/src/core/files.ts +25 -0
  91. package/src/core/index.ts +4 -0
  92. package/src/core/ls.ts +11 -0
  93. package/src/core/worker.test.ts +197 -0
  94. package/src/core/worker.ts +106 -93
  95. package/src/core/workerUtils.ts +32 -0
  96. package/src/core/wrpc.fixture.ts +72 -0
  97. package/src/core/wrpc.test.ts +225 -0
  98. package/src/core/wrpc.ts +359 -0
  99. package/src/mui/formControlFieldset.stories.tsx +1 -1
  100. package/src/react/form/form.tsx +53 -33
  101. package/src/react/index.ts +1 -0
  102. package/src/react/keyboard.tsx +49 -0
  103. package/src/tailwind/README.md +8 -0
  104. package/tsconfig.json +2 -1
  105. package/vite.config.ts +10 -3
  106. package/vite.exports.ts +3 -3
  107. package/demo/cache-demo.ts +0 -343
@@ -0,0 +1,197 @@
1
+ import { expect, describe, test, vi } from 'vitest';
2
+ import { RespondersBase, WorkerDialog } from './worker';
3
+ import EventEmitter from 'node:events';
4
+
5
+ // Prepare
6
+
7
+ //TODO @vitest/web-worker can ve used, coverage is collected, but this gives more direct access to worker internals
8
+ // class WorkerPeer extends EventEmitter implements WorkerLike {
9
+ class WorkerPeer extends EventEmitter {
10
+ peer: WorkerPeer;
11
+
12
+ setPeer(peer: WorkerPeer) {
13
+ this.peer = peer;
14
+ }
15
+
16
+ postMessage(data: any) {
17
+ this.peer?.emit('message', { data });
18
+ }
19
+
20
+ addEventListener(event: string, listener: (e: any) => void, options) {
21
+ this[options['once'] ? 'once' : 'on'](event, listener);
22
+ options?.signal?.addEventListener('abort', () => this.off(event, listener));
23
+ }
24
+
25
+ removeEventListener(event: string, listener: (e: any) => void) {
26
+ this.off(event, listener);
27
+ }
28
+ }
29
+
30
+ class Responders extends RespondersBase<Responders> {
31
+ lastContext: any; // for testing purposes
32
+
33
+ test = this.create(
34
+ 'test',
35
+ async (
36
+ { n, abort = false, buf }: { n: number; abort?: boolean; buf?: ArrayBuffer }, // should have all possible combinations for strong typing
37
+ ctx,
38
+ ) => {
39
+ this.lastContext = ctx;
40
+
41
+ const res: number[] = [];
42
+
43
+ let aborted = false;
44
+ let progress = 0;
45
+
46
+ ctx.listen(({ abort }) => {
47
+ if (abort) aborted = true;
48
+ });
49
+
50
+ for (const i of Array(n).keys()) {
51
+ if (aborted) break;
52
+ progress = (i + 1) / n;
53
+ ctx.send({ progress });
54
+ res.push(i);
55
+ }
56
+
57
+ return { res, progress, aborted };
58
+ },
59
+ );
60
+
61
+ error = this.create(
62
+ 'error',
63
+ async (
64
+ _, // should have all possible combinations for strong typing
65
+ ctx,
66
+ ) => {
67
+ this.lastContext = ctx;
68
+ throw new Error('Test');
69
+ },
70
+ );
71
+ }
72
+
73
+ const createWorker = () => {
74
+ const mainWorker = new WorkerPeer();
75
+ const mainDialog = new WorkerDialog(
76
+ mainWorker as never as Worker,
77
+ {} as WorkerDialog<Responders>['responders'],
78
+ 'Main',
79
+ );
80
+
81
+ const worker = new WorkerPeer();
82
+ const workerDialog = new WorkerDialog(worker as never as Worker, new Responders(), 'Worker');
83
+
84
+ mainWorker.setPeer(worker);
85
+ worker.setPeer(mainWorker);
86
+
87
+ return { mainWorker, mainDialog, worker, workerDialog };
88
+ };
89
+
90
+ // Tests
91
+
92
+ describe('Worker', async () => {
93
+ test('simple', async () => {
94
+ const { mainDialog, workerDialog } = createWorker();
95
+
96
+ const l = vi.fn();
97
+ let mctx;
98
+
99
+ const result = await mainDialog.withMessage('test').fetch({ n: 3, buf: new ArrayBuffer() }, (ctx) => {
100
+ mctx = ctx;
101
+ ctx.listen(l);
102
+ });
103
+
104
+ expect(l).toBeCalledTimes(4); // 3x progress, last
105
+ expect(result).toStrictEqual({ res: [0, 1, 2], progress: 1, aborted: false });
106
+
107
+ expect(mainDialog.contexts.size).toBe(0);
108
+ expect(mctx.debugInfo.closed).toBe(true);
109
+
110
+ expect(workerDialog.contexts.size).toBe(2);
111
+ expect(workerDialog.contexts.values().next().value.debugInfo).toStrictEqual({
112
+ id: '*',
113
+ parentId: undefined,
114
+ message: 'test',
115
+ closed: false,
116
+ type: 'response',
117
+ listeners: 1,
118
+ });
119
+ expect(workerDialog.responders.lastContext.debugInfo).toStrictEqual({
120
+ id: '1',
121
+ parentId: '*',
122
+ message: 'test',
123
+ closed: true,
124
+ type: 'response',
125
+ listeners: 0,
126
+ });
127
+ });
128
+
129
+ test('abort', async () => {
130
+ const { mainDialog } = createWorker();
131
+
132
+ const result = await mainDialog.withMessage('test').fetch({ n: 3 }, (ctx) => {
133
+ ctx.listen(() => {
134
+ ctx.send({ abort: true });
135
+ });
136
+ });
137
+
138
+ expect(result).toStrictEqual({ res: [0], progress: 0.3333333333333333, aborted: true });
139
+ });
140
+
141
+ test('faulty listener', async () => {
142
+ const { mainDialog } = createWorker();
143
+
144
+ await mainDialog.withMessage('test').fetch({ n: 3 }, (ctx) => {
145
+ ctx.listen(() => {
146
+ throw new Error('Listener');
147
+ });
148
+ });
149
+
150
+ expect.assertions(0); // nothing happened
151
+ });
152
+
153
+ test('error', async () => {
154
+ const { mainDialog } = createWorker();
155
+ await expect(async () => mainDialog.withMessage('error').fetch()).rejects.toThrowError('Test');
156
+ });
157
+
158
+ test('close', async () => {
159
+ const { mainDialog, workerDialog } = createWorker();
160
+
161
+ mainDialog.close();
162
+ workerDialog.close();
163
+
164
+ expect(mainDialog.contexts.size).toBe(0);
165
+ expect(workerDialog.contexts.size).toBe(0);
166
+ });
167
+
168
+ test('empty message', async () => {
169
+ expect(() => createWorker().mainDialog.withMessage(undefined as any)).toThrowError(
170
+ 'New context must have message',
171
+ );
172
+ });
173
+
174
+ test('empty ID', async () => {
175
+ const { mainDialog } = createWorker();
176
+ const first = mainDialog.withMessage('test');
177
+ expect(() => first.withMessage('error', '')).toThrowError('ID is required');
178
+ });
179
+
180
+ test('nesting', async () => {
181
+ const { mainDialog } = createWorker();
182
+ const first = mainDialog.withMessage('test');
183
+ const second = first.withMessage('error', 'x');
184
+ const third = second.withMessage('error');
185
+ expect(mainDialog.contexts.size).toBe(3);
186
+ expect(first.info).toBe('Main:*/1:test[open,request]');
187
+ first.close();
188
+ expect(mainDialog.contexts.size).toBe(0);
189
+ expect(first.debugInfo.closed).toBe(true);
190
+ expect(second.debugInfo.closed).toBe(true);
191
+ expect(third.debugInfo.closed).toBe(true);
192
+ expect(first.info).toBe('Main:*/1:test[closed,request]'); // id bumped from 0
193
+ expect(second.info).toBe('Main:1/x:error[closed,request]'); // new id
194
+ expect(third.info).toBe('Main:x/x:error[closed,request]'); // id kept
195
+ await expect(() => first.fetch({ n: 1 })).rejects.toThrowError(/Context 1 is closed/);
196
+ });
197
+ });
@@ -1,36 +1,6 @@
1
- export function isTransferable(obj: any): boolean {
2
- return transferrable.some((t) => obj instanceof t) ? obj : null;
3
- }
4
-
5
- function getTransferrable(data: any = {}): Transferable[] {
6
- if (!data) return [];
7
- return Object.values<any>(data)
8
- .reduce((r, v) => {
9
- return Array.isArray(v) ? [...r, ...v.map(isTransferable)] : [...r, isTransferable(v)];
10
- }, [])
11
- .filter(Boolean);
12
- }
1
+ import { getTransferrable } from './workerUtils';
13
2
 
14
- // @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
15
- const transferrable =
16
- typeof window !== 'undefined'
17
- ? [
18
- ArrayBuffer,
19
- MessagePort,
20
- ReadableStream,
21
- WritableStream,
22
- TransformStream,
23
- // WebTransportReceiveStream, // ReferenceError: WebTransportReceiveStream is not defined
24
- // WebTransportSendStream, // ReferenceError: WebTransportReceiveStream is not defined
25
- // AudioData, // TS2304: Cannot find name AudioData
26
- ImageBitmap,
27
- VideoFrame,
28
- OffscreenCanvas,
29
- RTCDataChannel,
30
- MediaSourceHandle,
31
- // MIDIAccess, // ReferenceError: MIDIAccess is not defined
32
- ].filter(Boolean)
33
- : [];
3
+ // Types
34
4
 
35
5
  type WebWorker = Worker | (Window & typeof globalThis) | (WorkerGlobalScope & typeof globalThis);
36
6
 
@@ -44,6 +14,7 @@ export type Ctx<R, K extends keyof R> = {
44
14
  id: string;
45
15
  parentId?: string;
46
16
  message: K;
17
+ done: boolean;
47
18
  };
48
19
 
49
20
  type Data<R, M extends keyof R> = MethodReturn<R, M> | MethodArg<R, M> | Error;
@@ -56,35 +27,37 @@ type Listener<R, M extends keyof R, D extends Data<R, M> = Data<R, M>, S extends
56
27
  event: Event<R, M>,
57
28
  ) => void | Promise<void>;
58
29
 
59
- const ALL = '*';
30
+ // Constants
60
31
 
61
- let id = 0;
32
+ const ALL = '*';
62
33
 
63
- function getID() {
64
- return (++id).toString();
65
- }
34
+ const RESPONDER = Symbol('responder');
66
35
 
67
36
  function checkClosed(obj: any, label: string = 'external') {
68
- if (obj?.closed) throw new Error(`Context is closed ${this.info} (${label})`, { cause: obj });
37
+ if (obj?.closed) throw new Error(`Context ${obj.id} is closed (${label})`, { cause: obj });
69
38
  }
70
39
 
71
40
  function checkMessage(obj: any, message: any) {
72
41
  if (!message) throw new Error('New context must have message', { cause: obj });
73
42
  }
74
43
 
44
+ // Classes
45
+
75
46
  export class WorkerDialog<R extends RespondersBase<any>> {
76
47
  readonly contexts = new Set<Context<R, keyof R>>();
77
48
  protected closed = false;
49
+ protected currentId = 0; // IDs are intentionally not global
78
50
 
79
51
  constructor(
80
52
  public worker: WebWorker,
81
53
  public responders: R,
82
54
  public name: string,
55
+ public debug: number = 0, // 0 - none, 1 - general messages, 2 - sub/unsub, 3 - transport TODO Object?
83
56
  ) {
84
57
  if (!this.worker) return; // Next.js SSR
85
58
 
86
59
  for (const message in this.responders) {
87
- if (['constructor', 'create'].includes(message)) continue;
60
+ if (!this.responders?.[message]?.[RESPONDER]) continue;
88
61
 
89
62
  // root context with id=all
90
63
  const rootContext = new ResponseContext<R, typeof message>(this, message, ALL);
@@ -94,33 +67,40 @@ export class WorkerDialog<R extends RespondersBase<any>> {
94
67
  // create sub context with ID
95
68
  const responseContext = context.withMessage(message, event.data.ctx.id);
96
69
 
97
- console.log('Root Listener', rootContext.info, {
98
- rootContext: context,
99
- data,
100
- responseContext,
101
- event,
102
- responder: this.responders[message],
103
- });
70
+ /* v8 ignore start */
71
+ if (this.debug)
72
+ console.log('Root Listener', rootContext.info, {
73
+ rootContext: context,
74
+ data,
75
+ responseContext,
76
+ event,
77
+ responder: this.responders[message],
78
+ });
79
+ /* v8 ignore stop */
104
80
 
105
81
  try {
106
- responseContext.send(await (this.responders[message] as any)(data, responseContext));
82
+ responseContext.send(await (this.responders[message] as any)(data, responseContext), true);
107
83
  responseContext.close('root.listener');
108
84
  } catch (e) {
109
- console.error(`Error in responder ${responseContext.info}`, { e });
110
- responseContext.send(e);
85
+ /* v8 ignore next */
86
+ if (this.debug) console.error(`Error in responder ${responseContext.info}`, { e });
87
+ responseContext.send(e, true);
111
88
  }
112
- });
89
+ }, true);
113
90
  }
114
91
  }
115
92
 
93
+ getID() {
94
+ return (++this.currentId).toString();
95
+ }
96
+
116
97
  close() {
117
98
  for (const context of this.contexts) context.close('root');
118
99
  this.contexts.clear();
119
- this.worker.onmessage = null;
120
100
  }
121
101
 
122
102
  withMessage<M extends keyof R>(message: M) {
123
- return new RequestContext<R, M>(this, message, getID());
103
+ return new RequestContext<R, M>(this, message, this.getID()); // always new context
124
104
  }
125
105
  }
126
106
 
@@ -129,6 +109,7 @@ export class RespondersBase<R> {
129
109
  message: M,
130
110
  responder: F,
131
111
  ): F {
112
+ responder[RESPONDER] = true;
132
113
  return responder as any;
133
114
  }
134
115
  }
@@ -144,17 +125,19 @@ abstract class Context<R, M extends keyof R> {
144
125
  public id: string | undefined,
145
126
  public parent: Context<R, keyof R> | undefined | null = null,
146
127
  ) {
147
- checkClosed(this.dialog, 'newContext');
148
- checkClosed(this.parent, 'newContext');
128
+ checkClosed(dialog, 'newContext');
129
+ checkClosed(parent, 'newContext');
149
130
  checkMessage(this, message);
150
- this.id = parent?.id === ALL ? getID() : id ? id : this.parent?.id;
131
+ this.id = id ?? parent?.id;
151
132
  if (!this.id) throw new Error('ID is required', { cause: this });
133
+ this.dialog.contexts.add(this);
152
134
  }
153
135
 
154
136
  abstract withMessage<M2 extends keyof R>(message: M2, id?: string): Context<R, M2>;
155
137
 
156
138
  close(reason: string = 'external') {
157
- // console.log('Closing', this.id, reason, this);
139
+ /* v8 ignore next */
140
+ if (this.dialog.debug >= 2) console.log('Closing', this.info, reason, this);
158
141
 
159
142
  this.dialog.contexts.delete(this);
160
143
 
@@ -171,33 +154,44 @@ abstract class Context<R, M extends keyof R> {
171
154
  }
172
155
 
173
156
  get info() {
174
- return this.dialog.name + '.' + this.parent?.id + '.' + this.id + '.' + this.message.toString();
157
+ return `${this.dialog.name}:${this.parent?.id ?? ALL}/${this.id}:${this.message.toString()}[${this.closed ? 'closed' : 'open'},${this.type}]`;
175
158
  }
176
159
 
177
- protected postMessage(data?: Data<R, M>) {
160
+ get debugInfo() {
161
+ return {
162
+ id: this.id,
163
+ parentId: this.parent?.id,
164
+ message: this.message,
165
+ closed: this.closed,
166
+ type: this.type,
167
+ listeners: this.unsub.size,
168
+ };
169
+ }
170
+
171
+ abstract type: 'request' | 'response' | string;
172
+
173
+ protected postMessage(data?: Data<R, M>, done = false) {
178
174
  checkClosed(this, 'postMessage');
179
175
 
180
176
  const transfer = getTransferrable(data);
181
177
 
182
- // console.log('postMessage', this.id, this, { mergedCtx, data, transfer });
178
+ const ctx = {
179
+ done,
180
+ id: this.id,
181
+ parentId: this.parent?.id,
182
+ message: this.message,
183
+ };
183
184
 
184
- this.dialog.worker.postMessage(
185
- {
186
- data,
187
- ctx: {
188
- id: this.id,
189
- parentId: this.parent?.id,
190
- message: this.message,
191
- },
192
- },
193
- transfer,
194
- );
185
+ /* v8 ignore next */
186
+ if (this.dialog.debug >= 3) console.log('postMessage', this.info, { ctx, data, transfer }, this);
187
+
188
+ this.dialog.worker.postMessage({ data, ctx }, transfer);
195
189
 
196
190
  return data;
197
191
  }
198
192
 
199
- send(data?: Data<R, M>) {
200
- return this.postMessage(data);
193
+ send(data?: Data<R, M>, done = false) {
194
+ return this.postMessage(data, done);
201
195
  }
202
196
 
203
197
  /**
@@ -216,33 +210,44 @@ abstract class Context<R, M extends keyof R> {
216
210
  * un(); // closes
217
211
  * ```
218
212
  */
219
- listen(callback: Listener<R, M>): () => void {
213
+ listen(callback: Listener<R, M>, done = false): () => void {
220
214
  checkClosed(this, 'listen');
221
215
 
222
- let workerListener: any;
216
+ const controller = new AbortController();
223
217
 
224
218
  this.dialog.worker.addEventListener(
225
219
  'message',
226
- (workerListener = async (event: Event<R, M>) => {
220
+ async (event: Event<R, M>) => {
227
221
  try {
228
222
  const {
229
223
  data: { ctx, data },
230
224
  } = event;
231
225
 
232
- if (!ctx || (this.id !== ALL && this.id !== ctx.id) || this.message !== ctx.message) return; // unknown message from devtools etc., or different context
226
+ if (
227
+ !ctx ||
228
+ (this.id !== ALL && this.id !== ctx.id) ||
229
+ this.message !== ctx.message ||
230
+ (done && !ctx.done)
231
+ )
232
+ return; // unknown message from devtools etc., or different context
233
233
 
234
- console.log('Shared listener event', this.info, this, event);
234
+ /* v8 ignore next */
235
+ if (this.dialog.debug) console.log('Listener event', this.info, event, this);
235
236
 
236
237
  await callback(data, this, event); // Requests can come here, but they're typed in responders
237
238
  } catch (e) {
238
- console.error(`Error in listener ${this.info}`, this, { event, e });
239
+ /* v8 ignore next */
240
+ if (this.dialog.debug) console.error(`Error in listener ${this.info}`, this, { event, e });
241
+ //TODO Respond with error?
239
242
  }
240
- }),
243
+ },
244
+ { signal: controller.signal },
241
245
  );
242
246
 
243
247
  const unsub = () => {
244
- // console.log('Stop listening', this.id, this);
245
- this.dialog.worker.removeEventListener('message', workerListener);
248
+ /* v8 ignore next */
249
+ if (this.dialog.debug >= 2) console.log('Stop listening', this.info, this);
250
+ controller.abort();
246
251
  this.unsub.delete(unsub);
247
252
  };
248
253
 
@@ -253,16 +258,18 @@ abstract class Context<R, M extends keyof R> {
253
258
  }
254
259
 
255
260
  class RequestContext<R, M extends keyof R> extends Context<R, M> {
261
+ type = 'request';
262
+
256
263
  withMessage<M2 extends keyof R>(message: M2, id?: string): RequestContext<R, M2> {
257
264
  return new RequestContext<R, M2>(this.dialog, message, id, this);
258
265
  }
259
266
 
260
- send(data?: MethodArg<R, M>): MethodArg<R, M> {
261
- return super.send(data) as any;
267
+ send(data?: MethodArg<R, M>, done = false): MethodArg<R, M> {
268
+ return super.send(data, done) as any;
262
269
  }
263
270
 
264
- listen(callback: Listener<R, M, MethodReturn<R, M>, RequestContext<R, M>>): () => void {
265
- return super.listen(callback);
271
+ listen(callback: Listener<R, M, MethodReturn<R, M>, RequestContext<R, M>>, done = false): () => void {
272
+ return super.listen(callback, done);
266
273
  }
267
274
 
268
275
  /**
@@ -280,13 +287,16 @@ class RequestContext<R, M extends keyof R> extends Context<R, M> {
280
287
  data?: MethodArg<R, M>,
281
288
  callback?: (context: RequestContext<R, M>) => void,
282
289
  ): Promise<MethodReturn<R, M>> {
290
+ checkClosed(this, 'fetch');
291
+
283
292
  const res = await Promise.all([
284
293
  callback?.(this),
285
294
  this.expect(), // will be closed in expect -> listener -> unsub
286
- this.send(data),
295
+ this.send(data, true),
287
296
  ]);
288
297
 
289
- // console.log('Fetch', this.id, this.contexts, this.ownContexts);
298
+ /* v8 ignore next */
299
+ if (this.dialog.debug >= 2) console.log('Fetch', this.info, this);
290
300
 
291
301
  this.close('fetch');
292
302
 
@@ -302,26 +312,29 @@ class RequestContext<R, M extends keyof R> extends Context<R, M> {
302
312
  try {
303
313
  if ((data as any) instanceof Error) throw data;
304
314
  responseListener();
305
- // console.log('Expect', this.id, this, eventData);
315
+ /* v8 ignore next */
316
+ if (this.dialog.debug >= 3) console.log('Expect', this.info, data, this);
306
317
  resolve(data as any);
307
318
  } catch (e) {
308
319
  reject(e);
309
320
  }
310
- });
321
+ }, true);
311
322
  });
312
323
  }
313
324
  }
314
325
 
315
326
  class ResponseContext<R, M extends keyof R> extends Context<R, M> {
327
+ type = 'response';
328
+
316
329
  withMessage<M2 extends keyof R>(message: M2, id?: string): ResponseContext<R, M2> {
317
330
  return new ResponseContext<R, M2>(this.dialog, message, id, this);
318
331
  }
319
332
 
320
- listen(callback: Listener<R, M, MethodArg<R, M>, ResponseContext<R, M>>): () => void {
321
- return super.listen(callback);
333
+ listen(callback: Listener<R, M, MethodArg<R, M>, ResponseContext<R, M>>, done = false): () => void {
334
+ return super.listen(callback, done);
322
335
  }
323
336
 
324
- send(data?: MethodReturn<R, M>): MethodReturn<R, M> {
325
- return super.send(data) as any;
337
+ send(data?: MethodReturn<R, M>, done = false): MethodReturn<R, M> {
338
+ return super.send(data, done) as any;
326
339
  }
327
340
  }
@@ -0,0 +1,32 @@
1
+ const g = globalThis;
2
+
3
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
4
+ const transferrable = [
5
+ g.ArrayBuffer,
6
+ g.MessagePort,
7
+ g.ReadableStream,
8
+ g.WritableStream,
9
+ g.TransformStream,
10
+ // WebTransportReceiveStream, // ReferenceError: WebTransportReceiveStream is not defined
11
+ // WebTransportSendStream, // ReferenceError: WebTransportReceiveStream is not defined
12
+ // AudioData, // TS2304: Cannot find name AudioData
13
+ g.ImageBitmap,
14
+ g.VideoFrame,
15
+ g.OffscreenCanvas,
16
+ g.RTCDataChannel,
17
+ g.MediaSourceHandle,
18
+ // MIDIAccess, // ReferenceError: MIDIAccess is not defined
19
+ ].filter(Boolean);
20
+
21
+ export function isTransferable(obj: any): boolean {
22
+ return transferrable.some((t) => obj instanceof t) ? obj : null;
23
+ }
24
+
25
+ export function getTransferrable(data: any = {}): Transferable[] {
26
+ if (!data) return [];
27
+ return Object.values<any>(data)
28
+ .reduce((r, v) => {
29
+ return Array.isArray(v) ? [...r, ...v.map(isTransferable)] : [...r, isTransferable(v)];
30
+ }, [])
31
+ .filter(Boolean);
32
+ }
@@ -0,0 +1,72 @@
1
+ import { wrpc } from './wrpc';
2
+
3
+ export const responder = wrpc().createResponder(self, {
4
+ // note it's not async, and works properly
5
+ test: function* ({ buf, signal }: { signal?: AbortSignal; buf?: ArrayBuffer } = {}) {
6
+ yield { progress: 0 };
7
+ yield { progress: 0.5 };
8
+ yield { progress: 1, aborted: !!signal?.aborted };
9
+ return 'foo';
10
+ },
11
+ // note it's not async, and works properly
12
+ returnYield: function* () {
13
+ yield 1;
14
+ yield 2;
15
+ return yield 3;
16
+ },
17
+ bidirectional: function* (prefix = 1): Generator<string, string, number> {
18
+ let i = 0;
19
+ while (i < 3) {
20
+ prefix = (yield prefix + '.' + i) || prefix;
21
+ i++;
22
+ }
23
+ return 'done';
24
+ },
25
+ // eslint-disable-next-line require-yield
26
+ error: async function* () {
27
+ throw new Error('Test');
28
+ },
29
+ promise: async function () {
30
+ return { test: 'test' };
31
+ },
32
+ promiseError: async function () {
33
+ throw new Error('Test');
34
+ },
35
+ setTimeout: async ({ timeout = 1000 }: { timeout?: number } = {}) =>
36
+ new Promise((resolve) => setTimeout(resolve, timeout)),
37
+
38
+ setInterval: async function* ({ timeout = 1000 }: { timeout?: number } = {}) {
39
+ while (true) {
40
+ yield await this.setTimeout(timeout);
41
+ }
42
+ },
43
+ });
44
+
45
+ // self.onmessage = (e) => {
46
+ // console.log('MSG', e);
47
+ // if (e.data.msg === 'info') {
48
+ // self.postMessage({
49
+ // responder: 'test', // responder.toString(),
50
+ // });
51
+ // }
52
+ // };
53
+
54
+ // self.onmessage = (e) => {
55
+ // console.log('MSG', e);
56
+ // self.postMessage(`${e.data} world`);
57
+ // };
58
+
59
+ self.addEventListener('message', (e) => {
60
+ if (e.data.msg === 'info') {
61
+ self.postMessage({
62
+ msg: e.data.msg,
63
+ responder: 'test', // responder.toString(),
64
+ });
65
+ }
66
+ if (e.data.msg === 'stop') {
67
+ responder.stop();
68
+ self.postMessage({
69
+ msg: e.data.msg,
70
+ });
71
+ }
72
+ });