knitting 0.1.56 → 0.1.61

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 (101) hide show
  1. package/BROWSER.md +134 -0
  2. package/README.md +347 -46
  3. package/knitting.browser.d.ts +6 -0
  4. package/knitting.browser.js +1 -0
  5. package/knitting.d.ts +3 -2
  6. package/knitting.js +3 -3
  7. package/map.md +21 -9
  8. package/package.json +17 -2
  9. package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
  10. package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
  11. package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
  12. package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
  13. package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
  14. package/prebuilds/darwin-arm64-node-137/knitting_shm.node +0 -0
  15. package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
  16. package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
  17. package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
  18. package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
  19. package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
  20. package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
  21. package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
  22. package/scripts/build-compiled-worker.ts +276 -0
  23. package/scripts/compiled-worker/porffor.d.ts +5 -0
  24. package/scripts/compiled-worker/runtime.ts +529 -0
  25. package/scripts/compiled-worker/task-shim.ts +32 -0
  26. package/src/api.d.ts +24 -1
  27. package/src/api.js +311 -27
  28. package/src/common/module-url.d.ts +1 -0
  29. package/src/common/module-url.js +30 -2
  30. package/src/common/node-compat.d.ts +2 -0
  31. package/src/common/path-canonical.js +20 -6
  32. package/src/common/runtime.d.ts +3 -1
  33. package/src/common/runtime.js +25 -4
  34. package/src/common/task-source.d.ts +2 -0
  35. package/src/common/task-source.js +17 -1
  36. package/src/common/worker-runtime.js +16 -7
  37. package/src/connections/buffer-reference-native.js +25 -1
  38. package/src/connections/buffer-reference.d.ts +23 -2
  39. package/src/connections/buffer-reference.js +143 -29
  40. package/src/connections/bun.d.ts +1 -1
  41. package/src/connections/bun.js +25 -14
  42. package/src/connections/deno.d.ts +3 -1
  43. package/src/connections/deno.js +35 -14
  44. package/src/connections/external-array-buffer.d.ts +2 -0
  45. package/src/connections/external-array-buffer.js +33 -0
  46. package/src/connections/node-addons.d.ts +11 -0
  47. package/src/connections/node-addons.js +49 -1
  48. package/src/connections/node-ffi-api.d.ts +19 -0
  49. package/src/connections/node-ffi-api.js +28 -0
  50. package/src/connections/node-ffi.d.ts +27 -0
  51. package/src/connections/node-ffi.js +262 -0
  52. package/src/connections/node.d.ts +1 -1
  53. package/src/connections/node.js +20 -20
  54. package/src/connections/package-assets.js +25 -15
  55. package/src/connections/posix.d.ts +42 -0
  56. package/src/connections/posix.js +101 -0
  57. package/src/connections/process-shared-buffer.d.ts +1 -0
  58. package/src/connections/process-shared-buffer.js +11 -45
  59. package/src/connections/types.d.ts +4 -0
  60. package/src/connections/windows.d.ts +19 -10
  61. package/src/connections/windows.js +87 -66
  62. package/src/error.d.ts +7 -6
  63. package/src/error.js +8 -7
  64. package/src/memory/lock.d.ts +112 -77
  65. package/src/memory/lock.js +314 -87
  66. package/src/memory/payloadCodec.d.ts +11 -2
  67. package/src/memory/payloadCodec.js +129 -37
  68. package/src/memory/regionRegistry.d.ts +1 -3
  69. package/src/memory/regionRegistry.js +16 -14
  70. package/src/permission/compatibility.d.ts +28 -0
  71. package/src/permission/compatibility.js +239 -0
  72. package/src/permission/index.d.ts +2 -0
  73. package/src/permission/index.js +1 -0
  74. package/src/permission/protocol.d.ts +1 -0
  75. package/src/permission/protocol.js +59 -15
  76. package/src/runtime/compiled-artifact.d.ts +21 -0
  77. package/src/runtime/compiled-artifact.js +260 -0
  78. package/src/runtime/compiled-builder.d.ts +8 -0
  79. package/src/runtime/compiled-builder.js +42 -0
  80. package/src/runtime/compiled-worker.d.ts +27 -0
  81. package/src/runtime/compiled-worker.js +562 -0
  82. package/src/runtime/dispatcher.js +9 -0
  83. package/src/runtime/inline-executor.js +27 -15
  84. package/src/runtime/pool.d.ts +121 -5
  85. package/src/runtime/pool.js +263 -49
  86. package/src/runtime/process-worker.d.ts +3 -13
  87. package/src/runtime/process-worker.js +97 -101
  88. package/src/runtime/tx-queue.d.ts +18 -2
  89. package/src/runtime/tx-queue.js +56 -16
  90. package/src/runtime/worker-common.d.ts +13 -0
  91. package/src/runtime/worker-common.js +102 -0
  92. package/src/types.d.ts +89 -9
  93. package/src/worker/composable-runners.js +3 -2
  94. package/src/worker/loop.d.ts +1 -0
  95. package/src/worker/loop.js +52 -17
  96. package/src/worker/rx-queue.d.ts +11 -1
  97. package/src/worker/rx-queue.js +4 -1
  98. package/src/worker/task-loader.d.ts +5 -4
  99. package/src/worker/task-loader.js +13 -7
  100. package/src/worker/timers.d.ts +8 -1
  101. package/src/worker/timers.js +28 -23
@@ -0,0 +1,529 @@
1
+ /// <reference path="./porffor.d.ts" />
2
+
3
+ type WorkerAbortSignal = { hasAborted: () => boolean; now: () => number };
4
+ type WorkerFunction = (value: unknown, signal: WorkerAbortSignal) => unknown;
5
+
6
+ const MAX_PAYLOAD_BYTES = 1024 * 1024;
7
+ const READ_EOF = -2147483648;
8
+ const input = new Uint8Array(MAX_PAYLOAD_BYTES);
9
+ const output = new Uint8Array(MAX_PAYLOAD_BYTES);
10
+ const sharedName = new Uint8Array(256);
11
+
12
+ const dataPointer = (view: Uint8Array): number => {
13
+ let pointer: i32 = 0;
14
+ Porffor.c`
15
+ pointer = *(i32*)(MEM + (i32)view.val + 4) + 4;
16
+ `;
17
+ return pointer;
18
+ };
19
+
20
+ const inputPointer = dataPointer(input);
21
+ const outputPointer = dataPointer(output);
22
+
23
+ Porffor.c`
24
+ #include <time.h>
25
+
26
+ static i32 knit_json_read_exact(void* output, i32 length) {
27
+ u8* cursor = (u8*)output;
28
+ i32 remaining = length;
29
+ while (remaining > 0) {
30
+ long count = read(STDIN_FILENO, cursor, (size_t)remaining);
31
+ if (count == 0) return 0;
32
+ if (count < 0) return -1;
33
+ cursor += count;
34
+ remaining -= (i32)count;
35
+ }
36
+ return 1;
37
+ }
38
+
39
+ static i32 knit_json_request_length = 0;
40
+ static i32 knit_json_abort_signal = -1;
41
+
42
+ static volatile int* knit_abort_words = (volatile int*)0;
43
+ static i32 knit_abort_bytes = 0;
44
+
45
+ /*
46
+ * CLOCK_MONOTONIC is what performance.now() reports on the other lanes, so
47
+ * signal.now() deltas stay immune to wall-clock steps. Letting time.h supply
48
+ * struct timespec also avoids hand-rolling an ABI: a hand-declared timeval
49
+ * would have needed a 32-bit tv_usec on macOS and a 64-bit one on Linux.
50
+ */
51
+ static f64 knit_now_ms(void) {
52
+ struct timespec ts;
53
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0.0;
54
+ return (f64)ts.tv_sec * 1000.0 + (f64)ts.tv_nsec / 1000000.0;
55
+ }
56
+
57
+ static i32 knit_abort_open(void) {
58
+ extern char* getenv(const char*);
59
+ extern int shm_open(const char*, int, unsigned int);
60
+ extern void* mmap(void*, unsigned long, int, int, int, long);
61
+ extern int close(int);
62
+ const int knit_abort_o_rdwr = 2;
63
+ const int knit_abort_prot_read = 1;
64
+ const int knit_abort_map_shared = 1;
65
+ char* name = getenv("KNITTING_COMPILED_ABORT_SHM");
66
+ char* bytesText = getenv("KNITTING_COMPILED_ABORT_BYTES");
67
+ if (name == (char*)0 || bytesText == (char*)0) return 0;
68
+ i32 bytes = 0;
69
+ while (*bytesText >= '0' && *bytesText <= '9') {
70
+ bytes = bytes * 10 + (*bytesText - '0');
71
+ bytesText++;
72
+ }
73
+ if (bytes <= 0) return 0;
74
+ int fd = shm_open(name, knit_abort_o_rdwr, 0600);
75
+ if (fd < 0) return 0;
76
+ void* mapped = mmap(
77
+ (void*)0,
78
+ (unsigned long)bytes,
79
+ knit_abort_prot_read,
80
+ knit_abort_map_shared,
81
+ fd,
82
+ 0
83
+ );
84
+ close(fd);
85
+ if (mapped == (void*)-1) return 0;
86
+ knit_abort_words = (volatile int*)mapped;
87
+ knit_abort_bytes = bytes;
88
+ return 1;
89
+ }
90
+
91
+ static i32 knit_abort_has(i32 signal) {
92
+ if (knit_abort_words == (volatile int*)0 || signal < 0) return 0;
93
+ i32 word = signal >> 5;
94
+ if ((word + 1) * 4 > knit_abort_bytes) return 0;
95
+ i32 value = __atomic_load_n(knit_abort_words + word, __ATOMIC_SEQ_CST);
96
+ return (value & (1 << (signal & 31))) != 0;
97
+ }
98
+
99
+ static i32 knit_map_named_shm(i32 namePtr, i32 hintOff, i32 bytes) {
100
+ extern int shm_open(const char*, int, unsigned int);
101
+ extern void* mmap(void*, unsigned long, int, int, int, long);
102
+ extern int close(int);
103
+ extern void perror(const char*);
104
+ const int knit_shm_o_rdwr = 2;
105
+ const int knit_shm_prot_read = 1;
106
+ const int knit_shm_prot_write = 2;
107
+ const int knit_shm_map_shared = 1;
108
+ const int knit_shm_map_fixed = 16;
109
+ int fd = shm_open((char*)(MEM + namePtr), knit_shm_o_rdwr, 0600);
110
+ if (fd < 0) { perror("knit compiled shm_open"); return -1; }
111
+ char* base = MEM + hintOff;
112
+ unsigned long aligned = ((unsigned long)base + 4095UL) & ~4095UL;
113
+ void* mapped = mmap(
114
+ (void*)aligned,
115
+ (unsigned long)bytes,
116
+ knit_shm_prot_read | knit_shm_prot_write,
117
+ knit_shm_map_shared | knit_shm_map_fixed,
118
+ fd,
119
+ 0
120
+ );
121
+ close(fd);
122
+ if (mapped == (void*)-1) { perror("knit compiled mmap"); return -1; }
123
+ return (i32)((u8*)mapped - MEM);
124
+ }
125
+
126
+ static i32 knit_json_read_request(i32 pointer, i32 capacity) {
127
+ i32 header[3] = {0};
128
+ if (knit_json_read_exact(header, (i32)sizeof(header)) != 1) {
129
+ return (i32)0x80000000u;
130
+ }
131
+ i32 task_index = header[0];
132
+ knit_json_abort_signal = header[1];
133
+ i32 length = header[2];
134
+ if (task_index == -1) return -1;
135
+ if (length < 0 || length > capacity) return -2;
136
+ if (knit_json_read_exact(MEM + pointer, length) != 1) {
137
+ return (i32)0x80000000u;
138
+ }
139
+ knit_json_request_length = length;
140
+ return task_index;
141
+ }
142
+
143
+ static i32 knit_json_write_exact(const void* input, i32 length) {
144
+ const u8* cursor = (const u8*)input;
145
+ i32 remaining = length;
146
+ while (remaining > 0) {
147
+ long count = write(STDOUT_FILENO, cursor, (size_t)remaining);
148
+ if (count <= 0) return -1;
149
+ cursor += count;
150
+ remaining -= (i32)count;
151
+ }
152
+ return 0;
153
+ }
154
+
155
+ static i32 knit_json_write_response(i32 status, i32 pointer, i32 length) {
156
+ i32 header[2] = {status, length};
157
+ if (knit_json_write_exact(header, (i32)sizeof(header)) != 0) return -1;
158
+ return knit_json_write_exact(MEM + pointer, length);
159
+ }
160
+ `;
161
+
162
+ const readRequest = (pointer: number, capacity: number): number => {
163
+ let taskIndex: i32 = 0;
164
+ Porffor.c`
165
+ taskIndex = knit_json_read_request((i32)pointer.val, (i32)capacity.val);
166
+ `;
167
+ return taskIndex;
168
+ };
169
+
170
+ const requestLength = (): number => {
171
+ let length: i32 = 0;
172
+ Porffor.c`length = knit_json_request_length;`;
173
+ return length;
174
+ };
175
+
176
+ const writeResponse = (
177
+ status: number,
178
+ pointer: number,
179
+ length: number,
180
+ ): number => {
181
+ let result: i32 = 0;
182
+ Porffor.c`
183
+ result = knit_json_write_response(
184
+ (i32)status.val,
185
+ (i32)pointer.val,
186
+ (i32)length.val
187
+ );
188
+ `;
189
+ return result;
190
+ };
191
+
192
+ const abortSignal: WorkerAbortSignal = {
193
+ hasAborted: (): boolean => {
194
+ let result: i32 = 0;
195
+ Porffor.c`result = knit_abort_has(knit_json_abort_signal);`;
196
+ return result !== 0;
197
+ },
198
+ now: (): number => {
199
+ let result = 0;
200
+ Porffor.c`result = knit_now_ms();`;
201
+ return result;
202
+ },
203
+ };
204
+
205
+ const dataPointerOf = (view: Uint8Array): number => {
206
+ let pointer: i32 = 0;
207
+ Porffor.c`pointer = *(i32*)(MEM + (i32)view.val + 4) + 4;`;
208
+ return pointer;
209
+ };
210
+
211
+ const sharedNamePointer = dataPointerOf(sharedName);
212
+
213
+ const mapProcessShared = (name: string, size: number): number => {
214
+ for (let index = 0; index < name.length; index++) {
215
+ const code = name.charCodeAt(index);
216
+ if (code > 0x7f) throw new TypeError("Compiled shared names must be ASCII");
217
+ sharedName[index] = code;
218
+ }
219
+ sharedName[name.length] = 0;
220
+ let mapped: i32 = 0;
221
+ const scratch = new Uint8Array(size + 8192);
222
+ const hint = dataPointerOf(scratch);
223
+ Porffor.c`
224
+ mapped = knit_map_named_shm(
225
+ (i32)sharedNamePointer.val,
226
+ (i32)hint.val,
227
+ (i32)size.val
228
+ );`;
229
+ if (mapped < 0) throw new Error("Compiled ProcessSharedBuffer mapping failed");
230
+ return mapped;
231
+ };
232
+
233
+ /**
234
+ * A mapping permanently claims linear memory, so each name is mapped once and
235
+ * every later call reuses the base pointer. Without this, a task called in a
236
+ * loop would grow the process by its buffer size on every single call.
237
+ */
238
+ const mappedNames: string[] = [];
239
+ const mappedBases: number[] = [];
240
+
241
+ const processSharedView = (
242
+ name: string,
243
+ size: number,
244
+ offset: number,
245
+ length: number,
246
+ ): Uint8Array => {
247
+ if (name.length === 0 || name.length >= sharedName.length) {
248
+ throw new TypeError("Compiled ProcessSharedBuffer name is invalid");
249
+ }
250
+ if (size <= 0 || offset < 0 || length < 0 || offset + length > size) {
251
+ throw new RangeError("Compiled ProcessSharedBuffer range is invalid");
252
+ }
253
+ let base = -1;
254
+ for (let index = 0; index < mappedNames.length; index++) {
255
+ if (mappedNames[index] === name) base = mappedBases[index]!;
256
+ }
257
+ if (base < 0) {
258
+ base = mapProcessShared(name, size);
259
+ mappedNames.push(name);
260
+ mappedBases.push(base);
261
+ }
262
+ const view = new Uint8Array(1);
263
+ // Porffor's element access ignores a view's byteOffset field, so the offset
264
+ // is folded into the buffer pointer instead and the field is left at zero.
265
+ // The trailing -4 steps back over the length prefix Porffor keeps in front
266
+ // of buffer data.
267
+ Porffor.c`
268
+ *(i32*)(MEM + (i32)view.val) = (i32)length.val;
269
+ *(i32*)(MEM + (i32)view.val + 4) = (i32)base.val + (i32)offset.val - 4;
270
+ *(i32*)(MEM + (i32)view.val + 8) = 0;
271
+ `;
272
+ return view;
273
+ };
274
+
275
+ /*
276
+ * Base64 keeps binary values inside the ASCII request budget at four characters
277
+ * per three bytes. A JSON array of decimal bytes costs up to four characters
278
+ * per single byte, so a buffer barely over 250 KiB could not fit in a frame at
279
+ * all. Both halves map sextets to character codes arithmetically, so neither
280
+ * needs to index into an alphabet string: 0-25 are A-Z, 26-51 a-z, 52-61 0-9,
281
+ * then + and /. Unknown codes, padding included, contribute no bits.
282
+ */
283
+ const base64Code = (sextet: number): number =>
284
+ sextet < 26
285
+ ? 65 + sextet
286
+ : sextet < 52
287
+ ? 71 + sextet
288
+ : sextet < 62
289
+ ? sextet - 4
290
+ : sextet === 62
291
+ ? 43
292
+ : 47;
293
+
294
+ const base64Sextet = (code: number): number => {
295
+ if (code >= 65 && code <= 90) return code - 65;
296
+ if (code >= 97 && code <= 122) return code - 71;
297
+ if (code >= 48 && code <= 57) return code + 4;
298
+ if (code === 43) return 62;
299
+ if (code === 47) return 63;
300
+ return 0;
301
+ };
302
+
303
+ const toBase64 = (view: Uint8Array): string => {
304
+ let text = "";
305
+ for (let at = 0; at < view.length; at += 3) {
306
+ const remaining = view.length - at;
307
+ const word = (view[at]! << 16) |
308
+ (remaining > 1 ? view[at + 1]! << 8 : 0) |
309
+ (remaining > 2 ? view[at + 2]! : 0);
310
+ text += String.fromCharCode(
311
+ base64Code((word >> 18) & 63),
312
+ base64Code((word >> 12) & 63),
313
+ );
314
+ text += remaining > 1
315
+ ? String.fromCharCode(base64Code((word >> 6) & 63))
316
+ : "=";
317
+ text += remaining > 2 ? String.fromCharCode(base64Code(word & 63)) : "=";
318
+ }
319
+ return text;
320
+ };
321
+
322
+ const fromBase64 = (text: string): Uint8Array => {
323
+ let padding = 0;
324
+ while (padding < 2 && text.charCodeAt(text.length - 1 - padding) === 61) {
325
+ padding++;
326
+ }
327
+ const bytes = new Uint8Array((text.length >> 2) * 3 - padding);
328
+ let at = 0;
329
+ for (let index = 0; index + 3 < text.length; index += 4) {
330
+ const word = (base64Sextet(text.charCodeAt(index)) << 18) |
331
+ (base64Sextet(text.charCodeAt(index + 1)) << 12) |
332
+ (base64Sextet(text.charCodeAt(index + 2)) << 6) |
333
+ base64Sextet(text.charCodeAt(index + 3));
334
+ bytes[at++] = (word >> 16) & 255;
335
+ if (at < bytes.length) bytes[at++] = (word >> 8) & 255;
336
+ if (at < bytes.length) bytes[at++] = word & 255;
337
+ }
338
+ return bytes;
339
+ };
340
+
341
+ const decodeWireValue = (value: unknown): unknown => {
342
+ if (Array.isArray(value)) {
343
+ for (let index = 0; index < value.length; index++) {
344
+ value[index] = decodeWireValue(value[index]);
345
+ }
346
+ return value;
347
+ }
348
+ if (value === null || typeof value !== "object") return value;
349
+ const record = value as Record<string, unknown>;
350
+ const tag = record["$knitting"];
351
+ if (typeof tag === "string") {
352
+ if (tag === "process-shared") {
353
+ return processSharedView(
354
+ record.name as string,
355
+ record.size as number,
356
+ record.offset as number,
357
+ record.length as number,
358
+ );
359
+ }
360
+ const bytes = fromBase64(record.data as string);
361
+ if (tag === "array-buffer") return bytes.buffer;
362
+ if (tag === "data-view") return new DataView(bytes.buffer);
363
+ if (tag === "u8") return bytes;
364
+ if (tag === "u8c") return new Uint8ClampedArray(bytes.buffer);
365
+ if (tag === "i8") return new Int8Array(bytes.buffer);
366
+ if (tag === "u16") return new Uint16Array(bytes.buffer);
367
+ if (tag === "i16") return new Int16Array(bytes.buffer);
368
+ if (tag === "u32") return new Uint32Array(bytes.buffer);
369
+ if (tag === "i32") return new Int32Array(bytes.buffer);
370
+ if (tag === "f32") return new Float32Array(bytes.buffer);
371
+ if (tag === "f64") return new Float64Array(bytes.buffer);
372
+ throw new TypeError("Compiled worker received an unknown binary value");
373
+ }
374
+ for (const key of Object.keys(record)) {
375
+ record[key] = decodeWireValue(record[key]);
376
+ }
377
+ return value;
378
+ };
379
+
380
+ const binaryTagOf = (value: unknown): string => {
381
+ if (value instanceof ArrayBuffer) return "array-buffer";
382
+ if (value instanceof DataView) return "data-view";
383
+ if (value instanceof Uint8Array) return "u8";
384
+ if (value instanceof Uint8ClampedArray) return "u8c";
385
+ if (value instanceof Int8Array) return "i8";
386
+ if (value instanceof Uint16Array) return "u16";
387
+ if (value instanceof Int16Array) return "i16";
388
+ if (value instanceof Uint32Array) return "u32";
389
+ if (value instanceof Int32Array) return "i32";
390
+ if (value instanceof Float32Array) return "f32";
391
+ if (value instanceof Float64Array) return "f64";
392
+ return "";
393
+ };
394
+
395
+ const encodeWireValue = (value: unknown): unknown => {
396
+ const tag = binaryTagOf(value);
397
+ if (tag !== "") {
398
+ const bytes = value instanceof ArrayBuffer
399
+ ? new Uint8Array(value)
400
+ : new Uint8Array(
401
+ (value as ArrayBufferView).buffer,
402
+ (value as ArrayBufferView).byteOffset,
403
+ (value as ArrayBufferView).byteLength,
404
+ );
405
+ return { $knitting: tag, data: toBase64(bytes) };
406
+ }
407
+ if (Array.isArray(value)) return value.map(encodeWireValue);
408
+ if (value !== null && typeof value === "object") {
409
+ const out: Record<string, unknown> = {};
410
+ for (const key of Object.keys(value as Record<string, unknown>)) {
411
+ out[key] = encodeWireValue((value as Record<string, unknown>)[key]);
412
+ }
413
+ return out;
414
+ }
415
+ return value;
416
+ };
417
+
418
+ const decodeUtf8 = (length: number): string => {
419
+ let text = "";
420
+ let index = 0;
421
+ while (index < length) {
422
+ const first = input[index++]!;
423
+ if (first < 0x80) {
424
+ text += String.fromCharCode(first);
425
+ continue;
426
+ }
427
+
428
+ if (first < 0xe0) {
429
+ const second = input[index++]!;
430
+ text += String.fromCharCode(
431
+ ((first & 0x1f) << 6) | (second & 0x3f),
432
+ );
433
+ continue;
434
+ }
435
+
436
+ if (first < 0xf0) {
437
+ const second = input[index++]!;
438
+ const third = input[index++]!;
439
+ text += String.fromCharCode(
440
+ ((first & 0x0f) << 12) | ((second & 0x3f) << 6) | (third & 0x3f),
441
+ );
442
+ continue;
443
+ }
444
+
445
+ const second = input[index++]!;
446
+ const third = input[index++]!;
447
+ const fourth = input[index++]!;
448
+ const point = ((first & 0x07) << 18) | ((second & 0x3f) << 12) |
449
+ ((third & 0x3f) << 6) | (fourth & 0x3f);
450
+ const adjusted = point - 0x10000;
451
+ text += String.fromCharCode(
452
+ 0xd800 + (adjusted >> 10),
453
+ 0xdc00 + (adjusted & 0x3ff),
454
+ );
455
+ }
456
+ return text;
457
+ };
458
+
459
+ const encodeUtf8 = (text: string): number => {
460
+ let offset = 0;
461
+ for (let index = 0; index < text.length; index++) {
462
+ let point = text.charCodeAt(index);
463
+ if (
464
+ point >= 0xd800 && point <= 0xdbff && index + 1 < text.length
465
+ ) {
466
+ const low = text.charCodeAt(index + 1);
467
+ if (low >= 0xdc00 && low <= 0xdfff) {
468
+ point = 0x10000 + ((point - 0xd800) << 10) + (low - 0xdc00);
469
+ index++;
470
+ }
471
+ }
472
+
473
+ if (point < 0x80) {
474
+ if (offset + 1 > MAX_PAYLOAD_BYTES) return -1;
475
+ output[offset++] = point;
476
+ } else if (point < 0x800) {
477
+ if (offset + 2 > MAX_PAYLOAD_BYTES) return -1;
478
+ output[offset++] = 0xc0 | (point >> 6);
479
+ output[offset++] = 0x80 | (point & 0x3f);
480
+ } else if (point < 0x10000) {
481
+ if (offset + 3 > MAX_PAYLOAD_BYTES) return -1;
482
+ output[offset++] = 0xe0 | (point >> 12);
483
+ output[offset++] = 0x80 | ((point >> 6) & 0x3f);
484
+ output[offset++] = 0x80 | (point & 0x3f);
485
+ } else {
486
+ if (offset + 4 > MAX_PAYLOAD_BYTES) return -1;
487
+ output[offset++] = 0xf0 | (point >> 18);
488
+ output[offset++] = 0x80 | ((point >> 12) & 0x3f);
489
+ output[offset++] = 0x80 | ((point >> 6) & 0x3f);
490
+ output[offset++] = 0x80 | (point & 0x3f);
491
+ }
492
+ }
493
+ return offset;
494
+ };
495
+
496
+ /**
497
+ * Requests use a twelve-byte little-endian header: i32 task index, i32 abort
498
+ * slot, and i32 byte length, followed by UTF-8 JSON. Responses keep the
499
+ * original eight-byte status/length header.
500
+ */
501
+ export const runJsonWorker = (functions: WorkerFunction[]): void => {
502
+ Porffor.c`knit_abort_open();`;
503
+ while (true) {
504
+ const taskIndex = readRequest(inputPointer, MAX_PAYLOAD_BYTES);
505
+ if (taskIndex === READ_EOF || taskIndex === -1) return;
506
+ if (taskIndex < 0 || taskIndex >= functions.length) {
507
+ if (writeResponse(1, outputPointer, 0) !== 0) return;
508
+ continue;
509
+ }
510
+
511
+ try {
512
+ const value = decodeWireValue(JSON.parse(decodeUtf8(requestLength())));
513
+ const result = functions[taskIndex]!(value, abortSignal);
514
+ const serialized = JSON.stringify(encodeWireValue(result));
515
+ if (typeof serialized !== "string") {
516
+ if (writeResponse(2, outputPointer, 0) !== 0) return;
517
+ continue;
518
+ }
519
+ const length = encodeUtf8(serialized);
520
+ if (length < 0) {
521
+ if (writeResponse(4, outputPointer, 0) !== 0) return;
522
+ continue;
523
+ }
524
+ if (writeResponse(0, outputPointer, length) !== 0) return;
525
+ } catch (_error) {
526
+ if (writeResponse(3, outputPointer, 0) !== 0) return;
527
+ }
528
+ }
529
+ };
@@ -0,0 +1,32 @@
1
+ /** Worker-side replacement for the small part of Knitting task modules use. */
2
+
3
+ export type PorfforAbortSignal = {
4
+ hasAborted: () => boolean;
5
+ now: () => number;
6
+ };
7
+
8
+ export type PorfforTaskDefinition<A = unknown, B = unknown> = {
9
+ f: (value: A, signal?: PorfforAbortSignal) => B;
10
+ [key: string | symbol]: unknown;
11
+ };
12
+
13
+ const endpointSymbol = Symbol.for("task");
14
+
15
+ export const task = <A = unknown, B = unknown>(
16
+ definition: PorfforTaskDefinition<A, B>,
17
+ ): PorfforTaskDefinition<A, B> => {
18
+ definition[endpointSymbol] = true;
19
+ return definition;
20
+ };
21
+
22
+ // Host-only branches are folded away by Bun while producing the worker bundle.
23
+ export const isMain = false;
24
+ export const setModuleUrl = (_url: string | URL): void => {};
25
+
26
+ // Some modules keep their host pool declaration at top level and guard only
27
+ // the calls with `isMain`. Keep that declaration inert in the native bundle.
28
+ export const createPool = (_options: unknown) => (_tasks: unknown) => ({
29
+ call: {},
30
+ shutdown: () => Promise.resolve(),
31
+ [Symbol.dispose]: () => {},
32
+ });
package/src/api.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { setModuleUrl } from "./common/task-source.js";
1
2
  import { endpointSymbol } from "./common/task-symbol.js";
2
- import type { AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, Args, ComposedWithKey, CreatePool, FixPoint, ImportTaskOptions, MaybePromise, Pool, ReturnFixed, TaskInput, tasks, TaskTimeout } from "./types.js";
3
+ import type { AbortSignalConfig, AbortSignalOption, AbortSignalToolkit, Args, CompiledWorkerCheck, CompiledWorkerOptions, CompiledWorkerSource, ComposedWithKey, CreatePool, FixPoint, ImportTaskOptions, MaybePromise, Pool, ReturnFixed, TaskInput, tasks, TaskTimeout } from "./types.js";
3
4
  type ToListAndIds = {
4
5
  list: string[];
5
6
  ids: number[];
@@ -21,10 +22,32 @@ type InferredTaskShape<F extends InferredTaskFunction, AS extends AbortSignalOpt
21
22
  });
22
23
  export declare const isMain: boolean;
23
24
  export { endpointSymbol as endpointSymbol };
25
+ /**
26
+ * Declare the task module's URL for runtimes without stack traces (e.g.
27
+ * Andromeda) that can't auto-discover the caller. Call once at module top
28
+ * level, before defining tasks:
29
+ *
30
+ * ```ts
31
+ * import { setModuleUrl, task } from "knitting";
32
+ * setModuleUrl(import.meta.url);
33
+ * export const add = task({ f: ([a, b]: [number, number]) => a + b });
34
+ * ```
35
+ *
36
+ * Runs in both host and worker (same module re-imported), so both agree on the
37
+ * path. No-op-safe on Node/Deno/Bun, where stack discovery already works.
38
+ */
39
+ export { setModuleUrl as setModuleUrl };
24
40
  /**
25
41
  * Reconstructs stable task order from top-level exports before names are bound.
26
42
  */
27
43
  export declare const toListAndIds: ToListAndIdsFn;
44
+ /**
45
+ * Check whether a task module has a compatible prebuilt native worker.
46
+ *
47
+ * By default tasks.ts resolves to tasks.knt plus tasks.knt.json.
48
+ * This only inspects the artifact; it never executes it.
49
+ */
50
+ export declare const checkCompiledWorker: (source: CompiledWorkerSource, options?: CompiledWorkerOptions) => CompiledWorkerCheck;
28
51
  /**
29
52
  * Create a typed worker pool from module-scope exported tasks/functions.
30
53
  *