footprintjs 9.4.0 → 9.5.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/esm/lib/builder/FlowChartBuilder.js +15 -2
- package/dist/esm/lib/capture/envelope.js +187 -0
- package/dist/esm/lib/engine/handlers/ContinuationResolver.js +23 -4
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/observer-queue/deferredDispatcher.js +226 -0
- package/dist/esm/lib/observer-queue/flushDriver.js +163 -0
- package/dist/esm/lib/observer-queue/index.js +22 -0
- package/dist/esm/lib/observer-queue/mergedQueue.js +91 -0
- package/dist/esm/lib/observer-queue/ring.js +122 -0
- package/dist/lib/builder/FlowChartBuilder.js +15 -2
- package/dist/lib/capture/envelope.js +192 -0
- package/dist/lib/engine/handlers/ContinuationResolver.js +23 -4
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/observer-queue/deferredDispatcher.js +230 -0
- package/dist/lib/observer-queue/flushDriver.js +167 -0
- package/dist/lib/observer-queue/index.js +36 -0
- package/dist/lib/observer-queue/mergedQueue.js +95 -0
- package/dist/lib/observer-queue/ring.js +126 -0
- package/dist/types/lib/capture/envelope.d.ts +169 -0
- package/dist/types/lib/engine/handlers/ContinuationResolver.d.ts +15 -2
- package/dist/types/lib/engine/types.d.ts +3 -0
- package/dist/types/lib/observer-queue/deferredDispatcher.d.ts +169 -0
- package/dist/types/lib/observer-queue/flushDriver.d.ts +124 -0
- package/dist/types/lib/observer-queue/index.d.ts +25 -0
- package/dist/types/lib/observer-queue/mergedQueue.d.ts +85 -0
- package/dist/types/lib/observer-queue/ring.d.ts +99 -0
- package/package.json +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* observer-queue/ring.ts — RFC-001 Block 2: bounded ring with overflow policies.
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Fixed-capacity circular buffer with explicit, COUNTED overflow
|
|
6
|
+
* behavior. The deferred-observer queue must never grow without
|
|
7
|
+
* bound (a slow consumer cannot OOM the producer), and must never
|
|
8
|
+
* lose an event silently (every loss increments `drops`).
|
|
9
|
+
* Role: Storage primitive under the merged queue (Block 3). Pure data
|
|
10
|
+
* structure — zero imports, zero engine knowledge, generic over T.
|
|
11
|
+
*
|
|
12
|
+
* Overflow policies (RFC-001 §5, with the accepted 'block' resolution):
|
|
13
|
+
* - `'drop-oldest'` — evict the oldest queued item to admit the new one.
|
|
14
|
+
* The evicted item is LOST (`drops`++) and returned on the push result
|
|
15
|
+
* so the caller can account for it. Sequence stamps on surviving items
|
|
16
|
+
* keep loss visible as seq gaps (honest loss accounting). DEFAULT
|
|
17
|
+
* posture for telemetry-grade delivery.
|
|
18
|
+
* - `'sample'` — while saturated, admit 1 in `sampleEvery` arrivals
|
|
19
|
+
* (evicting the oldest to make room — that eviction is also a counted
|
|
20
|
+
* loss); refuse the rest (each a counted loss). Keeps a thinned,
|
|
21
|
+
* still-fresh stream under sustained overload. The saturation counter
|
|
22
|
+
* is episode-scoped: it resets whenever a push succeeds through the
|
|
23
|
+
* non-full path.
|
|
24
|
+
* - `'block'` — the ring REFUSES the new item (`accepted: false`,
|
|
25
|
+
* `rejections`++) and drops NOTHING. In a single-threaded runtime a
|
|
26
|
+
* queue cannot literally block its producer; the dispatcher (Block 5)
|
|
27
|
+
* interprets a refusal as "deliver this event synchronously inline" —
|
|
28
|
+
* re-introducing blocking delivery by the consumer's EXPLICIT choice.
|
|
29
|
+
* Rejections are NOT losses: the event is still delivered (inline), so
|
|
30
|
+
* `drops` stays untouched.
|
|
31
|
+
*
|
|
32
|
+
* Conservation invariant (property-tested):
|
|
33
|
+
* pushes === delivered + drops + rejections + size
|
|
34
|
+
*
|
|
35
|
+
* CURSOR-READY (amendment A2): v1 consumes destructively through ONE cursor
|
|
36
|
+
* (`shift()` advances `head`). The designed v1.1 path keeps items in the
|
|
37
|
+
* ring and gives each listener its own read cursor; `head` then advances to
|
|
38
|
+
* `min(cursors)` (the reclaim watermark) instead of on read. The storage
|
|
39
|
+
* layout (contiguous circular window, `head` + `count`) already supports
|
|
40
|
+
* that — only the consumption surface changes. Documented, not implemented.
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.BoundedRing = void 0;
|
|
44
|
+
const DEFAULT_SAMPLE_EVERY = 10;
|
|
45
|
+
class BoundedRing {
|
|
46
|
+
buffer;
|
|
47
|
+
policy;
|
|
48
|
+
sampleEvery;
|
|
49
|
+
/** Index of the oldest queued item — the single v1 cursor (see header). */
|
|
50
|
+
head = 0;
|
|
51
|
+
count = 0;
|
|
52
|
+
/** Arrivals seen while saturated in the current episode (`'sample'`). */
|
|
53
|
+
saturatedArrivals = 0;
|
|
54
|
+
pushes = 0;
|
|
55
|
+
delivered = 0;
|
|
56
|
+
drops = 0;
|
|
57
|
+
rejections = 0;
|
|
58
|
+
constructor(opts) {
|
|
59
|
+
if (!Number.isInteger(opts.capacity) || opts.capacity <= 0) {
|
|
60
|
+
throw new RangeError(`BoundedRing capacity must be a positive integer (got ${opts.capacity})`);
|
|
61
|
+
}
|
|
62
|
+
const sampleEvery = opts.sampleEvery ?? DEFAULT_SAMPLE_EVERY;
|
|
63
|
+
if (!Number.isInteger(sampleEvery) || sampleEvery <= 0) {
|
|
64
|
+
throw new RangeError(`BoundedRing sampleEvery must be a positive integer (got ${opts.sampleEvery})`);
|
|
65
|
+
}
|
|
66
|
+
this.buffer = new Array(opts.capacity);
|
|
67
|
+
this.policy = opts.policy;
|
|
68
|
+
this.sampleEvery = sampleEvery;
|
|
69
|
+
}
|
|
70
|
+
get size() {
|
|
71
|
+
return this.count;
|
|
72
|
+
}
|
|
73
|
+
get capacity() {
|
|
74
|
+
return this.buffer.length;
|
|
75
|
+
}
|
|
76
|
+
/** Lifetime counters — see {@link RingCounters}. */
|
|
77
|
+
getCounters() {
|
|
78
|
+
return { pushes: this.pushes, delivered: this.delivered, drops: this.drops, rejections: this.rejections };
|
|
79
|
+
}
|
|
80
|
+
/** Admit, evict-and-admit, refuse, or sample per policy — never throws. */
|
|
81
|
+
push(item) {
|
|
82
|
+
this.pushes += 1;
|
|
83
|
+
if (this.count < this.buffer.length) {
|
|
84
|
+
this.saturatedArrivals = 0; // new saturation episode starts fresh
|
|
85
|
+
this.store(item);
|
|
86
|
+
return { accepted: true };
|
|
87
|
+
}
|
|
88
|
+
if (this.policy === 'block') {
|
|
89
|
+
this.rejections += 1;
|
|
90
|
+
return { accepted: false };
|
|
91
|
+
}
|
|
92
|
+
if (this.policy === 'sample') {
|
|
93
|
+
this.saturatedArrivals += 1;
|
|
94
|
+
if (this.saturatedArrivals % this.sampleEvery !== 0) {
|
|
95
|
+
this.drops += 1; // the incoming item is sampled out — lost
|
|
96
|
+
return { accepted: false };
|
|
97
|
+
}
|
|
98
|
+
// The 1-in-N admission falls through to evict-and-store.
|
|
99
|
+
}
|
|
100
|
+
// 'drop-oldest' (and the 'sample' admission): evict the oldest — lost.
|
|
101
|
+
const evicted = this.buffer[this.head];
|
|
102
|
+
this.buffer[this.head] = undefined;
|
|
103
|
+
this.head = (this.head + 1) % this.buffer.length;
|
|
104
|
+
this.count -= 1;
|
|
105
|
+
this.drops += 1;
|
|
106
|
+
this.store(item);
|
|
107
|
+
return { accepted: true, evicted };
|
|
108
|
+
}
|
|
109
|
+
/** Pop the oldest queued item (FIFO). `undefined` when empty. */
|
|
110
|
+
shift() {
|
|
111
|
+
if (this.count === 0)
|
|
112
|
+
return undefined;
|
|
113
|
+
const item = this.buffer[this.head];
|
|
114
|
+
this.buffer[this.head] = undefined; // release the reference for GC
|
|
115
|
+
this.head = (this.head + 1) % this.buffer.length;
|
|
116
|
+
this.count -= 1;
|
|
117
|
+
this.delivered += 1;
|
|
118
|
+
return item;
|
|
119
|
+
}
|
|
120
|
+
store(item) {
|
|
121
|
+
this.buffer[(this.head + this.count) % this.buffer.length] = item;
|
|
122
|
+
this.count += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.BoundedRing = BoundedRing;
|
|
126
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmluZy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9saWIvb2JzZXJ2ZXItcXVldWUvcmluZy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXVDRzs7O0FBeUNILE1BQU0sb0JBQW9CLEdBQUcsRUFBRSxDQUFDO0FBRWhDLE1BQWEsV0FBVztJQUNMLE1BQU0sQ0FBdUI7SUFDN0IsTUFBTSxDQUFpQjtJQUN2QixXQUFXLENBQVM7SUFDckMsMkVBQTJFO0lBQ25FLElBQUksR0FBRyxDQUFDLENBQUM7SUFDVCxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ2xCLHlFQUF5RTtJQUNqRSxpQkFBaUIsR0FBRyxDQUFDLENBQUM7SUFFdEIsTUFBTSxHQUFHLENBQUMsQ0FBQztJQUNYLFNBQVMsR0FBRyxDQUFDLENBQUM7SUFDZCxLQUFLLEdBQUcsQ0FBQyxDQUFDO0lBQ1YsVUFBVSxHQUFHLENBQUMsQ0FBQztJQUV2QixZQUFZLElBQWlCO1FBQzNCLElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxJQUFJLENBQUMsRUFBRSxDQUFDO1lBQzNELE1BQU0sSUFBSSxVQUFVLENBQUMsd0RBQXdELElBQUksQ0FBQyxRQUFRLEdBQUcsQ0FBQyxDQUFDO1FBQ2pHLENBQUM7UUFDRCxNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsV0FBVyxJQUFJLG9CQUFvQixDQUFDO1FBQzdELElBQUksQ0FBQyxNQUFNLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxJQUFJLFdBQVcsSUFBSSxDQUFDLEVBQUUsQ0FBQztZQUN2RCxNQUFNLElBQUksVUFBVSxDQUFDLDJEQUEyRCxJQUFJLENBQUMsV0FBVyxHQUFHLENBQUMsQ0FBQztRQUN2RyxDQUFDO1FBQ0QsSUFBSSxDQUFDLE1BQU0sR0FBRyxJQUFJLEtB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RyxJQUFJLENBQUMsTUFBTSxDQUFDLE1BQU0sQ0FBQztRQUNqRCxJQUFJLENBQUMsS0FBSyxJQUFJLENBQUMsQ0FBQztRQUNoQixJQUFJLENBQUMsU0FBUyxJQUFJLENBQUMsQ0FBQztRQUNwQixPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFFTyxLQUFLLENBQUMsSUFBTztRQUNuQixJQUFJLENBQUMsTUFBTSxDQUFDLENBQUMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsR0FBRyxJQUFJLENBQUM7UUFDbEUsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLENBQUM7SUFDbEIsQ0FBQztDQUNGO0FBMUZELGtDQTBGQyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogb2JzZXJ2ZXItcXVldWUvcmluZy50cyDigJQgUkZDLTAwMSBCbG9jayAyOiBib3VuZGVkIHJpbmcgd2l0aCBvdmVyZmxvdyBwb2xpY2llcy5cbiAqXG4gKiBQYXR0ZXJuOiAgRml4ZWQtY2FwYWNpdHkgY2lyY3VsYXIgYnVmZmVyIHdpdGggZXhwbGljaXQsIENPVU5URUQgb3ZlcmZsb3dcbiAqICAgICAgICAgICBiZWhhdmlvci4gVGhlIGRlZmVycmVkLW9ic2VydmVyIHF1ZXVlIG11c3QgbmV2ZXIgZ3JvdyB3aXRob3V0XG4gKiAgICAgICAgICAgYm91bmQgKGEgc2xvdyBjb25zdW1lciBjYW5ub3QgT09NIHRoZSBwcm9kdWNlciksIGFuZCBtdXN0IG5ldmVyXG4gKiAgICAgICAgICAgbG9zZSBhbiBldmVudCBzaWxlbnRseSAoZXZlcnkgbG9zcyBpbmNyZW1lbnRzIGBkcm9wc2ApLlxuICogUm9sZTogICAgIFN0b3JhZ2UgcHJpbWl0aXZlIHVuZGVyIHRoZSBtZXJnZWQgcXVldWUgKEJsb2NrIDMpLiBQdXJlIGRhdGFcbiAqICAgICAgICAgICBzdHJ1Y3R1cmUg4oCUIHplcm8gaW1wb3J0cywgemVybyBlbmdpbmUga25vd2xlZGdlLCBnZW5lcmljIG92ZXIgVC5cbiAqXG4gKiBPdmVyZmxvdyBwb2xpY2llcyAoUkZDLTAwMSDCpzUsIHdpdGggdGhlIGFjY2VwdGVkICdibG9jaycgcmVzb2x1dGlvbik6XG4gKiAgIC0gYCdkcm9wLW9sZGVzdCdgIOKAlCBldmljdCB0aGUgb2xkZXN0IHF1ZXVlZCBpdGVtIHRvIGFkbWl0IHRoZSBuZXcgb25lLlxuICogICAgIFRoZSBldmljdGVkIGl0ZW0gaXMgTE9TVCAoYGRyb3BzYCsrKSBhbmQgcmV0dXJuZWQgb24gdGhlIHB1c2ggcmVzdWx0XG4gKiAgICAgc28gdGhlIGNhbGxlciBjYW4gYWNjb3VudCBmb3IgaXQuIFNlcXVlbmNlIHN0YW1wcyBvbiBzdXJ2aXZpbmcgaXRlbXNcbiAqICAgICBrZWVwIGxvc3MgdmlzaWJsZSBhcyBzZXEgZ2FwcyAoaG9uZXN0IGxvc3MgYWNjb3VudGluZykuIERFRkFVTFRcbiAqICAgICBwb3N0dXJlIGZvciB0ZWxlbWV0cnktZ3JhZGUgZGVsaXZlcnkuXG4gKiAgIC0gYCdzYW1wbGUnYCDigJQgd2hpbGUgc2F0dXJhdGVkLCBhZG1pdCAxIGluIGBzYW1wbGVFdmVyeWAgYXJyaXZhbHNcbiAqICAgICAoZXZpY3RpbmcgdGhlIG9sZGVzdCB0byBtYWtlIHJvb20g4oCUIHRoYXQgZXZpY3Rpb24gaXMgYWxzbyBhIGNvdW50ZWRcbiAqICAgICBsb3NzKTsgcmVmdXNlIHRoZSByZXN0IChlYWNoIGEgY291bnRlZCBsb3NzKS4gS2VlcHMgYSB0aGlubmVkLFxuICogICAgIHN0aWxsLWZyZXNoIHN0cmVhbSB1bmRlciBzdXN0YWluZWQgb3ZlcmxvYWQuIFRoZSBzYXR1cmF0aW9uIGNvdW50ZXJcbiAqICAgICBpcyBlcGlzb2RlLXNjb3BlZDogaXQgcmVzZXRzIHdoZW5ldmVyIGEgcHVzaCBzdWNjZWVkcyB0aHJvdWdoIHRoZVxuICogICAgIG5vbi1mdWxsIHBhdGguXG4gKiAgIC0gYCdibG9jaydgIOKAlCB0aGUgcmluZyBSRUZVU0VTIHRoZSBuZXcgaXRlbSAoYGFjY2VwdGVkOiBmYWxzZWAsXG4gKiAgICAgYHJlamVjdGlvbnNgKyspIGFuZCBkcm9wcyBOT1RISU5HLiBJbiBhIHNpbmdsZS10aHJlYWRlZCBydW50aW1lIGFcbiAqICAgICBxdWV1ZSBjYW5ub3QgbGl0ZXJhbGx5IGJsb2NrIGl0cyBwcm9kdWNlcjsgdGhlIGRpc3BhdGNoZXIgKEJsb2NrIDUpXG4gKiAgICAgaW50ZXJwcmV0cyBhIHJlZnVzYWwgYXMgXCJkZWxpdmVyIHRoaXMgZXZlbnQgc3luY2hyb25vdXNseSBpbmxpbmVcIiDigJRcbiAqICAgICByZS1pbnRyb2R1Y2luZyBibG9ja2luZyBkZWxpdmVyeSBieSB0aGUgY29uc3VtZXIncyBFWFBMSUNJVCBjaG9pY2UuXG4gKiAgICAgUmVqZWN0aW9ucyBhcmUgTk9UIGxvc3NlczogdGhlIGV2ZW50IGlzIHN0aWxsIGRlbGl2ZXJlZCAoaW5saW5lKSwgc29cbiAqICAgICBgZHJvcHNgIHN0YXlzIHVudG91Y2hlZC5cbiAqXG4gKiBDb25zZXJ2YXRpb24gaW52YXJpYW50IChwcm9wZXJ0eS10ZXN0ZWQpOlxuICogICBwdXNoZXMgPT09IGRlbGl2ZXJlZCArIGRyb3BzICsgcmVqZWN0aW9ucyArIHNpemVcbiAqXG4gKiBDVVJTT1ItUkVBRFkgKGFtZW5kbWVudCBBMik6IHYxIGNvbnN1bWVzIGRlc3RydWN0aXZlbHkgdGhyb3VnaCBPTkUgY3Vyc29yXG4gKiAoYHNoaWZ0KClgIGFkdmFuY2VzIGBoZWFkYCkuIFRoZSBkZXNpZ25lZCB2MS4xIHBhdGgga2VlcHMgaXRlbXMgaW4gdGhlXG4gKiByaW5nIGFuZCBnaXZlcyBlYWNoIGxpc3RlbmVyIGl0cyBvd24gcmVhZCBjdXJzb3I7IGBoZWFkYCB0aGVuIGFkdmFuY2VzIHRvXG4gKiBgbWluKGN1cnNvcnMpYCAodGhlIHJlY2xhaW0gd2F0ZXJtYXJrKSBpbnN0ZWFkIG9mIG9uIHJlYWQuIFRoZSBzdG9yYWdlXG4gKiBsYXlvdXQgKGNvbnRpZ3VvdXMgY2lyY3VsYXIgd2luZG93LCBgaGVhZGAgKyBgY291bnRgKSBhbHJlYWR5IHN1cHBvcnRzXG4gKiB0aGF0IOKAlCBvbmx5IHRoZSBjb25zdW1wdGlvbiBzdXJmYWNlIGNoYW5nZXMuIERvY3VtZW50ZWQsIG5vdCBpbXBsZW1lbnRlZC5cbiAqL1xuXG4vKiogSG93IHRoZSByaW5nIHRyZWF0cyBhIHB1c2ggd2hlbiBpdCBpcyBhdCBjYXBhY2l0eSAoUkZDLTAwMSDCpzUpLiAqL1xuZXhwb3J0IHR5cGUgT3ZlcmZsb3dQb2xpY3kgPSAnYmxvY2snIHwgJ2Ryb3Atb2xkZXN0JyB8ICdzYW1wbGUnO1xuXG5leHBvcnQgaW50ZXJmYWNlIFJpbmdPcHRpb25zIHtcbiAgLyoqIE1heCBxdWV1ZWQgaXRlbXMuIFBvc2l0aXZlIGludGVnZXIuICovXG4gIHJlYWRvbmx5IGNhcGFjaXR5OiBudW1iZXI7XG4gIC8qKiBPdmVyZmxvdyBiZWhhdmlvciBhdCBjYXBhY2l0eSDigJQgc2VlIHRoZSBtb2R1bGUgaGVhZGVyLiAqL1xuICByZWFkb25seSBwb2xpY3k6IE92ZXJmbG93UG9saWN5O1xuICAvKipcbiAgICogYCdzYW1wbGUnYCBvbmx5OiBhZG1pdCAxIGluIHRoaXMgbWFueSBhcnJpdmFscyB3aGlsZSBzYXR1cmF0ZWQuXG4gICAqIFBvc2l0aXZlIGludGVnZXI7IGRlZmF1bHQgMTAuXG4gICAqL1xuICByZWFkb25seSBzYW1wbGVFdmVyeT86IG51bWJlcjtcbn1cblxuLyoqIE91dGNvbWUgb2Ygb25lIHtAbGluayBCb3VuZGVkUmluZy5wdXNofS4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmluZ1B1c2hSZXN1bHQ8VD4ge1xuICAvKiogVHJ1ZSB3aGVuIHRoZSBwdXNoZWQgaXRlbSBpcyBub3cgcXVldWVkLiAqL1xuICByZWFkb25seSBhY2NlcHRlZDogYm9vbGVhbjtcbiAgLyoqXG4gICAqIFRoZSBvbGRlc3QgaXRlbSwgd2hlbiBhZG1pdHRpbmcgdGhlIG5ldyBvbmUgZXZpY3RlZCBpdFxuICAgKiAoYCdkcm9wLW9sZGVzdCdgLCBvciBhIGAnc2FtcGxlJ2AgYWRtaXNzaW9uKS4gQWxyZWFkeSBjb3VudGVkIGluXG4gICAqIGBkcm9wc2Ag4oCUIHN1cmZhY2VkIHNvIGNhbGxlcnMgY2FuIGRvIHRoZWlyIG93biBsb3NzIGFjY291bnRpbmcuXG4gICAqL1xuICByZWFkb25seSBldmljdGVkPzogVDtcbn1cblxuLyoqIE1vbm90b25pYyBjb3VudGVycyDigJQgbmV2ZXIgcmVzZXQgZm9yIHRoZSBsaWZldGltZSBvZiB0aGUgcmluZy4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgUmluZ0NvdW50ZXJzIHtcbiAgLyoqIFRvdGFsIGBwdXNoKClgIGNhbGxzLiAqL1xuICByZWFkb25seSBwdXNoZXM6IG51bWJlcjtcbiAgLyoqIGBzaGlmdCgpYCBjYWxscyB0aGF0IHJldHVybmVkIGFuIGl0ZW0uICovXG4gIHJlYWRvbmx5IGRlbGl2ZXJlZDogbnVtYmVyO1xuICAvKiogSXRlbXMgTE9TVCDigJQgZXZpY3Rpb25zIHBsdXMgc2FtcGxlZC1vdXQgcmVmdXNhbHMuIE5ldmVyIHNpbGVudC4gKi9cbiAgcmVhZG9ubHkgZHJvcHM6IG51bWJlcjtcbiAgLyoqIGAnYmxvY2snYCByZWZ1c2FscyDigJQgTk9UIGxvc3NlczsgdGhlIGNhbGxlciBkZWxpdmVycyB0aGVzZSBpbmxpbmUuICovXG4gIHJlYWRvbmx5IHJlamVjdGlvbnM6IG51bWJlcjtcbn1cblxuY29uc3QgREVGQVVMVF9TQU1QTEVfRVZFUlkgPSAxMDtcblxuZXhwb3J0IGNsYXNzIEJvdW5kZWRSaW5nPFQ+IHtcbiAgcHJpdmF0ZSByZWFkb25seSBidWZmZXI6IEFycmF5PFQgfCB1bmRlZmluZWQ+O1xuICBwcml2YXRlIHJlYWRvbmx5IHBvbGljeTogT3ZlcmZsb3dQb2xpY3k7XG4gIHByaXZhdGUgcmVhZG9ubHkgc2FtcGxlRXZlcnk6IG51bWJlcjtcbiAgLyoqIEluZGV4IG9mIHRoZSBvbGRlc3QgcXVldWVkIGl0ZW0g4oCUIHRoZSBzaW5nbGUgdjEgY3Vyc29yIChzZWUgaGVhZGVyKS4gKi9cbiAgcHJpdmF0ZSBoZWFkID0gMDtcbiAgcHJpdmF0ZSBjb3VudCA9IDA7XG4gIC8qKiBBcnJpdmFscyBzZWVuIHdoaWxlIHNhdHVyYXRlZCBpbiB0aGUgY3VycmVudCBlcGlzb2RlIChgJ3NhbXBsZSdgKS4gKi9cbiAgcHJpdmF0ZSBzYXR1cmF0ZWRBcnJpdmFscyA9IDA7XG5cbiAgcHJpdmF0ZSBwdXNoZXMgPSAwO1xuICBwcml2YXRlIGRlbGl2ZXJlZCA9IDA7XG4gIHByaXZhdGUgZHJvcHMgPSAwO1xuICBwcml2YXRlIHJlamVjdGlvbnMgPSAwO1xuXG4gIGNvbnN0cnVjdG9yKG9wdHM6IFJpbmdPcHRpb25zKSB7XG4gICAgaWYgKCFOdW1iZXIuaXNJbnRlZ2VyKG9wdHMuY2FwYWNpdHkpIHx8IG9wdHMuY2FwYWNpdHkgPD0gMCkge1xuICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoYEJvdW5kZWRSaW5nIGNhcGFjaXR5IG11c3QgYmUgYSBwb3NpdGl2ZSBpbnRlZ2VyIChnb3QgJHtvcHRzLmNhcGFjaXR5fSlgKTtcbiAgICB9XG4gICAgY29uc3Qgc2FtcGxlRXZlcnkgPSBvcHRzLnNhbXBsZUV2ZXJ5ID8/IERFRkFVTFRfU0FNUExFX0VWRVJZO1xuICAgIGlmICghTnVtYmVyLmlzSW50ZWdlcihzYW1wbGVFdmVyeSkgfHwgc2FtcGxlRXZlcnkgPD0gMCkge1xuICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoYEJvdW5kZWRSaW5nIHNhbXBsZUV2ZXJ5IG11c3QgYmUgYSBwb3NpdGl2ZSBpbnRlZ2VyIChnb3QgJHtvcHRzLnNhbXBsZUV2ZXJ5fSlgKTtcbiAgICB9XG4gICAgdGhpcy5idWZmZXIgPSBuZXcgQXJyYXk8VCB8IHVuZGVmaW5lZD4ob3B0cy5jYXBhY2l0eSk7XG4gICAgdGhpcy5wb2xpY3kgPSBvcHRzLnBvbGljeTtcbiAgICB0aGlzLnNhbXBsZUV2ZXJ5ID0gc2FtcGxlRXZlcnk7XG4gIH1cblxuICBnZXQgc2l6ZSgpOiBudW1iZXIge1xuICAgIHJldHVybiB0aGlzLmNvdW50O1xuICB9XG5cbiAgZ2V0IGNhcGFjaXR5KCk6IG51bWJlciB7XG4gICAgcmV0dXJuIHRoaXMuYnVmZmVyLmxlbmd0aDtcbiAgfVxuXG4gIC8qKiBMaWZldGltZSBjb3VudGVycyDigJQgc2VlIHtAbGluayBSaW5nQ291bnRlcnN9LiAqL1xuICBnZXRDb3VudGVycygpOiBSaW5nQ291bnRlcnMge1xuICAgIHJldHVybiB7IHB1c2hlczogdGhpcy5wdXNoZXMsIGRlbGl2ZXJlZDogdGhpcy5kZWxpdmVyZWQsIGRyb3BzOiB0aGlzLmRyb3BzLCByZWplY3Rpb25zOiB0aGlzLnJlamVjdGlvbnMgfTtcbiAgfVxuXG4gIC8qKiBBZG1pdCwgZXZpY3QtYW5kLWFkbWl0LCByZWZ1c2UsIG9yIHNhbXBsZSBwZXIgcG9saWN5IOKAlCBuZXZlciB0aHJvd3MuICovXG4gIHB1c2goaXRlbTogVCk6IFJpbmdQdXNoUmVzdWx0PFQ+IHtcbiAgICB0aGlzLnB1c2hlcyArPSAxO1xuXG4gICAgaWYgKHRoaXMuY291bnQgPCB0aGlzLmJ1ZmZlci5sZW5ndGgpIHtcbiAgICAgIHRoaXMuc2F0dXJhdGVkQXJyaXZhbHMgPSAwOyAvLyBuZXcgc2F0dXJhdGlvbiBlcGlzb2RlIHN0YXJ0cyBmcmVzaFxuICAgICAgdGhpcy5zdG9yZShpdGVtKTtcbiAgICAgIHJldHVybiB7IGFjY2VwdGVkOiB0cnVlIH07XG4gICAgfVxuXG4gICAgaWYgKHRoaXMucG9saWN5ID09PSAnYmxvY2snKSB7XG4gICAgICB0aGlzLnJlamVjdGlvbnMgKz0gMTtcbiAgICAgIHJldHVybiB7IGFjY2VwdGVkOiBmYWxzZSB9O1xuICAgIH1cblxuICAgIGlmICh0aGlzLnBvbGljeSA9PT0gJ3NhbXBsZScpIHtcbiAgICAgIHRoaXMuc2F0dXJhdGVkQXJyaXZhbHMgKz0gMTtcbiAgICAgIGlmICh0aGlzLnNhdHVyYXRlZEFycml2YWxzICUgdGhpcy5zYW1wbGVFdmVyeSAhPT0gMCkge1xuICAgICAgICB0aGlzLmRyb3BzICs9IDE7IC8vIHRoZSBpbmNvbWluZyBpdGVtIGlzIHNhbXBsZWQgb3V0IOKAlCBsb3N0XG4gICAgICAgIHJldHVybiB7IGFjY2VwdGVkOiBmYWxzZSB9O1xuICAgICAgfVxuICAgICAgLy8gVGhlIDEtaW4tTiBhZG1pc3Npb24gZmFsbHMgdGhyb3VnaCB0byBldmljdC1hbmQtc3RvcmUuXG4gICAgfVxuXG4gICAgLy8gJ2Ryb3Atb2xkZXN0JyAoYW5kIHRoZSAnc2FtcGxlJyBhZG1pc3Npb24pOiBldmljdCB0aGUgb2xkZXN0IOKAlCBsb3N0LlxuICAgIGNvbnN0IGV2aWN0ZWQgPSB0aGlzLmJ1ZmZlclt0aGlzLmhlYWRdIGFzIFQ7XG4gICAgdGhpcy5idWZmZXJbdGhpcy5oZWFkXSA9IHVuZGVmaW5lZDtcbiAgICB0aGlzLmhlYWQgPSAodGhpcy5oZWFkICsgMSkgJSB0aGlzLmJ1ZmZlci5sZW5ndGg7XG4gICAgdGhpcy5jb3VudCAtPSAxO1xuICAgIHRoaXMuZHJvcHMgKz0gMTtcbiAgICB0aGlzLnN0b3JlKGl0ZW0pO1xuICAgIHJldHVybiB7IGFjY2VwdGVkOiB0cnVlLCBldmljdGVkIH07XG4gIH1cblxuICAvKiogUG9wIHRoZSBvbGRlc3QgcXVldWVkIGl0ZW0gKEZJRk8pLiBgdW5kZWZpbmVkYCB3aGVuIGVtcHR5LiAqL1xuICBzaGlmdCgpOiBUIHwgdW5kZWZpbmVkIHtcbiAgICBpZiAodGhpcy5jb3VudCA9PT0gMCkgcmV0dXJuIHVuZGVmaW5lZDtcbiAgICBjb25zdCBpdGVtID0gdGhpcy5idWZmZXJbdGhpcy5oZWFkXSBhcyBUO1xuICAgIHRoaXMuYnVmZmVyW3RoaXMuaGVhZF0gPSB1bmRlZmluZWQ7IC8vIHJlbGVhc2UgdGhlIHJlZmVyZW5jZSBmb3IgR0NcbiAgICB0aGlzLmhlYWQgPSAodGhpcy5oZWFkICsgMSkgJSB0aGlzLmJ1ZmZlci5sZW5ndGg7XG4gICAgdGhpcy5jb3VudCAtPSAxO1xuICAgIHRoaXMuZGVsaXZlcmVkICs9IDE7XG4gICAgcmV0dXJuIGl0ZW07XG4gIH1cblxuICBwcml2YXRlIHN0b3JlKGl0ZW06IFQpOiB2b2lkIHtcbiAgICB0aGlzLmJ1ZmZlclsodGhpcy5oZWFkICsgdGhpcy5jb3VudCkgJSB0aGlzLmJ1ZmZlci5sZW5ndGhdID0gaXRlbTtcbiAgICB0aGlzLmNvdW50ICs9IDE7XG4gIH1cbn1cbiJdfQ==
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capture/envelope.ts — RFC-001 Block 1: capture envelopes + payload summarizer.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Point-in-time capture. An observer event is snapshotted into a
|
|
5
|
+
* self-contained, immutable envelope at the moment it happens, so
|
|
6
|
+
* DELIVERY can be deferred (RFC-001 "one beat behind") without the
|
|
7
|
+
* payload drifting under later engine mutations.
|
|
8
|
+
* Role: The capture tier of the deferred-observer pipeline
|
|
9
|
+
* (`src/lib/observer-queue/`). Pure module — ZERO engine imports
|
|
10
|
+
* (only `capture/` internals); the engine wiring is RFC-001
|
|
11
|
+
* Blocks 6–10.
|
|
12
|
+
*
|
|
13
|
+
* Capture policies (RFC-001 §5):
|
|
14
|
+
* - `'summary'` — bounded, reference-free summarization via
|
|
15
|
+
* {@link summarizePayload}. Built on the SAME classification path as the
|
|
16
|
+
* retention markers in `summarize.ts` (#14 / #13c-A), extended with
|
|
17
|
+
* bounded structural descent. Structured-clone-safe by construction.
|
|
18
|
+
* - `'clone'` — `structuredClone` at capture time (the capture-tier
|
|
19
|
+
* spelling of retention `'full'` — see the mapping notes in
|
|
20
|
+
* `policies.ts`). If the payload is not clonable (functions, symbols,
|
|
21
|
+
* live handles), capture DEGRADES to `'summary'` and reports through the
|
|
22
|
+
* `warn` hook — a capture must never throw into the producer.
|
|
23
|
+
* - `'ref'` — pass-through of the live reference. The CALLER asserts
|
|
24
|
+
* immutability for the delivery window (safe e.g. for committed-state
|
|
25
|
+
* reads, proven immutable-after-swap in #13/#13b). Exempt from the
|
|
26
|
+
* clone-safety guarantee — see the dev-warn seam below.
|
|
27
|
+
*
|
|
28
|
+
* Dev-warn seam (resolves the isDevMode-would-be-an-engine-import problem):
|
|
29
|
+
* This module must not import `scope/detectCircular` (engine territory).
|
|
30
|
+
* Instead, `capture()` accepts {@link CaptureHooks} with an optional
|
|
31
|
+
* `warn` callback and invokes it on every `'ref'` capture and every
|
|
32
|
+
* `'clone'` degradation. The WIRING layer (Block 6) binds `warn` to an
|
|
33
|
+
* `isDevMode()`-gated, deduplicated console warner; the pure module stays
|
|
34
|
+
* engine-free and silent by default (no hooks ⇒ no warning, zero cost).
|
|
35
|
+
*
|
|
36
|
+
* Summarizer bounds (documented contract, exported as constants):
|
|
37
|
+
* - depth ≤ {@link PAYLOAD_SUMMARY_MAX_DEPTH} (3) — deeper structure
|
|
38
|
+
* collapses to a classified leaf with `depthClipped: true`.
|
|
39
|
+
* - breadth ≤ {@link PAYLOAD_SUMMARY_MAX_ENTRIES} (16) per object/array —
|
|
40
|
+
* the remainder is dropped and flagged `truncated: true` (the honest
|
|
41
|
+
* `size` still reports the real count).
|
|
42
|
+
* - total ≤ {@link PAYLOAD_SUMMARY_MAX_NODES} (128) summary nodes per
|
|
43
|
+
* payload — a global budget so wide×deep payloads stay O(1)-bounded.
|
|
44
|
+
* - string previews ≤ `SUMMARY_PREVIEW_LENGTH` (80) chars.
|
|
45
|
+
* Cycles are detected (ancestor set) and flagged `circular: true`;
|
|
46
|
+
* throwing getters yield a `'unreadable'` leaf; symbol-keyed properties
|
|
47
|
+
* are ignored (same as JSON / `Object.keys`); `Map`/`Set` are leaves with
|
|
48
|
+
* their REAL entry count (no descent — matches `summarize.ts`).
|
|
49
|
+
*/
|
|
50
|
+
import { type SummaryValueType } from './summarize.js';
|
|
51
|
+
/** Which observer channel produced a captured event (RFC-001 §5). */
|
|
52
|
+
export type CaptureChannel = 'scope' | 'flow' | 'emit';
|
|
53
|
+
/**
|
|
54
|
+
* How an event payload is materialized into the envelope (RFC-001 §5).
|
|
55
|
+
* See the module header for the full per-policy contract.
|
|
56
|
+
*/
|
|
57
|
+
export type CapturePolicy = 'summary' | 'clone' | 'ref';
|
|
58
|
+
/**
|
|
59
|
+
* A captured observer event — self-contained and immutable (shallow-frozen
|
|
60
|
+
* at creation). `seq` is the arrival stamp assigned at capture under the
|
|
61
|
+
* single JS thread: it totally orders events ACROSS all three channels, is
|
|
62
|
+
* monotonic, and is gap-detectable (a dropped event leaves a visible hole
|
|
63
|
+
* in the delivered `seq` sequence — honest loss accounting).
|
|
64
|
+
*/
|
|
65
|
+
export interface CaptureEnvelope {
|
|
66
|
+
/** Arrival stamp — total order across channels; gaps reveal drops. */
|
|
67
|
+
readonly seq: number;
|
|
68
|
+
/** Producing observer channel. */
|
|
69
|
+
readonly channel: CaptureChannel;
|
|
70
|
+
/** Producing hook name — `'onWrite'`, `'onStageExecuted'`, `'onEmit'`, ... */
|
|
71
|
+
readonly method: string;
|
|
72
|
+
/** The execution step that produced the event (`stageId#executionIndex`). */
|
|
73
|
+
readonly runtimeStageId: string;
|
|
74
|
+
/** The run that produced the event (Convention 4 per-run scoping). */
|
|
75
|
+
readonly runId: string;
|
|
76
|
+
/**
|
|
77
|
+
* Per {@link CapturePolicy} — NEVER a live engine reference under
|
|
78
|
+
* `'summary'` / `'clone'`; under `'ref'` the caller asserted immutability.
|
|
79
|
+
*/
|
|
80
|
+
readonly payload: unknown;
|
|
81
|
+
/** Capture wall-clock (ms epoch by default; injectable via hooks.now). */
|
|
82
|
+
readonly capturedAt: number;
|
|
83
|
+
}
|
|
84
|
+
/** The raw event description handed to {@link capture}. */
|
|
85
|
+
export interface CaptureRequest {
|
|
86
|
+
/** Arrival stamp — assigned by the merged queue's counter (Block 3). */
|
|
87
|
+
readonly seq: number;
|
|
88
|
+
readonly channel: CaptureChannel;
|
|
89
|
+
readonly method: string;
|
|
90
|
+
readonly runtimeStageId: string;
|
|
91
|
+
readonly runId: string;
|
|
92
|
+
/** The LIVE payload — `capture()` materializes it per policy. */
|
|
93
|
+
readonly payload: unknown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Engine-free seams injected by the wiring layer (Block 6). The pure module
|
|
97
|
+
* never imports dev-mode or clock infrastructure.
|
|
98
|
+
*/
|
|
99
|
+
export interface CaptureHooks {
|
|
100
|
+
/**
|
|
101
|
+
* Diagnostic sink — invoked on every `'ref'` capture (caller-asserted
|
|
102
|
+
* immutability) and every `'clone'` → `'summary'` degradation. Block 6
|
|
103
|
+
* binds this to an `isDevMode()`-gated, deduplicated warner.
|
|
104
|
+
*/
|
|
105
|
+
readonly warn?: (message: string) => void;
|
|
106
|
+
/** Clock for `capturedAt` — defaults to `Date.now`. Injectable for tests. */
|
|
107
|
+
readonly now?: () => number;
|
|
108
|
+
}
|
|
109
|
+
/** Max nesting depth a payload summary descends before clipping. */
|
|
110
|
+
export declare const PAYLOAD_SUMMARY_MAX_DEPTH = 3;
|
|
111
|
+
/** Max entries summarized per object/array level before truncating. */
|
|
112
|
+
export declare const PAYLOAD_SUMMARY_MAX_ENTRIES = 16;
|
|
113
|
+
/** Global per-payload budget of summary nodes (wide×deep hard bound). */
|
|
114
|
+
export declare const PAYLOAD_SUMMARY_MAX_NODES = 128;
|
|
115
|
+
/**
|
|
116
|
+
* Leaf classification for a summary node — the `summarize.ts` family plus
|
|
117
|
+
* `'unreadable'` for properties whose getter threw during capture.
|
|
118
|
+
*/
|
|
119
|
+
export type PayloadSummaryType = SummaryValueType | 'unreadable';
|
|
120
|
+
/**
|
|
121
|
+
* One node of a payload summary tree. Every field is a primitive, a plain
|
|
122
|
+
* object, or a plain array — structured-clone-safe by construction.
|
|
123
|
+
*/
|
|
124
|
+
export interface PayloadSummaryNode {
|
|
125
|
+
/** Classification — same rules as the retention markers (one code path). */
|
|
126
|
+
readonly type: PayloadSummaryType;
|
|
127
|
+
/** Honest size proxy: string length, array length, or key/entry count. */
|
|
128
|
+
readonly size?: number;
|
|
129
|
+
/** First `SUMMARY_PREVIEW_LENGTH` chars — primitives/strings only. */
|
|
130
|
+
readonly preview?: string;
|
|
131
|
+
/** Summarized own enumerable string-keyed properties (objects). */
|
|
132
|
+
readonly fields?: Readonly<Record<string, PayloadSummaryNode>>;
|
|
133
|
+
/** Summarized leading items (arrays). */
|
|
134
|
+
readonly items?: readonly PayloadSummaryNode[];
|
|
135
|
+
/** Entries were omitted here (breadth cap or node budget). */
|
|
136
|
+
readonly truncated?: boolean;
|
|
137
|
+
/** This value is an ancestor of itself — descent stopped. */
|
|
138
|
+
readonly circular?: true;
|
|
139
|
+
/** {@link PAYLOAD_SUMMARY_MAX_DEPTH} reached — children not descended. */
|
|
140
|
+
readonly depthClipped?: true;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Root of a payload summary — branded so consumers (and tests) can detect
|
|
144
|
+
* that a payload was summarized rather than cloned. Sibling of the
|
|
145
|
+
* `__readSummary` / `__writeSummary` retention markers.
|
|
146
|
+
*/
|
|
147
|
+
export interface PayloadSummary extends PayloadSummaryNode {
|
|
148
|
+
/** Discriminant — `'summary'`-policy envelope payloads carry this. */
|
|
149
|
+
readonly __payloadSummary: true;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Produce a bounded, reference-free, structured-clone-safe summary of an
|
|
153
|
+
* arbitrary payload. Never throws; never holds a reference into the source
|
|
154
|
+
* value (every node is a fresh object whose fields are primitives). Bounds
|
|
155
|
+
* are documented in the module header.
|
|
156
|
+
*/
|
|
157
|
+
export declare function summarizePayload(payload: unknown): PayloadSummary;
|
|
158
|
+
/**
|
|
159
|
+
* Capture one observer event into an immutable {@link CaptureEnvelope}.
|
|
160
|
+
*
|
|
161
|
+
* Guarantees:
|
|
162
|
+
* - Never throws into the producer (`'clone'` degradation, summarizer
|
|
163
|
+
* never-throws contract).
|
|
164
|
+
* - The returned envelope is shallow-frozen — `seq`/`channel`/... cannot
|
|
165
|
+
* be reassigned. Under `'summary'`/`'clone'` the payload holds no live
|
|
166
|
+
* reference into the source; under `'ref'` it intentionally does.
|
|
167
|
+
* - Default policy is `'summary'` (cheapest safe tier).
|
|
168
|
+
*/
|
|
169
|
+
export declare function capture(request: CaptureRequest, policy?: CapturePolicy, hooks?: CaptureHooks): CaptureEnvelope;
|
|
@@ -43,6 +43,18 @@ export declare class ContinuationResolver<TOut = any, TScope = any> {
|
|
|
43
43
|
* Key: node.id, Value: visit count (0 = first visit).
|
|
44
44
|
*/
|
|
45
45
|
private iterationCounters;
|
|
46
|
+
/**
|
|
47
|
+
* Total fn-bearing dynamic-next hops this traverser has followed.
|
|
48
|
+
*
|
|
49
|
+
* Fresh fn-bearing nodes bypass the per-node-id iteration counter (they
|
|
50
|
+
* are new nodes, often without stable ids — there is no back-edge to
|
|
51
|
+
* count). Without a bound, a stage that keeps returning a function-bearing
|
|
52
|
+
* dynamic `next` runs FOREVER on the flat trampoline (no stack overflow
|
|
53
|
+
* brakes it either). This run-total counter puts such chains under the
|
|
54
|
+
* same `maxIterations` budget (default 1000, tuned via
|
|
55
|
+
* `RunOptions.maxIterations`) that bounds loop edges.
|
|
56
|
+
*/
|
|
57
|
+
private dynamicNextHops;
|
|
46
58
|
private readonly onIterationUpdate?;
|
|
47
59
|
private readonly maxIterations;
|
|
48
60
|
constructor(deps: HandlerDeps<TOut, TScope>, nodeResolver: NodeResolver<TOut, TScope>, onIterationUpdate?: (nodeId: string, count: number) => void, maxIterations?: number);
|
|
@@ -62,8 +74,9 @@ export declare class ContinuationResolver<TOut = any, TScope = any> {
|
|
|
62
74
|
* `onLoop` narrative), in the same order.
|
|
63
75
|
*
|
|
64
76
|
* Three dynamicNext patterns:
|
|
65
|
-
* - StageNode with fn → truly dynamic node, returned as-is (no
|
|
66
|
-
* tracking — it is a fresh node, not a back-edge
|
|
77
|
+
* - StageNode with fn → truly dynamic node, returned as-is (no per-node
|
|
78
|
+
* iteration tracking — it is a fresh node, not a back-edge — but the
|
|
79
|
+
* run-total dynamic-hop budget applies; see `dynamicNextHops`).
|
|
67
80
|
* - String ID → reference to an existing node, resolved via NodeResolver.
|
|
68
81
|
* - StageNode without fn → reference by ID, resolved via NodeResolver.
|
|
69
82
|
*/
|
|
@@ -337,6 +337,9 @@ export interface RunOptions {
|
|
|
337
337
|
* This is the binding constraint for loop-heavy pipelines — raise it for
|
|
338
338
|
* legitimately long loops (`loopTo` chains run with a flat stack, so high
|
|
339
339
|
* values are safe; memory for state/narrative still grows per iteration).
|
|
340
|
+
* Also bounds the run-total chain of function-bearing dynamic `next`
|
|
341
|
+
* continuations (fresh nodes returned from stages) — a runaway dynamic
|
|
342
|
+
* chain errors instead of running forever.
|
|
340
343
|
* Propagates to subflows. Must be >= 1.
|
|
341
344
|
*/
|
|
342
345
|
maxIterations?: number;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* observer-queue/deferredDispatcher.ts — RFC-001 Block 5: deferred delivery façade.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: capture → enqueue → (microtask) flush → invoke, with per-listener
|
|
5
|
+
* error isolation. Composes the whole pure pipeline: MergedQueue
|
|
6
|
+
* (Block 3, which captures via Block 1) + FlushDriver (Block 4) +
|
|
7
|
+
* a listener registry with timing/inflight accounting.
|
|
8
|
+
* Role: The object the engine wiring (Block 6) will hold. Producers call
|
|
9
|
+
* `capture()` (cheap, never throws, never blocks); listeners
|
|
10
|
+
* receive envelopes at the next checkpoint, "one beat behind".
|
|
11
|
+
* Pure module — zero engine imports.
|
|
12
|
+
*
|
|
13
|
+
* Delivery semantics (normative, RFC-001 §5 + amendments A2/A4):
|
|
14
|
+
* - Per-listener FIFO: every listener sees envelopes in seq order
|
|
15
|
+
* (invocation order; an async listener's COMPLETION order is its own
|
|
16
|
+
* concern) — EXCEPT under `'block'` overflow, where a refused enqueue
|
|
17
|
+
* is delivered inline and overtakes the queued backlog. `seq` always
|
|
18
|
+
* records true arrival order, so order-sensitive consumers re-sort;
|
|
19
|
+
* see the `'block'` caveat below.
|
|
20
|
+
* - Error isolation: a throwing listener (sync) or rejecting listener
|
|
21
|
+
* (async) never affects siblings or the producer. Both failure modes
|
|
22
|
+
* route to the injected `onError`; a throwing `onError` is itself
|
|
23
|
+
* swallowed.
|
|
24
|
+
* - The flush NEVER awaits a listener. Async continuations are tracked in
|
|
25
|
+
* an inflight set; `drain({ timeoutMs })` settles them
|
|
26
|
+
* (`Promise.allSettled` + deadline, shaped like `flushAllDetached`).
|
|
27
|
+
* - `'block'` overflow: a refused enqueue is delivered synchronously
|
|
28
|
+
* INLINE from `capture()` — re-introducing blocking delivery by the
|
|
29
|
+
* consumer's explicit choice. Ordering caveat (documented + tested): an
|
|
30
|
+
* inline event overtakes the queued backlog — `'block'` trades global
|
|
31
|
+
* ordering for zero loss and bounded memory. `seq` still tells the
|
|
32
|
+
* true arrival order.
|
|
33
|
+
* - Listener registry is idempotent by id (same id replaces, different
|
|
34
|
+
* ids coexist) — mirrors the repo-wide recorder ID contract. Stats
|
|
35
|
+
* accumulate per id across replacement; `removeListener` keeps the
|
|
36
|
+
* id's accumulated stats for post-run reports.
|
|
37
|
+
* - Events captured BEFORE any listener attaches stay queued — a listener
|
|
38
|
+
* attached before the next checkpoint still receives the backlog.
|
|
39
|
+
*
|
|
40
|
+
* Per-listener time accounting (amendment A2 — "name the hog"): cumulative
|
|
41
|
+
* `totalMs` and per-checkpoint `lastFlushMs` of SYNC time per listener id —
|
|
42
|
+
* the time that actually blocks the flush. An async listener's continuation
|
|
43
|
+
* time is intentionally not attributed (it does not block delivery).
|
|
44
|
+
*/
|
|
45
|
+
import { type CaptureEnvelope, type CaptureHooks, type CapturePolicy } from '../capture/envelope.js';
|
|
46
|
+
import { type FlushSyncResult } from './flushDriver.js';
|
|
47
|
+
import { type EnqueueInput } from './mergedQueue.js';
|
|
48
|
+
import { type OverflowPolicy } from './ring.js';
|
|
49
|
+
/**
|
|
50
|
+
* One deferred observer. May return a Promise — the dispatcher tracks it in
|
|
51
|
+
* the inflight set but NEVER awaits it during a flush.
|
|
52
|
+
*/
|
|
53
|
+
export type DeferredListener = (envelope: CaptureEnvelope) => void | Promise<void>;
|
|
54
|
+
export interface DispatchErrorContext {
|
|
55
|
+
readonly listenerId: string;
|
|
56
|
+
readonly envelope: CaptureEnvelope;
|
|
57
|
+
/** `'sync'` = listener threw; `'async'` = returned promise rejected. */
|
|
58
|
+
readonly phase: 'sync' | 'async';
|
|
59
|
+
}
|
|
60
|
+
/** Injected error sink — the wiring layer routes these (Block 6). */
|
|
61
|
+
export type DispatchErrorHandler = (error: unknown, context: DispatchErrorContext) => void;
|
|
62
|
+
export interface DeferredDispatcherOptions {
|
|
63
|
+
/** Queue bound — default 10 000 (see `MergedQueue`). */
|
|
64
|
+
readonly maxQueue?: number;
|
|
65
|
+
/** Overflow policy — default `'drop-oldest'`. */
|
|
66
|
+
readonly overflow?: OverflowPolicy;
|
|
67
|
+
/** `'sample'` overflow only — admit 1 in this many saturated arrivals. */
|
|
68
|
+
readonly sampleEvery?: number;
|
|
69
|
+
/** Default capture policy — default `'summary'`. */
|
|
70
|
+
readonly capturePolicy?: CapturePolicy;
|
|
71
|
+
/** Per-flush time budget, ms (A1) — default 2; `Infinity` = full drain. */
|
|
72
|
+
readonly flushBudgetMs?: number;
|
|
73
|
+
/** Listener-failure sink. No default — without it, failures are silent. */
|
|
74
|
+
readonly onError?: DispatchErrorHandler;
|
|
75
|
+
/** Capture seams (dev-warn, capturedAt clock) — see `CaptureHooks`. */
|
|
76
|
+
readonly hooks?: CaptureHooks;
|
|
77
|
+
/** Timing clock for budget + per-listener accounting. Injectable. */
|
|
78
|
+
readonly now?: () => number;
|
|
79
|
+
/** Checkpoint primitive — default `queueMicrotask`. Injectable. */
|
|
80
|
+
readonly schedule?: (cb: () => void) => void;
|
|
81
|
+
}
|
|
82
|
+
/** Per-listener accounting (A2/A4). */
|
|
83
|
+
export interface ListenerStats {
|
|
84
|
+
/** Envelopes delivered (invocations, including ones that threw). */
|
|
85
|
+
readonly events: number;
|
|
86
|
+
/** Cumulative sync delivery time, ms. */
|
|
87
|
+
readonly totalMs: number;
|
|
88
|
+
/** Sync delivery time since the last flush started, ms. */
|
|
89
|
+
readonly lastFlushMs: number;
|
|
90
|
+
}
|
|
91
|
+
/** The Block 9 observability surface (amendment A4) — pure getter. */
|
|
92
|
+
export interface DispatcherStats {
|
|
93
|
+
/** Current backlog. */
|
|
94
|
+
readonly depth: number;
|
|
95
|
+
/** Events LOST (overflow) — never silent; also visible as seq gaps. */
|
|
96
|
+
readonly drops: number;
|
|
97
|
+
/** Completed checkpoint flushes. */
|
|
98
|
+
readonly flushes: number;
|
|
99
|
+
/** Flushes cut short by `flushBudgetMs` (A1). */
|
|
100
|
+
readonly budgetExhausted: number;
|
|
101
|
+
/** p95 flush duration, ms (rolling window). */
|
|
102
|
+
readonly p95FlushMs: number;
|
|
103
|
+
/** `'block'`-policy refusals delivered synchronously inline. */
|
|
104
|
+
readonly inlineDeliveries: number;
|
|
105
|
+
/** Async listener continuations not yet settled. */
|
|
106
|
+
readonly inflight: number;
|
|
107
|
+
/** Per-listener time accounting — "name the hog" (A2). */
|
|
108
|
+
readonly perListener: Readonly<Record<string, ListenerStats>>;
|
|
109
|
+
}
|
|
110
|
+
/** Result of {@link DeferredDispatcher.drain} — `flushAllDetached` shape. */
|
|
111
|
+
export interface DrainResult {
|
|
112
|
+
/** Async continuations seen settling fulfilled. Best-effort count — a
|
|
113
|
+
* continuation that settles between checks is drained but may not be
|
|
114
|
+
* counted (same semantics as `flushAllDetached`). */
|
|
115
|
+
readonly done: number;
|
|
116
|
+
/** Continuations whose listener promise rejected (routed to onError). */
|
|
117
|
+
readonly failed: number;
|
|
118
|
+
/** Still in flight (or queued) when the deadline expired. `0` = drained. */
|
|
119
|
+
readonly pending: number;
|
|
120
|
+
}
|
|
121
|
+
export declare class DeferredDispatcher {
|
|
122
|
+
private readonly queue;
|
|
123
|
+
private readonly driver;
|
|
124
|
+
private readonly listeners;
|
|
125
|
+
private readonly listenerStats;
|
|
126
|
+
/** Tracked async continuations — resolve `true` (ok) / `false` (failed). */
|
|
127
|
+
private readonly inflight;
|
|
128
|
+
private readonly onError?;
|
|
129
|
+
private readonly now;
|
|
130
|
+
private inlineDeliveries;
|
|
131
|
+
constructor(opts?: DeferredDispatcherOptions);
|
|
132
|
+
/** Idempotent by id — same id replaces (stats continue), ids coexist. */
|
|
133
|
+
addListener(id: string, listener: DeferredListener): void;
|
|
134
|
+
/** Stop delivering to `id`. Accumulated stats are kept for reports. */
|
|
135
|
+
removeListener(id: string): void;
|
|
136
|
+
/**
|
|
137
|
+
* Producer entry point: capture the event (seq-stamped, payload per
|
|
138
|
+
* policy) and stage it for the next checkpoint. Cheap; NEVER throws;
|
|
139
|
+
* never blocks — except under `'block'` overflow, where a refused
|
|
140
|
+
* enqueue is delivered synchronously inline (explicit consumer choice).
|
|
141
|
+
*/
|
|
142
|
+
capture(input: EnqueueInput, policy?: CapturePolicy): void;
|
|
143
|
+
/**
|
|
144
|
+
* Terminal flush — synchronously deliver everything queued (end of run /
|
|
145
|
+
* shutdown). Async listener continuations are NOT awaited; follow with
|
|
146
|
+
* `drain()` for that.
|
|
147
|
+
*/
|
|
148
|
+
flushNow(opts?: {
|
|
149
|
+
maxRounds?: number;
|
|
150
|
+
}): FlushSyncResult;
|
|
151
|
+
/**
|
|
152
|
+
* Flush the backlog, then settle all inflight async continuations —
|
|
153
|
+
* `Promise.allSettled` under a deadline, shaped like `flushAllDetached`.
|
|
154
|
+
* Loops while continuations spawn new captures, until quiescent or the
|
|
155
|
+
* deadline expires.
|
|
156
|
+
*/
|
|
157
|
+
drain(opts?: {
|
|
158
|
+
timeoutMs?: number;
|
|
159
|
+
}): Promise<DrainResult>;
|
|
160
|
+
/** A4 — the stats object Block 9 consumes. Pure getter, fresh snapshot. */
|
|
161
|
+
getStats(): DispatcherStats;
|
|
162
|
+
private deliverNext;
|
|
163
|
+
/** Invoke every listener with full error isolation + time accounting. */
|
|
164
|
+
private deliver;
|
|
165
|
+
/** Track an async continuation; route its rejection; never reject. */
|
|
166
|
+
private track;
|
|
167
|
+
/** The error sink must never become an error source. */
|
|
168
|
+
private safeOnError;
|
|
169
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* observer-queue/flushDriver.ts — RFC-001 Block 4: armed-once microtask batcher.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Kernel-style bottom-half. Producers only set a flag ("work
|
|
5
|
+
* pending") and return; the actual work runs at the next
|
|
6
|
+
* scheduling checkpoint (a microtask), drains under a time
|
|
7
|
+
* budget, and re-arms itself if backlog remains. Same shape as
|
|
8
|
+
* the detach module's `microtaskBatchDriver` — accumulate during
|
|
9
|
+
* the current sync slice, drain at the boundary.
|
|
10
|
+
* Role: The scheduler of the deferred-observer pipeline. Owns WHEN
|
|
11
|
+
* delivery happens; knows nothing about envelopes or listeners
|
|
12
|
+
* (the dispatcher, Block 5, injects `depth`/`processNext`).
|
|
13
|
+
* Pure module — zero imports, zero engine knowledge.
|
|
14
|
+
*
|
|
15
|
+
* Scheduling semantics (normative, RFC-001 §5 + amendment A1):
|
|
16
|
+
* - `arm()` is idempotent: at most ONE pending flush exists (armed flag).
|
|
17
|
+
* N captures between checkpoints ⇒ exactly 1 flush.
|
|
18
|
+
* - A flush drains a SNAPSHOT: at most `depth()`-at-flush-start items.
|
|
19
|
+
* Events enqueued BY listeners during the flush exceed the snapshot and
|
|
20
|
+
* land at the NEXT checkpoint — listener-driven cascades cannot starve
|
|
21
|
+
* the event loop.
|
|
22
|
+
* - `flushBudgetMs` (default 2; `Infinity` = full snapshot drain): the
|
|
23
|
+
* flush stops once the budget is exhausted, counts `budgetExhausted`,
|
|
24
|
+
* and re-arms. At least ONE item is processed per flush regardless of
|
|
25
|
+
* budget — guaranteed progress under any clock.
|
|
26
|
+
* - If backlog remains after the flush (budget cut OR listener enqueues),
|
|
27
|
+
* the driver re-arms for the next checkpoint.
|
|
28
|
+
*
|
|
29
|
+
* Why stage boundaries make this safe: the engine `await`s every stage, so
|
|
30
|
+
* the microtask queue runs at EVERY stage boundary — flushes are at most
|
|
31
|
+
* "one beat behind" the producing stage. See
|
|
32
|
+
* `docs/guides/execution-model.md` ("Stage boundaries are scheduling
|
|
33
|
+
* points") and the FAQ in `docs/design/rfc-001-deferred-observers.md`.
|
|
34
|
+
*
|
|
35
|
+
* Testability: `now` (clock) and `schedule` (checkpoint primitive) are
|
|
36
|
+
* injectable — tests pump flushes deterministically with a fake clock and
|
|
37
|
+
* a captured-callback scheduler; production uses `performance.now` and
|
|
38
|
+
* `queueMicrotask`.
|
|
39
|
+
*/
|
|
40
|
+
/** Result of one flush (also delivered to `onFlushEnd`). */
|
|
41
|
+
export interface FlushOutcome {
|
|
42
|
+
/** Items processed in this flush. */
|
|
43
|
+
readonly processed: number;
|
|
44
|
+
/** True when the time budget cut the flush before the snapshot drained. */
|
|
45
|
+
readonly budgetExhausted: boolean;
|
|
46
|
+
/** True when backlog remained and the driver re-armed itself. */
|
|
47
|
+
readonly rearmed: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** Result of a synchronous {@link FlushDriver.flushSync} drain. */
|
|
50
|
+
export interface FlushSyncResult {
|
|
51
|
+
/** Items processed across all rounds. */
|
|
52
|
+
readonly drained: number;
|
|
53
|
+
/** Items still queued when `maxRounds` stopped a runaway cascade. */
|
|
54
|
+
readonly remaining: number;
|
|
55
|
+
}
|
|
56
|
+
export interface FlushDriverOptions {
|
|
57
|
+
/** Current backlog of the queue this driver drains. */
|
|
58
|
+
readonly depth: () => number;
|
|
59
|
+
/** Process exactly ONE queued item. Precondition: `depth() > 0`. */
|
|
60
|
+
readonly processNext: () => void;
|
|
61
|
+
/**
|
|
62
|
+
* Per-flush time budget in ms. Default 2. `Infinity` drains the full
|
|
63
|
+
* snapshot every checkpoint. Must be > 0.
|
|
64
|
+
*/
|
|
65
|
+
readonly flushBudgetMs?: number;
|
|
66
|
+
/** Clock — default `performance.now` (falls back to `Date.now`). */
|
|
67
|
+
readonly now?: () => number;
|
|
68
|
+
/** Checkpoint primitive — default `queueMicrotask`. */
|
|
69
|
+
readonly schedule?: (cb: () => void) => void;
|
|
70
|
+
/** Fires before the first item of every flush (incl. `flushSync`). */
|
|
71
|
+
readonly onFlushStart?: () => void;
|
|
72
|
+
/** Fires after every flush with its outcome (incl. `flushSync`). */
|
|
73
|
+
readonly onFlushEnd?: (outcome: FlushOutcome) => void;
|
|
74
|
+
}
|
|
75
|
+
export interface FlushDriverStats {
|
|
76
|
+
/** Completed flushes (zero-work wakeups are not counted). */
|
|
77
|
+
readonly flushes: number;
|
|
78
|
+
/** Flushes cut short by `flushBudgetMs` (A1 — backlog visibility). */
|
|
79
|
+
readonly budgetExhausted: number;
|
|
80
|
+
/** Duration of the most recent flush, ms. */
|
|
81
|
+
readonly lastFlushMs: number;
|
|
82
|
+
/** p95 over the last {@link FLUSH_SAMPLE_WINDOW} flush durations, ms. */
|
|
83
|
+
readonly p95FlushMs: number;
|
|
84
|
+
/** True while a flush is scheduled but not yet run. */
|
|
85
|
+
readonly armed: boolean;
|
|
86
|
+
}
|
|
87
|
+
/** Rolling sample window for the p95 flush-duration stat (A4). */
|
|
88
|
+
export declare const FLUSH_SAMPLE_WINDOW = 128;
|
|
89
|
+
export declare class FlushDriver {
|
|
90
|
+
private readonly depth;
|
|
91
|
+
private readonly processNext;
|
|
92
|
+
private readonly flushBudgetMs;
|
|
93
|
+
private readonly now;
|
|
94
|
+
private readonly schedule;
|
|
95
|
+
private readonly onFlushStart?;
|
|
96
|
+
private readonly onFlushEnd?;
|
|
97
|
+
private armed;
|
|
98
|
+
private flushes;
|
|
99
|
+
private budgetExhaustedCount;
|
|
100
|
+
private lastFlushMs;
|
|
101
|
+
private readonly samples;
|
|
102
|
+
private sampleWriteIdx;
|
|
103
|
+
constructor(opts: FlushDriverOptions);
|
|
104
|
+
/**
|
|
105
|
+
* Request a flush at the next checkpoint. Idempotent — while one flush
|
|
106
|
+
* is pending, further arms are free no-ops (the armed-once invariant).
|
|
107
|
+
*/
|
|
108
|
+
arm(): void;
|
|
109
|
+
/**
|
|
110
|
+
* Synchronous full drain — the terminal-flush primitive (end of run /
|
|
111
|
+
* shutdown). Repeats snapshot rounds until the queue is empty so
|
|
112
|
+
* listener-enqueued cascades drain too, capped at `maxRounds` so a
|
|
113
|
+
* listener that enqueues forever cannot hang the process (`remaining`
|
|
114
|
+
* reports what the cap left behind).
|
|
115
|
+
*/
|
|
116
|
+
flushSync(opts?: {
|
|
117
|
+
maxRounds?: number;
|
|
118
|
+
}): FlushSyncResult;
|
|
119
|
+
getStats(): FlushDriverStats;
|
|
120
|
+
/** The microtask body — see the module-header semantics. */
|
|
121
|
+
private flush;
|
|
122
|
+
private recordFlush;
|
|
123
|
+
private p95FlushMs;
|
|
124
|
+
}
|