@rybosome/tspice 0.0.3 → 0.0.8

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 (221) hide show
  1. package/README.md +145 -84
  2. package/backend-contract/dist/.tsbuildinfo +1 -1
  3. package/backend-contract/dist/domains/cells-windows.d.ts +94 -0
  4. package/backend-contract/dist/domains/cells-windows.js +10 -0
  5. package/backend-contract/dist/domains/coords-vectors.d.ts +53 -3
  6. package/backend-contract/dist/domains/dsk.d.ts +49 -0
  7. package/backend-contract/dist/domains/dsk.js +2 -0
  8. package/backend-contract/dist/domains/ek.d.ts +186 -0
  9. package/backend-contract/dist/domains/ek.js +8 -0
  10. package/backend-contract/dist/domains/ephemeris.d.ts +141 -3
  11. package/backend-contract/dist/domains/error.d.ts +42 -0
  12. package/backend-contract/dist/domains/error.js +33 -0
  13. package/backend-contract/dist/domains/file-io.d.ts +114 -0
  14. package/backend-contract/dist/domains/file-io.js +8 -0
  15. package/backend-contract/dist/domains/frames.d.ts +44 -4
  16. package/backend-contract/dist/domains/geometry-gf.d.ts +44 -0
  17. package/backend-contract/dist/domains/geometry-gf.js +14 -0
  18. package/backend-contract/dist/domains/geometry.d.ts +21 -1
  19. package/backend-contract/dist/domains/ids-names-normalize.d.ts +3 -0
  20. package/backend-contract/dist/domains/ids-names-normalize.js +74 -0
  21. package/backend-contract/dist/domains/ids-names.d.ts +37 -0
  22. package/backend-contract/dist/domains/kernel-pool.d.ts +134 -0
  23. package/backend-contract/dist/domains/kernel-pool.js +2 -0
  24. package/backend-contract/dist/domains/kernels-utils.d.ts +44 -0
  25. package/backend-contract/dist/domains/kernels-utils.js +265 -0
  26. package/backend-contract/dist/domains/kernels.d.ts +39 -3
  27. package/backend-contract/dist/domains/time.d.ts +102 -0
  28. package/backend-contract/dist/index.d.ts +34 -15
  29. package/backend-contract/dist/index.js +15 -1
  30. package/backend-contract/dist/shared/errors.d.ts +6 -0
  31. package/backend-contract/dist/shared/errors.js +8 -0
  32. package/backend-contract/dist/shared/mat3.d.ts +52 -0
  33. package/backend-contract/dist/shared/mat3.js +150 -0
  34. package/backend-contract/dist/shared/mat6.d.ts +34 -0
  35. package/backend-contract/dist/shared/mat6.js +116 -0
  36. package/backend-contract/dist/shared/spice-handles.d.ts +20 -0
  37. package/backend-contract/dist/shared/spice-handles.js +82 -0
  38. package/backend-contract/dist/shared/spice-int.d.ts +32 -0
  39. package/backend-contract/dist/shared/spice-int.js +41 -0
  40. package/backend-contract/dist/shared/types.d.ts +136 -5
  41. package/backend-contract/dist/shared/types.js +1 -1
  42. package/backend-contract/dist/shared/vec.d.ts +54 -0
  43. package/backend-contract/dist/shared/vec.js +162 -0
  44. package/backend-fake/dist/.tsbuildinfo +1 -1
  45. package/backend-fake/dist/index.d.ts +21 -1
  46. package/backend-fake/dist/index.js +1112 -33
  47. package/backend-node/dist/.tsbuildinfo +1 -1
  48. package/backend-node/dist/codec/arrays.d.ts +9 -0
  49. package/backend-node/dist/codec/arrays.js +36 -0
  50. package/backend-node/dist/codec/errors.d.ts +6 -6
  51. package/backend-node/dist/codec/errors.js +6 -6
  52. package/backend-node/dist/domains/cells-windows.d.ts +5 -0
  53. package/backend-node/dist/domains/cells-windows.js +112 -0
  54. package/backend-node/dist/domains/coords-vectors.d.ts +1 -0
  55. package/backend-node/dist/domains/coords-vectors.js +66 -0
  56. package/backend-node/dist/domains/dsk.d.ts +6 -0
  57. package/backend-node/dist/domains/dsk.js +108 -0
  58. package/backend-node/dist/domains/ek.d.ts +10 -0
  59. package/backend-node/dist/domains/ek.js +100 -0
  60. package/backend-node/dist/domains/ephemeris.d.ts +5 -1
  61. package/backend-node/dist/domains/ephemeris.js +150 -1
  62. package/backend-node/dist/domains/error.d.ts +5 -0
  63. package/backend-node/dist/domains/error.js +34 -0
  64. package/backend-node/dist/domains/file-io.d.ts +7 -0
  65. package/backend-node/dist/domains/file-io.js +105 -0
  66. package/backend-node/dist/domains/frames.d.ts +1 -0
  67. package/backend-node/dist/domains/frames.js +58 -6
  68. package/backend-node/dist/domains/geometry-gf.d.ts +5 -0
  69. package/backend-node/dist/domains/geometry-gf.js +74 -0
  70. package/backend-node/dist/domains/geometry.d.ts +1 -0
  71. package/backend-node/dist/domains/geometry.js +62 -0
  72. package/backend-node/dist/domains/ids-names.d.ts +2 -1
  73. package/backend-node/dist/domains/ids-names.js +30 -0
  74. package/backend-node/dist/domains/kernel-pool.d.ts +5 -0
  75. package/backend-node/dist/domains/kernel-pool.js +74 -0
  76. package/backend-node/dist/domains/kernels.d.ts +1 -0
  77. package/backend-node/dist/domains/kernels.js +100 -13
  78. package/backend-node/dist/domains/time.d.ts +1 -0
  79. package/backend-node/dist/domains/time.js +75 -1
  80. package/backend-node/dist/index.d.ts +5 -1
  81. package/backend-node/dist/index.js +62 -1
  82. package/backend-node/dist/lowlevel/binding.d.ts +3 -0
  83. package/backend-node/dist/lowlevel/binding.js +115 -0
  84. package/backend-node/dist/runtime/addon.d.ts +273 -2
  85. package/backend-node/dist/runtime/addon.js +3 -0
  86. package/backend-node/dist/runtime/kernel-staging.d.ts +17 -0
  87. package/backend-node/dist/runtime/kernel-staging.js +80 -7
  88. package/backend-node/dist/runtime/spice-handles.d.ts +3 -0
  89. package/backend-node/dist/runtime/spice-handles.js +2 -0
  90. package/backend-node/dist/runtime/virtual-output-staging.d.ts +16 -0
  91. package/backend-node/dist/runtime/virtual-output-staging.js +148 -0
  92. package/backend-wasm/dist/.tsbuildinfo +1 -1
  93. package/backend-wasm/dist/codec/alloc.d.ts +19 -0
  94. package/backend-wasm/dist/codec/alloc.js +64 -0
  95. package/backend-wasm/dist/codec/calls.d.ts +2 -0
  96. package/backend-wasm/dist/codec/calls.js +13 -24
  97. package/backend-wasm/dist/codec/errors.d.ts +6 -0
  98. package/backend-wasm/dist/codec/errors.js +34 -2
  99. package/backend-wasm/dist/codec/found.d.ts +2 -0
  100. package/backend-wasm/dist/codec/found.js +20 -43
  101. package/backend-wasm/dist/codec/strings.d.ts +31 -1
  102. package/backend-wasm/dist/codec/strings.js +93 -6
  103. package/backend-wasm/dist/domains/cells-windows.d.ts +9 -0
  104. package/backend-wasm/dist/domains/cells-windows.js +392 -0
  105. package/backend-wasm/dist/domains/coords-vectors.d.ts +1 -0
  106. package/backend-wasm/dist/domains/coords-vectors.js +377 -184
  107. package/backend-wasm/dist/domains/dsk.d.ts +6 -0
  108. package/backend-wasm/dist/domains/dsk.js +179 -0
  109. package/backend-wasm/dist/domains/ek.d.ts +6 -0
  110. package/backend-wasm/dist/domains/ek.js +543 -0
  111. package/backend-wasm/dist/domains/ephemeris.d.ts +4 -1
  112. package/backend-wasm/dist/domains/ephemeris.js +405 -46
  113. package/backend-wasm/dist/domains/error.d.ts +5 -0
  114. package/backend-wasm/dist/domains/error.js +109 -0
  115. package/backend-wasm/dist/domains/file-io.d.ts +7 -0
  116. package/backend-wasm/dist/domains/file-io.js +462 -0
  117. package/backend-wasm/dist/domains/frames.d.ts +1 -0
  118. package/backend-wasm/dist/domains/frames.js +139 -6
  119. package/backend-wasm/dist/domains/geometry-gf.d.ts +5 -0
  120. package/backend-wasm/dist/domains/geometry-gf.js +178 -0
  121. package/backend-wasm/dist/domains/geometry.d.ts +1 -0
  122. package/backend-wasm/dist/domains/geometry.js +210 -0
  123. package/backend-wasm/dist/domains/ids-names.d.ts +2 -1
  124. package/backend-wasm/dist/domains/ids-names.js +89 -0
  125. package/backend-wasm/dist/domains/kernel-pool.d.ts +5 -0
  126. package/backend-wasm/dist/domains/kernel-pool.js +357 -0
  127. package/backend-wasm/dist/domains/kernels.d.ts +1 -0
  128. package/backend-wasm/dist/domains/kernels.js +108 -4
  129. package/backend-wasm/dist/domains/time.d.ts +2 -0
  130. package/backend-wasm/dist/domains/time.js +235 -133
  131. package/backend-wasm/dist/index.d.ts +4 -2
  132. package/backend-wasm/dist/lowlevel/exports.d.ts +215 -1
  133. package/backend-wasm/dist/lowlevel/exports.js +217 -38
  134. package/backend-wasm/dist/runtime/create-backend-options.d.ts +21 -0
  135. package/backend-wasm/dist/runtime/create-backend.node.d.ts +11 -2
  136. package/backend-wasm/dist/runtime/create-backend.node.js +283 -14
  137. package/backend-wasm/dist/runtime/create-backend.web.d.ts +5 -2
  138. package/backend-wasm/dist/runtime/create-backend.web.js +40 -6
  139. package/backend-wasm/dist/runtime/fs.d.ts +6 -0
  140. package/backend-wasm/dist/runtime/fs.js +29 -3
  141. package/backend-wasm/dist/runtime/spice-handles.d.ts +3 -0
  142. package/backend-wasm/dist/runtime/spice-handles.js +2 -0
  143. package/backend-wasm/dist/runtime/virtual-outputs.d.ts +16 -0
  144. package/backend-wasm/dist/runtime/virtual-outputs.js +35 -0
  145. package/backend-wasm/dist/tspice_backend_wasm.node.js +3 -3
  146. package/backend-wasm/dist/tspice_backend_wasm.wasm +0 -0
  147. package/backend-wasm/dist/tspice_backend_wasm.web.js +1 -1
  148. package/core/dist/.tsbuildinfo +1 -1
  149. package/core/dist/index.d.ts +21 -0
  150. package/core/dist/index.js +57 -0
  151. package/dist/.tsbuildinfo +1 -1
  152. package/dist/backend.d.ts +15 -6
  153. package/dist/backend.js +3 -6
  154. package/dist/clients/createSpiceAsyncFromTransport.d.ts +5 -0
  155. package/dist/clients/createSpiceAsyncFromTransport.js +90 -0
  156. package/dist/clients/createSpiceSyncFromTransport.d.ts +5 -0
  157. package/dist/clients/createSpiceSyncFromTransport.js +88 -0
  158. package/dist/clients/spiceClients.d.ts +59 -0
  159. package/dist/clients/spiceClients.js +292 -0
  160. package/dist/errors.d.ts +4 -0
  161. package/dist/errors.js +4 -0
  162. package/dist/index.d.ts +12 -7
  163. package/dist/index.js +5 -2
  164. package/dist/kernels/defaultKernelPathFromUrl.d.ts +8 -0
  165. package/dist/kernels/defaultKernelPathFromUrl.js +32 -0
  166. package/dist/kernels/kernelPack.d.ts +88 -0
  167. package/dist/kernels/kernelPack.js +122 -0
  168. package/dist/kernels/kernels.d.ts +98 -0
  169. package/dist/kernels/kernels.js +217 -0
  170. package/dist/kernels/naifKernelId.d.ts +2 -0
  171. package/dist/kernels/naifKernelId.js +2 -0
  172. package/dist/kit/index.d.ts +4 -0
  173. package/dist/kit/index.js +3 -0
  174. package/dist/kit/math/mat3.d.ts +31 -0
  175. package/dist/kit/math/mat3.js +82 -0
  176. package/dist/kit/spice/create-kit.d.ts +12 -0
  177. package/dist/kit/spice/create-kit.js +23 -0
  178. package/dist/kit/spice/frames.d.ts +8 -0
  179. package/dist/kit/spice/frames.js +16 -0
  180. package/dist/kit/spice/kernels.d.ts +14 -0
  181. package/dist/kit/spice/kernels.js +39 -0
  182. package/dist/kit/spice/state.d.ts +7 -0
  183. package/dist/kit/spice/state.js +36 -0
  184. package/dist/kit/spice/time.d.ts +9 -0
  185. package/dist/kit/spice/time.js +31 -0
  186. package/dist/kit/types/spice-types.d.ts +51 -0
  187. package/dist/spice.d.ts +10 -1
  188. package/dist/spice.js +84 -72
  189. package/dist/transport/caching/policy.d.ts +16 -0
  190. package/dist/transport/caching/policy.js +77 -0
  191. package/dist/transport/caching/withCaching.d.ts +125 -0
  192. package/dist/transport/caching/withCaching.js +335 -0
  193. package/dist/transport/caching/withCachingSync.d.ts +24 -0
  194. package/dist/transport/caching/withCachingSync.js +161 -0
  195. package/dist/transport/rpc/protocol.d.ts +35 -0
  196. package/dist/transport/rpc/protocol.js +56 -0
  197. package/dist/transport/rpc/taskScheduling.d.ts +20 -0
  198. package/dist/transport/rpc/taskScheduling.js +98 -0
  199. package/dist/transport/rpc/valueCodec.d.ts +5 -0
  200. package/dist/transport/rpc/valueCodec.js +106 -0
  201. package/dist/transport/types.d.ts +7 -0
  202. package/dist/transport/types.js +2 -0
  203. package/dist/types.d.ts +8 -17
  204. package/dist/types.js +2 -1
  205. package/dist/worker/browser/createSpiceWorker.d.ts +22 -0
  206. package/dist/worker/browser/createSpiceWorker.js +41 -0
  207. package/dist/worker/browser/createSpiceWorkerClient.d.ts +40 -0
  208. package/dist/worker/browser/createSpiceWorkerClient.js +99 -0
  209. package/dist/worker/browser/spiceWorkerEntry.d.ts +2 -0
  210. package/dist/worker/browser/spiceWorkerEntry.js +129 -0
  211. package/dist/worker/browser/spiceWorkerInlineSource.d.ts +2 -0
  212. package/dist/worker/browser/spiceWorkerInlineSource.js +4 -0
  213. package/dist/worker/index.d.ts +10 -0
  214. package/dist/worker/index.js +7 -0
  215. package/dist/worker/transport/createWorkerTransport.d.ts +69 -0
  216. package/dist/worker/transport/createWorkerTransport.js +398 -0
  217. package/dist/worker/transport/exposeTransportToWorker.d.ts +51 -0
  218. package/dist/worker/transport/exposeTransportToWorker.js +196 -0
  219. package/package.json +4 -4
  220. package/dist/spice-types.d.ts +0 -36
  221. /package/dist/{spice-types.js → kit/types/spice-types.js} +0 -0
@@ -0,0 +1,398 @@
1
+ import { deserializeError, tspiceRpcDisposeType, tspiceRpcRequestType, tspiceRpcResponseType, } from "../../transport/rpc/protocol.js";
2
+ import { decodeRpcValue, encodeRpcValue } from "../../transport/rpc/valueCodec.js";
3
+ import { canQueueMacrotask, queueMacrotask } from "../../transport/rpc/taskScheduling.js";
4
+ function createAbortError() {
5
+ // DOMException is the most accurate in browser contexts, but isn't guaranteed
6
+ // to exist in all runtimes (e.g. some test environments).
7
+ try {
8
+ return new DOMException("Aborted", "AbortError");
9
+ }
10
+ catch {
11
+ const err = new Error("Aborted");
12
+ err.name = "AbortError";
13
+ return err;
14
+ }
15
+ }
16
+ function createNoMacrotaskSchedulerError() {
17
+ return new Error("Worker transport cannot schedule macrotask settlement (MessageChannel/setTimeout missing). " +
18
+ "Refusing to settle responses synchronously because it would break dispose ordering guarantees.");
19
+ }
20
+ /**
21
+ * Create a `SpiceTransport` backed by a `Worker`.
22
+ *
23
+ * ## Macrotask settlement ordering
24
+ *
25
+ * Worker responses are resolved/rejected on a later macrotask (via
26
+ * `queueMacrotask(...)`) so that calling `dispose()` in the same tick
27
+ * deterministically wins.
28
+ *
29
+ * Implications:
30
+ * - Requests never resolve/reject on the same tick a response is received.
31
+ * - A response received in the current tick may be ignored if `dispose()` is
32
+ * called before the next tick.
33
+ * - If no macrotask scheduler exists in the runtime (no `MessageChannel` and no `setTimeout`),
34
+ * the transport fails closed by rejecting requests rather than settling synchronously.
35
+ */
36
+ export function createWorkerTransport(opts) {
37
+ let worker;
38
+ // Retain a reference for best-effort dispose signaling even after terminal
39
+ // teardown clears `worker`.
40
+ let workerForDisposeSignal;
41
+ let disposed = false;
42
+ let terminalError;
43
+ // If we ever discover we can't schedule a macrotask, we permanently fail
44
+ // closed (reject) to avoid violating settlement ordering guarantees.
45
+ let canScheduleMacrotask;
46
+ const terminateOnDispose = opts.terminateOnDispose ?? (typeof opts.worker === "function" ? true : false);
47
+ const signalDispose = opts.signalDispose ?? terminateOnDispose;
48
+ let didSignalDispose = false;
49
+ const signalDisposeOnce = () => {
50
+ if (didSignalDispose)
51
+ return;
52
+ didSignalDispose = true;
53
+ if (!signalDispose)
54
+ return;
55
+ const w = worker ?? workerForDisposeSignal;
56
+ if (!w)
57
+ return;
58
+ try {
59
+ const msg = { type: tspiceRpcDisposeType };
60
+ w.postMessage(msg);
61
+ }
62
+ catch {
63
+ // ignore
64
+ }
65
+ };
66
+ const pendingById = new Map();
67
+ // Requests with a received response that are awaiting next-macrotask
68
+ // settlement. Keeping these separate from `pendingById` lets us remove a
69
+ // request from the timeout/abort race immediately upon response receipt,
70
+ // while still allowing `dispose()` (and worker errors) to deterministically
71
+ // win before settlement.
72
+ const queuedSettlementById = new Map();
73
+ // Whether a single macrotask has been queued to settle all currently-queued responses.
74
+ let settlementQueued = false;
75
+ let nextId = 1;
76
+ const formatRequestContext = (op, id) => id === undefined ? `(op=${op})` : `(op=${op}, id=${id})`;
77
+ const ensureCanScheduleMacrotask = () => {
78
+ if (canScheduleMacrotask === undefined) {
79
+ // Fail fast before posting any messages if the runtime lacks a macrotask
80
+ // scheduler.
81
+ canScheduleMacrotask = canQueueMacrotask();
82
+ }
83
+ if (canScheduleMacrotask === false) {
84
+ throw createNoMacrotaskSchedulerError();
85
+ }
86
+ };
87
+ const rejectAllPending = (getReason) => {
88
+ for (const [id, pending] of pendingById) {
89
+ pendingById.delete(id);
90
+ pending.rejectAndCleanup(getReason(pending, id));
91
+ }
92
+ // Queued settlements already ran `cleanup()` when their response was received.
93
+ for (const [id, settlement] of Array.from(queuedSettlementById)) {
94
+ queuedSettlementById.delete(id);
95
+ const pending = settlement.pending;
96
+ pending.reject(getReason(pending, id));
97
+ // Explicitly drop response payload references to help GC.
98
+ settlement.value = undefined;
99
+ settlement.error = undefined;
100
+ }
101
+ };
102
+ const terminalTeardown = (err, opts) => {
103
+ if (terminalError)
104
+ return;
105
+ terminalError = err;
106
+ // Terminal teardown transitions the transport into a disposed state even if
107
+ // the caller never explicitly invoked `dispose()`.
108
+ disposed = true;
109
+ // Prevent any future settlement work from being queued.
110
+ settlementQueued = false;
111
+ rejectAllPending(opts?.getReason ?? (() => err));
112
+ // Prefer the latest live worker for any best-effort dispose signaling.
113
+ if (worker)
114
+ workerForDisposeSignal = worker;
115
+ const w = worker;
116
+ worker = undefined;
117
+ if (!w)
118
+ return;
119
+ w.removeEventListener("message", onMessage);
120
+ w.removeEventListener("error", onError);
121
+ w.removeEventListener("messageerror", onMessageError);
122
+ if (!terminateOnDispose)
123
+ return;
124
+ if (opts?.deferTerminate) {
125
+ const ok = queueMacrotask(() => {
126
+ try {
127
+ w.terminate();
128
+ }
129
+ catch {
130
+ // ignore
131
+ }
132
+ }, { allowSyncFallback: false });
133
+ // No scheduler: explicitly fall back to a synchronous terminate so we
134
+ // don't leave an owned worker running.
135
+ if (!ok) {
136
+ try {
137
+ w.terminate();
138
+ }
139
+ catch {
140
+ // ignore
141
+ }
142
+ }
143
+ return;
144
+ }
145
+ try {
146
+ w.terminate();
147
+ }
148
+ catch {
149
+ // ignore
150
+ }
151
+ };
152
+ const hardFailNoMacrotaskScheduler = (err) => {
153
+ // Permanently fail closed.
154
+ canScheduleMacrotask = false;
155
+ if (terminalError)
156
+ return;
157
+ // Best-effort: if configured, attempt to notify the worker to dispose any
158
+ // server-side resources *before* we remove listeners/terminate.
159
+ signalDisposeOnce();
160
+ terminalTeardown(err);
161
+ };
162
+ const flushQueuedSettlements = () => {
163
+ settlementQueued = false;
164
+ for (const [id, settlement] of Array.from(queuedSettlementById)) {
165
+ queuedSettlementById.delete(id);
166
+ const pending = settlement.pending;
167
+ const op = pending.op;
168
+ const kind = settlement.kind;
169
+ // Drop references to response payloads as soon as possible.
170
+ let value = settlement.value;
171
+ let error = settlement.error;
172
+ settlement.value = undefined;
173
+ settlement.error = undefined;
174
+ if (disposed) {
175
+ pending.reject(new Error(`Worker transport disposed ${formatRequestContext(op, id)}`));
176
+ continue;
177
+ }
178
+ if (kind === "resolve") {
179
+ pending.resolve(value);
180
+ value = undefined;
181
+ continue;
182
+ }
183
+ pending.reject(error);
184
+ error = undefined;
185
+ }
186
+ };
187
+ const scheduleSettlementMacrotask = () => {
188
+ if (settlementQueued)
189
+ return;
190
+ settlementQueued = true;
191
+ const ok = queueMacrotask(flushQueuedSettlements, { allowSyncFallback: false });
192
+ if (ok) {
193
+ canScheduleMacrotask = true;
194
+ return;
195
+ }
196
+ // No macrotask scheduler available. Hard-fail and tear down rather than
197
+ // settling synchronously and violating ordering guarantees.
198
+ hardFailNoMacrotaskScheduler(createNoMacrotaskSchedulerError());
199
+ };
200
+ const onMessage = (ev) => {
201
+ const msg = ev?.data;
202
+ if (!msg || msg.type !== tspiceRpcResponseType || typeof msg.id !== "number")
203
+ return;
204
+ const id = msg.id;
205
+ const pending = pendingById.get(id);
206
+ if (!pending)
207
+ return;
208
+ // Remove immediately so a queued timeout handler can't win after we've
209
+ // already received a legitimate response.
210
+ pendingById.delete(id);
211
+ // Clean up request-specific resources immediately (abort listeners, timers),
212
+ // but defer settling to a macrotask so `dispose()` can deterministically win.
213
+ pending.cleanup();
214
+ // If we already know we can't schedule macrotasks, reject immediately.
215
+ if (canScheduleMacrotask === false) {
216
+ pending.reject(createNoMacrotaskSchedulerError());
217
+ return;
218
+ }
219
+ const op = pending.op;
220
+ // Extract response fields up-front so the deferred macrotask doesn't close
221
+ // over the full `msg` payload.
222
+ let kind;
223
+ let value = undefined;
224
+ let error = undefined;
225
+ if (msg.ok === true) {
226
+ if (!("value" in msg)) {
227
+ kind = "reject";
228
+ error = new Error(`Malformed worker response: ok=true but missing value ${formatRequestContext(op, id)}`);
229
+ }
230
+ else {
231
+ kind = "resolve";
232
+ value = decodeRpcValue(msg.value);
233
+ }
234
+ }
235
+ else if (msg.ok === false) {
236
+ if (!("error" in msg)) {
237
+ kind = "reject";
238
+ error = new Error(`Malformed worker response: ok=false but missing error ${formatRequestContext(op, id)}`);
239
+ }
240
+ else {
241
+ kind = "reject";
242
+ error = deserializeError(msg.error);
243
+ }
244
+ }
245
+ else {
246
+ kind = "reject";
247
+ error = new Error(`Malformed worker response: missing ok flag ${formatRequestContext(op, id)}`);
248
+ }
249
+ queuedSettlementById.set(id, { pending, kind, value, error });
250
+ scheduleSettlementMacrotask();
251
+ };
252
+ const onError = (ev) => {
253
+ let message;
254
+ try {
255
+ message = ev?.message;
256
+ }
257
+ catch {
258
+ // ignore
259
+ }
260
+ const safeMessage = typeof message === "string" && message.length > 0 ? message : "Worker error";
261
+ terminalTeardown(new Error(safeMessage), {
262
+ getReason: (pending, id) => new Error(`${safeMessage} ${formatRequestContext(pending.op, id)}`),
263
+ });
264
+ };
265
+ const onMessageError = (_ev) => {
266
+ const err = new Error("Worker message deserialization failed");
267
+ terminalTeardown(err, {
268
+ getReason: (pending, id) => new Error(`Worker message deserialization failed ${formatRequestContext(pending.op, id)}`),
269
+ });
270
+ };
271
+ const ensureWorker = () => {
272
+ ensureCanScheduleMacrotask();
273
+ if (!worker) {
274
+ // Lazily construct/attach so transports can be created in environments
275
+ // that don't immediately support `Worker`.
276
+ try {
277
+ worker = typeof opts.worker === "function" ? opts.worker() : opts.worker;
278
+ }
279
+ catch (err) {
280
+ const out = new Error("Failed to create Worker");
281
+ out.cause = err;
282
+ throw out;
283
+ }
284
+ workerForDisposeSignal = worker;
285
+ worker.addEventListener("message", onMessage);
286
+ worker.addEventListener("error", onError);
287
+ worker.addEventListener("messageerror", onMessageError);
288
+ }
289
+ return worker;
290
+ };
291
+ const dispose = () => {
292
+ // Idempotent. Even if we're already in a terminal state, callers should be
293
+ // able to invoke `dispose()` and still get a best-effort dispose signal.
294
+ if (disposed) {
295
+ signalDisposeOnce();
296
+ return;
297
+ }
298
+ disposed = true;
299
+ // Best-effort: tell the worker it should dispose any server-side resources.
300
+ //
301
+ // This is intentionally opt-in for shared workers; `tspice:dispose` is a
302
+ // global cleanup signal and may affect other clients.
303
+ signalDisposeOnce();
304
+ if (terminalError)
305
+ return;
306
+ terminalTeardown(new Error("Worker transport disposed"), {
307
+ getReason: (pending, id) => new Error(`Worker transport disposed ${formatRequestContext(pending.op, id)}`),
308
+ deferTerminate: true,
309
+ });
310
+ };
311
+ const request = async (op, args, requestOpts) => {
312
+ if (terminalError)
313
+ throw terminalError;
314
+ if (disposed)
315
+ throw new Error(`Worker transport disposed ${formatRequestContext(op)}`);
316
+ if (canScheduleMacrotask === false)
317
+ throw createNoMacrotaskSchedulerError();
318
+ const id = nextId++;
319
+ const w = ensureWorker();
320
+ const timeoutMs = requestOpts?.timeoutMs ?? opts.timeoutMs;
321
+ const signal = requestOpts?.signal;
322
+ if (timeoutMs !== undefined && timeoutMs > 0) {
323
+ const hasTimers = typeof setTimeout === "function" && typeof clearTimeout === "function";
324
+ if (!hasTimers) {
325
+ throw new Error(`Worker request timeoutMs=${timeoutMs} requires timers (setTimeout/clearTimeout) ${formatRequestContext(op, id)}`);
326
+ }
327
+ }
328
+ return await new Promise((resolve, reject) => {
329
+ let requestTimeout;
330
+ // Cleanup is intentionally idempotent. It may be called from multiple
331
+ // paths (timeout, abort, response receipt, or dispose()) depending on
332
+ // ordering.
333
+ let didCleanup = false;
334
+ let onAbort;
335
+ const cleanup = () => {
336
+ if (didCleanup)
337
+ return;
338
+ didCleanup = true;
339
+ if (requestTimeout !== undefined) {
340
+ clearTimeout(requestTimeout);
341
+ requestTimeout = undefined;
342
+ }
343
+ if (signal && onAbort)
344
+ signal.removeEventListener("abort", onAbort);
345
+ };
346
+ const rejectAndCleanup = (reason) => {
347
+ cleanup();
348
+ reject(reason);
349
+ };
350
+ const pending = {
351
+ op,
352
+ resolve,
353
+ reject,
354
+ cleanup,
355
+ rejectAndCleanup,
356
+ };
357
+ pendingById.set(id, pending);
358
+ onAbort = () => {
359
+ if (pendingById.get(id) !== pending)
360
+ return;
361
+ pendingById.delete(id);
362
+ pending.rejectAndCleanup(createAbortError());
363
+ };
364
+ if (signal) {
365
+ if (signal.aborted) {
366
+ pendingById.delete(id);
367
+ pending.rejectAndCleanup(createAbortError());
368
+ return;
369
+ }
370
+ signal.addEventListener("abort", onAbort);
371
+ }
372
+ if (timeoutMs !== undefined && timeoutMs > 0) {
373
+ requestTimeout = setTimeout(() => {
374
+ if (pendingById.get(id) !== pending)
375
+ return;
376
+ pendingById.delete(id);
377
+ pending.rejectAndCleanup(new Error(`Worker request timed out after ${timeoutMs}ms ${formatRequestContext(op, id)}`));
378
+ }, timeoutMs);
379
+ }
380
+ const msg = { type: tspiceRpcRequestType, id, op, args };
381
+ try {
382
+ w.postMessage({ ...msg, args: args.map(encodeRpcValue) });
383
+ }
384
+ catch (err) {
385
+ if (pendingById.get(id) === pending)
386
+ pendingById.delete(id);
387
+ const out = new Error(`Worker postMessage failed ${formatRequestContext(op, id)}`);
388
+ out.cause = err;
389
+ pending.rejectAndCleanup(out);
390
+ }
391
+ });
392
+ };
393
+ return {
394
+ request,
395
+ dispose,
396
+ };
397
+ }
398
+ //# sourceMappingURL=createWorkerTransport.js.map
@@ -0,0 +1,51 @@
1
+ import type { SpiceTransport } from "../../transport/types.js";
2
+ type WorkerGlobalScopeLike = {
3
+ addEventListener(type: "message", listener: (ev: {
4
+ data: unknown;
5
+ }) => void): void;
6
+ removeEventListener(type: "message", listener: (ev: {
7
+ data: unknown;
8
+ }) => void): void;
9
+ postMessage(msg: unknown): void;
10
+ close?: () => void;
11
+ };
12
+ /** Expose a {@link SpiceTransport} inside a Worker, handling RPC request/response messaging. */
13
+ export declare function exposeTransportToWorker(opts: {
14
+ transport: SpiceTransport;
15
+ /**
16
+ * Override the Worker global scope for testing.
17
+ *
18
+ * Defaults to `globalThis`.
19
+ */
20
+ self?: WorkerGlobalScopeLike;
21
+ /** Optional cleanup hook called when a dispose message is received. */
22
+ onDispose?: () => void | Promise<void>;
23
+ /** Whether to call `self.close()` after disposing. Defaults to `true`. */
24
+ closeOnDispose?: boolean;
25
+ /**
26
+ * Maximum number of in-flight `transport.request()` calls allowed at once.
27
+ * Additional requests are queued (FIFO) until prior requests settle.
28
+ *
29
+ * Defaults to `Infinity`.
30
+ */
31
+ maxConcurrentRequests?: number;
32
+ /**
33
+ * Maximum number of queued requests allowed when `maxConcurrentRequests` is
34
+ * finite.
35
+ *
36
+ * When `maxConcurrentRequests` is finite, additional requests are queued
37
+ * (FIFO) until prior requests settle.
38
+ *
39
+ * When the queue is full, new incoming requests are immediately rejected
40
+ * with an RPC error response.
41
+ *
42
+ * Defaults:
43
+ * - `Infinity` when `maxConcurrentRequests` is `Infinity` (preserves existing behavior)
44
+ * - `1000` when `maxConcurrentRequests` is finite
45
+ */
46
+ maxQueuedRequests?: number;
47
+ }): {
48
+ dispose: () => void;
49
+ };
50
+ export {};
51
+ //# sourceMappingURL=exposeTransportToWorker.d.ts.map
@@ -0,0 +1,196 @@
1
+ import { serializeError, tspiceRpcDisposeType, tspiceRpcRequestType, tspiceRpcResponseType, } from "../../transport/rpc/protocol.js";
2
+ import { decodeRpcValue, encodeRpcValue } from "../../transport/rpc/valueCodec.js";
3
+ import { queueMacrotask } from "../../transport/rpc/taskScheduling.js";
4
+ /** Expose a {@link SpiceTransport} inside a Worker, handling RPC request/response messaging. */
5
+ export function exposeTransportToWorker(opts) {
6
+ const self = opts.self ?? globalThis;
7
+ let disposed = false;
8
+ const maxConcurrentRequests = opts.maxConcurrentRequests ?? Infinity;
9
+ if (maxConcurrentRequests !== Infinity) {
10
+ // Guard against deadlocking the queue when `maxConcurrentRequests` is 0/NaN/etc.
11
+ if (!Number.isFinite(maxConcurrentRequests) || maxConcurrentRequests < 1) {
12
+ throw new Error("exposeTransportToWorker(): maxConcurrentRequests must be >= 1");
13
+ }
14
+ // Prefer integers for deterministic queue semantics.
15
+ if (!Number.isInteger(maxConcurrentRequests)) {
16
+ throw new Error("exposeTransportToWorker(): maxConcurrentRequests must be an integer");
17
+ }
18
+ }
19
+ let inFlight = 0;
20
+ const defaultMaxQueuedRequests = maxConcurrentRequests === Infinity ? Infinity : 1000;
21
+ const maxQueuedRequests = opts.maxQueuedRequests ?? defaultMaxQueuedRequests;
22
+ if (maxQueuedRequests !== Infinity) {
23
+ if (!Number.isFinite(maxQueuedRequests) || maxQueuedRequests < 0) {
24
+ throw new Error("exposeTransportToWorker(): maxQueuedRequests must be >= 0");
25
+ }
26
+ if (!Number.isInteger(maxQueuedRequests)) {
27
+ throw new Error("exposeTransportToWorker(): maxQueuedRequests must be an integer");
28
+ }
29
+ }
30
+ // FIFO queue with a moving head index to avoid O(n) `shift()`.
31
+ const queued = [];
32
+ let queuedHead = 0;
33
+ const queuedSize = () => queued.length - queuedHead;
34
+ const FAIL_QUEUED_REQUESTS_CHUNK_SIZE = 100;
35
+ const failQueuedRequests = async (reqs, err) => {
36
+ // Best-effort (avoid throwing during disposal).
37
+ try {
38
+ for (let i = 0; i < reqs.length; i += FAIL_QUEUED_REQUESTS_CHUNK_SIZE) {
39
+ const end = Math.min(i + FAIL_QUEUED_REQUESTS_CHUNK_SIZE, reqs.length);
40
+ for (let j = i; j < end; j++) {
41
+ const q = reqs[j];
42
+ const res = {
43
+ type: tspiceRpcResponseType,
44
+ id: q.id,
45
+ ok: false,
46
+ // Serialize per response so each postMessage gets a fresh object.
47
+ error: serializeError(err),
48
+ };
49
+ self.postMessage(res);
50
+ }
51
+ // Yield between chunks so disposing a huge queue doesn't block the
52
+ // worker for an unbounded amount of time.
53
+ if (end < reqs.length) {
54
+ await new Promise((resolve) => {
55
+ queueMacrotask(resolve, { allowSyncFallback: true });
56
+ });
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // ignore
62
+ }
63
+ };
64
+ const clearQueue = () => {
65
+ queued.length = 0;
66
+ queuedHead = 0;
67
+ };
68
+ const maybeCompactQueue = () => {
69
+ // Periodically compact to avoid unbounded memory growth from a large queue.
70
+ if (queuedHead === 0)
71
+ return;
72
+ if (queuedHead < 100)
73
+ return;
74
+ queued.splice(0, queuedHead);
75
+ queuedHead = 0;
76
+ };
77
+ const drain = () => {
78
+ if (disposed)
79
+ return;
80
+ while (inFlight < maxConcurrentRequests && queuedHead < queued.length) {
81
+ const req = queued[queuedHead];
82
+ queuedHead += 1;
83
+ runRequest(req);
84
+ }
85
+ maybeCompactQueue();
86
+ };
87
+ const runRequest = (req) => {
88
+ inFlight += 1;
89
+ void (async () => {
90
+ try {
91
+ const value = await opts.transport.request(req.op, req.args.map(decodeRpcValue));
92
+ if (disposed)
93
+ return;
94
+ const res = {
95
+ type: tspiceRpcResponseType,
96
+ id: req.id,
97
+ ok: true,
98
+ value: encodeRpcValue(value),
99
+ };
100
+ self.postMessage(res);
101
+ }
102
+ catch (err) {
103
+ if (disposed)
104
+ return;
105
+ const res = {
106
+ type: tspiceRpcResponseType,
107
+ id: req.id,
108
+ ok: false,
109
+ error: serializeError(err),
110
+ };
111
+ self.postMessage(res);
112
+ }
113
+ finally {
114
+ inFlight -= 1;
115
+ drain();
116
+ }
117
+ })();
118
+ };
119
+ const onMessage = (ev) => {
120
+ const msg = ev.data;
121
+ if (!msg || typeof msg.type !== "string")
122
+ return;
123
+ if (msg.type === tspiceRpcDisposeType) {
124
+ // Dispose is a one-way signal; ignore any further traffic.
125
+ if (disposed)
126
+ return;
127
+ disposed = true;
128
+ const queuedToFail = queued.slice(queuedHead);
129
+ clearQueue();
130
+ void (async () => {
131
+ try {
132
+ await failQueuedRequests(queuedToFail, new Error("Worker disposed"));
133
+ await opts.onDispose?.();
134
+ }
135
+ finally {
136
+ self.removeEventListener("message", onMessage);
137
+ const closeOnDispose = opts.closeOnDispose ?? true;
138
+ if (closeOnDispose) {
139
+ try {
140
+ self.close?.();
141
+ }
142
+ catch {
143
+ // ignore
144
+ }
145
+ }
146
+ }
147
+ })();
148
+ return;
149
+ }
150
+ // Ignore any further traffic after disposal (including in-flight requests
151
+ // that may finish later).
152
+ if (disposed)
153
+ return;
154
+ if (msg.type === tspiceRpcRequestType) {
155
+ const req = msg;
156
+ const id = req.id;
157
+ const op = req.op;
158
+ const args = req.args;
159
+ if (typeof id !== "number" || typeof op !== "string" || !Array.isArray(args)) {
160
+ return;
161
+ }
162
+ const queuedReq = { id, op, args };
163
+ if (inFlight < maxConcurrentRequests) {
164
+ runRequest(queuedReq);
165
+ }
166
+ else {
167
+ if (queuedSize() >= maxQueuedRequests) {
168
+ const res = {
169
+ type: tspiceRpcResponseType,
170
+ id,
171
+ ok: false,
172
+ error: serializeError(new Error(`Worker backpressure queue overflow (maxQueuedRequests=${maxQueuedRequests})`)),
173
+ };
174
+ self.postMessage(res);
175
+ return;
176
+ }
177
+ queued.push(queuedReq);
178
+ }
179
+ return;
180
+ }
181
+ };
182
+ self.addEventListener("message", onMessage);
183
+ const dispose = () => {
184
+ if (disposed)
185
+ return;
186
+ disposed = true;
187
+ const queuedToFail = queued.slice(queuedHead);
188
+ clearQueue();
189
+ void failQueuedRequests(queuedToFail, new Error("Worker disposed"));
190
+ // Note: this does not cancel any in-flight `transport.request()` calls; it
191
+ // just prevents any further responses from being posted.
192
+ self.removeEventListener("message", onMessage);
193
+ };
194
+ return { dispose };
195
+ }
196
+ //# sourceMappingURL=exposeTransportToWorker.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rybosome/tspice",
3
- "version": "0.0.3",
3
+ "version": "0.0.8",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "imports": {
@@ -23,9 +23,9 @@
23
23
  }
24
24
  },
25
25
  "optionalDependencies": {
26
- "@rybosome/tspice-native-darwin-arm64": "0.0.3",
27
- "@rybosome/tspice-native-darwin-x64": "0.0.3",
28
- "@rybosome/tspice-native-linux-x64-gnu": "0.0.3"
26
+ "@rybosome/tspice-native-darwin-arm64": "0.0.8",
27
+ "@rybosome/tspice-native-darwin-x64": "0.0.8",
28
+ "@rybosome/tspice-native-linux-x64-gnu": "0.0.8"
29
29
  },
30
30
  "files": [
31
31
  "dist",