@nimbus-sh/platform 0.5.1 → 0.7.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 +2 -1
- package/dist/composition.d.ts.map +1 -1
- package/dist/composition.js +33 -1
- package/dist/crc32.d.ts +19 -0
- package/dist/crc32.d.ts.map +1 -0
- package/dist/crc32.js +67 -0
- package/dist/diag-counters.d.ts +0 -8
- package/dist/diag-counters.d.ts.map +1 -1
- package/dist/diag-counters.js +0 -6
- package/dist/heap-estimate.d.ts +1 -7
- package/dist/heap-estimate.d.ts.map +1 -1
- package/dist/heap-estimate.js +1 -3
- package/dist/heavy-alloc-coord.d.ts +0 -13
- package/dist/heavy-alloc-coord.d.ts.map +1 -1
- package/dist/heavy-alloc-coord.js +2 -19
- package/dist/limits.d.ts +4 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +29 -1
- package/dist/oom-classify.d.ts +1 -1
- package/dist/oom-classify.d.ts.map +1 -1
- package/dist/oom-classify.js +8 -6
- package/dist/tracing.d.ts +65 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +107 -0
- package/dist/w7-frame.d.ts.map +1 -1
- package/dist/w7-frame.js +12 -35
- package/dist/weighted-credit-pool.d.ts +0 -13
- package/dist/weighted-credit-pool.d.ts.map +1 -1
- package/dist/weighted-credit-pool.js +6 -54
- package/package.json +4 -4
- package/src/composition.ts +34 -1
- package/src/crc32.ts +71 -0
- package/src/diag-counters.ts +0 -13
- package/src/heap-estimate.ts +1 -9
- package/src/heavy-alloc-coord.ts +1 -26
- package/src/limits.ts +32 -1
- package/src/oom-classify.ts +8 -6
- package/src/tracing.ts +129 -0
- package/src/w7-frame.ts +12 -38
- package/src/weighted-credit-pool.ts +6 -61
package/dist/tracing.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tracing.ts — Workers custom spans, for modules that cannot import
|
|
3
|
+
* `cloudflare:workers`, as best-effort telemetry that never changes an
|
|
4
|
+
* outcome.
|
|
5
|
+
*
|
|
6
|
+
* The span API is `tracing` from `cloudflare:workers`
|
|
7
|
+
* (https://developers.cloudflare.com/workers/observability/traces/custom-spans/):
|
|
8
|
+
* `enterSpan(name, callback)` runs the callback in a span that is the active
|
|
9
|
+
* parent of every span and platform call made inside it — automatic RPC
|
|
10
|
+
* spans included — and ends it when the callback returns or its promise
|
|
11
|
+
* settles. A span records nothing unless the invocation was head-sampled
|
|
12
|
+
* (`observability.traces.head_sampling_rate`).
|
|
13
|
+
*
|
|
14
|
+
* `setAttributes` and `recordException` shipped on 2026-09-25
|
|
15
|
+
* (https://developers.cloudflare.com/changelog/post/2026-09-25-custom-span-apis/);
|
|
16
|
+
* a runtime from before then has `isTraced` and `setAttribute` only. So the
|
|
17
|
+
* code being traced never touches the runtime's span. It gets a
|
|
18
|
+
* {@link SpanRecorder}, which records only while the span is traced, uses
|
|
19
|
+
* whichever methods the runtime has, and swallows anything a span method
|
|
20
|
+
* throws.
|
|
21
|
+
*
|
|
22
|
+
* Core and fabric stay importable outside workerd, so they reach the API
|
|
23
|
+
* through this leaf: the Worker's composition root hands it over once, at
|
|
24
|
+
* module scope, with {@link adoptTracing}, exactly as it does `ctx.exports`
|
|
25
|
+
* (./composition.ts). With nothing adopted — unit tests, an embedder that
|
|
26
|
+
* never adopts — {@link traced} runs its callback with {@link untraced}.
|
|
27
|
+
*/
|
|
28
|
+
/** The recorder of a span that is not traced, or of no span at all. */
|
|
29
|
+
export const untraced = Object.freeze({ set() { }, exception() { } });
|
|
30
|
+
let tracer = null;
|
|
31
|
+
/** Hand this isolate's `tracing` over. The first adoption stands, like `adoptCtxExports`. */
|
|
32
|
+
export function adoptTracing(value) {
|
|
33
|
+
tracer ??= value;
|
|
34
|
+
}
|
|
35
|
+
function recorderFor(span) {
|
|
36
|
+
try {
|
|
37
|
+
if (!span.isTraced)
|
|
38
|
+
return untraced;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return untraced;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
set(attributes) {
|
|
45
|
+
try {
|
|
46
|
+
if (typeof span.setAttributes === 'function') {
|
|
47
|
+
span.setAttributes(attributes);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof span.setAttribute === 'function') {
|
|
50
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
51
|
+
if (value !== undefined)
|
|
52
|
+
span.setAttribute(key, value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch { /* telemetry is best-effort */ }
|
|
57
|
+
},
|
|
58
|
+
exception(error, code, context = '') {
|
|
59
|
+
try {
|
|
60
|
+
if (typeof span.recordException !== 'function')
|
|
61
|
+
return;
|
|
62
|
+
if (error instanceof Error) {
|
|
63
|
+
const own = 'code' in error ? error.code : undefined;
|
|
64
|
+
span.recordException({
|
|
65
|
+
code: code ?? (typeof own === 'string' || typeof own === 'number' ? own : undefined),
|
|
66
|
+
name: error.name,
|
|
67
|
+
message: context + error.message,
|
|
68
|
+
stack: error.stack,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
span.recordException(code === undefined && !context
|
|
73
|
+
? String(error)
|
|
74
|
+
: { code, message: context + String(error) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch { /* telemetry is best-effort */ }
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Run `fn` in a span named `name` carrying `attributes`, and record on it the
|
|
83
|
+
* exception `fn` throws or its promise rejects with. What `fn` returns or
|
|
84
|
+
* throws reaches the caller unchanged, whatever the span does.
|
|
85
|
+
*/
|
|
86
|
+
export function traced(name, attributes, fn) {
|
|
87
|
+
if (!tracer)
|
|
88
|
+
return fn(untraced);
|
|
89
|
+
return tracer.enterSpan(name, (runtimeSpan) => {
|
|
90
|
+
const span = recorderFor(runtimeSpan);
|
|
91
|
+
span.set(attributes);
|
|
92
|
+
let result;
|
|
93
|
+
try {
|
|
94
|
+
result = fn(span);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
span.exception(error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (!(result instanceof Promise))
|
|
101
|
+
return result;
|
|
102
|
+
return result.catch((error) => {
|
|
103
|
+
span.exception(error);
|
|
104
|
+
throw error;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
package/dist/w7-frame.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"w7-frame.d.ts","sourceRoot":"","sources":["../src/w7-frame.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"w7-frame.d.ts","sourceRoot":"","sources":["../src/w7-frame.ts"],"names":[],"mappings":"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"}
|
package/dist/w7-frame.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* W7 v3 — incremental typed records for streamed bulk filesystem writes.
|
|
3
3
|
* The format is internal: every producer and consumer deploys together.
|
|
4
4
|
*/
|
|
5
|
+
import { crc32 } from './crc32.js';
|
|
5
6
|
import { CHUNK_SIZE } from './limits.js';
|
|
6
7
|
export const W7_MAGIC = new Uint8Array([0x4e, 0x57, 0x37, 0x03]);
|
|
7
8
|
const ENCODER_QUEUE_HWM = 0;
|
|
@@ -101,7 +102,7 @@ export async function decodeWriteBatchStream(stream, options = {}) {
|
|
|
101
102
|
}
|
|
102
103
|
const beginPayload = await buffer.readExact(beginEnvelope.length, 'batch-begin payload');
|
|
103
104
|
const begin = parseBatchBegin(beginPayload);
|
|
104
|
-
const initialCheck = updateRecordCheck(
|
|
105
|
+
const initialCheck = updateRecordCheck(0, beginEnvelope.header, beginPayload);
|
|
105
106
|
handedOff = true;
|
|
106
107
|
return {
|
|
107
108
|
batchId: begin.id,
|
|
@@ -180,7 +181,7 @@ async function* decodeRecords(stream, reader, buffer, options, initialCheck) {
|
|
|
180
181
|
throwIfAborted(options.signal);
|
|
181
182
|
const data = await buffer.readExact(dataLength, 'file-chunk data');
|
|
182
183
|
batchCheck = updateRecordCheck(batchCheck, envelope.header, headerPayload, data);
|
|
183
|
-
active.check =
|
|
184
|
+
active.check = crc32(data, active.check);
|
|
184
185
|
active.nextChunkId++;
|
|
185
186
|
active.receivedBytes += dataLength;
|
|
186
187
|
summary.recordCount++;
|
|
@@ -242,7 +243,7 @@ async function* decodeRecords(stream, reader, buffer, options, initialCheck) {
|
|
|
242
243
|
inode,
|
|
243
244
|
nextChunkId: 0,
|
|
244
245
|
receivedBytes: 0,
|
|
245
|
-
check:
|
|
246
|
+
check: 0,
|
|
246
247
|
};
|
|
247
248
|
yield { type: 'file-begin', streamContentId: metadata.contentId, inode };
|
|
248
249
|
break;
|
|
@@ -251,7 +252,7 @@ async function* decodeRecords(stream, reader, buffer, options, initialCheck) {
|
|
|
251
252
|
if (!active)
|
|
252
253
|
throw new Error('w7-frame: file-end without active file');
|
|
253
254
|
const metadata = parseFileEnd(payload);
|
|
254
|
-
const actualCheck =
|
|
255
|
+
const actualCheck = active.check;
|
|
255
256
|
if (metadata.contentId !== active.metadata.contentId) {
|
|
256
257
|
throw new Error(`w7-frame: file-end content id mismatch for ${active.inode.path}`);
|
|
257
258
|
}
|
|
@@ -281,7 +282,7 @@ async function* decodeRecords(stream, reader, buffer, options, initialCheck) {
|
|
|
281
282
|
if (active)
|
|
282
283
|
throw new Error(`w7-frame: batch-end while file ${active.inode.path} is active`);
|
|
283
284
|
const actual = parseBatchEnd(payload);
|
|
284
|
-
const expected = { ...summary, check:
|
|
285
|
+
const expected = { ...summary, check: batchCheck };
|
|
285
286
|
if (!sameSummary(actual, expected)) {
|
|
286
287
|
throw new Error(`w7-frame: batch-end summary mismatch; expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
287
288
|
}
|
|
@@ -315,7 +316,7 @@ async function* decodeRecords(stream, reader, buffer, options, initialCheck) {
|
|
|
315
316
|
}
|
|
316
317
|
function* encodeRecords(batchId, deletes, directories, files) {
|
|
317
318
|
const state = {
|
|
318
|
-
batchCheck:
|
|
319
|
+
batchCheck: 0,
|
|
319
320
|
summary: {
|
|
320
321
|
recordCount: 0,
|
|
321
322
|
pathCount: 0,
|
|
@@ -350,7 +351,7 @@ function* encodeRecords(batchId, deletes, directories, files) {
|
|
|
350
351
|
size: file.inode.size,
|
|
351
352
|
chunkCount: file.inode.chunkCount,
|
|
352
353
|
}, state);
|
|
353
|
-
let fileCheck =
|
|
354
|
+
let fileCheck = 0;
|
|
354
355
|
for (const chunk of file.chunks) {
|
|
355
356
|
const data = chunk.data;
|
|
356
357
|
const contentBytes = new TextEncoder().encode(file.contentId);
|
|
@@ -364,19 +365,19 @@ function* encodeRecords(batchId, deletes, directories, files) {
|
|
|
364
365
|
state.summary.recordCount++;
|
|
365
366
|
state.summary.chunkCount++;
|
|
366
367
|
state.summary.byteCount += data.byteLength;
|
|
367
|
-
fileCheck =
|
|
368
|
+
fileCheck = crc32(data, fileCheck);
|
|
368
369
|
yield [concatBytes(header, prefix), data];
|
|
369
370
|
}
|
|
370
371
|
yield encodeMetadataRecord(RecordTag.FileEnd, {
|
|
371
372
|
contentId: file.contentId,
|
|
372
373
|
size: file.inode.size,
|
|
373
374
|
chunkCount: file.inode.chunkCount,
|
|
374
|
-
check:
|
|
375
|
+
check: fileCheck,
|
|
375
376
|
}, state);
|
|
376
377
|
}
|
|
377
378
|
const end = {
|
|
378
379
|
...state.summary,
|
|
379
|
-
check:
|
|
380
|
+
check: state.batchCheck,
|
|
380
381
|
};
|
|
381
382
|
yield encodeMetadataRecord(RecordTag.BatchEnd, end);
|
|
382
383
|
}
|
|
@@ -718,33 +719,9 @@ function readU32LE(bytes, offset) {
|
|
|
718
719
|
function updateRecordCheck(seed, ...parts) {
|
|
719
720
|
let check = seed;
|
|
720
721
|
for (const part of parts)
|
|
721
|
-
check =
|
|
722
|
+
check = crc32(part, check);
|
|
722
723
|
return check;
|
|
723
724
|
}
|
|
724
|
-
const CRC_SEED = 0xffff_ffff;
|
|
725
|
-
let crcTable = null;
|
|
726
|
-
function crc32Update(check, bytes) {
|
|
727
|
-
crcTable ??= createCrcTable();
|
|
728
|
-
const table = crcTable;
|
|
729
|
-
let value = check;
|
|
730
|
-
for (const byte of bytes)
|
|
731
|
-
value = table[(value ^ byte) & 0xff] ^ (value >>> 8);
|
|
732
|
-
return value >>> 0;
|
|
733
|
-
}
|
|
734
|
-
function crc32Finish(check) {
|
|
735
|
-
return (check ^ 0xffff_ffff) >>> 0;
|
|
736
|
-
}
|
|
737
|
-
function createCrcTable() {
|
|
738
|
-
const table = new Uint32Array(256);
|
|
739
|
-
for (let index = 0; index < table.length; index++) {
|
|
740
|
-
let value = index;
|
|
741
|
-
for (let bit = 0; bit < 8; bit++) {
|
|
742
|
-
value = (value & 1) !== 0 ? 0xedb8_8320 ^ (value >>> 1) : value >>> 1;
|
|
743
|
-
}
|
|
744
|
-
table[index] = value >>> 0;
|
|
745
|
-
}
|
|
746
|
-
return table;
|
|
747
|
-
}
|
|
748
725
|
function noopRetention(bytes) {
|
|
749
726
|
return { bytes, release() { } };
|
|
750
727
|
}
|
|
@@ -9,11 +9,6 @@ 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;
|
|
17
12
|
}
|
|
18
13
|
export interface WeightedCreditPoolOptions {
|
|
19
14
|
/**
|
|
@@ -61,14 +56,6 @@ export declare class WeightedCreditPool {
|
|
|
61
56
|
private isSmall;
|
|
62
57
|
tryAcquire(bytes: number): ResizableCreditLease | null;
|
|
63
58
|
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>;
|
|
72
59
|
private validateRequest;
|
|
73
60
|
}
|
|
74
61
|
//# 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;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"}
|
|
@@ -3,52 +3,27 @@ class CreditLane {
|
|
|
3
3
|
capacity;
|
|
4
4
|
current = 0;
|
|
5
5
|
peak = 0;
|
|
6
|
-
resident = 0;
|
|
7
6
|
waiters = [];
|
|
8
7
|
constructor(capacity) {
|
|
9
8
|
this.capacity = capacity;
|
|
10
9
|
}
|
|
11
10
|
get stats() {
|
|
12
|
-
return {
|
|
13
|
-
current: this.current, peak: this.peak, queued: this.waiters.length, resident: this.resident,
|
|
14
|
-
};
|
|
11
|
+
return { current: this.current, peak: this.peak, queued: this.waiters.length };
|
|
15
12
|
}
|
|
16
13
|
tryAcquire(bytes) {
|
|
17
14
|
if (this.waiters.length > 0 || this.current + bytes > this.capacity)
|
|
18
15
|
return null;
|
|
19
|
-
return this.grant(bytes
|
|
16
|
+
return this.grant(bytes);
|
|
20
17
|
}
|
|
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) {
|
|
18
|
+
acquire(bytes, signal) {
|
|
31
19
|
if (signal?.aborted)
|
|
32
20
|
return Promise.reject(abortError(signal));
|
|
33
|
-
|
|
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);
|
|
21
|
+
const immediate = this.tryAcquire(bytes);
|
|
43
22
|
if (immediate)
|
|
44
23
|
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
|
-
}
|
|
48
24
|
return new Promise((resolve, reject) => {
|
|
49
25
|
const waiter = {
|
|
50
26
|
bytes,
|
|
51
|
-
resident,
|
|
52
27
|
resolve,
|
|
53
28
|
reject,
|
|
54
29
|
signal,
|
|
@@ -73,10 +48,8 @@ class CreditLane {
|
|
|
73
48
|
this.drain();
|
|
74
49
|
});
|
|
75
50
|
}
|
|
76
|
-
grant(bytes
|
|
51
|
+
grant(bytes) {
|
|
77
52
|
this.current += bytes;
|
|
78
|
-
if (resident)
|
|
79
|
-
this.resident += bytes;
|
|
80
53
|
this.peak = Math.max(this.peak, this.current);
|
|
81
54
|
let leasedBytes = bytes;
|
|
82
55
|
let released = false;
|
|
@@ -94,8 +67,6 @@ class CreditLane {
|
|
|
94
67
|
throw new RangeError(`weighted credit lease can only shrink to a positive safe integer no larger than ${leasedBytes}: ${nextBytes}`);
|
|
95
68
|
}
|
|
96
69
|
this.current -= leasedBytes - nextBytes;
|
|
97
|
-
if (resident)
|
|
98
|
-
this.resident -= leasedBytes - nextBytes;
|
|
99
70
|
leasedBytes = nextBytes;
|
|
100
71
|
this.drain();
|
|
101
72
|
},
|
|
@@ -104,8 +75,6 @@ class CreditLane {
|
|
|
104
75
|
return;
|
|
105
76
|
released = true;
|
|
106
77
|
this.current -= leasedBytes;
|
|
107
|
-
if (resident)
|
|
108
|
-
this.resident -= leasedBytes;
|
|
109
78
|
if (this.current < 0) {
|
|
110
79
|
throw new Error('weighted credit accounting underflow');
|
|
111
80
|
}
|
|
@@ -126,7 +95,7 @@ class CreditLane {
|
|
|
126
95
|
return;
|
|
127
96
|
this.waiters.shift();
|
|
128
97
|
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
129
|
-
waiter.resolve(this.grant(waiter.bytes
|
|
98
|
+
waiter.resolve(this.grant(waiter.bytes));
|
|
130
99
|
}
|
|
131
100
|
}
|
|
132
101
|
}
|
|
@@ -185,7 +154,6 @@ export class WeightedCreditPool {
|
|
|
185
154
|
current: general.current + small.current,
|
|
186
155
|
peak: general.peak + small.peak,
|
|
187
156
|
queued: general.queued + small.queued,
|
|
188
|
-
resident: general.resident + small.resident,
|
|
189
157
|
};
|
|
190
158
|
}
|
|
191
159
|
isSmall(bytes) {
|
|
@@ -216,22 +184,6 @@ export class WeightedCreditPool {
|
|
|
216
184
|
}
|
|
217
185
|
return this.general.acquire(bytes, signal);
|
|
218
186
|
}
|
|
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
|
-
}
|
|
235
187
|
validateRequest(bytes) {
|
|
236
188
|
if (!Number.isSafeInteger(bytes) || bytes <= 0 || bytes > this.capacity) {
|
|
237
189
|
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.7.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",
|
|
@@ -43,12 +43,12 @@
|
|
|
43
43
|
"LICENSE"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"build": "tsc -p tsconfig.json --noCheck --noEmit false --declaration true --declarationMap true --outDir dist --rootDir src",
|
|
47
|
-
"
|
|
46
|
+
"build": "node ../../scripts/clean-dist.mjs && tsc -p tsconfig.json --noCheck --noEmit false --declaration true --declarationMap true --outDir dist --rootDir src",
|
|
47
|
+
"prepublishOnly": "bun ../../scripts/dist-integrity.mjs --publish",
|
|
48
48
|
"typecheck": "tsc --noEmit"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@cloudflare/workers-types": "^
|
|
51
|
+
"@cloudflare/workers-types": "^5.20260928.1",
|
|
52
52
|
"typescript": "^5.7.0"
|
|
53
53
|
},
|
|
54
54
|
"publishConfig": {
|
package/src/composition.ts
CHANGED
|
@@ -66,13 +66,46 @@ export const DEFAULT_HOST_DISPATCH_METHOD = 'supervisorOp';
|
|
|
66
66
|
let composition: FabricComposition | null = null;
|
|
67
67
|
let ctxExports: CtxExports | null = null;
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* A program's filesystem errors reach it across workerd RPC, from the host
|
|
71
|
+
* that answers its supervisor binding, and the program branches on the POSIX
|
|
72
|
+
* `code` each one carries. workerd carries an error's own properties across
|
|
73
|
+
* RPC only when the sending AND the receiving isolate have
|
|
74
|
+
* `enhanced_error_serialization`, on by compatibility date from 2026-04-21:
|
|
75
|
+
* the serializer writes a native error as a host object with its own
|
|
76
|
+
* properties only under the flag (src/workerd/jsg/ser.c++,
|
|
77
|
+
* Serializer::IsHostObject), and the receiver reads that detail back only
|
|
78
|
+
* under it (src/workerd/jsg/util.c++, decodeTunneledException); see
|
|
79
|
+
* https://developers.cloudflare.com/workers/runtime-apis/rpc/error-handling/.
|
|
80
|
+
* Programs load at CF_COMPAT_DATE, past it. The host runs at its embedder's
|
|
81
|
+
* date, and below it every ENOENT would reach every program as a bare message
|
|
82
|
+
* it maps to EIO. So composing refuses such a host, wherever it composes
|
|
83
|
+
* (module scope: the Worker fails at startup; NimbusWorkspace.create: that
|
|
84
|
+
* call throws), and names both fixes: the flag alone keeps
|
|
85
|
+
* every other behavior of the embedder's date. Off workerd (bun, node) there
|
|
86
|
+
* is no RPC hop and no `Cloudflare` global to read.
|
|
87
|
+
*/
|
|
88
|
+
function requireEnhancedErrorSerialization(): void {
|
|
89
|
+
// `typeof` of an undeclared global is 'undefined', not a ReferenceError.
|
|
90
|
+
if (typeof Cloudflare === 'undefined') return;
|
|
91
|
+
if (Cloudflare.compatibilityFlags.enhanced_error_serialization === true) return;
|
|
92
|
+
throw new Error(
|
|
93
|
+
'fabric: this Worker runs without the enhanced_error_serialization compatibility flag, so the '
|
|
94
|
+
+ "code on a program's filesystem error (ENOENT, EEXIST, ...) would not survive the RPC back "
|
|
95
|
+
+ 'to it. Add "enhanced_error_serialization" to compatibility_flags in your wrangler config, '
|
|
96
|
+
+ 'or set compatibility_date to 2026-04-21 or later.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
/**
|
|
70
101
|
* Compose once per isolate. A second call with the same values is a no-op;
|
|
71
102
|
* a second call with different values throws, naming both, so an embedder
|
|
72
103
|
* whose composition lost to an earlier import learns it at startup rather
|
|
73
|
-
* than from a facet that reached the wrong host.
|
|
104
|
+
* than from a facet that reached the wrong host. A host without
|
|
105
|
+
* `enhanced_error_serialization` is refused.
|
|
74
106
|
*/
|
|
75
107
|
export function composeFabric(value: FabricComposition): void {
|
|
108
|
+
requireEnhancedErrorSerialization();
|
|
76
109
|
if (!composition) {
|
|
77
110
|
composition = value;
|
|
78
111
|
return;
|
package/src/crc32.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRC-32 (IEEE 802.3, the zip/gzip/PNG polynomial), the one implementation
|
|
3
|
+
* every Nimbus caller uses: the W7 write-batch framing and zip archives.
|
|
4
|
+
*
|
|
5
|
+
* `crc32(bytes, previous)` has node:zlib's contract: `previous` is the
|
|
6
|
+
* finished CRC of the bytes before these, so a checksum computed in pieces
|
|
7
|
+
* equals the checksum of the whole.
|
|
8
|
+
*
|
|
9
|
+
* Measured 2026-09-24 (5k files x 2 KiB; 64 MiB in 1 MiB buffers):
|
|
10
|
+
* node:zlib's crc32 is 5-40x faster than any JavaScript loop in bun, node and
|
|
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 reached through
|
|
13
|
+
* `process.getBuiltinModule`, like core's incremental SHA-256, so this module
|
|
14
|
+
* imports nothing and still loads where node:zlib is absent (a browser,
|
|
15
|
+
* workerd without nodejs_compat). The JavaScript path is slicing-by-8: 2-3x
|
|
16
|
+
* the byte-at-a-time table loop in bun and node, and level with it in workerd.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type NodeZlib = { crc32?(data: Uint8Array, value?: number): number };
|
|
20
|
+
type BuiltinModules = { process?: { getBuiltinModule?(id: 'node:zlib'): NodeZlib | undefined } };
|
|
21
|
+
|
|
22
|
+
/** Below this many bytes the native call's overhead exceeds the JavaScript loop's work (workerd). */
|
|
23
|
+
const CRC_NATIVE_MIN_BYTES = 128;
|
|
24
|
+
|
|
25
|
+
const nativeCrc32 = (() => {
|
|
26
|
+
try {
|
|
27
|
+
const crc32 = (globalThis as BuiltinModules).process?.getBuiltinModule?.('node:zlib')?.crc32;
|
|
28
|
+
return typeof crc32 === 'function' ? crc32 : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
/** Eight 256-entry tables: table k maps a byte to its CRC after k further zero bytes. */
|
|
35
|
+
let crcTables: Uint32Array | null = null;
|
|
36
|
+
|
|
37
|
+
function createCrcTables(): Uint32Array {
|
|
38
|
+
const table = new Uint32Array(256 * 8);
|
|
39
|
+
for (let index = 0; index < 256; index++) {
|
|
40
|
+
let value = index;
|
|
41
|
+
for (let bit = 0; bit < 8; bit++) value = (value & 1) !== 0 ? 0xedb8_8320 ^ (value >>> 1) : value >>> 1;
|
|
42
|
+
table[index] = value;
|
|
43
|
+
}
|
|
44
|
+
for (let index = 0; index < 256; index++) {
|
|
45
|
+
let value = table[index];
|
|
46
|
+
for (let k = 1; k < 8; k++) {
|
|
47
|
+
value = table[value & 0xff] ^ (value >>> 8);
|
|
48
|
+
table[k * 256 + index] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return table;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function crc32(bytes: Uint8Array, previous = 0): number {
|
|
55
|
+
if (nativeCrc32 !== null && bytes.length >= CRC_NATIVE_MIN_BYTES) return nativeCrc32(bytes, previous) >>> 0;
|
|
56
|
+
const t = crcTables ??= createCrcTables();
|
|
57
|
+
let value = ~previous;
|
|
58
|
+
const length = bytes.length;
|
|
59
|
+
const whole = length - (length & 7);
|
|
60
|
+
let i = 0;
|
|
61
|
+
for (; i < whole; i += 8) {
|
|
62
|
+
const low = value ^ (bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24));
|
|
63
|
+
const high = bytes[i + 4] | (bytes[i + 5] << 8) | (bytes[i + 6] << 16) | (bytes[i + 7] << 24);
|
|
64
|
+
value = t[1792 + (low & 0xff)] ^ t[1536 + ((low >>> 8) & 0xff)]
|
|
65
|
+
^ t[1280 + ((low >>> 16) & 0xff)] ^ t[1024 + (low >>> 24)]
|
|
66
|
+
^ t[768 + (high & 0xff)] ^ t[512 + ((high >>> 8) & 0xff)]
|
|
67
|
+
^ t[256 + ((high >>> 16) & 0xff)] ^ t[high >>> 24];
|
|
68
|
+
}
|
|
69
|
+
for (; i < length; i++) value = t[(value ^ bytes[i]) & 0xff] ^ (value >>> 8);
|
|
70
|
+
return ~value >>> 0;
|
|
71
|
+
}
|
package/src/diag-counters.ts
CHANGED
|
@@ -82,12 +82,6 @@ export interface DiagCounters {
|
|
|
82
82
|
* Surfaced as `breakdown.prefetchCacheBytes`. A gauge, not a delta.
|
|
83
83
|
*/
|
|
84
84
|
prefetchCacheBytes: number;
|
|
85
|
-
/**
|
|
86
|
-
* Bytes retained by the FacetManager's ESM→CJS transform cache, bounded by
|
|
87
|
-
* ESM_TRANSFORM_CACHE_MAX_BYTES. Surfaced as `breakdown.transformCacheBytes`.
|
|
88
|
-
* A gauge, not a delta.
|
|
89
|
-
*/
|
|
90
|
-
transformCacheBytes: number;
|
|
91
85
|
/** Install-facet counters. Populated by npm-installer after a
|
|
92
86
|
* successful batch-facet dispatch returns. Confirms the install ran
|
|
93
87
|
* in the facet (tarballsCompleted > 0) and surfaces the
|
|
@@ -147,7 +141,6 @@ const _counters: DiagCounters = {
|
|
|
147
141
|
inFlightRpcPayloadBytes: 0,
|
|
148
142
|
prefetchBundleBytes: 0,
|
|
149
143
|
prefetchCacheBytes: 0,
|
|
150
|
-
transformCacheBytes: 0,
|
|
151
144
|
installFacet: {
|
|
152
145
|
tarballsCompleted: 0,
|
|
153
146
|
cumulativeBytesDecoded: 0,
|
|
@@ -250,12 +243,6 @@ export function setPrefetchCacheBytes(bytes: number): void {
|
|
|
250
243
|
_counters.prefetchCacheBytes = Number.isFinite(n) && n > 0 ? n : 0;
|
|
251
244
|
}
|
|
252
245
|
|
|
253
|
-
/** Publish the transform cache's live retained total; same contract as above. */
|
|
254
|
-
export function setTransformCacheBytes(bytes: number): void {
|
|
255
|
-
const n = Number(bytes);
|
|
256
|
-
_counters.transformCacheBytes = Number.isFinite(n) && n > 0 ? n : 0;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
246
|
/** Fold facet-returned counters into the supervisor's diag state.
|
|
260
247
|
* Called by npm-installer after the batch-facet returns; aggregates
|
|
261
248
|
* rather than replaces so multiple install runs in the same DO
|
package/src/heap-estimate.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `process.memoryUsage()` returns 0 for every field inside a Durable
|
|
7
7
|
* Object class context (only dynamic-worker isolates under nodejs_compat
|
|
8
8
|
* get the real implementation). The previous `readNodeMem` /
|
|
9
|
-
* `sampleMemory` helpers in
|
|
9
|
+
* `sampleMemory` helpers in session/diag.ts called it anyway and
|
|
10
10
|
* therefore reported zero forever — useless for verifying memory-
|
|
11
11
|
* containment work in plan §3 Track A'.
|
|
12
12
|
*
|
|
@@ -200,12 +200,6 @@ export interface HeapBreakdown {
|
|
|
200
200
|
* the cache holds rather than a reservation it might use.
|
|
201
201
|
*/
|
|
202
202
|
prefetchCacheBytes: number;
|
|
203
|
-
/**
|
|
204
|
-
* Bytes retained by the FacetManager's ESM→CJS transform cache
|
|
205
|
-
* (diag-counters `transformCacheBytes`), the other cache that persists
|
|
206
|
-
* across execs. Bounded by ESM_TRANSFORM_CACHE_MAX_BYTES.
|
|
207
|
-
*/
|
|
208
|
-
transformCacheBytes: number;
|
|
209
203
|
/**
|
|
210
204
|
* Shared-budget occupancy not already represented by the named transient
|
|
211
205
|
* counters. This covers full-budget owners and keeps cross-DO module-local
|
|
@@ -303,7 +297,6 @@ export function estimateSupervisorHeap(
|
|
|
303
297
|
streamingBuffersBytes: c.inFlightRpcPayloadBytes,
|
|
304
298
|
prefetchBundleBytes: c.prefetchBundleBytes,
|
|
305
299
|
prefetchCacheBytes: c.prefetchCacheBytes,
|
|
306
|
-
transformCacheBytes: c.transformCacheBytes,
|
|
307
300
|
unattributedReservationBytes: Math.max(
|
|
308
301
|
0,
|
|
309
302
|
allocationBudget.current - transientAttributedBytes,
|
|
@@ -318,7 +311,6 @@ export function estimateSupervisorHeap(
|
|
|
318
311
|
breakdown.streamingBuffersBytes +
|
|
319
312
|
breakdown.prefetchBundleBytes +
|
|
320
313
|
breakdown.prefetchCacheBytes +
|
|
321
|
-
breakdown.transformCacheBytes +
|
|
322
314
|
breakdown.unattributedReservationBytes;
|
|
323
315
|
|
|
324
316
|
const percentOfCeiling = Math.round(
|