@atolljs/core 0.1.5 → 0.1.7
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/dist/contract/reef.d.ts +88 -28
- package/dist/contract/sharedMemory.d.ts +2 -0
- package/dist/contract/types.d.ts +2 -0
- package/dist/contract/zod.d.ts +114 -14
- package/dist/devtools.d.ts +223 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +19 -17
- package/dist/index10.js +14 -185
- package/dist/index11.js +310 -47
- package/dist/index12.js +46 -59
- package/dist/index13.js +59 -20
- package/dist/index14.js +24 -60
- package/dist/index15.js +56 -43
- package/dist/index16.js +44 -50
- package/dist/index17.js +52 -38
- package/dist/index18.js +36 -54
- package/dist/index19.js +81 -47
- package/dist/index20.js +48 -51
- package/dist/index21.js +57 -0
- package/dist/index3.js +101 -41
- package/dist/index5.js +156 -35
- package/dist/index6.js +32 -485
- package/dist/index7.js +508 -19
- package/dist/index8.js +19 -33
- package/dist/index9.js +29 -14
- package/dist/pool/workerClient.d.ts +2 -0
- package/dist/pool/workerPool.d.ts +4 -0
- package/package.json +1 -1
- package/src/contract/sharedMemory.ts +19 -3
- package/src/contract/types.ts +2 -0
- package/src/contract/zod.ts +130 -14
- package/src/devtools.ts +490 -0
- package/src/index.ts +16 -0
- package/src/pool/workerClient.ts +3 -0
- package/src/pool/workerPool.ts +56 -7
- package/src/reactive.ts +107 -42
- package/src/worker/workerBootstrap.ts +3 -0
package/src/pool/workerPool.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { MemoryManager } from './memory';
|
|
|
2
2
|
import { PoolQueueFullError, TaskAbortedError, TaskTimeoutError, WorkerCrashedError } from './errors';
|
|
3
3
|
import { SharedAccess, SharedMemory, SharedSpec } from '../contract/sharedMemory';
|
|
4
4
|
import { MemoryPersistence, PoolTasks, TaskContract, TaskMap, TaskResult, WorkerPoolConfig } from '../contract/types';
|
|
5
|
+
import { devtoolsEnabled, emitDevtools, forwardDevtools, nextDevtoolsId, type EmittedDevtoolsEvent } from '../devtools';
|
|
5
6
|
import { fmtBytes, scoped } from '../log';
|
|
6
7
|
|
|
7
8
|
const poolLog = scoped('pool');
|
|
@@ -59,6 +60,8 @@ export interface PoolStats {
|
|
|
59
60
|
interface TaskEntry {
|
|
60
61
|
contract: TaskContract<any[], any>;
|
|
61
62
|
args: any[];
|
|
63
|
+
/** Per-pool sequence — correlates a call's devtools events across its lifecycle. */
|
|
64
|
+
callId: number;
|
|
62
65
|
enqueuedAt: number;
|
|
63
66
|
resolve: (v: any) => void;
|
|
64
67
|
reject: (e: unknown) => void;
|
|
@@ -100,6 +103,10 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
100
103
|
waitMs: agg(),
|
|
101
104
|
runMs: agg(),
|
|
102
105
|
};
|
|
106
|
+
/** Devtools correlation id ('pool-N') — stamped on every event this pool emits. */
|
|
107
|
+
readonly poolId = nextDevtoolsId('pool');
|
|
108
|
+
private readonly label?: string;
|
|
109
|
+
private nextCallId = 0;
|
|
103
110
|
public readonly sharedMemory: (SharedMemory<S> & SharedAccess<S>) | undefined;
|
|
104
111
|
/** The attached memory-persistence adapter, if the config supplied one. */
|
|
105
112
|
public readonly persistence: MemoryPersistence | undefined;
|
|
@@ -121,6 +128,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
121
128
|
this.maxQueue = config.maxQueue ?? Infinity;
|
|
122
129
|
this.respawn = config.respawn ?? true;
|
|
123
130
|
this.taskTimeout = config.taskTimeout;
|
|
131
|
+
this.label = config.name;
|
|
124
132
|
|
|
125
133
|
if (config.sharedMemory) {
|
|
126
134
|
this.sharedMemory = config.sharedMemory;
|
|
@@ -142,6 +150,15 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
142
150
|
);
|
|
143
151
|
poolLog.debug(`tip: setLogLevel('trace') for per-task detail`);
|
|
144
152
|
|
|
153
|
+
emitDevtools({
|
|
154
|
+
type: 'pool:init',
|
|
155
|
+
poolId: this.poolId,
|
|
156
|
+
label: this.label,
|
|
157
|
+
poolSize,
|
|
158
|
+
concurrency: this.concurrency,
|
|
159
|
+
memoryBytes: this.sharedMemory?.totalBytes,
|
|
160
|
+
});
|
|
161
|
+
|
|
145
162
|
for (let i = 0; i < poolSize; i++) {
|
|
146
163
|
this.addWorker(this.spawn());
|
|
147
164
|
}
|
|
@@ -167,18 +184,33 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
167
184
|
private addWorker(worker: Worker): WorkerSlot {
|
|
168
185
|
const slot: WorkerSlot = { worker, inFlight: 0 };
|
|
169
186
|
worker.addEventListener('message', (event: MessageEvent) => {
|
|
170
|
-
|
|
187
|
+
const data = event.data as TaskResult & { messageId?: number; type?: string; event?: EmittedDevtoolsEvent };
|
|
188
|
+
// Worker-side instrumentation forwarded on the task channel — re-emit
|
|
189
|
+
// into the local sink so one sink sees the whole app. Stamp which
|
|
190
|
+
// slot produced it so worker-side fetches/heap/memory writes
|
|
191
|
+
// attribute to a specific worker.
|
|
192
|
+
if (data.type === 'ATOLL_DEVTOOLS' && data.event) {
|
|
193
|
+
forwardDevtools({
|
|
194
|
+
...data.event,
|
|
195
|
+
worker: { poolId: this.poolId, slot: this.slots.indexOf(slot) },
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.handleReply(slot, data);
|
|
171
200
|
});
|
|
172
201
|
worker.addEventListener('error', (event: unknown) => {
|
|
173
202
|
this.handleWorkerError(slot, event);
|
|
174
203
|
});
|
|
204
|
+
// `devtools` tells the worker to forward its instrumentation events back
|
|
205
|
+
// on this channel (ATOLL_DEVTOOLS) — only when a sink is live at spawn.
|
|
175
206
|
if (this.memoryManager) {
|
|
176
|
-
worker.postMessage({ type: 'INIT_MEMORY', memory: this.memoryManager.memory });
|
|
207
|
+
worker.postMessage({ type: 'INIT_MEMORY', memory: this.memoryManager.memory, devtools: devtoolsEnabled() });
|
|
177
208
|
} else {
|
|
178
|
-
worker.postMessage({ type: 'INIT' });
|
|
209
|
+
worker.postMessage({ type: 'INIT', devtools: devtoolsEnabled() });
|
|
179
210
|
}
|
|
180
211
|
this.slots.push(slot);
|
|
181
212
|
poolLog.debug(`worker spawned, ${this.memoryManager ? 'INIT_MEMORY' : 'INIT'} sent`);
|
|
213
|
+
emitDevtools({ type: 'worker:spawn', poolId: this.poolId, slot: this.slots.indexOf(slot) });
|
|
182
214
|
return slot;
|
|
183
215
|
}
|
|
184
216
|
|
|
@@ -192,14 +224,17 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
192
224
|
// stats and settlement — the caller was already rejected.
|
|
193
225
|
if (!entry.done) {
|
|
194
226
|
entry.done = true;
|
|
195
|
-
|
|
227
|
+
const runMs = performance.now() - (entry.sentAt ?? entry.enqueuedAt);
|
|
228
|
+
this.statsAgg.runMs.add(runMs);
|
|
196
229
|
if (data.success) {
|
|
197
230
|
this.statsAgg.completed++;
|
|
198
231
|
taskLog.debug(`← ${entry.contract.taskId} ok`);
|
|
232
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'ok', runMs });
|
|
199
233
|
entry.resolve(data.result);
|
|
200
234
|
} else {
|
|
201
235
|
this.statsAgg.failed++;
|
|
202
236
|
taskLog.warn(`← ${entry.contract.taskId} failed: ${data.error}`);
|
|
237
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'error', runMs, error: data.error });
|
|
203
238
|
entry.reject(new Error(data.error));
|
|
204
239
|
}
|
|
205
240
|
}
|
|
@@ -212,6 +247,8 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
212
247
|
const e = event as { message?: string; error?: Error } | undefined;
|
|
213
248
|
const message = e?.message ?? (e?.error ? String(e.error) : 'worker error');
|
|
214
249
|
poolLog.warn(`worker error: ${message}`);
|
|
250
|
+
const idx = this.slots.indexOf(slot);
|
|
251
|
+
emitDevtools({ type: 'worker:error', poolId: this.poolId, slot: idx, message });
|
|
215
252
|
|
|
216
253
|
// Reject the dead worker's in-flight tasks. Orphaned (already
|
|
217
254
|
// aborted/timed-out) entries are dropped without re-counting.
|
|
@@ -222,23 +259,25 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
222
259
|
if (entry.done) continue;
|
|
223
260
|
entry.done = true;
|
|
224
261
|
this.statsAgg.failed++;
|
|
262
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: message });
|
|
225
263
|
entry.reject(new WorkerCrashedError(message));
|
|
226
264
|
}
|
|
227
265
|
}
|
|
228
266
|
|
|
229
|
-
const idx = this.slots.indexOf(slot);
|
|
230
267
|
if (idx >= 0) this.slots.splice(idx, 1);
|
|
231
268
|
slot.worker.terminate();
|
|
232
269
|
|
|
233
270
|
if (this.respawn) {
|
|
234
271
|
poolLog.info('respawning crashed worker');
|
|
235
272
|
this.addWorker(this.spawn());
|
|
273
|
+
emitDevtools({ type: 'worker:respawn', poolId: this.poolId, slot: this.slots.length - 1 });
|
|
236
274
|
} else if (this.slots.length === 0) {
|
|
237
275
|
// No workers left to ever serve the queue.
|
|
238
276
|
for (const entry of this.queue.splice(0)) {
|
|
239
277
|
entry.done = true;
|
|
240
278
|
this.detach(entry);
|
|
241
279
|
this.statsAgg.failed++;
|
|
280
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: message });
|
|
242
281
|
entry.reject(new WorkerCrashedError(message));
|
|
243
282
|
}
|
|
244
283
|
}
|
|
@@ -270,10 +309,13 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
270
309
|
entry.messageId = messageId;
|
|
271
310
|
entry.slot = slot;
|
|
272
311
|
entry.sentAt = performance.now();
|
|
273
|
-
|
|
312
|
+
const waitMs = entry.sentAt - entry.enqueuedAt;
|
|
313
|
+
this.statsAgg.waitMs.add(waitMs);
|
|
274
314
|
slot.inFlight++;
|
|
275
315
|
this.pending.set(messageId, entry);
|
|
276
|
-
|
|
316
|
+
const slotIndex = this.slots.indexOf(slot);
|
|
317
|
+
taskLog.debug(`→ ${entry.contract.taskId}`, { worker: slotIndex, args: entry.args });
|
|
318
|
+
emitDevtools({ type: 'task:dispatch', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, slot: slotIndex, waitMs });
|
|
277
319
|
slot.worker.postMessage({
|
|
278
320
|
type: 'EXECUTE_TASK',
|
|
279
321
|
messageId,
|
|
@@ -302,6 +344,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
302
344
|
entry.done = true;
|
|
303
345
|
this.detach(entry);
|
|
304
346
|
this.statsAgg.aborted++;
|
|
347
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: err instanceof TaskTimeoutError ? 'timeout' : 'aborted' });
|
|
305
348
|
entry.reject(err);
|
|
306
349
|
this.drainQueue();
|
|
307
350
|
this.checkDrained();
|
|
@@ -333,6 +376,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
333
376
|
const entry: TaskEntry = {
|
|
334
377
|
contract,
|
|
335
378
|
args: args as any[],
|
|
379
|
+
callId: ++this.nextCallId,
|
|
336
380
|
enqueuedAt: performance.now(),
|
|
337
381
|
resolve,
|
|
338
382
|
reject,
|
|
@@ -348,12 +392,14 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
348
392
|
entry.timer = setTimeout(() => this.cancel(entry, new TaskTimeoutError()), timeout);
|
|
349
393
|
}
|
|
350
394
|
|
|
395
|
+
emitDevtools({ type: 'task:enqueue', poolId: this.poolId, callId: entry.callId, taskId: contract.taskId });
|
|
351
396
|
const slot = this.pickWorker();
|
|
352
397
|
if (slot) {
|
|
353
398
|
this.send(slot, entry);
|
|
354
399
|
} else if (this.queue.length >= this.maxQueue) {
|
|
355
400
|
entry.done = true;
|
|
356
401
|
this.detach(entry);
|
|
402
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: contract.taskId, outcome: 'queue-full' });
|
|
357
403
|
reject(new PoolQueueFullError()); // never dispatched — stats untouched
|
|
358
404
|
return;
|
|
359
405
|
} else {
|
|
@@ -425,12 +471,14 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
425
471
|
poolLog.warn('memory persistence stop failed', e),
|
|
426
472
|
);
|
|
427
473
|
poolLog.info(`terminating ${this.slots.length} worker(s)`);
|
|
474
|
+
emitDevtools({ type: 'pool:terminate', poolId: this.poolId });
|
|
428
475
|
const crashed = new WorkerCrashedError('pool terminated');
|
|
429
476
|
for (const [, entry] of this.pending) {
|
|
430
477
|
this.detach(entry);
|
|
431
478
|
if (entry.done) continue; // orphaned — already counted as aborted
|
|
432
479
|
entry.done = true;
|
|
433
480
|
this.statsAgg.failed++;
|
|
481
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: 'pool terminated' });
|
|
434
482
|
entry.reject(crashed);
|
|
435
483
|
}
|
|
436
484
|
this.pending.clear();
|
|
@@ -438,6 +486,7 @@ class WorkerPoolImpl<S extends SharedSpec = SharedSpec> {
|
|
|
438
486
|
entry.done = true;
|
|
439
487
|
this.detach(entry);
|
|
440
488
|
this.statsAgg.failed++;
|
|
489
|
+
emitDevtools({ type: 'task:settle', poolId: this.poolId, callId: entry.callId, taskId: entry.contract.taskId, outcome: 'crashed', error: 'pool terminated' });
|
|
441
490
|
entry.reject(crashed);
|
|
442
491
|
}
|
|
443
492
|
for (const slot of this.slots) {
|
package/src/reactive.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createEffect, createMemo, createRoot, createSignal } from 'solid-js';
|
|
1
|
+
import { createEffect, createMemo, createRenderEffect, createRoot, createSignal } from 'solid-js';
|
|
2
2
|
import { Connector } from './contract/sharedMemory';
|
|
3
3
|
import { scoped } from './log';
|
|
4
4
|
|
|
@@ -40,6 +40,87 @@ export interface ReactiveConnector<T> {
|
|
|
40
40
|
observeRemote(): () => void;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Park on the connector's shared version counter and invoke `onBump` on each
|
|
45
|
+
* write — local or remote, both bump it. waitAsync when available, 50ms poll
|
|
46
|
+
* otherwise. No solid-js involved: this is the whole remote-write mechanism.
|
|
47
|
+
*/
|
|
48
|
+
const watchVersion = <T>(connector: Connector<T>, onBump: () => void): (() => void) => {
|
|
49
|
+
const version = connector._version;
|
|
50
|
+
if (!version) {
|
|
51
|
+
throw new Error('This connector does not support remote observation.');
|
|
52
|
+
}
|
|
53
|
+
let stopped = false;
|
|
54
|
+
let last = Atomics.load(version.view, version.index);
|
|
55
|
+
|
|
56
|
+
if (typeof Atomics.waitAsync === 'function') {
|
|
57
|
+
reactiveLog.debug(`observing remote writes (offset ${connector.byteOffset}, waitAsync)`);
|
|
58
|
+
void (async () => {
|
|
59
|
+
while (!stopped) {
|
|
60
|
+
const result = Atomics.waitAsync(version.view, version.index, last);
|
|
61
|
+
if (result.async) {
|
|
62
|
+
await result.value;
|
|
63
|
+
}
|
|
64
|
+
last = Atomics.load(version.view, version.index);
|
|
65
|
+
if (!stopped) {
|
|
66
|
+
reactiveLog.trace(`remote write observed (offset ${connector.byteOffset})`);
|
|
67
|
+
onBump();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
reactiveLog.debug(`stopped observing (offset ${connector.byteOffset})`);
|
|
71
|
+
})();
|
|
72
|
+
return () => { stopped = true; };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reactiveLog.warn(`Atomics.waitAsync unavailable — observing via 50ms poll (offset ${connector.byteOffset})`);
|
|
76
|
+
const timer = setInterval(() => {
|
|
77
|
+
const current = Atomics.load(version.view, version.index);
|
|
78
|
+
if (current !== last) {
|
|
79
|
+
last = current;
|
|
80
|
+
onBump();
|
|
81
|
+
}
|
|
82
|
+
}, 50);
|
|
83
|
+
return () => {
|
|
84
|
+
stopped = true;
|
|
85
|
+
clearInterval(timer);
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Under `node`/`worker`/`deno` export conditions, `solid-js` resolves to the
|
|
91
|
+
* SSR build — `createEffect` is a no-op, so signal-driven `watch`/`observe`
|
|
92
|
+
* die silently. Probe once. `createRenderEffect` is the discriminator: it
|
|
93
|
+
* runs synchronously at creation in BOTH builds but only re-fires on `set`
|
|
94
|
+
* in the client build. `createEffect` can't be used — the client build
|
|
95
|
+
* defers it past the synchronous `set` below, probing a false negative.
|
|
96
|
+
* The write must land AFTER the root's create pass returns for the same
|
|
97
|
+
* reason (a set issued mid-subscription is swallowed).
|
|
98
|
+
*/
|
|
99
|
+
let solidLive: boolean | null = null;
|
|
100
|
+
const solidIsLive = (): boolean => {
|
|
101
|
+
if (solidLive !== null) return solidLive;
|
|
102
|
+
try {
|
|
103
|
+
let runs = 0;
|
|
104
|
+
let bump: ((v: number) => number) | undefined;
|
|
105
|
+
createRoot(() => {
|
|
106
|
+
const [get, set] = createSignal(0);
|
|
107
|
+
bump = set;
|
|
108
|
+
createRenderEffect(() => {
|
|
109
|
+
get();
|
|
110
|
+
runs++;
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
bump?.(1);
|
|
114
|
+
solidLive = runs > 1;
|
|
115
|
+
} catch {
|
|
116
|
+
solidLive = false;
|
|
117
|
+
}
|
|
118
|
+
if (!solidLive) {
|
|
119
|
+
reactiveLog.warn('solid-js resolved to the SSR build — watch() falls back to direct version watching');
|
|
120
|
+
}
|
|
121
|
+
return solidLive;
|
|
122
|
+
};
|
|
123
|
+
|
|
43
124
|
export function reactive<T>(connector: Connector<T>): ReactiveConnector<T> {
|
|
44
125
|
const [value, setValue] = createSignal<T>(connector.read(), { equals: false });
|
|
45
126
|
const bump = () => setValue(() => connector.read());
|
|
@@ -51,47 +132,7 @@ export function reactive<T>(connector: Connector<T>): ReactiveConnector<T> {
|
|
|
51
132
|
connector.write(v);
|
|
52
133
|
bump();
|
|
53
134
|
},
|
|
54
|
-
observeRemote: () =>
|
|
55
|
-
const version = connector._version;
|
|
56
|
-
if (!version) {
|
|
57
|
-
throw new Error('This connector does not support remote observation.');
|
|
58
|
-
}
|
|
59
|
-
let stopped = false;
|
|
60
|
-
let last = Atomics.load(version.view, version.index);
|
|
61
|
-
|
|
62
|
-
if (typeof Atomics.waitAsync === 'function') {
|
|
63
|
-
reactiveLog.debug(`observing remote writes (offset ${connector.byteOffset}, waitAsync)`);
|
|
64
|
-
void (async () => {
|
|
65
|
-
while (!stopped) {
|
|
66
|
-
const result = Atomics.waitAsync(version.view, version.index, last);
|
|
67
|
-
if (result.async) {
|
|
68
|
-
await result.value;
|
|
69
|
-
}
|
|
70
|
-
last = Atomics.load(version.view, version.index);
|
|
71
|
-
if (!stopped) {
|
|
72
|
-
reactiveLog.trace(`remote write observed (offset ${connector.byteOffset})`);
|
|
73
|
-
bump();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
reactiveLog.debug(`stopped observing (offset ${connector.byteOffset})`);
|
|
77
|
-
})();
|
|
78
|
-
return () => { stopped = true; };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Fallback for environments without Atomics.waitAsync
|
|
82
|
-
reactiveLog.warn(`Atomics.waitAsync unavailable — observing via 50ms poll (offset ${connector.byteOffset})`);
|
|
83
|
-
const timer = setInterval(() => {
|
|
84
|
-
const current = Atomics.load(version.view, version.index);
|
|
85
|
-
if (current !== last) {
|
|
86
|
-
last = current;
|
|
87
|
-
bump();
|
|
88
|
-
}
|
|
89
|
-
}, 50);
|
|
90
|
-
return () => {
|
|
91
|
-
stopped = true;
|
|
92
|
-
clearInterval(timer);
|
|
93
|
-
};
|
|
94
|
-
},
|
|
135
|
+
observeRemote: () => watchVersion(connector, bump),
|
|
95
136
|
};
|
|
96
137
|
}
|
|
97
138
|
|
|
@@ -127,6 +168,30 @@ export function watch<T, S = T>(
|
|
|
127
168
|
const select = onChange ? (selOrCb as (value: T) => S) : ((v: T) => v as unknown as S);
|
|
128
169
|
const cb = (onChange ?? selOrCb) as (slice: S, previous: S | undefined) => void;
|
|
129
170
|
|
|
171
|
+
// SSR solid build (node/worker conditions): effects never re-run, so the
|
|
172
|
+
// signal pipeline below would silently never emit. Same semantics, driven
|
|
173
|
+
// straight off the version counter instead.
|
|
174
|
+
if (!solidIsLive()) {
|
|
175
|
+
const eq = (a: S | undefined, b: S | undefined): boolean =>
|
|
176
|
+
a === undefined || b === undefined ? a === b : (options?.equals ?? Object.is)(a, b);
|
|
177
|
+
let prev: S | undefined;
|
|
178
|
+
const emit = () => {
|
|
179
|
+
const v = connector.read() as T | null | undefined;
|
|
180
|
+
const s = v == null ? undefined : select(v);
|
|
181
|
+
if (s === undefined || eq(prev, s)) return;
|
|
182
|
+
const old = prev;
|
|
183
|
+
prev = s;
|
|
184
|
+
cb(s, old);
|
|
185
|
+
};
|
|
186
|
+
emit();
|
|
187
|
+
const stop = watchVersion(connector, emit);
|
|
188
|
+
reactiveLog.debug(`watching connector at offset ${connector.byteOffset} (direct)`);
|
|
189
|
+
return () => {
|
|
190
|
+
reactiveLog.debug(`watch stopped (offset ${connector.byteOffset})`);
|
|
191
|
+
stop();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
130
195
|
const sig = reactive(connector);
|
|
131
196
|
const stopRemote = sig.observeRemote();
|
|
132
197
|
const dispose = createRoot((d) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TaskRegistry } from './registry';
|
|
2
2
|
import { bindSharedMemories, getDefinedSharedMemoryCount } from '../contract/sharedMemory';
|
|
3
|
+
import { enableWorkerDevtoolsForwarding } from '../devtools';
|
|
3
4
|
import { fmtBytes, scoped } from '../log';
|
|
4
5
|
|
|
5
6
|
const workerLog = scoped('worker');
|
|
@@ -30,6 +31,7 @@ export function installWorkerListener(): void {
|
|
|
30
31
|
|
|
31
32
|
if (data.type === 'INIT') {
|
|
32
33
|
memoryInitialized = true;
|
|
34
|
+
if (data.devtools) enableWorkerDevtoolsForwarding();
|
|
33
35
|
if (getDefinedSharedMemoryCount() > 0) {
|
|
34
36
|
workerLog.warn(
|
|
35
37
|
'pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound',
|
|
@@ -43,6 +45,7 @@ export function installWorkerListener(): void {
|
|
|
43
45
|
const memory = data.memory as WebAssembly.Memory;
|
|
44
46
|
bindSharedMemories(memory.buffer as unknown as SharedArrayBuffer);
|
|
45
47
|
memoryInitialized = true;
|
|
48
|
+
if (data.devtools) enableWorkerDevtoolsForwarding();
|
|
46
49
|
workerLog.info(`shared memory initialized (${fmtBytes(memory.buffer.byteLength)})`);
|
|
47
50
|
return;
|
|
48
51
|
}
|