knitting 0.1.70 → 0.1.73

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/README.md +101 -10
  2. package/knitting.browser.d.ts +3 -1
  3. package/knitting.browser.js +1 -1
  4. package/knitting.d.ts +3 -1
  5. package/knitting.js +2 -1
  6. package/package.json +8 -3
  7. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  8. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  9. package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
  10. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  11. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  12. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
  14. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  15. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  16. package/src/api.js +58 -34
  17. package/src/connections/node-addons.js +11 -1
  18. package/src/debug/gate.js +1 -1
  19. package/src/debug/handle.d.ts +6 -1
  20. package/src/debug/handle.js +14 -6
  21. package/src/error.d.ts +9 -0
  22. package/src/error.js +16 -2
  23. package/src/memory/lock.d.ts +48 -6
  24. package/src/memory/lock.js +350 -142
  25. package/src/memory/payloadCodec.js +31 -11
  26. package/src/permission/protocol.d.ts +1 -0
  27. package/src/permission/protocol.js +8 -3
  28. package/src/runtime/dispatcher.d.ts +6 -1
  29. package/src/runtime/dispatcher.js +24 -8
  30. package/src/runtime/inline-executor.js +2 -1
  31. package/src/runtime/pool.d.ts +13 -4
  32. package/src/runtime/pool.js +107 -47
  33. package/src/runtime/process-worker.js +10 -1
  34. package/src/runtime/tx-queue.d.ts +2 -1
  35. package/src/runtime/tx-queue.js +11 -0
  36. package/src/runtime/worker-common.d.ts +7 -0
  37. package/src/runtime/worker-common.js +28 -2
  38. package/src/types.d.ts +35 -11
  39. package/src/worker/loop.js +17 -4
  40. package/src/worker/safety/index.d.ts +1 -1
  41. package/src/worker/safety/index.js +1 -1
  42. package/src/worker/safety/process.d.ts +2 -0
  43. package/src/worker/safety/process.js +8 -1
  44. package/src/worker/safety/startup.js +11 -6
  45. package/src/worker/timers.js +25 -3
package/README.md CHANGED
@@ -104,6 +104,9 @@ if (isMain) {
104
104
  }
105
105
  ```
106
106
 
107
+ The examples use `using`, which Node.js 22 cannot parse: there, write
108
+ `const pool = ...` and call `await pool.shutdown()` when you are done.
109
+
107
110
  Use the `isMain` guard when a module can be loaded by both the host and its
108
111
  workers. Export tasks at module scope so Knitting can find them, then create and
109
112
  use the pool only from the main program.
@@ -153,8 +156,8 @@ if (isMain) {
153
156
  ```
154
157
 
155
158
  `using` starts pool shutdown when the scope exits and does not wait for it. Use
156
- `await pool.shutdown()` when you need to wait for shutdown or pass a shutdown
157
- delay.
159
+ `await using pool = ...` or `await pool.shutdown()` when you need to wait for
160
+ shutdown; only `shutdown()` takes a shutdown delay.
158
161
 
159
162
  Deno 2+, Bun 1+, and Node.js 24+ parse `using` natively. Node.js 22 does not: it
160
163
  has `Symbol.dispose`, but the declaration itself is a `SyntaxError`, and Node's
@@ -474,7 +477,11 @@ Common options you might tweak:
474
477
  | `permission` | Runtime permission policy for workers. |
475
478
  | `host.dispatcher` | Experimental host dispatcher topology: `"per-thread"` or `"serial-channel"`. |
476
479
  | `host.steal` | Shared-submit work stealing for compatible multi-worker thread/process pools; enabled by default. Set `false` to use private submit lanes. |
477
- | `debug` | Enable diagnostics (`host`, `globals`, `signals`, `imports`, `lifecycle`) or use `KNITTING_DEBUG`. |
480
+ | `host.stealRegionLanes` | Submit lanes claimed per stealing handshake (a power of two). Smaller regions are fairer for expensive tasks; wider regions amortise arbitration for cheap ones. |
481
+ | `host.stealClaim` | Claim discipline: `"ticket"` (default) or `"dekker"`. Also settable with `KNITTING_STEAL_CLAIM`; an unrecognised value is rejected. |
482
+ | `host.doorbell` | Wait for completion notifications instead of polling an empty return mailbox; enabled by default where supported. Set `false` to force polling. |
483
+ | `host.nativeDoorbell` | Opt into Node's native `uv_async_t` completion bridge for thread workers. Off by default; ignored when `host.doorbell` is `false`. |
484
+ | `debug` | Enable diagnostics (`host`, `globals`, `signals`, `imports`, `lifecycle`, `steal`) or use `KNITTING_DEBUG`. |
478
485
  | `source` | Worker source override for advanced runtimes. |
479
486
 
480
487
  ### Worker bootstrap
@@ -530,6 +537,65 @@ protocol limit also fall back. Set `host: { steal: false }` or
530
537
  where arbitration has nothing to rebalance. `host: { steal: true }` or
531
538
  `KNITTING_STEAL=1` forces it for an otherwise compatible pool.
532
539
 
540
+ Two options tune the arbitration itself, and both only apply to a stealing pool:
541
+
542
+ - `host.stealRegionLanes` is how many submit lanes one handshake claims (a power
543
+ of two). **A region is a batch**: a wide region amortises arbitration best for
544
+ cheap tasks, but it also lets one worker claim work the others could have run
545
+ in parallel. Set it to `1` (or a small value) when per-task cost dominates
546
+ arbitration cost. The default is the widest region the lane budget allows.
547
+ - `host.stealClaim` selects the claim discipline: `"ticket"` (the default)
548
+ claims in publication order from one monotonic counter; `"dekker"` gives each
549
+ consumer its own intent slot and requires at least one region per consumer.
550
+ Set `KNITTING_STEAL_CLAIM=ticket` or `KNITTING_STEAL_CLAIM=dekker` to select
551
+ it from the environment; the explicit option wins. **An unrecognised value is
552
+ an error, not a fallback**: a typo, or a `cas-mask` setting left over from
553
+ when that discipline existed, fails at pool creation rather than quietly
554
+ running a discipline you did not choose.
555
+
556
+ The ticket discipline uses a 64-bit claim head and a wrapping 32-bit
557
+ publication tail. The head CAS validates the tail snapshot: at most 32 tickets
558
+ can be pending, so unsigned subtraction recovers the distance across a tail wrap
559
+ without ever recycling a claim identity. `stealRegionLanes` caps the number of
560
+ tickets one claim takes, and its default width is unchanged from Dekker's.
561
+ Claims are ordered, but workers may decode or complete later claims first.
562
+ Ticket identities never wrap: the queue fails closed if a claim would exhaust
563
+ the signed 64-bit sequence.
564
+
565
+ Ticket is the default while it is under evaluation, so ordinary runs exercise
566
+ it; `"dekker"` remains explicitly selectable and is unchanged. The two differ in
567
+ how they behave after a fatal fault. Dekker releases the region and lets a peer
568
+ finish the rest. **Ticket fails closed**: a fatal decoder or worker failure
569
+ closes the shared queue, rejects outstanding calls, and rejects every future
570
+ call, so that pool has to be shut down and replaced. Claimed work is not
571
+ replayed, because a failed decoder may already have consumed payload state.
572
+ Ordinary task promise rejections stay task-local under both.
573
+
574
+ ### Completion doorbells
575
+
576
+ By default the host waits to be told a result is ready instead of repeatedly
577
+ polling an empty return mailbox. `host.doorbell` is enabled wherever a wake path
578
+ exists, and each runtime uses the one it has:
579
+
580
+ - Node and Bun thread workers use `Atomics.waitAsync`.
581
+ - Deno uses a thread-safe FFI callback, because its `waitAsync` does not wake an
582
+ idle event loop. It is skipped when FFI permission is unavailable.
583
+ - Process workers use a process-local completion transport, since Atomics
584
+ waiters are per-isolate and cannot be rung from another process.
585
+ - Anything unsupported or denied falls back to the portable polling path.
586
+
587
+ `host.nativeDoorbell: true` additionally opts Node thread workers into the
588
+ native `uv_async_t` bridge from the `knitting_doorbell` addon. It is off by
589
+ default, does not apply to process workers, and is ignored entirely when
590
+ `host.doorbell` is `false`.
591
+ Node thread permissions must allow native addons for this bridge; otherwise
592
+ Knitting uses the portable wake path.
593
+
594
+ Set `host: { doorbell: false }` to force polling — useful for controlled
595
+ comparisons, and for pools that oversubscribe the machine. A doorbell only makes
596
+ progress when the host gets scheduled, so once workers occupy every core a wake
597
+ has to preempt one.
598
+
533
599
  ### Useful tuning options
534
600
 
535
601
  - Increase `threads` for parallel CPU-heavy work.
@@ -795,7 +861,8 @@ const pool = createPool({
795
861
 
796
862
  ## Permissions
797
863
 
798
- Knitting defaults to a strict worker permission policy:
864
+ Knitting defaults to a strict worker permission policy where the selected
865
+ runtime supports it:
799
866
 
800
867
  ```ts
801
868
  permission: { mode: "strict", allowImport: true }
@@ -853,16 +920,33 @@ backward compatible and produce a once-per-runtime warning.
853
920
  cannot be represented. When a wrapper or cross-runtime host hides the target
854
921
  Node version, Knitting uses the conservative Node 22/24 capability set.
855
922
 
856
- These compatibility checks currently cover process workers. Thread workers use
857
- the host runtime's worker behavior and should not be treated as a sandbox.
858
- Runtime permissions are guardrails, not the only security boundary for hostile
859
- code.
923
+ These compatibility checks currently cover process workers. Node thread
924
+ workers receive their resolved Node permission flags, and Knitting fails pool
925
+ creation if Node cannot apply them. Deno thread workers inherit the creator's
926
+ permissions because Knitting does not yet set Deno's worker-specific permission
927
+ options, which are unstable and gated by `--unstable-worker-options`; Bun
928
+ thread workers do not have a matching permission mechanism here.
929
+ For cross-runtime permission enforcement, use a process worker with a runtime
930
+ that supports the restrictions you need; Deno has the broadest coverage in the
931
+ table above. Runtime permissions are guardrails, not the only security boundary
932
+ for hostile code.
860
933
 
861
934
  The top-level `ffi` permission is the explicit cross-runtime native-code
862
935
  capability. On Node it enables both native addons and `node:ffi`; Node's
863
936
  `--allow-ffi` permission is currently unrestricted. The legacy/runtime-specific
864
937
  `node.allowAddons` and `node.allowFfi` switches are independent—enabling addons
865
- does not silently enable FFI.
938
+ does not silently enable FFI. Node thread workers deny addon loading by default
939
+ under the strict policy. Set `permission.node.allowAddons: true` when a task
940
+ needs an addon-backed feature on addon-backed Node versions: `SharedArrayBuffer`
941
+ arguments, returns and `Envelope` bodies, `ProcessSharedBuffer`, and
942
+ `BufferReference`. Node 26 maps the same pointers through `node:ffi`, so there
943
+ the switch is `permission.node.allowFfi`; top-level `ffi: true` grants both.
944
+ Without it a Node thread worker cannot map those pointers: the worker crashes
945
+ and later calls on it fail. Either switch lets task code load native code
946
+ generally, so use it only for trusted tasks. Two paths degrade instead of
947
+ failing when addon loading is denied: large returns are copied rather than
948
+ moved, and the optional native completion doorbell falls back to the portable
949
+ wake path.
866
950
 
867
951
  Node process workers are a transport exception: Knitting needs `--allow-addons`
868
952
  on Node 22/24 or `--allow-ffi` on Node 26 to map their shared memory. Deno
@@ -883,6 +967,13 @@ Knitting aims to make the safer path the default:
883
967
  - Shutdown can stop immediately or wait for submitted work with
884
968
  `worker.resolveAfterFinishingAll`.
885
969
 
970
+ When Knitting itself rejects a call, the reason is a `KnittingError` with a
971
+ `code`: `KNT_ERROR_0`–`KNT_ERROR_3` for an argument the host cannot encode, and
972
+ `WORKER_STARTUP_FAILED`, `WORKER_CRASHED`, `WORKER_EXITED` or `THREAD_CLOSED`
973
+ when the worker behind the call is gone. Calls to a dead worker reject at once
974
+ instead of staying pending. A return value the worker cannot encode still
975
+ rejects with the bare `KNT_ERROR_n` string.
976
+
886
977
  That said, workers still run code. If you treat tasks like plugins, keep
887
978
  permissions tight, keep named shared-memory names hard to guess, and avoid
888
979
  passing broad capabilities into worker code.
@@ -1248,7 +1339,7 @@ ownership move on thread workers:
1248
1339
 
1249
1340
  | Runtime | Result ownership |
1250
1341
  | --- | --- |
1251
- | Node 22/24 with the addon | The host co-owns the V8 backing store: zero byte copies. |
1342
+ | Node 22/24 with the addon | The host co-owns the V8 backing store: zero byte copies. Under the strict permission policy this needs `permission.node.allowAddons`; otherwise it takes the one-private-copy fallback. |
1252
1343
  | Deno and Bun | The host makes one private copy before the worker releases its pin. |
1253
1344
  | Older Node backend | The same one-private-copy fallback. |
1254
1345
 
@@ -1,6 +1,8 @@
1
1
  import { workerMainLoop } from "./src/worker/loop.ts";
2
2
  import { createPool, importTask, isMain, setModuleUrl, task } from "./src/api.ts";
3
3
  import { Envelope } from "./src/common/envelope.ts";
4
+ import { KnittingError } from "./src/error.ts";
4
5
  import { isNumericArray, NumericArray } from "./src/connections/numeric-array.ts";
5
- export { createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
6
+ export { createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, KnittingError as KnittingError, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
6
7
  export type { EnvelopeBody as EnvelopeBody, EnvelopeHeader as EnvelopeHeader, } from "./src/common/envelope.ts";
8
+ export type { KnittingErrorCode } from "./src/error.ts";