@nimbus-sh/platform 0.8.2 → 0.14.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 (67) hide show
  1. package/dist/composition.d.ts +3 -2
  2. package/dist/composition.d.ts.map +1 -1
  3. package/dist/composition.js +34 -19
  4. package/dist/crc32.d.ts +5 -5
  5. package/dist/crc32.d.ts.map +1 -1
  6. package/dist/crc32.js +8 -12
  7. package/dist/diag-counters.d.ts +18 -10
  8. package/dist/diag-counters.d.ts.map +1 -1
  9. package/dist/diag-counters.js +7 -20
  10. package/dist/heap-estimate.d.ts +5 -20
  11. package/dist/heap-estimate.d.ts.map +1 -1
  12. package/dist/heap-estimate.js +6 -33
  13. package/dist/heavy-alloc-coord.d.ts +131 -11
  14. package/dist/heavy-alloc-coord.d.ts.map +1 -1
  15. package/dist/heavy-alloc-coord.js +241 -22
  16. package/dist/hex.d.ts +20 -0
  17. package/dist/hex.d.ts.map +1 -0
  18. package/dist/hex.js +29 -0
  19. package/dist/install-phase.d.ts +1 -15
  20. package/dist/install-phase.d.ts.map +1 -1
  21. package/dist/lexical-path.d.ts +11 -0
  22. package/dist/lexical-path.d.ts.map +1 -0
  23. package/dist/lexical-path.js +19 -0
  24. package/dist/limits.d.ts +4 -0
  25. package/dist/limits.d.ts.map +1 -1
  26. package/dist/limits.js +25 -0
  27. package/dist/module-source.d.ts.map +1 -1
  28. package/dist/module-source.js +2 -1
  29. package/dist/oom-classify.d.ts +4 -0
  30. package/dist/oom-classify.d.ts.map +1 -1
  31. package/dist/oom-classify.js +6 -0
  32. package/dist/realm-builtin.d.ts +2 -0
  33. package/dist/realm-builtin.d.ts.map +1 -0
  34. package/dist/realm-builtin.js +11 -0
  35. package/dist/utf8.d.ts +21 -1
  36. package/dist/utf8.d.ts.map +1 -1
  37. package/dist/utf8.js +73 -1
  38. package/dist/w7-frame.d.ts +92 -17
  39. package/dist/w7-frame.d.ts.map +1 -1
  40. package/dist/w7-frame.js +87 -35
  41. package/dist/wave-writer.d.ts +11 -6
  42. package/dist/wave-writer.d.ts.map +1 -1
  43. package/dist/wave-writer.js +28 -29
  44. package/dist/weighted-credit-pool.d.ts +43 -0
  45. package/dist/weighted-credit-pool.d.ts.map +1 -1
  46. package/dist/weighted-credit-pool.js +250 -46
  47. package/dist/worker-code.d.ts +48 -0
  48. package/dist/worker-code.d.ts.map +1 -0
  49. package/dist/worker-code.js +10 -0
  50. package/package.json +2 -2
  51. package/src/composition.ts +41 -21
  52. package/src/crc32.ts +9 -12
  53. package/src/diag-counters.ts +23 -22
  54. package/src/heap-estimate.ts +11 -40
  55. package/src/heavy-alloc-coord.ts +275 -20
  56. package/src/hex.ts +29 -0
  57. package/src/install-phase.ts +1 -15
  58. package/src/lexical-path.ts +17 -0
  59. package/src/limits.ts +29 -0
  60. package/src/module-source.ts +3 -1
  61. package/src/oom-classify.ts +8 -0
  62. package/src/realm-builtin.ts +22 -0
  63. package/src/utf8.ts +75 -1
  64. package/src/w7-frame.ts +152 -53
  65. package/src/wave-writer.ts +28 -27
  66. package/src/weighted-credit-pool.ts +256 -51
  67. package/src/worker-code.ts +38 -0
@@ -5,6 +5,11 @@ export interface CreditLease {
5
5
 
6
6
  export interface ResizableCreditLease extends CreditLease {
7
7
  shrinkTo(bytes: number): void;
8
+ /**
9
+ * Holds `bytes` in all if the rest can be granted now, in its own lane, as
10
+ * tryAcquire would grant it; else holds what it held and answers false.
11
+ */
12
+ tryGrowTo(bytes: number): boolean;
8
13
  }
9
14
 
10
15
  export interface WeightedCreditPoolStats {
@@ -27,61 +32,197 @@ export interface WeightedCreditPoolOptions {
27
32
  readonly reserve?: number;
28
33
  }
29
34
 
30
- interface CreditWaiter {
31
- readonly bytes: number;
32
- readonly resolve: (lease: ResizableCreditLease) => void;
33
- readonly reject: (error: Error) => void;
34
- readonly signal?: AbortSignal;
35
- readonly onAbort?: () => void;
35
+ /**
36
+ * How long a waiter woken to its turn keeps it for its own continuation to
37
+ * take the credit. workerd cancels the continuations of a request that has
38
+ * completed or been canceled when another request's code resolves its
39
+ * promise (the cross-request promise rule, on by date since 2024-10-14), so
40
+ * a waiter whose request is gone is woken and never takes: its turn lapses
41
+ * and the next waiter's comes. A live continuation takes it in far less.
42
+ */
43
+ export const CREDIT_TURN_MS = 1000;
44
+
45
+ /**
46
+ * A place in a lane's queue. `woken` resolves when its turn comes; `take`,
47
+ * called from the waiter's own continuation, holds the credit then, or
48
+ * answers null when the turn lapsed, the credit no longer fits, or it was
49
+ * aborted, and the place waits again. `leave` gives up the place.
50
+ */
51
+ export interface CreditTurn {
52
+ woken(): Promise<void>;
53
+ take(): ResizableCreditLease | null;
54
+ leave(): void;
55
+ }
56
+
57
+ /**
58
+ * An ask queued in a lane. `waiting` for its turn; `woken` to it at
59
+ * `wokenAt`, its credit free for it to take; `lapsed` when the turn passed
60
+ * untaken (out of the queue, back in at the end if it ever takes); `done`
61
+ * once it took its credit, left, or was aborted.
62
+ */
63
+ class Turn implements CreditTurn {
64
+ state: 'waiting' | 'woken' | 'lapsed' | 'done' = 'waiting';
65
+ wokenAt = 0;
66
+ /** Its wait for the turn it is queued for, resolved as it is woken or aborted. */
67
+ turn: Promise<void>;
68
+ wake: () => void = () => {};
69
+ /** Its look at the line every CREDIT_TURN_MS while it is queued. */
70
+ recheck: ReturnType<typeof setInterval> | undefined = undefined;
71
+
72
+ constructor(readonly lane: CreditLane, readonly bytes: number, readonly signal: AbortSignal | undefined, readonly ahead: boolean) {
73
+ this.turn = this.arm();
74
+ }
75
+
76
+ /** A fresh wait for its turn. */
77
+ arm(): Promise<void> {
78
+ this.state = 'waiting';
79
+ this.turn = new Promise<void>((resolve) => { this.wake = resolve; });
80
+ return this.turn;
81
+ }
82
+
83
+ woken(): Promise<void> { return this.turn; }
84
+ take(): ResizableCreditLease | null { return this.lane.take(this); }
85
+ leave(): void { this.lane.leave(this); }
86
+ handleEvent(): void { this.lane.abandon(this); }
36
87
  }
37
88
 
38
- /** One FIFO queue over one byte capacity. */
89
+ /**
90
+ * One byte capacity and its queues: FIFO, but for asks `ahead` of it (a
91
+ * running program's), which are served first, FIFO among themselves.
92
+ */
39
93
  class CreditLane {
40
94
  private current = 0;
41
95
  private peak = 0;
42
- private readonly waiters: CreditWaiter[] = [];
96
+ private readonly waiters: Turn[] = [];
97
+ private readonly ahead: Turn[] = [];
43
98
 
44
- constructor(readonly capacity: number) {}
99
+ /** `largest`: the most one lease of this lane may hold (a small request's bound, for the reserve). */
100
+ constructor(readonly capacity: number, private readonly largest = capacity) {}
45
101
 
46
102
  get stats(): WeightedCreditPoolStats {
47
- return { current: this.current, peak: this.peak, queued: this.waiters.length };
103
+ return { current: this.current, peak: this.peak, queued: this.waiters.length + this.ahead.length };
48
104
  }
49
105
 
50
- tryAcquire(bytes: number): ResizableCreditLease | null {
51
- if (this.waiters.length > 0 || this.current + bytes > this.capacity) return null;
106
+ /**
107
+ * Whether the asks ahead want more than is free: what reclaiming more is
108
+ * for. Each holds its bytes once its turn is taken, so they are counted
109
+ * together, a turn woken and not yet taken included.
110
+ */
111
+ get aheadStarved(): boolean {
112
+ return this.ahead.reduce((wanted, waiter) => wanted + waiter.bytes, this.current) > this.capacity;
113
+ }
114
+
115
+ /** Granted now, or null: an ask ahead waits only for asks ahead of it, any other for every ask queued. */
116
+ tryAcquire(bytes: number, ahead = false): ResizableCreditLease | null {
117
+ if (this.ahead.length > 0 || (!ahead && this.waiters.length > 0) || this.current + bytes > this.capacity) return null;
52
118
  return this.grant(bytes);
53
119
  }
54
120
 
55
121
  acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
122
+ return this.wait(bytes, signal, false);
123
+ }
124
+
125
+ /** acquire, ahead of every ordinary ask: granted as soon as it fits, after only the asks ahead before it. */
126
+ acquireAhead(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
127
+ return this.wait(bytes, signal, true);
128
+ }
129
+
130
+ private wait(bytes: number, signal: AbortSignal | undefined, ahead: boolean): Promise<ResizableCreditLease> {
56
131
  if (signal?.aborted) return Promise.reject(abortError(signal));
57
- const immediate = this.tryAcquire(bytes);
58
- if (immediate) return Promise.resolve(immediate);
59
-
60
- return new Promise<ResizableCreditLease>((resolve, reject) => {
61
- const waiter: CreditWaiter = {
62
- bytes,
63
- resolve,
64
- reject,
65
- signal,
66
- onAbort: signal
67
- ? () => {
68
- const index = this.waiters.indexOf(waiter);
69
- if (index < 0) return;
70
- this.waiters.splice(index, 1);
71
- signal.removeEventListener('abort', waiter.onAbort!);
72
- reject(abortError(signal));
73
- this.drain();
74
- }
75
- : undefined,
76
- };
77
- this.waiters.push(waiter);
78
- signal?.addEventListener('abort', waiter.onAbort!, { once: true });
79
- if (signal?.aborted) {
80
- waiter.onAbort!();
81
- return;
132
+ const immediate = this.tryAcquire(bytes, ahead);
133
+ return immediate ? Promise.resolve(immediate) : this.takeInTurn(this.queue(bytes, signal, ahead), signal);
134
+ }
135
+
136
+ private async takeInTurn(turn: Turn, signal: AbortSignal | undefined): Promise<ResizableCreditLease> {
137
+ try {
138
+ for (;;) {
139
+ await turn.turn;
140
+ const lease = this.take(turn);
141
+ if (lease) return lease;
142
+ if (signal?.aborted) throw abortError(signal);
82
143
  }
144
+ } finally {
145
+ this.leave(turn);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * A place at the end of the queue for `bytes` (ahead of every ordinary ask,
151
+ * if `ahead`). While it is queued it looks at the line again every
152
+ * CREDIT_TURN_MS, so a turn another waiter never took lapses even when
153
+ * nothing else touches the lane. Asked with a signal already aborted, it
154
+ * is done at once, as it would be on the abort.
155
+ */
156
+ queue(bytes: number, signal: AbortSignal | undefined, ahead: boolean): Turn {
157
+ const turn = new Turn(this, bytes, signal, ahead);
158
+ signal?.addEventListener('abort', turn, { once: true });
159
+ this.place(turn);
160
+ if (signal?.aborted) this.abandon(turn);
161
+ else this.drain();
162
+ return turn;
163
+ }
164
+
165
+ private place(turn: Turn): void {
166
+ (turn.ahead ? this.ahead : this.waiters).push(turn);
167
+ if (turn.recheck !== undefined) return;
168
+ const timer = setInterval(() => {
169
+ if (turn.state === 'waiting' || turn.state === 'woken') this.drain();
170
+ else { clearInterval(timer); turn.recheck = undefined; }
171
+ }, CREDIT_TURN_MS);
172
+ turn.recheck = timer;
173
+ }
174
+
175
+ private remove(turn: Turn): void {
176
+ const queue = turn.ahead ? this.ahead : this.waiters;
177
+ const index = queue.indexOf(turn);
178
+ if (index >= 0) queue.splice(index, 1);
179
+ }
180
+
181
+ /** Its signal aborted: it is done, its wait ends, and the next turn may come. */
182
+ abandon(turn: Turn): void {
183
+ if (turn.state === 'done') return;
184
+ this.remove(turn);
185
+ turn.state = 'done';
186
+ turn.wake();
187
+ this.drain();
188
+ }
189
+
190
+ take(turn: Turn): ResizableCreditLease | null {
191
+ if (turn.state === 'done') return null;
192
+ // Taken past its turn: the turn passed, as if a look at the line had seen it first.
193
+ if (turn.state === 'woken' && Date.now() - turn.wokenAt >= CREDIT_TURN_MS) {
194
+ this.remove(turn);
195
+ turn.state = 'lapsed';
196
+ this.drain();
197
+ }
198
+ if (turn.state === 'lapsed') {
199
+ turn.arm();
200
+ this.place(turn);
201
+ this.drain();
202
+ return null;
203
+ }
204
+ // Its turn, its credit still free, and no running ask before an ordinary one: its own now.
205
+ if (turn.state === 'woken' && this.current + turn.bytes <= this.capacity && (turn.ahead || this.ahead.length === 0)) {
206
+ this.remove(turn);
207
+ turn.state = 'done';
208
+ const lease = this.grant(turn.bytes);
83
209
  this.drain();
84
- });
210
+ return lease;
211
+ }
212
+ // Not its turn yet, or no longer room: it waits again, in its place.
213
+ if (turn.state === 'woken') turn.arm();
214
+ return null;
215
+ }
216
+
217
+ leave(turn: Turn): void {
218
+ turn.signal?.removeEventListener('abort', turn);
219
+ if (turn.recheck !== undefined) clearInterval(turn.recheck);
220
+ turn.recheck = undefined;
221
+ if (turn.state !== 'done') {
222
+ this.remove(turn);
223
+ turn.state = 'done';
224
+ }
225
+ this.drain();
85
226
  }
86
227
 
87
228
  private grant(bytes: number): ResizableCreditLease {
@@ -110,6 +251,21 @@ class CreditLane {
110
251
  leasedBytes = nextBytes;
111
252
  this.drain();
112
253
  },
254
+ tryGrowTo: (nextBytes: number) => {
255
+ if (released) {
256
+ throw new Error('cannot grow a released weighted credit lease');
257
+ }
258
+ if (!Number.isSafeInteger(nextBytes) || nextBytes < leasedBytes) {
259
+ throw new RangeError(`weighted credit lease can only grow to a safe integer no smaller than ${leasedBytes}: ${nextBytes}`);
260
+ }
261
+ // Never ahead of a queued ask, as an ordinary tryAcquire.
262
+ if (nextBytes > this.largest || this.ahead.length > 0 || this.waiters.length > 0
263
+ || this.current + nextBytes - leasedBytes > this.capacity) return false;
264
+ this.current += nextBytes - leasedBytes;
265
+ this.peak = Math.max(this.peak, this.current);
266
+ leasedBytes = nextBytes;
267
+ return true;
268
+ },
113
269
  release: () => {
114
270
  if (released) return;
115
271
  released = true;
@@ -122,19 +278,30 @@ class CreditLane {
122
278
  };
123
279
  }
124
280
 
125
- private drain(): void {
126
- while (this.waiters.length > 0) {
127
- const waiter = this.waiters[0];
128
- if (waiter.signal?.aborted) {
129
- this.waiters.shift();
130
- waiter.signal.removeEventListener('abort', waiter.onAbort!);
131
- waiter.reject(abortError(waiter.signal));
132
- continue;
281
+ /**
282
+ * Wake the first waiter whose credit is free to its turn, asks ahead first;
283
+ * the ordinary queue only once none is waiting. The credit is never handed
284
+ * over: the waiter takes it in its own continuation (CreditTurn.take), so
285
+ * one whose continuation never runs holds nothing. A woken turn holds its
286
+ * queue until it is taken or lapses (CREDIT_TURN_MS), one turn at a time.
287
+ */
288
+ private drain(now?: number): void {
289
+ for (const queue of [this.ahead, this.waiters]) {
290
+ for (let waiter = queue[0]; waiter !== undefined; waiter = queue[0]) {
291
+ if (waiter.state === 'woken') {
292
+ now ??= Date.now();
293
+ if (now - waiter.wokenAt < CREDIT_TURN_MS) return;
294
+ // Never taken: its continuation is gone, or late and back at the end of the line if it ever takes.
295
+ queue.shift();
296
+ waiter.state = 'lapsed';
297
+ continue;
298
+ }
299
+ if (this.current + waiter.bytes > this.capacity) return;
300
+ waiter.state = 'woken';
301
+ waiter.wokenAt = now ?? Date.now();
302
+ waiter.wake();
303
+ return;
133
304
  }
134
- if (this.current + waiter.bytes > this.capacity) return;
135
- this.waiters.shift();
136
- waiter.signal?.removeEventListener('abort', waiter.onAbort!);
137
- waiter.resolve(this.grant(waiter.bytes));
138
305
  }
139
306
  }
140
307
  }
@@ -184,7 +351,7 @@ export class WeightedCreditPool {
184
351
  }
185
352
  this.general = new CreditLane(capacity);
186
353
  this.smallRequestBytes = smallRequestBytes;
187
- this.small = smallRequestBytes > 0 ? new CreditLane(reserve) : null;
354
+ this.small = smallRequestBytes > 0 ? new CreditLane(reserve, smallRequestBytes) : null;
188
355
  }
189
356
 
190
357
  get stats(): WeightedCreditPoolStats {
@@ -209,12 +376,50 @@ export class WeightedCreditPool {
209
376
  return this.small!.tryAcquire(bytes);
210
377
  }
211
378
 
379
+ /**
380
+ * A running program's ask (SupervisorAllocationBudget.acquireRunning),
381
+ * ahead of every ordinary ask: granted now if it fits beside what is held,
382
+ * whatever is queued (a small one in its reserve too), or else as soon as
383
+ * it fits in the shared capacity, the credit running work and reclaims
384
+ * free. It never waits on the reserve, which neither frees.
385
+ */
386
+ acquireAhead(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
387
+ this.validateRequest(bytes);
388
+ const now = signal?.aborted ? null : this.tryAcquireAhead(bytes);
389
+ return now ? Promise.resolve(now) : this.general.acquireAhead(bytes, signal);
390
+ }
391
+
392
+ /** Whether a running program's ask (acquireAhead) waits for credit no woken turn is taking (CreditLane.aheadStarved). */
393
+ get aheadStarved(): boolean {
394
+ return this.general.aheadStarved;
395
+ }
396
+
397
+ /** acquireAhead's credit now, or null. */
398
+ tryAcquireAhead(bytes: number): ResizableCreditLease | null {
399
+ this.validateRequest(bytes);
400
+ const shared = this.general.tryAcquire(bytes, true);
401
+ const small = this.isSmall(bytes) ? this.small : null;
402
+ return shared ?? small?.tryAcquire(bytes, true) ?? null;
403
+ }
404
+
405
+ /**
406
+ * A place in line for `bytes`, in the lane it would wait in, for a caller
407
+ * that takes its turn itself (CreditTurn): what acquire does once nothing
408
+ * is free.
409
+ */
410
+ queue(bytes: number, signal?: AbortSignal): CreditTurn {
411
+ this.validateRequest(bytes);
412
+ return (this.small !== null && this.isSmall(bytes) ? this.small : this.general).queue(bytes, signal, false);
413
+ }
414
+
212
415
  acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
213
416
  try {
214
417
  this.validateRequest(bytes);
215
418
  } catch (error) {
216
419
  return Promise.reject(error as Error);
217
420
  }
421
+ // Refused before any lane is asked, whichever would have granted it.
422
+ if (signal?.aborted) return Promise.reject(abortError(signal));
218
423
  if (this.isSmall(bytes)) {
219
424
  // Shared capacity while it is free — a small request should get the
220
425
  // same concurrency as any other when nothing is contending. It never
@@ -0,0 +1,38 @@
1
+ export interface JsModule { js: string }
2
+ export interface CjsModule { cjs: string }
3
+ export interface PyModule { py: string }
4
+ export interface TextModule { text: string }
5
+ export interface DataModule { data: ArrayBuffer }
6
+ export interface JsonModule { json: unknown }
7
+ export interface WasmModule { wasm: ArrayBuffer | WebAssembly.Module }
8
+
9
+ /**
10
+ * `bytes` as the ArrayBuffer a `{ wasm }` or `{ data }` member carries: their
11
+ * own buffer when they span all of it, and only otherwise a copy of them, so
12
+ * an image read whole for a load is never held twice.
13
+ */
14
+ export function memberBuffer(bytes: Uint8Array): ArrayBuffer {
15
+ return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength && bytes.buffer instanceof ArrayBuffer
16
+ ? bytes.buffer
17
+ : bytes.slice().buffer;
18
+ }
19
+
20
+ /** Plain strings use the module name's extension. */
21
+ export type ModuleContent = string | JsModule | CjsModule | PyModule | TextModule | DataModule | JsonModule | WasmModule;
22
+
23
+ export interface ServiceStub {
24
+ fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
25
+ }
26
+
27
+ /** The shared Worker Loader contract, including staged resident assemblies. */
28
+ export interface WorkerCode {
29
+ compatibilityDate: string;
30
+ compatibilityFlags?: string[];
31
+ allowExperimental?: boolean;
32
+ limits?: { cpuMs?: number; subRequests?: number };
33
+ mainModule: string;
34
+ modules: Record<string, ModuleContent>;
35
+ env?: Record<string, unknown>;
36
+ globalOutbound?: ServiceStub | null;
37
+ tails?: ServiceStub[];
38
+ }