@nimbus-sh/platform 0.8.1 → 0.13.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.
- package/dist/composition.d.ts +3 -2
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +34 -19
- package/dist/crc32.d.ts +5 -5
- package/dist/crc32.d.ts.map +1 -1
- package/dist/crc32.js +8 -12
- package/dist/diag-counters.d.ts +18 -10
- package/dist/diag-counters.d.ts.map +1 -1
- package/dist/diag-counters.js +7 -20
- package/dist/heap-estimate.d.ts +5 -20
- package/dist/heap-estimate.d.ts.map +1 -1
- package/dist/heap-estimate.js +6 -33
- package/dist/heavy-alloc-coord.d.ts +131 -11
- package/dist/heavy-alloc-coord.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.js +241 -22
- package/dist/hex.d.ts +20 -0
- package/dist/hex.d.ts.map +1 -0
- package/dist/hex.js +29 -0
- package/dist/install-phase.d.ts +1 -15
- package/dist/install-phase.d.ts.map +1 -1
- package/dist/lexical-path.d.ts +11 -0
- package/dist/lexical-path.d.ts.map +1 -0
- package/dist/lexical-path.js +19 -0
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +25 -0
- package/dist/module-source.d.ts.map +1 -1
- package/dist/module-source.js +2 -1
- package/dist/oom-classify.d.ts +5 -1
- package/dist/oom-classify.d.ts.map +1 -1
- package/dist/oom-classify.js +7 -1
- package/dist/realm-builtin.d.ts +2 -0
- package/dist/realm-builtin.d.ts.map +1 -0
- package/dist/realm-builtin.js +11 -0
- package/dist/utf8.d.ts +13 -1
- package/dist/utf8.d.ts.map +1 -1
- package/dist/utf8.js +67 -1
- package/dist/w7-frame.d.ts +47 -13
- package/dist/w7-frame.d.ts.map +1 -1
- package/dist/w7-frame.js +42 -24
- package/dist/wave-writer.d.ts +11 -6
- package/dist/wave-writer.d.ts.map +1 -1
- package/dist/wave-writer.js +28 -29
- package/dist/weighted-credit-pool.d.ts +43 -0
- package/dist/weighted-credit-pool.d.ts.map +1 -1
- package/dist/weighted-credit-pool.js +250 -46
- package/dist/worker-code.d.ts +48 -0
- package/dist/worker-code.d.ts.map +1 -0
- package/dist/worker-code.js +10 -0
- package/package.json +1 -1
- package/src/composition.ts +41 -21
- package/src/crc32.ts +9 -12
- package/src/diag-counters.ts +23 -22
- package/src/heap-estimate.ts +11 -40
- package/src/heavy-alloc-coord.ts +275 -20
- package/src/hex.ts +29 -0
- package/src/install-phase.ts +1 -15
- package/src/lexical-path.ts +17 -0
- package/src/limits.ts +29 -0
- package/src/module-source.ts +3 -1
- package/src/oom-classify.ts +9 -1
- package/src/realm-builtin.ts +22 -0
- package/src/utf8.ts +69 -1
- package/src/w7-frame.ts +74 -33
- package/src/wave-writer.ts +28 -27
- package/src/weighted-credit-pool.ts +256 -51
- package/src/worker-code.ts +38 -0
|
@@ -4,6 +4,11 @@ export interface CreditLease {
|
|
|
4
4
|
}
|
|
5
5
|
export interface ResizableCreditLease extends CreditLease {
|
|
6
6
|
shrinkTo(bytes: number): void;
|
|
7
|
+
/**
|
|
8
|
+
* Holds `bytes` in all if the rest can be granted now, in its own lane, as
|
|
9
|
+
* tryAcquire would grant it; else holds what it held and answers false.
|
|
10
|
+
*/
|
|
11
|
+
tryGrowTo(bytes: number): boolean;
|
|
7
12
|
}
|
|
8
13
|
export interface WeightedCreditPoolStats {
|
|
9
14
|
readonly current: number;
|
|
@@ -23,6 +28,26 @@ export interface WeightedCreditPoolOptions {
|
|
|
23
28
|
*/
|
|
24
29
|
readonly reserve?: number;
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* How long a waiter woken to its turn keeps it for its own continuation to
|
|
33
|
+
* take the credit. workerd cancels the continuations of a request that has
|
|
34
|
+
* completed or been canceled when another request's code resolves its
|
|
35
|
+
* promise (the cross-request promise rule, on by date since 2024-10-14), so
|
|
36
|
+
* a waiter whose request is gone is woken and never takes: its turn lapses
|
|
37
|
+
* and the next waiter's comes. A live continuation takes it in far less.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CREDIT_TURN_MS = 1000;
|
|
40
|
+
/**
|
|
41
|
+
* A place in a lane's queue. `woken` resolves when its turn comes; `take`,
|
|
42
|
+
* called from the waiter's own continuation, holds the credit then, or
|
|
43
|
+
* answers null when the turn lapsed, the credit no longer fits, or it was
|
|
44
|
+
* aborted, and the place waits again. `leave` gives up the place.
|
|
45
|
+
*/
|
|
46
|
+
export interface CreditTurn {
|
|
47
|
+
woken(): Promise<void>;
|
|
48
|
+
take(): ResizableCreditLease | null;
|
|
49
|
+
leave(): void;
|
|
50
|
+
}
|
|
26
51
|
/**
|
|
27
52
|
* FIFO byte-credit pool shared by concurrent allocation owners.
|
|
28
53
|
* Capacity is measured in retained bytes, not operation count.
|
|
@@ -55,6 +80,24 @@ export declare class WeightedCreditPool {
|
|
|
55
80
|
get stats(): WeightedCreditPoolStats;
|
|
56
81
|
private isSmall;
|
|
57
82
|
tryAcquire(bytes: number): ResizableCreditLease | null;
|
|
83
|
+
/**
|
|
84
|
+
* A running program's ask (SupervisorAllocationBudget.acquireRunning),
|
|
85
|
+
* ahead of every ordinary ask: granted now if it fits beside what is held,
|
|
86
|
+
* whatever is queued (a small one in its reserve too), or else as soon as
|
|
87
|
+
* it fits in the shared capacity, the credit running work and reclaims
|
|
88
|
+
* free. It never waits on the reserve, which neither frees.
|
|
89
|
+
*/
|
|
90
|
+
acquireAhead(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
91
|
+
/** Whether a running program's ask (acquireAhead) waits for credit no woken turn is taking (CreditLane.aheadStarved). */
|
|
92
|
+
get aheadStarved(): boolean;
|
|
93
|
+
/** acquireAhead's credit now, or null. */
|
|
94
|
+
tryAcquireAhead(bytes: number): ResizableCreditLease | null;
|
|
95
|
+
/**
|
|
96
|
+
* A place in line for `bytes`, in the lane it would wait in, for a caller
|
|
97
|
+
* that takes its turn itself (CreditTurn): what acquire does once nothing
|
|
98
|
+
* is free.
|
|
99
|
+
*/
|
|
100
|
+
queue(bytes: number, signal?: AbortSignal): CreditTurn;
|
|
58
101
|
acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
59
102
|
private validateRequest;
|
|
60
103
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"weighted-credit-pool.d.ts","sourceRoot":"","sources":["../src/weighted-credit-pool.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"weighted-credit-pool.d.ts","sourceRoot":"","sources":["../src/weighted-credit-pool.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAqB,SAAQ,WAAW;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B;;;OAGG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,oBAAoB,GAAG,IAAI,CAAC;IACpC,KAAK,IAAI,IAAI,CAAC;CACf;AA8PD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,kBAAkB;IAKjB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAJrC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAa;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAEtB,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,yBAA8B;IAoB9E,IAAI,KAAK,IAAI,uBAAuB,CASnC;IAED,OAAO,CAAC,OAAO;IAIf,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAOtD;;;;;;OAMG;IACH,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAMhF,yHAAyH;IACzH,IAAI,YAAY,IAAI,OAAO,CAE1B;IAED,0CAA0C;IAC1C,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAO3D;;;;OAIG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,UAAU;IAKtD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAmB3E,OAAO,CAAC,eAAe;CAOxB"}
|
|
@@ -1,52 +1,193 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* How long a waiter woken to its turn keeps it for its own continuation to
|
|
3
|
+
* take the credit. workerd cancels the continuations of a request that has
|
|
4
|
+
* completed or been canceled when another request's code resolves its
|
|
5
|
+
* promise (the cross-request promise rule, on by date since 2024-10-14), so
|
|
6
|
+
* a waiter whose request is gone is woken and never takes: its turn lapses
|
|
7
|
+
* and the next waiter's comes. A live continuation takes it in far less.
|
|
8
|
+
*/
|
|
9
|
+
export const CREDIT_TURN_MS = 1000;
|
|
10
|
+
/**
|
|
11
|
+
* An ask queued in a lane. `waiting` for its turn; `woken` to it at
|
|
12
|
+
* `wokenAt`, its credit free for it to take; `lapsed` when the turn passed
|
|
13
|
+
* untaken (out of the queue, back in at the end if it ever takes); `done`
|
|
14
|
+
* once it took its credit, left, or was aborted.
|
|
15
|
+
*/
|
|
16
|
+
class Turn {
|
|
17
|
+
lane;
|
|
18
|
+
bytes;
|
|
19
|
+
signal;
|
|
20
|
+
ahead;
|
|
21
|
+
state = 'waiting';
|
|
22
|
+
wokenAt = 0;
|
|
23
|
+
/** Its wait for the turn it is queued for, resolved as it is woken or aborted. */
|
|
24
|
+
turn;
|
|
25
|
+
wake = () => { };
|
|
26
|
+
/** Its look at the line every CREDIT_TURN_MS while it is queued. */
|
|
27
|
+
recheck = undefined;
|
|
28
|
+
constructor(lane, bytes, signal, ahead) {
|
|
29
|
+
this.lane = lane;
|
|
30
|
+
this.bytes = bytes;
|
|
31
|
+
this.signal = signal;
|
|
32
|
+
this.ahead = ahead;
|
|
33
|
+
this.turn = this.arm();
|
|
34
|
+
}
|
|
35
|
+
/** A fresh wait for its turn. */
|
|
36
|
+
arm() {
|
|
37
|
+
this.state = 'waiting';
|
|
38
|
+
this.turn = new Promise((resolve) => { this.wake = resolve; });
|
|
39
|
+
return this.turn;
|
|
40
|
+
}
|
|
41
|
+
woken() { return this.turn; }
|
|
42
|
+
take() { return this.lane.take(this); }
|
|
43
|
+
leave() { this.lane.leave(this); }
|
|
44
|
+
handleEvent() { this.lane.abandon(this); }
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One byte capacity and its queues: FIFO, but for asks `ahead` of it (a
|
|
48
|
+
* running program's), which are served first, FIFO among themselves.
|
|
49
|
+
*/
|
|
2
50
|
class CreditLane {
|
|
3
51
|
capacity;
|
|
52
|
+
largest;
|
|
4
53
|
current = 0;
|
|
5
54
|
peak = 0;
|
|
6
55
|
waiters = [];
|
|
7
|
-
|
|
56
|
+
ahead = [];
|
|
57
|
+
/** `largest`: the most one lease of this lane may hold (a small request's bound, for the reserve). */
|
|
58
|
+
constructor(capacity, largest = capacity) {
|
|
8
59
|
this.capacity = capacity;
|
|
60
|
+
this.largest = largest;
|
|
9
61
|
}
|
|
10
62
|
get stats() {
|
|
11
|
-
return { current: this.current, peak: this.peak, queued: this.waiters.length };
|
|
63
|
+
return { current: this.current, peak: this.peak, queued: this.waiters.length + this.ahead.length };
|
|
12
64
|
}
|
|
13
|
-
|
|
14
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Whether the asks ahead want more than is free: what reclaiming more is
|
|
67
|
+
* for. Each holds its bytes once its turn is taken, so they are counted
|
|
68
|
+
* together, a turn woken and not yet taken included.
|
|
69
|
+
*/
|
|
70
|
+
get aheadStarved() {
|
|
71
|
+
return this.ahead.reduce((wanted, waiter) => wanted + waiter.bytes, this.current) > this.capacity;
|
|
72
|
+
}
|
|
73
|
+
/** Granted now, or null: an ask ahead waits only for asks ahead of it, any other for every ask queued. */
|
|
74
|
+
tryAcquire(bytes, ahead = false) {
|
|
75
|
+
if (this.ahead.length > 0 || (!ahead && this.waiters.length > 0) || this.current + bytes > this.capacity)
|
|
15
76
|
return null;
|
|
16
77
|
return this.grant(bytes);
|
|
17
78
|
}
|
|
18
79
|
acquire(bytes, signal) {
|
|
80
|
+
return this.wait(bytes, signal, false);
|
|
81
|
+
}
|
|
82
|
+
/** acquire, ahead of every ordinary ask: granted as soon as it fits, after only the asks ahead before it. */
|
|
83
|
+
acquireAhead(bytes, signal) {
|
|
84
|
+
return this.wait(bytes, signal, true);
|
|
85
|
+
}
|
|
86
|
+
wait(bytes, signal, ahead) {
|
|
19
87
|
if (signal?.aborted)
|
|
20
88
|
return Promise.reject(abortError(signal));
|
|
21
|
-
const immediate = this.tryAcquire(bytes);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (index < 0)
|
|
34
|
-
return;
|
|
35
|
-
this.waiters.splice(index, 1);
|
|
36
|
-
signal.removeEventListener('abort', waiter.onAbort);
|
|
37
|
-
reject(abortError(signal));
|
|
38
|
-
this.drain();
|
|
39
|
-
}
|
|
40
|
-
: undefined,
|
|
41
|
-
};
|
|
42
|
-
this.waiters.push(waiter);
|
|
43
|
-
signal?.addEventListener('abort', waiter.onAbort, { once: true });
|
|
44
|
-
if (signal?.aborted) {
|
|
45
|
-
waiter.onAbort();
|
|
46
|
-
return;
|
|
89
|
+
const immediate = this.tryAcquire(bytes, ahead);
|
|
90
|
+
return immediate ? Promise.resolve(immediate) : this.takeInTurn(this.queue(bytes, signal, ahead), signal);
|
|
91
|
+
}
|
|
92
|
+
async takeInTurn(turn, signal) {
|
|
93
|
+
try {
|
|
94
|
+
for (;;) {
|
|
95
|
+
await turn.turn;
|
|
96
|
+
const lease = this.take(turn);
|
|
97
|
+
if (lease)
|
|
98
|
+
return lease;
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
throw abortError(signal);
|
|
47
101
|
}
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
this.leave(turn);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A place at the end of the queue for `bytes` (ahead of every ordinary ask,
|
|
109
|
+
* if `ahead`). While it is queued it looks at the line again every
|
|
110
|
+
* CREDIT_TURN_MS, so a turn another waiter never took lapses even when
|
|
111
|
+
* nothing else touches the lane. Asked with a signal already aborted, it
|
|
112
|
+
* is done at once, as it would be on the abort.
|
|
113
|
+
*/
|
|
114
|
+
queue(bytes, signal, ahead) {
|
|
115
|
+
const turn = new Turn(this, bytes, signal, ahead);
|
|
116
|
+
signal?.addEventListener('abort', turn, { once: true });
|
|
117
|
+
this.place(turn);
|
|
118
|
+
if (signal?.aborted)
|
|
119
|
+
this.abandon(turn);
|
|
120
|
+
else
|
|
121
|
+
this.drain();
|
|
122
|
+
return turn;
|
|
123
|
+
}
|
|
124
|
+
place(turn) {
|
|
125
|
+
(turn.ahead ? this.ahead : this.waiters).push(turn);
|
|
126
|
+
if (turn.recheck !== undefined)
|
|
127
|
+
return;
|
|
128
|
+
const timer = setInterval(() => {
|
|
129
|
+
if (turn.state === 'waiting' || turn.state === 'woken')
|
|
130
|
+
this.drain();
|
|
131
|
+
else {
|
|
132
|
+
clearInterval(timer);
|
|
133
|
+
turn.recheck = undefined;
|
|
134
|
+
}
|
|
135
|
+
}, CREDIT_TURN_MS);
|
|
136
|
+
turn.recheck = timer;
|
|
137
|
+
}
|
|
138
|
+
remove(turn) {
|
|
139
|
+
const queue = turn.ahead ? this.ahead : this.waiters;
|
|
140
|
+
const index = queue.indexOf(turn);
|
|
141
|
+
if (index >= 0)
|
|
142
|
+
queue.splice(index, 1);
|
|
143
|
+
}
|
|
144
|
+
/** Its signal aborted: it is done, its wait ends, and the next turn may come. */
|
|
145
|
+
abandon(turn) {
|
|
146
|
+
if (turn.state === 'done')
|
|
147
|
+
return;
|
|
148
|
+
this.remove(turn);
|
|
149
|
+
turn.state = 'done';
|
|
150
|
+
turn.wake();
|
|
151
|
+
this.drain();
|
|
152
|
+
}
|
|
153
|
+
take(turn) {
|
|
154
|
+
if (turn.state === 'done')
|
|
155
|
+
return null;
|
|
156
|
+
// Taken past its turn: the turn passed, as if a look at the line had seen it first.
|
|
157
|
+
if (turn.state === 'woken' && Date.now() - turn.wokenAt >= CREDIT_TURN_MS) {
|
|
158
|
+
this.remove(turn);
|
|
159
|
+
turn.state = 'lapsed';
|
|
160
|
+
this.drain();
|
|
161
|
+
}
|
|
162
|
+
if (turn.state === 'lapsed') {
|
|
163
|
+
turn.arm();
|
|
164
|
+
this.place(turn);
|
|
48
165
|
this.drain();
|
|
49
|
-
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
// Its turn, its credit still free, and no running ask before an ordinary one: its own now.
|
|
169
|
+
if (turn.state === 'woken' && this.current + turn.bytes <= this.capacity && (turn.ahead || this.ahead.length === 0)) {
|
|
170
|
+
this.remove(turn);
|
|
171
|
+
turn.state = 'done';
|
|
172
|
+
const lease = this.grant(turn.bytes);
|
|
173
|
+
this.drain();
|
|
174
|
+
return lease;
|
|
175
|
+
}
|
|
176
|
+
// Not its turn yet, or no longer room: it waits again, in its place.
|
|
177
|
+
if (turn.state === 'woken')
|
|
178
|
+
turn.arm();
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
leave(turn) {
|
|
182
|
+
turn.signal?.removeEventListener('abort', turn);
|
|
183
|
+
if (turn.recheck !== undefined)
|
|
184
|
+
clearInterval(turn.recheck);
|
|
185
|
+
turn.recheck = undefined;
|
|
186
|
+
if (turn.state !== 'done') {
|
|
187
|
+
this.remove(turn);
|
|
188
|
+
turn.state = 'done';
|
|
189
|
+
}
|
|
190
|
+
this.drain();
|
|
50
191
|
}
|
|
51
192
|
grant(bytes) {
|
|
52
193
|
this.current += bytes;
|
|
@@ -70,6 +211,22 @@ class CreditLane {
|
|
|
70
211
|
leasedBytes = nextBytes;
|
|
71
212
|
this.drain();
|
|
72
213
|
},
|
|
214
|
+
tryGrowTo: (nextBytes) => {
|
|
215
|
+
if (released) {
|
|
216
|
+
throw new Error('cannot grow a released weighted credit lease');
|
|
217
|
+
}
|
|
218
|
+
if (!Number.isSafeInteger(nextBytes) || nextBytes < leasedBytes) {
|
|
219
|
+
throw new RangeError(`weighted credit lease can only grow to a safe integer no smaller than ${leasedBytes}: ${nextBytes}`);
|
|
220
|
+
}
|
|
221
|
+
// Never ahead of a queued ask, as an ordinary tryAcquire.
|
|
222
|
+
if (nextBytes > this.largest || this.ahead.length > 0 || this.waiters.length > 0
|
|
223
|
+
|| this.current + nextBytes - leasedBytes > this.capacity)
|
|
224
|
+
return false;
|
|
225
|
+
this.current += nextBytes - leasedBytes;
|
|
226
|
+
this.peak = Math.max(this.peak, this.current);
|
|
227
|
+
leasedBytes = nextBytes;
|
|
228
|
+
return true;
|
|
229
|
+
},
|
|
73
230
|
release: () => {
|
|
74
231
|
if (released)
|
|
75
232
|
return;
|
|
@@ -82,20 +239,32 @@ class CreditLane {
|
|
|
82
239
|
},
|
|
83
240
|
};
|
|
84
241
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
242
|
+
/**
|
|
243
|
+
* Wake the first waiter whose credit is free to its turn, asks ahead first;
|
|
244
|
+
* the ordinary queue only once none is waiting. The credit is never handed
|
|
245
|
+
* over: the waiter takes it in its own continuation (CreditTurn.take), so
|
|
246
|
+
* one whose continuation never runs holds nothing. A woken turn holds its
|
|
247
|
+
* queue until it is taken or lapses (CREDIT_TURN_MS), one turn at a time.
|
|
248
|
+
*/
|
|
249
|
+
drain(now) {
|
|
250
|
+
for (const queue of [this.ahead, this.waiters]) {
|
|
251
|
+
for (let waiter = queue[0]; waiter !== undefined; waiter = queue[0]) {
|
|
252
|
+
if (waiter.state === 'woken') {
|
|
253
|
+
now ??= Date.now();
|
|
254
|
+
if (now - waiter.wokenAt < CREDIT_TURN_MS)
|
|
255
|
+
return;
|
|
256
|
+
// Never taken: its continuation is gone, or late and back at the end of the line if it ever takes.
|
|
257
|
+
queue.shift();
|
|
258
|
+
waiter.state = 'lapsed';
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (this.current + waiter.bytes > this.capacity)
|
|
262
|
+
return;
|
|
263
|
+
waiter.state = 'woken';
|
|
264
|
+
waiter.wokenAt = now ?? Date.now();
|
|
265
|
+
waiter.wake();
|
|
95
266
|
return;
|
|
96
|
-
|
|
97
|
-
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
98
|
-
waiter.resolve(this.grant(waiter.bytes));
|
|
267
|
+
}
|
|
99
268
|
}
|
|
100
269
|
}
|
|
101
270
|
}
|
|
@@ -143,7 +312,7 @@ export class WeightedCreditPool {
|
|
|
143
312
|
}
|
|
144
313
|
this.general = new CreditLane(capacity);
|
|
145
314
|
this.smallRequestBytes = smallRequestBytes;
|
|
146
|
-
this.small = smallRequestBytes > 0 ? new CreditLane(reserve) : null;
|
|
315
|
+
this.small = smallRequestBytes > 0 ? new CreditLane(reserve, smallRequestBytes) : null;
|
|
147
316
|
}
|
|
148
317
|
get stats() {
|
|
149
318
|
const general = this.general.stats;
|
|
@@ -166,6 +335,38 @@ export class WeightedCreditPool {
|
|
|
166
335
|
return shared;
|
|
167
336
|
return this.small.tryAcquire(bytes);
|
|
168
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* A running program's ask (SupervisorAllocationBudget.acquireRunning),
|
|
340
|
+
* ahead of every ordinary ask: granted now if it fits beside what is held,
|
|
341
|
+
* whatever is queued (a small one in its reserve too), or else as soon as
|
|
342
|
+
* it fits in the shared capacity, the credit running work and reclaims
|
|
343
|
+
* free. It never waits on the reserve, which neither frees.
|
|
344
|
+
*/
|
|
345
|
+
acquireAhead(bytes, signal) {
|
|
346
|
+
this.validateRequest(bytes);
|
|
347
|
+
const now = signal?.aborted ? null : this.tryAcquireAhead(bytes);
|
|
348
|
+
return now ? Promise.resolve(now) : this.general.acquireAhead(bytes, signal);
|
|
349
|
+
}
|
|
350
|
+
/** Whether a running program's ask (acquireAhead) waits for credit no woken turn is taking (CreditLane.aheadStarved). */
|
|
351
|
+
get aheadStarved() {
|
|
352
|
+
return this.general.aheadStarved;
|
|
353
|
+
}
|
|
354
|
+
/** acquireAhead's credit now, or null. */
|
|
355
|
+
tryAcquireAhead(bytes) {
|
|
356
|
+
this.validateRequest(bytes);
|
|
357
|
+
const shared = this.general.tryAcquire(bytes, true);
|
|
358
|
+
const small = this.isSmall(bytes) ? this.small : null;
|
|
359
|
+
return shared ?? small?.tryAcquire(bytes, true) ?? null;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* A place in line for `bytes`, in the lane it would wait in, for a caller
|
|
363
|
+
* that takes its turn itself (CreditTurn): what acquire does once nothing
|
|
364
|
+
* is free.
|
|
365
|
+
*/
|
|
366
|
+
queue(bytes, signal) {
|
|
367
|
+
this.validateRequest(bytes);
|
|
368
|
+
return (this.small !== null && this.isSmall(bytes) ? this.small : this.general).queue(bytes, signal, false);
|
|
369
|
+
}
|
|
169
370
|
acquire(bytes, signal) {
|
|
170
371
|
try {
|
|
171
372
|
this.validateRequest(bytes);
|
|
@@ -173,6 +374,9 @@ export class WeightedCreditPool {
|
|
|
173
374
|
catch (error) {
|
|
174
375
|
return Promise.reject(error);
|
|
175
376
|
}
|
|
377
|
+
// Refused before any lane is asked, whichever would have granted it.
|
|
378
|
+
if (signal?.aborted)
|
|
379
|
+
return Promise.reject(abortError(signal));
|
|
176
380
|
if (this.isSmall(bytes)) {
|
|
177
381
|
// Shared capacity while it is free — a small request should get the
|
|
178
382
|
// same concurrency as any other when nothing is contending. It never
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface JsModule {
|
|
2
|
+
js: string;
|
|
3
|
+
}
|
|
4
|
+
export interface CjsModule {
|
|
5
|
+
cjs: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PyModule {
|
|
8
|
+
py: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TextModule {
|
|
11
|
+
text: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DataModule {
|
|
14
|
+
data: ArrayBuffer;
|
|
15
|
+
}
|
|
16
|
+
export interface JsonModule {
|
|
17
|
+
json: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface WasmModule {
|
|
20
|
+
wasm: ArrayBuffer | WebAssembly.Module;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `bytes` as the ArrayBuffer a `{ wasm }` or `{ data }` member carries: their
|
|
24
|
+
* own buffer when they span all of it, and only otherwise a copy of them, so
|
|
25
|
+
* an image read whole for a load is never held twice.
|
|
26
|
+
*/
|
|
27
|
+
export declare function memberBuffer(bytes: Uint8Array): ArrayBuffer;
|
|
28
|
+
/** Plain strings use the module name's extension. */
|
|
29
|
+
export type ModuleContent = string | JsModule | CjsModule | PyModule | TextModule | DataModule | JsonModule | WasmModule;
|
|
30
|
+
export interface ServiceStub {
|
|
31
|
+
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
|
|
32
|
+
}
|
|
33
|
+
/** The shared Worker Loader contract, including staged resident assemblies. */
|
|
34
|
+
export interface WorkerCode {
|
|
35
|
+
compatibilityDate: string;
|
|
36
|
+
compatibilityFlags?: string[];
|
|
37
|
+
allowExperimental?: boolean;
|
|
38
|
+
limits?: {
|
|
39
|
+
cpuMs?: number;
|
|
40
|
+
subRequests?: number;
|
|
41
|
+
};
|
|
42
|
+
mainModule: string;
|
|
43
|
+
modules: Record<string, ModuleContent>;
|
|
44
|
+
env?: Record<string, unknown>;
|
|
45
|
+
globalOutbound?: ServiceStub | null;
|
|
46
|
+
tails?: ServiceStub[];
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=worker-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-code.d.ts","sourceRoot":"","sources":["../src/worker-code.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,SAAS;IAAG,GAAG,EAAE,MAAM,CAAA;CAAE;AAC1C,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5C,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,CAAA;CAAE;AACjD,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,OAAO,CAAA;CAAE;AAC7C,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,GAAG,WAAW,CAAC,MAAM,CAAA;CAAE;AAEtE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAI3D;AAED,qDAAqD;AACrD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AAEzH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClE;AAED,+EAA+E;AAC/E,MAAM,WAAW,UAAU;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,cAAc,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;CACvB"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bytes` as the ArrayBuffer a `{ wasm }` or `{ data }` member carries: their
|
|
3
|
+
* own buffer when they span all of it, and only otherwise a copy of them, so
|
|
4
|
+
* an image read whole for a load is never held twice.
|
|
5
|
+
*/
|
|
6
|
+
export function memberBuffer(bytes) {
|
|
7
|
+
return bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength && bytes.buffer instanceof ArrayBuffer
|
|
8
|
+
? bytes.buffer
|
|
9
|
+
: bytes.slice().buffer;
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "The measured Cloudflare platform truths Nimbus is built on \u2014 error taxonomy, limits tables, budget coordination, and wire-format primitives, with zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
package/src/composition.ts
CHANGED
|
@@ -33,9 +33,11 @@
|
|
|
33
33
|
* the Durable Object classes, which is what lets the pool be unit-tested
|
|
34
34
|
* in a plain Node/Bun process.
|
|
35
35
|
*/
|
|
36
|
+
import type { WorkerCode } from './worker-code.js';
|
|
37
|
+
|
|
36
38
|
export type EntrypointLoopbackFactory = <Stub = unknown>(options: { props: object }) => Stub;
|
|
37
39
|
export type CtxExports = Record<string, EntrypointLoopbackFactory | undefined>;
|
|
38
|
-
export type StagedBootAssembler = (env: unknown, stage: unknown) => Promise<
|
|
40
|
+
export type StagedBootAssembler = (env: unknown, stage: unknown) => Promise<WorkerCode>;
|
|
39
41
|
|
|
40
42
|
export interface FabricComposition {
|
|
41
43
|
supervisorEntrypoint: string;
|
|
@@ -67,11 +69,17 @@ let composition: FabricComposition | null = null;
|
|
|
67
69
|
let ctxExports: CtxExports | null = null;
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
72
|
+
* The compatibility flags a host composes the fabric under, refused without.
|
|
73
|
+
* Composing refuses wherever it composes (module scope: the Worker fails at
|
|
74
|
+
* startup; NimbusWorkspace.create: that call throws), naming the fix. Off
|
|
75
|
+
* workerd (bun, node) there is no `Cloudflare` global to read, and neither
|
|
76
|
+
* flag has anything to act on.
|
|
77
|
+
*
|
|
78
|
+
* `enhanced_error_serialization`: a program's filesystem errors reach it
|
|
79
|
+
* across workerd RPC, from the host that answers its supervisor binding, and
|
|
80
|
+
* the program branches on the POSIX `code` each one carries. workerd carries
|
|
81
|
+
* an error's own properties across RPC only when the sending AND the
|
|
82
|
+
* receiving isolate have the flag, on by compatibility date from 2026-04-21:
|
|
75
83
|
* the serializer writes a native error as a host object with its own
|
|
76
84
|
* properties only under the flag (src/workerd/jsg/ser.c++,
|
|
77
85
|
* Serializer::IsHostObject), and the receiver reads that detail back only
|
|
@@ -79,22 +87,34 @@ let ctxExports: CtxExports | null = null;
|
|
|
79
87
|
* https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/.
|
|
80
88
|
* Programs load at CF_COMPAT_DATE, past it. The host runs at its embedder's
|
|
81
89
|
* date, and below it every ENOENT would reach every program as a bare message
|
|
82
|
-
* it maps to EIO.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* is
|
|
90
|
+
* it maps to EIO. The flag alone keeps every other behavior of that date.
|
|
91
|
+
*
|
|
92
|
+
* `enable_request_signal`, which no compatibility date enables: a process
|
|
93
|
+
* owns work for a response until its client is done with it, and a client
|
|
94
|
+
* that goes away is seen only as the incoming request's `signal` aborting
|
|
95
|
+
* (src/workerd/io/worker-entrypoint.c++, "The client has disconnected").
|
|
96
|
+
* Without it a response whose client left keeps its process's work, and the
|
|
97
|
+
* host that runs it, held until the process writes again, or for good.
|
|
87
98
|
*/
|
|
88
|
-
function
|
|
99
|
+
function requireHostFlags(): void {
|
|
89
100
|
// `typeof` of an undeclared global is 'undefined', not a ReferenceError.
|
|
90
101
|
if (typeof Cloudflare === 'undefined') return;
|
|
91
|
-
if (Cloudflare.compatibilityFlags.enhanced_error_serialization
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
if (Cloudflare.compatibilityFlags.enhanced_error_serialization !== true) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'fabric: this Worker runs without the enhanced_error_serialization compatibility flag, so the '
|
|
105
|
+
+ "code on a program's filesystem error (ENOENT, EEXIST, ...) would not survive the RPC back "
|
|
106
|
+
+ 'to it. Add "enhanced_error_serialization" to compatibility_flags in your wrangler config, '
|
|
107
|
+
+ 'or set compatibility_date to 2026-04-21 or later.',
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (Cloudflare.compatibilityFlags.enable_request_signal !== true) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
'fabric: this Worker runs without the enable_request_signal compatibility flag, so a process '
|
|
113
|
+
+ 'cannot see a client leave the response it is serving, and keeps that work held. Add '
|
|
114
|
+
+ '"enable_request_signal" to compatibility_flags in your wrangler config; no compatibility '
|
|
115
|
+
+ 'date enables it.',
|
|
116
|
+
);
|
|
117
|
+
}
|
|
98
118
|
}
|
|
99
119
|
|
|
100
120
|
/**
|
|
@@ -102,10 +122,10 @@ function requireEnhancedErrorSerialization(): void {
|
|
|
102
122
|
* a second call with different values throws, naming both, so an embedder
|
|
103
123
|
* whose composition lost to an earlier import learns it at startup rather
|
|
104
124
|
* than from a facet that reached the wrong host. A host without
|
|
105
|
-
* `enhanced_error_serialization` is refused.
|
|
125
|
+
* `enhanced_error_serialization` or `enable_request_signal` is refused.
|
|
106
126
|
*/
|
|
107
127
|
export function composeFabric(value: FabricComposition): void {
|
|
108
|
-
|
|
128
|
+
requireHostFlags();
|
|
109
129
|
if (!composition) {
|
|
110
130
|
composition = value;
|
|
111
131
|
return;
|
package/src/crc32.ts
CHANGED
|
@@ -9,26 +9,23 @@
|
|
|
9
9
|
* Measured 2026-09-24 (5k files x 2 KiB; 64 MiB in 1 MiB buffers):
|
|
10
10
|
* node:zlib's crc32 is 5-40x faster than any JavaScript loop in bun, node and
|
|
11
11
|
* workerd (compat 2026-04-01, nodejs_compat), but each call costs ~0.14 us in
|
|
12
|
-
* workerd, so below 128 bytes the table loop wins there. It is
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* workerd, so below 128 bytes the table loop wins there. It is the realm's
|
|
13
|
+
* own (realmBuiltin), like core's incremental SHA-256's, so this module still
|
|
14
|
+
* loads where node:zlib is absent (a browser, workerd without nodejs_compat).
|
|
15
|
+
* The JavaScript path is slicing-by-8: 2-3x the byte-at-a-time table loop in
|
|
16
|
+
* bun and node, and level with it in workerd.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { realmBuiltin } from './realm-builtin.js';
|
|
20
|
+
|
|
19
21
|
type NodeZlib = { crc32?(data: Uint8Array, value?: number): number };
|
|
20
|
-
type BuiltinModules = { process?: { getBuiltinModule?(id: 'node:zlib'): NodeZlib | undefined } };
|
|
21
22
|
|
|
22
23
|
/** Below this many bytes the native call's overhead exceeds the JavaScript loop's work (workerd). */
|
|
23
24
|
const CRC_NATIVE_MIN_BYTES = 128;
|
|
24
25
|
|
|
25
26
|
const nativeCrc32 = (() => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return typeof crc32 === 'function' ? crc32 : null;
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
27
|
+
const crc32 = realmBuiltin<NodeZlib>('node:zlib')?.crc32;
|
|
28
|
+
return typeof crc32 === 'function' ? crc32 : null;
|
|
32
29
|
})();
|
|
33
30
|
|
|
34
31
|
/** Eight 256-entry tables: table k maps a byte to its CRC after k further zero bytes. */
|