@bquery/bquery 1.9.0 → 1.11.0

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 (216) hide show
  1. package/README.md +221 -40
  2. package/dist/{a11y-_9X-kt-_.js → a11y-DgUQ8-fI.js} +3 -3
  3. package/dist/{a11y-_9X-kt-_.js.map → a11y-DgUQ8-fI.js.map} +1 -1
  4. package/dist/a11y.es.mjs +1 -1
  5. package/dist/{component-L3-JfOFz.js → component-D8ydhe58.js} +20 -19
  6. package/dist/{component-L3-JfOFz.js.map → component-D8ydhe58.js.map} +1 -1
  7. package/dist/component.es.mjs +1 -1
  8. package/dist/concurrency/errors.d.ts +29 -0
  9. package/dist/concurrency/errors.d.ts.map +1 -0
  10. package/dist/concurrency/high-level.d.ts +85 -0
  11. package/dist/concurrency/high-level.d.ts.map +1 -0
  12. package/dist/concurrency/index.d.ts +19 -0
  13. package/dist/concurrency/index.d.ts.map +1 -0
  14. package/dist/concurrency/internal.d.ts +26 -0
  15. package/dist/concurrency/internal.d.ts.map +1 -0
  16. package/dist/concurrency/pipeline.d.ts +30 -0
  17. package/dist/concurrency/pipeline.d.ts.map +1 -0
  18. package/dist/concurrency/pool.d.ts +48 -0
  19. package/dist/concurrency/pool.d.ts.map +1 -0
  20. package/dist/concurrency/reactive.d.ts +107 -0
  21. package/dist/concurrency/reactive.d.ts.map +1 -0
  22. package/dist/concurrency/rpc.d.ts +46 -0
  23. package/dist/concurrency/rpc.d.ts.map +1 -0
  24. package/dist/concurrency/support.d.ts +23 -0
  25. package/dist/concurrency/support.d.ts.map +1 -0
  26. package/dist/concurrency/task.d.ts +31 -0
  27. package/dist/concurrency/task.d.ts.map +1 -0
  28. package/dist/concurrency/types.d.ts +343 -0
  29. package/dist/concurrency/types.d.ts.map +1 -0
  30. package/dist/concurrency-BU1wPEsZ.js +826 -0
  31. package/dist/concurrency-BU1wPEsZ.js.map +1 -0
  32. package/dist/concurrency.es.mjs +29 -0
  33. package/dist/{constraints-D5RHQLmP.js → constraints-Dlbx_m1b.js} +1 -1
  34. package/dist/{constraints-D5RHQLmP.js.map → constraints-Dlbx_m1b.js.map} +1 -1
  35. package/dist/core-CongXJuo.js +87 -0
  36. package/dist/core-CongXJuo.js.map +1 -0
  37. package/dist/{core-EMYSLzaT.js → core-tOP6QOrY.js} +2 -2
  38. package/dist/{core-EMYSLzaT.js.map → core-tOP6QOrY.js.map} +1 -1
  39. package/dist/core.es.mjs +1 -1
  40. package/dist/{custom-directives-Dr4C5lVV.js → custom-directives-5DlKqvd2.js} +1 -1
  41. package/dist/{custom-directives-Dr4C5lVV.js.map → custom-directives-5DlKqvd2.js.map} +1 -1
  42. package/dist/{devtools-BhB2iDPT.js → devtools-QosAqo0T.js} +2 -2
  43. package/dist/{devtools-BhB2iDPT.js.map → devtools-QosAqo0T.js.map} +1 -1
  44. package/dist/devtools.es.mjs +1 -1
  45. package/dist/{dnd-NwZBYh4l.js → dnd-d2OU4len.js} +1 -1
  46. package/dist/{dnd-NwZBYh4l.js.map → dnd-d2OU4len.js.map} +1 -1
  47. package/dist/dnd.es.mjs +1 -1
  48. package/dist/effect-Cc51IH91.js +87 -0
  49. package/dist/effect-Cc51IH91.js.map +1 -0
  50. package/dist/{env-CTdvLaH2.js → env-PvwYHnJq.js} +1 -1
  51. package/dist/{env-CTdvLaH2.js.map → env-PvwYHnJq.js.map} +1 -1
  52. package/dist/{forms-UhAeJEoO.js → forms-BLx4ZzT7.js} +41 -40
  53. package/dist/{forms-UhAeJEoO.js.map → forms-BLx4ZzT7.js.map} +1 -1
  54. package/dist/forms.es.mjs +1 -1
  55. package/dist/full.d.ts +6 -2
  56. package/dist/full.d.ts.map +1 -1
  57. package/dist/full.es.mjs +282 -214
  58. package/dist/full.iife.js +108 -20
  59. package/dist/full.iife.js.map +1 -1
  60. package/dist/full.umd.js +108 -20
  61. package/dist/full.umd.js.map +1 -1
  62. package/dist/{i18n-kuF6Ekj6.js → i18n--p7PM-9r.js} +3 -3
  63. package/dist/{i18n-kuF6Ekj6.js.map → i18n--p7PM-9r.js.map} +1 -1
  64. package/dist/i18n.es.mjs +1 -1
  65. package/dist/index.d.ts +2 -0
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.es.mjs +320 -252
  68. package/dist/match-CrZRVC4z.js +174 -0
  69. package/dist/match-CrZRVC4z.js.map +1 -0
  70. package/dist/media/observers.d.ts.map +1 -1
  71. package/dist/{media-D4zLj9t-.js → media-gjbWNq50.js} +3 -3
  72. package/dist/{media-D4zLj9t-.js.map → media-gjbWNq50.js.map} +1 -1
  73. package/dist/media.es.mjs +1 -1
  74. package/dist/{motion-BJsAuULb.js → motion-BBMso9Ir.js} +1 -1
  75. package/dist/{motion-BJsAuULb.js.map → motion-BBMso9Ir.js.map} +1 -1
  76. package/dist/motion.es.mjs +1 -1
  77. package/dist/{mount-B-JvH6Y0.js → mount-0A9qtcRJ.js} +11 -10
  78. package/dist/{mount-B-JvH6Y0.js.map → mount-0A9qtcRJ.js.map} +1 -1
  79. package/dist/{platform-Dw2gE3zI.js → platform-BPHIXbw8.js} +17 -16
  80. package/dist/{platform-Dw2gE3zI.js.map → platform-BPHIXbw8.js.map} +1 -1
  81. package/dist/platform.es.mjs +1 -1
  82. package/dist/{plugin-C2WuC8SF.js → plugin-SZEirbwq.js} +2 -2
  83. package/dist/{plugin-C2WuC8SF.js.map → plugin-SZEirbwq.js.map} +1 -1
  84. package/dist/plugin.es.mjs +1 -1
  85. package/dist/reactive/watch.d.ts.map +1 -1
  86. package/dist/reactive/websocket.d.ts +6 -3
  87. package/dist/reactive/websocket.d.ts.map +1 -1
  88. package/dist/{reactive-BjpLkclt.js → reactive-BAd2hfl8.js} +436 -449
  89. package/dist/reactive-BAd2hfl8.js.map +1 -0
  90. package/dist/reactive.es.mjs +42 -40
  91. package/dist/readonly-C0ZwS1Tf.js +35 -0
  92. package/dist/readonly-C0ZwS1Tf.js.map +1 -0
  93. package/dist/{registry-B08iilIh.js → registry-jpUQHf4E.js} +1 -1
  94. package/dist/{registry-B08iilIh.js.map → registry-jpUQHf4E.js.map} +1 -1
  95. package/dist/router-C4weu0QL.js +333 -0
  96. package/dist/router-C4weu0QL.js.map +1 -0
  97. package/dist/router.es.mjs +1 -1
  98. package/dist/{sanitize-B1V4JswB.js → sanitize-DOMkRO9G.js} +12 -7
  99. package/dist/{sanitize-B1V4JswB.js.map → sanitize-DOMkRO9G.js.map} +1 -1
  100. package/dist/security.es.mjs +1 -1
  101. package/dist/server/create-server.d.ts +25 -0
  102. package/dist/server/create-server.d.ts.map +1 -0
  103. package/dist/server/index.d.ts +11 -0
  104. package/dist/server/index.d.ts.map +1 -0
  105. package/dist/server/types.d.ts +396 -0
  106. package/dist/server/types.d.ts.map +1 -0
  107. package/dist/server-QdyKtCS1.js +349 -0
  108. package/dist/server-QdyKtCS1.js.map +1 -0
  109. package/dist/server.es.mjs +6 -0
  110. package/dist/ssr/adapters.d.ts +74 -0
  111. package/dist/ssr/adapters.d.ts.map +1 -0
  112. package/dist/ssr/async.d.ts +40 -0
  113. package/dist/ssr/async.d.ts.map +1 -0
  114. package/dist/ssr/config.d.ts +60 -0
  115. package/dist/ssr/config.d.ts.map +1 -0
  116. package/dist/ssr/context.d.ts +73 -0
  117. package/dist/ssr/context.d.ts.map +1 -0
  118. package/dist/ssr/defer-brand.d.ts +5 -0
  119. package/dist/ssr/defer-brand.d.ts.map +1 -0
  120. package/dist/ssr/escape.d.ts +17 -0
  121. package/dist/ssr/escape.d.ts.map +1 -0
  122. package/dist/ssr/expression.d.ts +44 -0
  123. package/dist/ssr/expression.d.ts.map +1 -0
  124. package/dist/ssr/hash.d.ts +39 -0
  125. package/dist/ssr/hash.d.ts.map +1 -0
  126. package/dist/ssr/head.d.ts +102 -0
  127. package/dist/ssr/head.d.ts.map +1 -0
  128. package/dist/ssr/html-parser.d.ts +58 -0
  129. package/dist/ssr/html-parser.d.ts.map +1 -0
  130. package/dist/ssr/index.d.ts +49 -43
  131. package/dist/ssr/index.d.ts.map +1 -1
  132. package/dist/ssr/mismatch.d.ts +60 -0
  133. package/dist/ssr/mismatch.d.ts.map +1 -0
  134. package/dist/ssr/render-async.d.ts +84 -0
  135. package/dist/ssr/render-async.d.ts.map +1 -0
  136. package/dist/ssr/render.d.ts.map +1 -1
  137. package/dist/ssr/renderer.d.ts +25 -0
  138. package/dist/ssr/renderer.d.ts.map +1 -0
  139. package/dist/ssr/resumability.d.ts +65 -0
  140. package/dist/ssr/resumability.d.ts.map +1 -0
  141. package/dist/ssr/router-bridge.d.ts +101 -0
  142. package/dist/ssr/router-bridge.d.ts.map +1 -0
  143. package/dist/ssr/runtime.d.ts +63 -0
  144. package/dist/ssr/runtime.d.ts.map +1 -0
  145. package/dist/ssr/serialize.d.ts.map +1 -1
  146. package/dist/ssr/store-snapshot.d.ts +87 -0
  147. package/dist/ssr/store-snapshot.d.ts.map +1 -0
  148. package/dist/ssr/strategies.d.ts +43 -0
  149. package/dist/ssr/strategies.d.ts.map +1 -0
  150. package/dist/ssr/suspense.d.ts +47 -0
  151. package/dist/ssr/suspense.d.ts.map +1 -0
  152. package/dist/ssr/types.d.ts +17 -0
  153. package/dist/ssr/types.d.ts.map +1 -1
  154. package/dist/ssr-Bt6BQA3J.js +2127 -0
  155. package/dist/ssr-Bt6BQA3J.js.map +1 -0
  156. package/dist/ssr.es.mjs +42 -7
  157. package/dist/{store-CY6sjTW3.js → store-DnXuu6Li.js} +6 -6
  158. package/dist/{store-CY6sjTW3.js.map → store-DnXuu6Li.js.map} +1 -1
  159. package/dist/store.es.mjs +2 -2
  160. package/dist/storybook.es.mjs +1 -1
  161. package/dist/{testing-UjAtu9aQ.js → testing-CeMUwrRD.js} +7 -7
  162. package/dist/{testing-UjAtu9aQ.js.map → testing-CeMUwrRD.js.map} +1 -1
  163. package/dist/testing.es.mjs +1 -1
  164. package/dist/{untrack-D0fnO5k2.js → untrack-bjWDNdyE.js} +11 -10
  165. package/dist/{untrack-D0fnO5k2.js.map → untrack-bjWDNdyE.js.map} +1 -1
  166. package/dist/view.es.mjs +12 -11
  167. package/package.json +24 -15
  168. package/src/concurrency/errors.ts +57 -0
  169. package/src/concurrency/high-level.ts +387 -0
  170. package/src/concurrency/index.ts +63 -0
  171. package/src/concurrency/internal.ts +100 -0
  172. package/src/concurrency/pipeline.ts +133 -0
  173. package/src/concurrency/pool.ts +450 -0
  174. package/src/concurrency/reactive.ts +339 -0
  175. package/src/concurrency/rpc.ts +380 -0
  176. package/src/concurrency/support.ts +44 -0
  177. package/src/concurrency/task.ts +318 -0
  178. package/src/concurrency/types.ts +431 -0
  179. package/src/full.ts +164 -0
  180. package/src/index.ts +6 -0
  181. package/src/media/observers.ts +5 -8
  182. package/src/reactive/watch.ts +10 -9
  183. package/src/reactive/websocket.ts +31 -8
  184. package/src/server/create-server.ts +754 -0
  185. package/src/server/index.ts +33 -0
  186. package/src/server/types.ts +490 -0
  187. package/src/ssr/adapters.ts +330 -0
  188. package/src/ssr/async.ts +125 -0
  189. package/src/ssr/config.ts +86 -0
  190. package/src/ssr/context.ts +245 -0
  191. package/src/ssr/defer-brand.ts +3 -0
  192. package/src/ssr/escape.ts +25 -0
  193. package/src/ssr/expression.ts +669 -0
  194. package/src/ssr/hash.ts +71 -0
  195. package/src/ssr/head.ts +240 -0
  196. package/src/ssr/html-parser.ts +387 -0
  197. package/src/ssr/index.ts +136 -43
  198. package/src/ssr/mismatch.ts +110 -0
  199. package/src/ssr/render-async.ts +286 -0
  200. package/src/ssr/render.ts +130 -59
  201. package/src/ssr/renderer.ts +453 -0
  202. package/src/ssr/resumability.ts +142 -0
  203. package/src/ssr/router-bridge.ts +177 -0
  204. package/src/ssr/runtime.ts +131 -0
  205. package/src/ssr/serialize.ts +1 -27
  206. package/src/ssr/store-snapshot.ts +209 -0
  207. package/src/ssr/strategies.ts +245 -0
  208. package/src/ssr/suspense.ts +504 -0
  209. package/src/ssr/types.ts +18 -0
  210. package/dist/core-DdtZHzsS.js +0 -168
  211. package/dist/core-DdtZHzsS.js.map +0 -1
  212. package/dist/reactive-BjpLkclt.js.map +0 -1
  213. package/dist/router-BieVwgci.js +0 -492
  214. package/dist/router-BieVwgci.js.map +0 -1
  215. package/dist/ssr-CrGSJySz.js +0 -248
  216. package/dist/ssr-CrGSJySz.js.map +0 -1
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Reactive wrappers around reusable concurrency primitives.
3
+ *
4
+ * @module bquery/concurrency
5
+ */
6
+
7
+ import { batch } from '../reactive/batch';
8
+ import { signal, type Signal } from '../reactive/core';
9
+ import { readonly } from '../reactive/readonly';
10
+ import { createRpcPool, createTaskPool } from './pool';
11
+ import { createRpcWorker } from './rpc';
12
+ import { createTaskWorker } from './task';
13
+ import type {
14
+ CreateRpcPoolOptions,
15
+ CreateRpcWorkerOptions,
16
+ CreateTaskPoolOptions,
17
+ CreateTaskWorkerOptions,
18
+ ReactiveRpcPool,
19
+ ReactiveRpcWorker,
20
+ ReactiveTaskPool,
21
+ ReactiveTaskWorker,
22
+ TaskPool,
23
+ TaskRunOptions,
24
+ TaskWorker,
25
+ TaskWorkerState,
26
+ WorkerRpcHandlers,
27
+ WorkerTaskHandler,
28
+ } from './types';
29
+
30
+ interface WorkerSignalMirror {
31
+ busy: Signal<boolean>;
32
+ state: Signal<TaskWorkerState>;
33
+ }
34
+
35
+ interface PoolSignalMirror extends WorkerSignalMirror {
36
+ concurrency: Signal<number>;
37
+ pending: Signal<number>;
38
+ size: Signal<number>;
39
+ }
40
+
41
+ type WorkerStateSource = Pick<TaskWorker<unknown, unknown>, 'busy' | 'state'>;
42
+ type PoolStateSource = Pick<
43
+ TaskPool<unknown, unknown>,
44
+ 'busy' | 'concurrency' | 'pending' | 'size' | 'state'
45
+ >;
46
+
47
+ const syncWorkerSignals = (source: WorkerStateSource, mirror: WorkerSignalMirror): void => {
48
+ batch(() => {
49
+ mirror.state.value = source.state;
50
+ mirror.busy.value = source.busy;
51
+ });
52
+ };
53
+
54
+ const syncPoolSignals = (source: PoolStateSource, mirror: PoolSignalMirror): void => {
55
+ batch(() => {
56
+ mirror.state.value = source.state;
57
+ mirror.busy.value = source.busy;
58
+ mirror.concurrency.value = source.concurrency;
59
+ mirror.pending.value = source.pending;
60
+ mirror.size.value = source.size;
61
+ });
62
+ };
63
+
64
+ const createWorkerSignalMirror = (source: WorkerStateSource): WorkerSignalMirror => {
65
+ return {
66
+ busy: signal(source.busy),
67
+ state: signal(source.state),
68
+ };
69
+ };
70
+
71
+ const createPoolSignalMirror = (source: PoolStateSource): PoolSignalMirror => {
72
+ return {
73
+ busy: signal(source.busy),
74
+ concurrency: signal(source.concurrency),
75
+ pending: signal(source.pending),
76
+ size: signal(source.size),
77
+ state: signal(source.state),
78
+ };
79
+ };
80
+
81
+ const attachRunSync = <TResult>(run: Promise<TResult>, sync: () => void): Promise<TResult> => {
82
+ sync();
83
+ return run.finally(() => {
84
+ sync();
85
+ queueMicrotask(sync);
86
+ });
87
+ };
88
+
89
+ /**
90
+ * Creates a reactive wrapper around a reusable task worker.
91
+ *
92
+ * The returned wrapper preserves the standard `run()` / `terminate()` API and
93
+ * adds readonly signals such as `state$` and `busy$` for UI bindings.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * import { createReactiveTaskWorker } from '@bquery/bquery/concurrency';
98
+ * import { effect } from '@bquery/bquery/reactive';
99
+ *
100
+ * const worker = createReactiveTaskWorker((value: number) => value * 2);
101
+ *
102
+ * effect(() => {
103
+ * console.log(worker.state$.value, worker.busy$.value);
104
+ * });
105
+ *
106
+ * await worker.run(21);
107
+ * worker.terminate();
108
+ * ```
109
+ */
110
+ export function createReactiveTaskWorker<TInput = void, TResult = unknown>(
111
+ handler: WorkerTaskHandler<TInput, TResult>,
112
+ options: CreateTaskWorkerOptions = {}
113
+ ): ReactiveTaskWorker<TInput, TResult> {
114
+ const worker = createTaskWorker(handler, options);
115
+ const mirror = createWorkerSignalMirror(worker as WorkerStateSource);
116
+ const sync = (): void => {
117
+ syncWorkerSignals(worker as WorkerStateSource, mirror);
118
+ };
119
+
120
+ return {
121
+ get busy(): boolean {
122
+ return worker.busy;
123
+ },
124
+ get state(): TaskWorkerState {
125
+ return worker.state;
126
+ },
127
+ busy$: readonly(mirror.busy),
128
+ state$: readonly(mirror.state),
129
+ run(input: TInput, runOptions: TaskRunOptions = {}): Promise<TResult> {
130
+ return attachRunSync(worker.run(input, runOptions), sync);
131
+ },
132
+ terminate(): void {
133
+ worker.terminate();
134
+ sync();
135
+ },
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Creates a reactive wrapper around a reusable RPC worker.
141
+ *
142
+ * The returned wrapper preserves the standard `call()` / `terminate()` API and
143
+ * adds readonly signals such as `state$` and `busy$` for UI bindings.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * import { createReactiveRpcWorker } from '@bquery/bquery/concurrency';
148
+ * import { effect } from '@bquery/bquery/reactive';
149
+ *
150
+ * const rpc = createReactiveRpcWorker({
151
+ * sum: ({ values }: { values: number[] }) => values.reduce((total, value) => total + value, 0),
152
+ * });
153
+ *
154
+ * effect(() => {
155
+ * console.log(rpc.state$.value, rpc.busy$.value);
156
+ * });
157
+ *
158
+ * await rpc.call('sum', { values: [1, 2, 3] });
159
+ * rpc.terminate();
160
+ * ```
161
+ */
162
+ export function createReactiveRpcWorker<TRoutes extends WorkerRpcHandlers>(
163
+ handlers: TRoutes,
164
+ options: CreateRpcWorkerOptions = {}
165
+ ): ReactiveRpcWorker<TRoutes> {
166
+ const worker = createRpcWorker(handlers, options);
167
+ const mirror = createWorkerSignalMirror(worker as WorkerStateSource);
168
+ const sync = (): void => {
169
+ syncWorkerSignals(worker as WorkerStateSource, mirror);
170
+ };
171
+
172
+ return {
173
+ get busy(): boolean {
174
+ return worker.busy;
175
+ },
176
+ get state(): TaskWorkerState {
177
+ return worker.state;
178
+ },
179
+ busy$: readonly(mirror.busy),
180
+ state$: readonly(mirror.state),
181
+ call<TMethod extends keyof TRoutes & string>(
182
+ method: TMethod,
183
+ input: Parameters<TRoutes[TMethod]>[0],
184
+ runOptions: TaskRunOptions = {}
185
+ ): Promise<Awaited<ReturnType<TRoutes[TMethod]>>> {
186
+ return attachRunSync(worker.call(method, input, runOptions), sync);
187
+ },
188
+ terminate(): void {
189
+ worker.terminate();
190
+ sync();
191
+ },
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Creates a reactive wrapper around a reusable task pool.
197
+ *
198
+ * The returned wrapper preserves the standard `run()` / `clear()` /
199
+ * `terminate()` API and adds readonly signals for pool state and queue load.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * import { createReactiveTaskPool } from '@bquery/bquery/concurrency';
204
+ * import { effect } from '@bquery/bquery/reactive';
205
+ *
206
+ * const pool = createReactiveTaskPool((value: number) => value * 2, { concurrency: 2 });
207
+ *
208
+ * effect(() => {
209
+ * console.log(pool.pending$.value, pool.size$.value, pool.state$.value);
210
+ * });
211
+ *
212
+ * await Promise.all([pool.run(1), pool.run(2), pool.run(3)]);
213
+ * pool.terminate();
214
+ * ```
215
+ */
216
+ export function createReactiveTaskPool<TInput = void, TResult = unknown>(
217
+ handler: WorkerTaskHandler<TInput, TResult>,
218
+ options: CreateTaskPoolOptions = {}
219
+ ): ReactiveTaskPool<TInput, TResult> {
220
+ const pool = createTaskPool(handler, options);
221
+ const mirror = createPoolSignalMirror(pool as PoolStateSource);
222
+ const sync = (): void => {
223
+ syncPoolSignals(pool as PoolStateSource, mirror);
224
+ };
225
+
226
+ return {
227
+ get busy(): boolean {
228
+ return pool.busy;
229
+ },
230
+ get concurrency(): number {
231
+ return pool.concurrency;
232
+ },
233
+ get pending(): number {
234
+ return pool.pending;
235
+ },
236
+ get size(): number {
237
+ return pool.size;
238
+ },
239
+ get state(): TaskWorkerState {
240
+ return pool.state;
241
+ },
242
+ busy$: readonly(mirror.busy),
243
+ concurrency$: readonly(mirror.concurrency),
244
+ pending$: readonly(mirror.pending),
245
+ size$: readonly(mirror.size),
246
+ state$: readonly(mirror.state),
247
+ run(input: TInput, runOptions: TaskRunOptions = {}): Promise<TResult> {
248
+ return attachRunSync(pool.run(input, runOptions), sync);
249
+ },
250
+ clear(): void {
251
+ pool.clear();
252
+ sync();
253
+ },
254
+ terminate(): void {
255
+ pool.terminate();
256
+ sync();
257
+ },
258
+ };
259
+ }
260
+
261
+ /**
262
+ * Creates a reactive wrapper around a reusable RPC pool.
263
+ *
264
+ * The returned wrapper preserves the standard `call()` / `clear()` /
265
+ * `terminate()` API and adds readonly signals for pool state and queue load.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * import { createReactiveRpcPool } from '@bquery/bquery/concurrency';
270
+ * import { effect } from '@bquery/bquery/reactive';
271
+ *
272
+ * const pool = createReactiveRpcPool(
273
+ * {
274
+ * sum: ({ values }: { values: number[] }) => values.reduce((total, value) => total + value, 0),
275
+ * },
276
+ * { concurrency: 2 }
277
+ * );
278
+ *
279
+ * effect(() => {
280
+ * console.log(pool.pending$.value, pool.size$.value, pool.state$.value);
281
+ * });
282
+ *
283
+ * await Promise.all([
284
+ * pool.call('sum', { values: [1, 2] }),
285
+ * pool.call('sum', { values: [3, 4] }),
286
+ * pool.call('sum', { values: [5, 6] }),
287
+ * ]);
288
+ *
289
+ * pool.terminate();
290
+ * ```
291
+ */
292
+ export function createReactiveRpcPool<TRoutes extends WorkerRpcHandlers>(
293
+ handlers: TRoutes,
294
+ options: CreateRpcPoolOptions = {}
295
+ ): ReactiveRpcPool<TRoutes> {
296
+ const pool = createRpcPool(handlers, options);
297
+ const mirror = createPoolSignalMirror(pool as PoolStateSource);
298
+ const sync = (): void => {
299
+ syncPoolSignals(pool as PoolStateSource, mirror);
300
+ };
301
+
302
+ return {
303
+ get busy(): boolean {
304
+ return pool.busy;
305
+ },
306
+ get concurrency(): number {
307
+ return pool.concurrency;
308
+ },
309
+ get pending(): number {
310
+ return pool.pending;
311
+ },
312
+ get size(): number {
313
+ return pool.size;
314
+ },
315
+ get state(): TaskWorkerState {
316
+ return pool.state;
317
+ },
318
+ busy$: readonly(mirror.busy),
319
+ concurrency$: readonly(mirror.concurrency),
320
+ pending$: readonly(mirror.pending),
321
+ size$: readonly(mirror.size),
322
+ state$: readonly(mirror.state),
323
+ call<TMethod extends keyof TRoutes & string>(
324
+ method: TMethod,
325
+ input: Parameters<TRoutes[TMethod]>[0],
326
+ runOptions: TaskRunOptions = {}
327
+ ): Promise<Awaited<ReturnType<TRoutes[TMethod]>>> {
328
+ return attachRunSync(pool.call(method, input, runOptions), sync);
329
+ },
330
+ clear(): void {
331
+ pool.clear();
332
+ sync();
333
+ },
334
+ terminate(): void {
335
+ pool.terminate();
336
+ sync();
337
+ },
338
+ };
339
+ }
@@ -0,0 +1,380 @@
1
+ /**
2
+ * RPC-style worker communication helpers.
3
+ *
4
+ * @module bquery/concurrency
5
+ */
6
+
7
+ import {
8
+ TaskWorkerAbortError,
9
+ TaskWorkerError,
10
+ TaskWorkerSerializationError,
11
+ TaskWorkerTimeoutError,
12
+ TaskWorkerUnsupportedError,
13
+ } from './errors';
14
+ import {
15
+ createWorkerInstance,
16
+ normalizeTimeout,
17
+ restoreWorkerError,
18
+ validateTaskHandler,
19
+ type SerializedWorkerError,
20
+ } from './internal';
21
+ import { isConcurrencySupported } from './support';
22
+ import type {
23
+ CallWorkerMethodOptions,
24
+ CreateRpcWorkerOptions,
25
+ RpcWorker,
26
+ TaskRunOptions,
27
+ TaskWorkerState,
28
+ WorkerRpcHandlers,
29
+ } from './types';
30
+
31
+ interface WorkerSuccessMessage<TResult> {
32
+ id: number;
33
+ result: TResult;
34
+ type: 'bq:result';
35
+ }
36
+
37
+ interface WorkerErrorMessage {
38
+ error: SerializedWorkerError;
39
+ id: number;
40
+ type: 'bq:error';
41
+ }
42
+
43
+ type WorkerResponse<TResult> = WorkerSuccessMessage<TResult> | WorkerErrorMessage;
44
+
45
+ interface PendingRun<TResult> {
46
+ abortHandler?: () => void;
47
+ id: number;
48
+ reject: (reason?: unknown) => void;
49
+ resolve: (value: TResult | PromiseLike<TResult>) => void;
50
+ timeoutId?: ReturnType<typeof setTimeout>;
51
+ }
52
+
53
+ const WORKER_RPC_MESSAGE = 'bq:rpc';
54
+
55
+ const validateRpcHandlers = <TRoutes extends WorkerRpcHandlers>(
56
+ handlers: TRoutes
57
+ ): Array<[keyof TRoutes & string, string]> => {
58
+ const methodNames = Object.keys(handlers) as Array<keyof TRoutes & string>;
59
+
60
+ if (methodNames.length === 0) {
61
+ throw new TaskWorkerSerializationError(
62
+ 'RPC workers require at least one standalone method handler.'
63
+ );
64
+ }
65
+
66
+ return methodNames.map((method) => {
67
+ const handler = handlers[method];
68
+ if (typeof handler !== 'function') {
69
+ throw new TaskWorkerSerializationError(
70
+ `RPC handler "${method}" must be a standalone function.`
71
+ );
72
+ }
73
+
74
+ return [method, validateTaskHandler(handler)];
75
+ });
76
+ };
77
+
78
+ const createRpcWorkerScript = (handlerSources: Array<[string, string]>): string => {
79
+ const assignments = handlerSources
80
+ .map(([method, source]) => `handlers[${JSON.stringify(method)}] = (${source});`)
81
+ .join('\n');
82
+
83
+ return `'use strict';
84
+ const serializeError = (error) => {
85
+ if (error && typeof error === 'object') {
86
+ return {
87
+ code: typeof error.code === 'string' ? error.code : undefined,
88
+ message: typeof error.message === 'string' ? error.message : 'Worker RPC call failed.',
89
+ name: typeof error.name === 'string' ? error.name : 'Error',
90
+ stack: typeof error.stack === 'string' ? error.stack : undefined,
91
+ };
92
+ }
93
+
94
+ return {
95
+ message: typeof error === 'string' ? error : 'Worker RPC call failed.',
96
+ name: 'Error',
97
+ };
98
+ };
99
+
100
+ const handlers = Object.create(null);
101
+ ${assignments}
102
+
103
+ const hasOwn = Object.prototype.hasOwnProperty;
104
+
105
+ self.onmessage = async (event) => {
106
+ const message = event.data;
107
+ if (!message || message.type !== '${WORKER_RPC_MESSAGE}') {
108
+ return;
109
+ }
110
+
111
+ const method = typeof message.method === 'string' ? message.method : '';
112
+ if (!hasOwn.call(handlers, method)) {
113
+ self.postMessage({
114
+ error: {
115
+ code: 'METHOD_NOT_FOUND',
116
+ message: 'Unknown RPC method "' + String(method) + '".',
117
+ name: 'TaskWorkerError',
118
+ },
119
+ id: message.id,
120
+ type: 'bq:error',
121
+ });
122
+ return;
123
+ }
124
+
125
+ try {
126
+ const result = await handlers[method](message.payload);
127
+ self.postMessage({ id: message.id, result, type: 'bq:result' });
128
+ } catch (error) {
129
+ self.postMessage({ error: serializeError(error), id: message.id, type: 'bq:error' });
130
+ }
131
+ };`;
132
+ };
133
+
134
+ /**
135
+ * Creates a reusable RPC-style worker with explicit named method dispatch.
136
+ *
137
+ * The worker processes one request at a time to keep lifecycle, timeout, abort,
138
+ * and cleanup semantics aligned with the minimal Milestone 1 task API.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * import { createRpcWorker } from '@bquery/bquery/concurrency';
143
+ *
144
+ * const rpc = createRpcWorker({
145
+ * sum: ({ values }: { values: number[] }) => values.reduce((total, value) => total + value, 0),
146
+ * double: (value: number) => value * 2,
147
+ * });
148
+ *
149
+ * const total = await rpc.call('sum', { values: [1, 2, 3] });
150
+ * rpc.terminate();
151
+ * ```
152
+ */
153
+ export function createRpcWorker<TRoutes extends WorkerRpcHandlers>(
154
+ handlers: TRoutes,
155
+ options: CreateRpcWorkerOptions = {}
156
+ ): RpcWorker<TRoutes> {
157
+ if (!isConcurrencySupported()) {
158
+ throw new TaskWorkerUnsupportedError();
159
+ }
160
+
161
+ const handlerSources = validateRpcHandlers(handlers);
162
+ const scriptSource = createRpcWorkerScript(handlerSources);
163
+ const defaultTimeout = normalizeTimeout(options.timeout);
164
+ let disposed = false;
165
+ let worker: Worker | null = null;
166
+ let pending: PendingRun<unknown> | null = null;
167
+ let nextRunId = 0;
168
+ let pendingAbortSignal: AbortSignal | undefined;
169
+
170
+ const cleanupPending = (): void => {
171
+ if (!pending) {
172
+ return;
173
+ }
174
+
175
+ if (pending.timeoutId !== undefined) {
176
+ clearTimeout(pending.timeoutId);
177
+ }
178
+
179
+ if (pending.abortHandler) {
180
+ pendingAbortSignal?.removeEventListener('abort', pending.abortHandler);
181
+ }
182
+
183
+ pending = null;
184
+ pendingAbortSignal = undefined;
185
+ };
186
+
187
+ const detachWorker = (): void => {
188
+ if (!worker) {
189
+ return;
190
+ }
191
+
192
+ worker.onmessage = null;
193
+ worker.onerror = null;
194
+ worker.terminate();
195
+ worker = null;
196
+ };
197
+
198
+ const rejectPending = (error: Error): void => {
199
+ if (!pending) {
200
+ return;
201
+ }
202
+
203
+ const current = pending;
204
+ cleanupPending();
205
+ current.reject(error);
206
+ };
207
+
208
+ const ensureWorker = (): Worker => {
209
+ if (disposed) {
210
+ throw new TaskWorkerError('The RPC worker has already been terminated.', 'TERMINATED');
211
+ }
212
+
213
+ if (worker) {
214
+ return worker;
215
+ }
216
+
217
+ const instance = createWorkerInstance(scriptSource, options.name);
218
+ instance.onmessage = (event: MessageEvent<WorkerResponse<unknown>>) => {
219
+ const current = pending;
220
+ if (!current) {
221
+ return;
222
+ }
223
+
224
+ const message = event.data;
225
+ if (!message || message.id !== current.id) {
226
+ return;
227
+ }
228
+
229
+ cleanupPending();
230
+
231
+ if (message.type === 'bq:error') {
232
+ current.reject(restoreWorkerError(message.error));
233
+ return;
234
+ }
235
+
236
+ current.resolve(message.result);
237
+ };
238
+
239
+ instance.onerror = (event: ErrorEvent) => {
240
+ const error = new TaskWorkerError(event.message || 'Worker RPC execution failed.', 'WORKER');
241
+ detachWorker();
242
+ rejectPending(error);
243
+ };
244
+
245
+ worker = instance;
246
+ return instance;
247
+ };
248
+
249
+ const resetAfterInterruptedRun = (error: Error): void => {
250
+ detachWorker();
251
+ rejectPending(error);
252
+ };
253
+
254
+ return {
255
+ get busy(): boolean {
256
+ return pending !== null;
257
+ },
258
+ get state(): TaskWorkerState {
259
+ if (disposed) {
260
+ return 'terminated';
261
+ }
262
+
263
+ return pending ? 'running' : 'idle';
264
+ },
265
+ call<TMethod extends keyof TRoutes & string>(
266
+ method: TMethod,
267
+ input: Parameters<TRoutes[TMethod]>[0],
268
+ runOptions: TaskRunOptions = {}
269
+ ): Promise<Awaited<ReturnType<TRoutes[TMethod]>>> {
270
+ if (disposed) {
271
+ return Promise.reject(
272
+ new TaskWorkerError('The RPC worker has already been terminated.', 'TERMINATED')
273
+ );
274
+ }
275
+
276
+ if (pending) {
277
+ return Promise.reject(
278
+ new TaskWorkerError(
279
+ 'This RPC worker is already processing a request. Wait for the current call to finish or create another worker.',
280
+ 'BUSY'
281
+ )
282
+ );
283
+ }
284
+
285
+ if (runOptions.signal?.aborted) {
286
+ return Promise.reject(new TaskWorkerAbortError());
287
+ }
288
+
289
+ const activeWorker = ensureWorker();
290
+ const timeout = normalizeTimeout(runOptions.timeout) ?? defaultTimeout;
291
+ const runId = nextRunId++;
292
+
293
+ return new Promise<Awaited<ReturnType<TRoutes[TMethod]>>>((resolve, reject) => {
294
+ const current: PendingRun<Awaited<ReturnType<TRoutes[TMethod]>>> = {
295
+ id: runId,
296
+ reject,
297
+ resolve,
298
+ };
299
+
300
+ if (runOptions.signal) {
301
+ current.abortHandler = () => {
302
+ resetAfterInterruptedRun(new TaskWorkerAbortError());
303
+ };
304
+ pendingAbortSignal = runOptions.signal;
305
+ runOptions.signal.addEventListener('abort', current.abortHandler, { once: true });
306
+ }
307
+
308
+ if (timeout !== undefined) {
309
+ current.timeoutId = setTimeout(() => {
310
+ resetAfterInterruptedRun(
311
+ new TaskWorkerTimeoutError(`Worker RPC call exceeded the timeout of ${timeout}ms.`)
312
+ );
313
+ }, timeout);
314
+ }
315
+
316
+ pending = current as PendingRun<unknown>;
317
+
318
+ try {
319
+ activeWorker.postMessage(
320
+ { id: runId, method, payload: input, type: WORKER_RPC_MESSAGE },
321
+ runOptions.transfer ?? []
322
+ );
323
+ } catch (error) {
324
+ detachWorker();
325
+ rejectPending(
326
+ new TaskWorkerSerializationError(
327
+ 'Failed to serialize the RPC payload or transfer list for worker execution.',
328
+ error
329
+ )
330
+ );
331
+ }
332
+ });
333
+ },
334
+ terminate(): void {
335
+ if (disposed) {
336
+ return;
337
+ }
338
+
339
+ disposed = true;
340
+ detachWorker();
341
+ rejectPending(new TaskWorkerError('The RPC worker was terminated.', 'TERMINATED'));
342
+ },
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Executes a single named RPC method in a fresh worker and tears it down after
348
+ * the response is received.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * import { callWorkerMethod } from '@bquery/bquery/concurrency';
353
+ *
354
+ * const total = await callWorkerMethod(
355
+ * {
356
+ * sum: ({ values }: { values: number[] }) =>
357
+ * values.reduce((result, value) => result + value, 0),
358
+ * },
359
+ * 'sum',
360
+ * { values: [1, 2, 3] }
361
+ * );
362
+ * ```
363
+ */
364
+ export async function callWorkerMethod<
365
+ TRoutes extends WorkerRpcHandlers,
366
+ TMethod extends keyof TRoutes & string,
367
+ >(
368
+ handlers: TRoutes,
369
+ method: TMethod,
370
+ input: Parameters<TRoutes[TMethod]>[0],
371
+ options: CallWorkerMethodOptions = {}
372
+ ): Promise<Awaited<ReturnType<TRoutes[TMethod]>>> {
373
+ const worker = createRpcWorker(handlers, options);
374
+
375
+ try {
376
+ return await worker.call(method, input, options);
377
+ } finally {
378
+ worker.terminate();
379
+ }
380
+ }