@nimbus-sh/platform 0.3.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.
@@ -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;CACzB;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;YAI7E,QAAQ;CAmCvB;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,wBAAsB,iBAAiB,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAMjF;AAED,wBAAgB,8BAA8B,IAAI,+BAA+B,CAEhF"}
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
- async _acquire(bytes, signal, drivesLifecycle) {
59
- const credit = await this.credits.acquire(bytes, signal);
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;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;AAkHD;;;;;;;;;;;;;;;;;;;;;;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,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAiB3E,OAAO,CAAC,eAAe;CAOxB"}
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 { current: this.current, peak: this.peak, queued: this.waiters.length };
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
- acquire(bytes, signal) {
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
- const immediate = this.tryAcquire(bytes);
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.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",
@@ -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 = await this.credits.acquire(bytes, signal);
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 { current: this.current, peak: this.peak, queued: this.waiters.length };
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
- acquire(bytes: number, signal?: AbortSignal): Promise<ResizableCreditLease> {
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
- const immediate = this.tryAcquire(bytes);
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(