@atolljs/core 0.1.4 → 0.1.6
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/listSchema.d.ts +2 -2
- package/dist/contract/msgpackrCodec.d.ts +1 -1
- package/dist/contract/reef.d.ts +140 -80
- package/dist/contract/sharedMemory.d.ts +2 -2
- package/dist/contract/types.d.ts +1 -1
- package/dist/contract/zod.d.ts +114 -14
- package/dist/index.d.ts +31 -29
- package/dist/index.js +18 -17
- package/dist/index10.js +0 -2
- package/dist/index11.js +0 -2
- package/dist/index12.js +0 -2
- package/dist/index13.js +0 -2
- package/dist/index14.js +0 -2
- package/dist/index15.js +0 -2
- package/dist/index16.js +46 -38
- package/dist/index17.js +20 -21
- package/dist/index18.js +64 -37
- package/dist/index19.js +0 -2
- package/dist/index2.js +0 -2
- package/dist/index20.js +0 -2
- package/dist/index3.js +101 -43
- package/dist/index4.js +0 -2
- package/dist/index5.js +0 -2
- package/dist/index6.js +0 -2
- package/dist/index7.js +0 -2
- package/dist/index8.js +0 -2
- package/dist/index9.js +0 -2
- package/dist/observable.d.ts +2 -2
- package/dist/pool/memory.d.ts +1 -1
- package/dist/pool/workerClient.d.ts +6 -6
- package/dist/pool/workerPool.d.ts +2 -2
- package/dist/reactive.d.ts +1 -1
- package/dist/service.d.ts +1 -1
- package/dist/shared/sharedWorkerClient.d.ts +2 -2
- package/dist/task.d.ts +1 -1
- package/dist/testing/inProcessWorker.d.ts +27 -0
- package/dist/worker/defineWorker.d.ts +3 -4
- package/dist/worker/registry.d.ts +1 -1
- package/dist/worker/workerBootstrap.d.ts +9 -1
- package/package.json +6 -3
- package/src/contract/zod.ts +130 -14
- package/src/index.ts +2 -0
- package/src/reactive.ts +107 -42
- package/src/worker/defineWorker.ts +6 -2
- package/src/worker/workerBootstrap.ts +53 -37
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,4 +1,4 @@
|
|
|
1
|
-
import './workerBootstrap';
|
|
1
|
+
import { installWorkerListener } from './workerBootstrap';
|
|
2
2
|
import { TaskRegistry } from './registry';
|
|
3
3
|
import { scoped } from '../log';
|
|
4
4
|
import type { ServiceMethod } from '../service';
|
|
@@ -62,7 +62,7 @@ const RESERVED = new Set<string>(RESERVED_CLIENT_KEYS);
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* The worker-side half of the tRPC-style pair: the worker script owns the
|
|
65
|
-
* runtime (
|
|
65
|
+
* runtime (calling this wires `self.onmessage` via workerBootstrap)
|
|
66
66
|
* and the method list — the main thread imports only `typeof` the returned
|
|
67
67
|
* definition and drives it through a {@link connectWorker} Proxy client.
|
|
68
68
|
*
|
|
@@ -88,6 +88,10 @@ export function defineWorker<
|
|
|
88
88
|
M extends WorkerMethodMap = {},
|
|
89
89
|
Svc extends WorkerServiceMap = {},
|
|
90
90
|
>(config: DefineWorkerConfig<S, M, Svc>): WorkerDefinition<S, M, Svc> {
|
|
91
|
+
// Called (not a bare import) so the wiring survives bundler tree-shaking —
|
|
92
|
+
// a `sideEffects` glob can't match renamed dist chunks (index16.js), which
|
|
93
|
+
// is how vite's dep-optimizer used to drop the entire message pump.
|
|
94
|
+
installWorkerListener();
|
|
91
95
|
const methods = (config.methods ?? {}) as M;
|
|
92
96
|
const services = (config.services ?? {}) as Svc;
|
|
93
97
|
|
|
@@ -5,52 +5,68 @@ import { fmtBytes, scoped } from '../log';
|
|
|
5
5
|
const workerLog = scoped('worker');
|
|
6
6
|
|
|
7
7
|
let memoryInitialized = false;
|
|
8
|
+
let wired = false;
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
workerLog.info('initialized (message-only pool — no shared memory)');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (data.type === 'INIT_MEMORY') {
|
|
29
|
-
const memory = data.memory as WebAssembly.Memory;
|
|
30
|
-
bindSharedMemories(memory.buffer as unknown as SharedArrayBuffer);
|
|
31
|
-
memoryInitialized = true;
|
|
32
|
-
workerLog.info(`shared memory initialized (${fmtBytes(memory.buffer.byteLength)})`);
|
|
10
|
+
/**
|
|
11
|
+
* Wire `self.onmessage` to the pool protocol (INIT / INIT_MEMORY /
|
|
12
|
+
* EXECUTE_TASK). Runs automatically on import for the public
|
|
13
|
+
* `import '@atolljs/core/worker/workerBootstrap'` entry, and is also called
|
|
14
|
+
* from `defineWorker()` — an explicit call survives bundler tree-shaking,
|
|
15
|
+
* unlike a bare side-effect import whose `sideEffects` glob can miss
|
|
16
|
+
* renamed dist chunks. Idempotent: safe on repeated defineWorker() calls.
|
|
17
|
+
*/
|
|
18
|
+
export function installWorkerListener(): void {
|
|
19
|
+
if (wired) return;
|
|
20
|
+
if (typeof self === 'undefined') {
|
|
21
|
+
// Imported outside a worker context (e.g. a Nest entry that shares the
|
|
22
|
+
// atoll-nestjs barrel with the worker entry) — nothing to wire.
|
|
23
|
+
workerLog.warn('workerBootstrap: no worker global found — skipping message wiring');
|
|
33
24
|
return;
|
|
34
25
|
}
|
|
26
|
+
wired = true;
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
const
|
|
28
|
+
self.onmessage = async (event) => {
|
|
29
|
+
const data = event.data;
|
|
38
30
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
if (data.type === 'INIT') {
|
|
32
|
+
memoryInitialized = true;
|
|
33
|
+
if (getDefinedSharedMemoryCount() > 0) {
|
|
34
|
+
workerLog.warn(
|
|
35
|
+
'pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
workerLog.info('initialized (message-only pool — no shared memory)');
|
|
42
39
|
return;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
workerLog.warn(`task ${taskId} failed: ${err.message || String(err)}`);
|
|
52
|
-
self.postMessage({ messageId, success: false, error: err.message || String(err) });
|
|
42
|
+
if (data.type === 'INIT_MEMORY') {
|
|
43
|
+
const memory = data.memory as WebAssembly.Memory;
|
|
44
|
+
bindSharedMemories(memory.buffer as unknown as SharedArrayBuffer);
|
|
45
|
+
memoryInitialized = true;
|
|
46
|
+
workerLog.info(`shared memory initialized (${fmtBytes(memory.buffer.byteLength)})`);
|
|
47
|
+
return;
|
|
53
48
|
}
|
|
49
|
+
|
|
50
|
+
if (data.type === 'EXECUTE_TASK') {
|
|
51
|
+
const { messageId, taskId, args } = data;
|
|
52
|
+
|
|
53
|
+
if (!memoryInitialized) {
|
|
54
|
+
workerLog.warn(`rejected ${taskId} — shared memory not initialized`);
|
|
55
|
+
self.postMessage({ messageId, success: false, error: 'Shared memory not initialized in worker.' });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const t0 = performance.now();
|
|
60
|
+
try {
|
|
61
|
+
const result = await TaskRegistry.execute(taskId, ...(args || []));
|
|
62
|
+
workerLog.debug(`task ${taskId} → ${(performance.now() - t0).toFixed(1)}ms`);
|
|
63
|
+
self.postMessage({ messageId, success: true, result });
|
|
64
|
+
} catch (err: any) {
|
|
65
|
+
workerLog.warn(`task ${taskId} failed: ${err.message || String(err)}`);
|
|
66
|
+
self.postMessage({ messageId, success: false, error: err.message || String(err) });
|
|
67
|
+
}
|
|
54
68
|
}
|
|
55
69
|
};
|
|
56
70
|
}
|
|
71
|
+
|
|
72
|
+
installWorkerListener();
|