@nimbus-sh/platform 0.2.0 → 0.4.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 +25 -1
- package/dist/composition.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.d.ts +13 -0
- package/dist/heavy-alloc-coord.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.js +19 -2
- package/dist/weighted-credit-pool.d.ts +13 -0
- package/dist/weighted-credit-pool.d.ts.map +1 -1
- package/dist/weighted-credit-pool.js +54 -6
- package/package.json +1 -1
- package/src/composition.ts +25 -1
- package/src/heavy-alloc-coord.ts +26 -1
- package/src/weighted-credit-pool.ts +61 -6
package/dist/composition.d.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* composition.ts — the ONE seam an embedder wires the fabric through.
|
|
3
|
+
*
|
|
4
|
+
* The fabric mints supervisor bindings and assembles staged boots for the
|
|
5
|
+
* programs it hosts, but the entrypoint class that answers those bindings
|
|
6
|
+
* and the artifact sources a stage names both belong to the embedder. The
|
|
7
|
+
* embedder states them once, in its composition root, with one call:
|
|
8
|
+
*
|
|
9
|
+
* composeFabric({
|
|
10
|
+
* supervisorEntrypoint: 'SupervisorRPC',
|
|
11
|
+
* stagedBootAssembler: (env, stage) => assembleConfig(env, stage),
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* First-write-wins, like every holder in this module: the composition
|
|
15
|
+
* root's module scope runs once per isolate, before any request.
|
|
16
|
+
*
|
|
17
|
+
* `ctx.exports` is runtime state, not composition: workerd mints it per
|
|
18
|
+
* instance, so the embedder captures it where the platform hands it over —
|
|
19
|
+
* the first fetch, or the DO constructor — with {@link adoptCtxExports}.
|
|
20
|
+
*
|
|
21
|
+
* This module stays a leaf (no fabric imports) so helpers (notably
|
|
22
|
+
* isolate-pool.ts) can read `ctx.exports` without transitively importing
|
|
23
|
+
* the Durable Object classes, which is what lets the pool be unit-tested
|
|
24
|
+
* in a plain Node/Bun process.
|
|
25
|
+
*/
|
|
2
26
|
export type EntrypointLoopbackFactory = <Stub = unknown>(options: {
|
|
3
27
|
props: object;
|
|
4
28
|
}) => Stub;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"composition.d.ts","sourceRoot":"","sources":["../src/composition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,IAAI,GAAG,OAAO,EAAE,OAAO,EAAE;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,KAAK,IAAI,CAAC;AAC7F,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,yBAAyB,GAAG,SAAS,CAAC,CAAC;AAC/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEpF,MAAM,WAAW,iBAAiB;IAChC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,sBAAsB,mBAAmB,CAAC;AACvD,eAAO,MAAM,4BAA4B,iBAAiB,CAAC;AAI3D,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAE5D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAEvD;AAED,wBAAgB,aAAa,IAAI,UAAU,GAAG,IAAI,CAEjD;AAED,wBAAgB,oBAAoB,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,yBAAyB,GAAG,IAAI,CAO3F;AAED,wBAAgB,wBAAwB,IAAI,MAAM,GAAG,IAAI,CAExD;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,mBAAmB,IAAI,mBAAmB,CASzD"}
|
|
@@ -13,6 +13,8 @@ export interface SupervisorAllocationBudgetStats {
|
|
|
13
13
|
readonly current: number;
|
|
14
14
|
readonly peak: number;
|
|
15
15
|
readonly queued: number;
|
|
16
|
+
/** Credit held for an owner's whole lifetime; see `acquireResident`. */
|
|
17
|
+
readonly resident: number;
|
|
16
18
|
}
|
|
17
19
|
interface AllocationBudgetLifecycle {
|
|
18
20
|
onActive?: () => void;
|
|
@@ -51,6 +53,12 @@ export declare class SupervisorAllocationBudget {
|
|
|
51
53
|
* stops a large read overlapping a large write.
|
|
52
54
|
*/
|
|
53
55
|
acquireWithoutLifecycle(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
56
|
+
/**
|
|
57
|
+
* Reserve bytes an owner holds for its whole lifetime, not for one
|
|
58
|
+
* operation. Recorded as a floor: a later claim larger than what remains
|
|
59
|
+
* around it is refused with both numbers rather than parked forever.
|
|
60
|
+
*/
|
|
61
|
+
acquireResidentBytes(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
54
62
|
private _acquire;
|
|
55
63
|
}
|
|
56
64
|
/**
|
|
@@ -83,6 +91,11 @@ export declare function acquireSupervisorAllocation(bytes: number, signal?: Abor
|
|
|
83
91
|
* there to fill.
|
|
84
92
|
*/
|
|
85
93
|
export declare function acquireSupervisorReadAllocation(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
94
|
+
/**
|
|
95
|
+
* Reserve bytes for the whole life of the owner that takes them — an image
|
|
96
|
+
* a pool keeps until it is disposed. See `acquireResidentBytes`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function acquireResidentSupervisorAllocation(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
86
99
|
/**
|
|
87
100
|
* Reserve the full budget for an allocation whose retained size is not known
|
|
88
101
|
* accurately enough to overlap safely with other heavy work.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"heavy-alloc-coord.d.ts","sourceRoot":"","sources":["../src/heavy-alloc-coord.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC/B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"heavy-alloc-coord.d.ts","sourceRoot":"","sources":["../src/heavy-alloc-coord.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAOH,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC/B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,UAAU,yBAAyB;IACjC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,0BAA0B;IAWnC,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAX5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,MAAM,CAAS;IACvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB,CAAK;gBAGlB,QAAQ,EAAE,MAAM,EACR,SAAS,GAAE,yBAA8B,EAC1D,OAAO,GAAE,yBAA8B;IAKzC,IAAI,KAAK,IAAI,+BAA+B,CAK3C;IAED;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAI3E;;;;;;;;OAQG;IACH,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAI3F;;;;OAIG;IACH,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;YAI1E,QAAQ;CAsCvB;AAED;;;GAGG;AACH,UAAU,aAAa;IACrB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;CACxB;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,aAAa,GAAG,MAAM,IAAI,CAWlE;AAqCD;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED;;;GAGG;AACH,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,MAAM,EACb,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAMjF;AAED,wBAAgB,8BAA8B,IAAI,+BAA+B,CAEhF"}
|
|
@@ -55,8 +55,18 @@ export class SupervisorAllocationBudget {
|
|
|
55
55
|
acquireWithoutLifecycle(bytes, signal) {
|
|
56
56
|
return this._acquire(bytes, signal, false);
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Reserve bytes an owner holds for its whole lifetime, not for one
|
|
60
|
+
* operation. Recorded as a floor: a later claim larger than what remains
|
|
61
|
+
* around it is refused with both numbers rather than parked forever.
|
|
62
|
+
*/
|
|
63
|
+
acquireResidentBytes(bytes, signal) {
|
|
64
|
+
return this._acquire(bytes, signal, true, true);
|
|
65
|
+
}
|
|
66
|
+
async _acquire(bytes, signal, drivesLifecycle, resident = false) {
|
|
67
|
+
const credit = resident
|
|
68
|
+
? await this.credits.acquireResident(bytes, signal)
|
|
69
|
+
: await this.credits.acquire(bytes, signal);
|
|
60
70
|
if (drivesLifecycle) {
|
|
61
71
|
this.lifecycleHolders++;
|
|
62
72
|
if (!this.active) {
|
|
@@ -168,6 +178,13 @@ export function acquireSupervisorAllocation(bytes, signal) {
|
|
|
168
178
|
export function acquireSupervisorReadAllocation(bytes, signal) {
|
|
169
179
|
return supervisorAllocationBudget.acquireWithoutLifecycle(bytes, signal);
|
|
170
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Reserve bytes for the whole life of the owner that takes them — an image
|
|
183
|
+
* a pool keeps until it is disposed. See `acquireResidentBytes`.
|
|
184
|
+
*/
|
|
185
|
+
export function acquireResidentSupervisorAllocation(bytes, signal) {
|
|
186
|
+
return supervisorAllocationBudget.acquireResidentBytes(bytes, signal);
|
|
187
|
+
}
|
|
171
188
|
/**
|
|
172
189
|
* Reserve the full budget for an allocation whose retained size is not known
|
|
173
190
|
* accurately enough to overlap safely with other heavy work.
|
|
@@ -9,6 +9,11 @@ export interface WeightedCreditPoolStats {
|
|
|
9
9
|
readonly current: number;
|
|
10
10
|
readonly peak: number;
|
|
11
11
|
readonly queued: number;
|
|
12
|
+
/**
|
|
13
|
+
* Credit held by owners that keep it for their whole lifetime rather than
|
|
14
|
+
* for one operation — the floor no waiter can ever expect to get back.
|
|
15
|
+
*/
|
|
16
|
+
readonly resident: number;
|
|
12
17
|
}
|
|
13
18
|
export interface WeightedCreditPoolOptions {
|
|
14
19
|
/**
|
|
@@ -56,6 +61,14 @@ export declare class WeightedCreditPool {
|
|
|
56
61
|
private isSmall;
|
|
57
62
|
tryAcquire(bytes: number): ResizableCreditLease | null;
|
|
58
63
|
acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
64
|
+
/**
|
|
65
|
+
* Take credit that will be held for the owner's whole lifetime rather than
|
|
66
|
+
* for one operation — the esbuild pool's wasm image is the case.
|
|
67
|
+
*
|
|
68
|
+
* Recorded as a floor so a later claim that could never fit around it is
|
|
69
|
+
* refused with both numbers instead of parking in the FIFO forever.
|
|
70
|
+
*/
|
|
71
|
+
acquireResident(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease>;
|
|
59
72
|
private validateRequest;
|
|
60
73
|
}
|
|
61
74
|
//# sourceMappingURL=weighted-credit-pool.d.ts.map
|
|
@@ -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;CAC/B;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;
|
|
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;CAC/B;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;IACxB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;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;AAmJD;;;;;;;;;;;;;;;;;;;;;;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,CAUnC;IAED,OAAO,CAAC,OAAO;IAIf,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAOtD,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAiB3E;;;;;;OAMG;IACH,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IASnF,OAAO,CAAC,eAAe;CAOxB"}
|
|
@@ -3,27 +3,52 @@ class CreditLane {
|
|
|
3
3
|
capacity;
|
|
4
4
|
current = 0;
|
|
5
5
|
peak = 0;
|
|
6
|
+
resident = 0;
|
|
6
7
|
waiters = [];
|
|
7
8
|
constructor(capacity) {
|
|
8
9
|
this.capacity = capacity;
|
|
9
10
|
}
|
|
10
11
|
get stats() {
|
|
11
|
-
return {
|
|
12
|
+
return {
|
|
13
|
+
current: this.current, peak: this.peak, queued: this.waiters.length, resident: this.resident,
|
|
14
|
+
};
|
|
12
15
|
}
|
|
13
16
|
tryAcquire(bytes) {
|
|
14
17
|
if (this.waiters.length > 0 || this.current + bytes > this.capacity)
|
|
15
18
|
return null;
|
|
16
|
-
return this.grant(bytes);
|
|
19
|
+
return this.grant(bytes, false);
|
|
17
20
|
}
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The largest claim this lane can ever grant: capacity less what resident
|
|
23
|
+
* owners hold for their whole lifetime. A claim above it cannot be
|
|
24
|
+
* satisfied by waiting, because nothing it is waiting for will be
|
|
25
|
+
* released.
|
|
26
|
+
*/
|
|
27
|
+
get grantableCeiling() {
|
|
28
|
+
return this.capacity - this.resident;
|
|
29
|
+
}
|
|
30
|
+
acquire(bytes, signal, resident = false) {
|
|
19
31
|
if (signal?.aborted)
|
|
20
32
|
return Promise.reject(abortError(signal));
|
|
21
|
-
|
|
33
|
+
// Refuse a claim no amount of waiting can satisfy, rather than parking it
|
|
34
|
+
// in the FIFO forever. A parked claim is indistinguishable from a caller
|
|
35
|
+
// that stopped: no error, no timeout, and — because the queue refuses
|
|
36
|
+
// everyone behind it — no further progress anywhere in the isolate. That
|
|
37
|
+
// is what a real-vite launch looked like for four rounds of debugging.
|
|
38
|
+
if (bytes > this.grantableCeiling) {
|
|
39
|
+
return Promise.reject(new RangeError(`weighted credit claim of ${bytes} bytes can never be granted: capacity is ${this.capacity} `
|
|
40
|
+
+ `and ${this.resident} bytes are held by resident owners, leaving ${this.grantableCeiling}`));
|
|
41
|
+
}
|
|
42
|
+
const immediate = resident ? null : this.tryAcquire(bytes);
|
|
22
43
|
if (immediate)
|
|
23
44
|
return Promise.resolve(immediate);
|
|
45
|
+
if (resident && this.waiters.length === 0 && this.current + bytes <= this.capacity) {
|
|
46
|
+
return Promise.resolve(this.grant(bytes, true));
|
|
47
|
+
}
|
|
24
48
|
return new Promise((resolve, reject) => {
|
|
25
49
|
const waiter = {
|
|
26
50
|
bytes,
|
|
51
|
+
resident,
|
|
27
52
|
resolve,
|
|
28
53
|
reject,
|
|
29
54
|
signal,
|
|
@@ -48,8 +73,10 @@ class CreditLane {
|
|
|
48
73
|
this.drain();
|
|
49
74
|
});
|
|
50
75
|
}
|
|
51
|
-
grant(bytes) {
|
|
76
|
+
grant(bytes, resident) {
|
|
52
77
|
this.current += bytes;
|
|
78
|
+
if (resident)
|
|
79
|
+
this.resident += bytes;
|
|
53
80
|
this.peak = Math.max(this.peak, this.current);
|
|
54
81
|
let leasedBytes = bytes;
|
|
55
82
|
let released = false;
|
|
@@ -67,6 +94,8 @@ class CreditLane {
|
|
|
67
94
|
throw new RangeError(`weighted credit lease can only shrink to a positive safe integer no larger than ${leasedBytes}: ${nextBytes}`);
|
|
68
95
|
}
|
|
69
96
|
this.current -= leasedBytes - nextBytes;
|
|
97
|
+
if (resident)
|
|
98
|
+
this.resident -= leasedBytes - nextBytes;
|
|
70
99
|
leasedBytes = nextBytes;
|
|
71
100
|
this.drain();
|
|
72
101
|
},
|
|
@@ -75,6 +104,8 @@ class CreditLane {
|
|
|
75
104
|
return;
|
|
76
105
|
released = true;
|
|
77
106
|
this.current -= leasedBytes;
|
|
107
|
+
if (resident)
|
|
108
|
+
this.resident -= leasedBytes;
|
|
78
109
|
if (this.current < 0) {
|
|
79
110
|
throw new Error('weighted credit accounting underflow');
|
|
80
111
|
}
|
|
@@ -95,7 +126,7 @@ class CreditLane {
|
|
|
95
126
|
return;
|
|
96
127
|
this.waiters.shift();
|
|
97
128
|
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
98
|
-
waiter.resolve(this.grant(waiter.bytes));
|
|
129
|
+
waiter.resolve(this.grant(waiter.bytes, waiter.resident));
|
|
99
130
|
}
|
|
100
131
|
}
|
|
101
132
|
}
|
|
@@ -154,6 +185,7 @@ export class WeightedCreditPool {
|
|
|
154
185
|
current: general.current + small.current,
|
|
155
186
|
peak: general.peak + small.peak,
|
|
156
187
|
queued: general.queued + small.queued,
|
|
188
|
+
resident: general.resident + small.resident,
|
|
157
189
|
};
|
|
158
190
|
}
|
|
159
191
|
isSmall(bytes) {
|
|
@@ -184,6 +216,22 @@ export class WeightedCreditPool {
|
|
|
184
216
|
}
|
|
185
217
|
return this.general.acquire(bytes, signal);
|
|
186
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Take credit that will be held for the owner's whole lifetime rather than
|
|
221
|
+
* for one operation — the esbuild pool's wasm image is the case.
|
|
222
|
+
*
|
|
223
|
+
* Recorded as a floor so a later claim that could never fit around it is
|
|
224
|
+
* refused with both numbers instead of parking in the FIFO forever.
|
|
225
|
+
*/
|
|
226
|
+
acquireResident(bytes, signal) {
|
|
227
|
+
try {
|
|
228
|
+
this.validateRequest(bytes);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
return Promise.reject(error);
|
|
232
|
+
}
|
|
233
|
+
return this.general.acquire(bytes, signal, true);
|
|
234
|
+
}
|
|
187
235
|
validateRequest(bytes) {
|
|
188
236
|
if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > this.capacity) {
|
|
189
237
|
throw new RangeError(`weighted credit request must be a positive safe integer no larger than ${this.capacity}: ${bytes}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The measured Cloudflare platform truths Nimbus is built on — 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
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* composition.ts — the ONE seam an embedder wires the fabric through.
|
|
3
|
+
*
|
|
4
|
+
* The fabric mints supervisor bindings and assembles staged boots for the
|
|
5
|
+
* programs it hosts, but the entrypoint class that answers those bindings
|
|
6
|
+
* and the artifact sources a stage names both belong to the embedder. The
|
|
7
|
+
* embedder states them once, in its composition root, with one call:
|
|
8
|
+
*
|
|
9
|
+
* composeFabric({
|
|
10
|
+
* supervisorEntrypoint: 'SupervisorRPC',
|
|
11
|
+
* stagedBootAssembler: (env, stage) => assembleConfig(env, stage),
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* First-write-wins, like every holder in this module: the composition
|
|
15
|
+
* root's module scope runs once per isolate, before any request.
|
|
16
|
+
*
|
|
17
|
+
* `ctx.exports` is runtime state, not composition: workerd mints it per
|
|
18
|
+
* instance, so the embedder captures it where the platform hands it over —
|
|
19
|
+
* the first fetch, or the DO constructor — with {@link adoptCtxExports}.
|
|
20
|
+
*
|
|
21
|
+
* This module stays a leaf (no fabric imports) so helpers (notably
|
|
22
|
+
* isolate-pool.ts) can read `ctx.exports` without transitively importing
|
|
23
|
+
* the Durable Object classes, which is what lets the pool be unit-tested
|
|
24
|
+
* in a plain Node/Bun process.
|
|
25
|
+
*/
|
|
2
26
|
export type EntrypointLoopbackFactory = <Stub = unknown>(options: { props: object }) => Stub;
|
|
3
27
|
export type CtxExports = Record<string, EntrypointLoopbackFactory | undefined>;
|
|
4
28
|
export type StagedBootAssembler = (env: unknown, stage: unknown) => Promise<object>;
|
package/src/heavy-alloc-coord.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface SupervisorAllocationBudgetStats {
|
|
|
24
24
|
readonly current: number;
|
|
25
25
|
readonly peak: number;
|
|
26
26
|
readonly queued: number;
|
|
27
|
+
/** Credit held for an owner's whole lifetime; see `acquireResident`. */
|
|
28
|
+
readonly resident: number;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
interface AllocationBudgetLifecycle {
|
|
@@ -82,12 +84,24 @@ export class SupervisorAllocationBudget {
|
|
|
82
84
|
return this._acquire(bytes, signal, false);
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Reserve bytes an owner holds for its whole lifetime, not for one
|
|
89
|
+
* operation. Recorded as a floor: a later claim larger than what remains
|
|
90
|
+
* around it is refused with both numbers rather than parked forever.
|
|
91
|
+
*/
|
|
92
|
+
acquireResidentBytes(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
93
|
+
return this._acquire(bytes, signal, true, true);
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
private async _acquire(
|
|
86
97
|
bytes: number,
|
|
87
98
|
signal: AbortSignal | undefined,
|
|
88
99
|
drivesLifecycle: boolean,
|
|
100
|
+
resident = false,
|
|
89
101
|
): Promise<ResizableCreditLease> {
|
|
90
|
-
const credit =
|
|
102
|
+
const credit = resident
|
|
103
|
+
? await this.credits.acquireResident(bytes, signal)
|
|
104
|
+
: await this.credits.acquire(bytes, signal);
|
|
91
105
|
if (drivesLifecycle) {
|
|
92
106
|
this.lifecycleHolders++;
|
|
93
107
|
if (!this.active) {
|
|
@@ -224,6 +238,17 @@ export function acquireSupervisorReadAllocation(
|
|
|
224
238
|
return supervisorAllocationBudget.acquireWithoutLifecycle(bytes, signal);
|
|
225
239
|
}
|
|
226
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Reserve bytes for the whole life of the owner that takes them — an image
|
|
243
|
+
* a pool keeps until it is disposed. See `acquireResidentBytes`.
|
|
244
|
+
*/
|
|
245
|
+
export function acquireResidentSupervisorAllocation(
|
|
246
|
+
bytes: number,
|
|
247
|
+
signal?: AbortSignal,
|
|
248
|
+
): Promise<ResizableCreditLease> {
|
|
249
|
+
return supervisorAllocationBudget.acquireResidentBytes(bytes, signal);
|
|
250
|
+
}
|
|
251
|
+
|
|
227
252
|
/**
|
|
228
253
|
* Reserve the full budget for an allocation whose retained size is not known
|
|
229
254
|
* accurately enough to overlap safely with other heavy work.
|
|
@@ -11,6 +11,11 @@ export interface WeightedCreditPoolStats {
|
|
|
11
11
|
readonly current: number;
|
|
12
12
|
readonly peak: number;
|
|
13
13
|
readonly queued: number;
|
|
14
|
+
/**
|
|
15
|
+
* Credit held by owners that keep it for their whole lifetime rather than
|
|
16
|
+
* for one operation — the floor no waiter can ever expect to get back.
|
|
17
|
+
*/
|
|
18
|
+
readonly resident: number;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export interface WeightedCreditPoolOptions {
|
|
@@ -29,6 +34,8 @@ export interface WeightedCreditPoolOptions {
|
|
|
29
34
|
|
|
30
35
|
interface CreditWaiter {
|
|
31
36
|
readonly bytes: number;
|
|
37
|
+
/** Whether the granted lease counts toward the resident floor. */
|
|
38
|
+
readonly resident: boolean;
|
|
32
39
|
readonly resolve: (lease: ResizableCreditLease) => void;
|
|
33
40
|
readonly reject: (error: Error) => void;
|
|
34
41
|
readonly signal?: AbortSignal;
|
|
@@ -39,27 +46,55 @@ interface CreditWaiter {
|
|
|
39
46
|
class CreditLane {
|
|
40
47
|
private current = 0;
|
|
41
48
|
private peak = 0;
|
|
49
|
+
private resident = 0;
|
|
42
50
|
private readonly waiters: CreditWaiter[] = [];
|
|
43
51
|
|
|
44
52
|
constructor(readonly capacity: number) {}
|
|
45
53
|
|
|
46
54
|
get stats(): WeightedCreditPoolStats {
|
|
47
|
-
return {
|
|
55
|
+
return {
|
|
56
|
+
current: this.current, peak: this.peak, queued: this.waiters.length, resident: this.resident,
|
|
57
|
+
};
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
tryAcquire(bytes: number): ResizableCreditLease | null {
|
|
51
61
|
if (this.waiters.length > 0 || this.current + bytes > this.capacity) return null;
|
|
52
|
-
return this.grant(bytes);
|
|
62
|
+
return this.grant(bytes, false);
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The largest claim this lane can ever grant: capacity less what resident
|
|
67
|
+
* owners hold for their whole lifetime. A claim above it cannot be
|
|
68
|
+
* satisfied by waiting, because nothing it is waiting for will be
|
|
69
|
+
* released.
|
|
70
|
+
*/
|
|
71
|
+
get grantableCeiling(): number {
|
|
72
|
+
return this.capacity - this.resident;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
acquire(bytes: number, signal?: AbortSignal, resident = false): Promise<ResizableCreditLease> {
|
|
56
76
|
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
57
|
-
|
|
77
|
+
// Refuse a claim no amount of waiting can satisfy, rather than parking it
|
|
78
|
+
// in the FIFO forever. A parked claim is indistinguishable from a caller
|
|
79
|
+
// that stopped: no error, no timeout, and — because the queue refuses
|
|
80
|
+
// everyone behind it — no further progress anywhere in the isolate. That
|
|
81
|
+
// is what a real-vite launch looked like for four rounds of debugging.
|
|
82
|
+
if (bytes > this.grantableCeiling) {
|
|
83
|
+
return Promise.reject(new RangeError(
|
|
84
|
+
`weighted credit claim of ${bytes} bytes can never be granted: capacity is ${this.capacity} `
|
|
85
|
+
+ `and ${this.resident} bytes are held by resident owners, leaving ${this.grantableCeiling}`,
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
const immediate = resident ? null : this.tryAcquire(bytes);
|
|
58
89
|
if (immediate) return Promise.resolve(immediate);
|
|
90
|
+
if (resident && this.waiters.length === 0 && this.current + bytes <= this.capacity) {
|
|
91
|
+
return Promise.resolve(this.grant(bytes, true));
|
|
92
|
+
}
|
|
59
93
|
|
|
60
94
|
return new Promise<ResizableCreditLease>((resolve, reject) => {
|
|
61
95
|
const waiter: CreditWaiter = {
|
|
62
96
|
bytes,
|
|
97
|
+
resident,
|
|
63
98
|
resolve,
|
|
64
99
|
reject,
|
|
65
100
|
signal,
|
|
@@ -84,8 +119,9 @@ class CreditLane {
|
|
|
84
119
|
});
|
|
85
120
|
}
|
|
86
121
|
|
|
87
|
-
private grant(bytes: number): ResizableCreditLease {
|
|
122
|
+
private grant(bytes: number, resident: boolean): ResizableCreditLease {
|
|
88
123
|
this.current += bytes;
|
|
124
|
+
if (resident) this.resident += bytes;
|
|
89
125
|
this.peak = Math.max(this.peak, this.current);
|
|
90
126
|
let leasedBytes = bytes;
|
|
91
127
|
let released = false;
|
|
@@ -107,6 +143,7 @@ class CreditLane {
|
|
|
107
143
|
);
|
|
108
144
|
}
|
|
109
145
|
this.current -= leasedBytes - nextBytes;
|
|
146
|
+
if (resident) this.resident -= leasedBytes - nextBytes;
|
|
110
147
|
leasedBytes = nextBytes;
|
|
111
148
|
this.drain();
|
|
112
149
|
},
|
|
@@ -114,6 +151,7 @@ class CreditLane {
|
|
|
114
151
|
if (released) return;
|
|
115
152
|
released = true;
|
|
116
153
|
this.current -= leasedBytes;
|
|
154
|
+
if (resident) this.resident -= leasedBytes;
|
|
117
155
|
if (this.current < 0) {
|
|
118
156
|
throw new Error('weighted credit accounting underflow');
|
|
119
157
|
}
|
|
@@ -134,7 +172,7 @@ class CreditLane {
|
|
|
134
172
|
if (this.current + waiter.bytes > this.capacity) return;
|
|
135
173
|
this.waiters.shift();
|
|
136
174
|
waiter.signal?.removeEventListener('abort', waiter.onAbort!);
|
|
137
|
-
waiter.resolve(this.grant(waiter.bytes));
|
|
175
|
+
waiter.resolve(this.grant(waiter.bytes, waiter.resident));
|
|
138
176
|
}
|
|
139
177
|
}
|
|
140
178
|
}
|
|
@@ -195,6 +233,7 @@ export class WeightedCreditPool {
|
|
|
195
233
|
current: general.current + small.current,
|
|
196
234
|
peak: general.peak + small.peak,
|
|
197
235
|
queued: general.queued + small.queued,
|
|
236
|
+
resident: general.resident + small.resident,
|
|
198
237
|
};
|
|
199
238
|
}
|
|
200
239
|
|
|
@@ -226,6 +265,22 @@ export class WeightedCreditPool {
|
|
|
226
265
|
return this.general.acquire(bytes, signal);
|
|
227
266
|
}
|
|
228
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Take credit that will be held for the owner's whole lifetime rather than
|
|
270
|
+
* for one operation — the esbuild pool's wasm image is the case.
|
|
271
|
+
*
|
|
272
|
+
* Recorded as a floor so a later claim that could never fit around it is
|
|
273
|
+
* refused with both numbers instead of parking in the FIFO forever.
|
|
274
|
+
*/
|
|
275
|
+
acquireResident(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
|
|
276
|
+
try {
|
|
277
|
+
this.validateRequest(bytes);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return Promise.reject(error as Error);
|
|
280
|
+
}
|
|
281
|
+
return this.general.acquire(bytes, signal, true);
|
|
282
|
+
}
|
|
283
|
+
|
|
229
284
|
private validateRequest(bytes: number): void {
|
|
230
285
|
if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > this.capacity) {
|
|
231
286
|
throw new RangeError(
|