@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.
Files changed (45) hide show
  1. package/dist/contract/listSchema.d.ts +2 -2
  2. package/dist/contract/msgpackrCodec.d.ts +1 -1
  3. package/dist/contract/reef.d.ts +140 -80
  4. package/dist/contract/sharedMemory.d.ts +2 -2
  5. package/dist/contract/types.d.ts +1 -1
  6. package/dist/contract/zod.d.ts +114 -14
  7. package/dist/index.d.ts +31 -29
  8. package/dist/index.js +18 -17
  9. package/dist/index10.js +0 -2
  10. package/dist/index11.js +0 -2
  11. package/dist/index12.js +0 -2
  12. package/dist/index13.js +0 -2
  13. package/dist/index14.js +0 -2
  14. package/dist/index15.js +0 -2
  15. package/dist/index16.js +46 -38
  16. package/dist/index17.js +20 -21
  17. package/dist/index18.js +64 -37
  18. package/dist/index19.js +0 -2
  19. package/dist/index2.js +0 -2
  20. package/dist/index20.js +0 -2
  21. package/dist/index3.js +101 -43
  22. package/dist/index4.js +0 -2
  23. package/dist/index5.js +0 -2
  24. package/dist/index6.js +0 -2
  25. package/dist/index7.js +0 -2
  26. package/dist/index8.js +0 -2
  27. package/dist/index9.js +0 -2
  28. package/dist/observable.d.ts +2 -2
  29. package/dist/pool/memory.d.ts +1 -1
  30. package/dist/pool/workerClient.d.ts +6 -6
  31. package/dist/pool/workerPool.d.ts +2 -2
  32. package/dist/reactive.d.ts +1 -1
  33. package/dist/service.d.ts +1 -1
  34. package/dist/shared/sharedWorkerClient.d.ts +2 -2
  35. package/dist/task.d.ts +1 -1
  36. package/dist/testing/inProcessWorker.d.ts +27 -0
  37. package/dist/worker/defineWorker.d.ts +3 -4
  38. package/dist/worker/registry.d.ts +1 -1
  39. package/dist/worker/workerBootstrap.d.ts +9 -1
  40. package/package.json +6 -3
  41. package/src/contract/zod.ts +130 -14
  42. package/src/index.ts +2 -0
  43. package/src/reactive.ts +107 -42
  44. package/src/worker/defineWorker.ts +6 -2
  45. 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'; // side-effect: wires self.onmessage (guarded for main-thread import)
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 (importing this module wires `self.onmessage` via workerBootstrap)
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
- if (typeof self === 'undefined') {
10
- // Imported outside a worker context (e.g. a Nest entry that shares the
11
- // atoll-nestjs barrel with the worker entry) — nothing to wire.
12
- workerLog.warn('workerBootstrap: no worker global found — skipping message wiring');
13
- } else {
14
- self.onmessage = async (event) => {
15
- const data = event.data;
16
-
17
- if (data.type === 'INIT') {
18
- memoryInitialized = true;
19
- if (getDefinedSharedMemoryCount() > 0) {
20
- workerLog.warn(
21
- 'pool sent INIT without memory, but shared-memory contracts are defined — field access will throw until bound',
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
- if (data.type === 'EXECUTE_TASK') {
37
- const { messageId, taskId, args } = data;
28
+ self.onmessage = async (event) => {
29
+ const data = event.data;
38
30
 
39
- if (!memoryInitialized) {
40
- workerLog.warn(`rejected ${taskId} — shared memory not initialized`);
41
- self.postMessage({ messageId, success: false, error: 'Shared memory not initialized in worker.' });
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
- const t0 = performance.now();
46
- try {
47
- const result = await TaskRegistry.execute(taskId, ...(args || []));
48
- workerLog.debug(`task ${taskId} → ${(performance.now() - t0).toFixed(1)}ms`);
49
- self.postMessage({ messageId, success: true, result });
50
- } catch (err: any) {
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();