footprintjs 4.16.0 → 4.17.2
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/AGENTS.md +567 -82
- package/CLAUDE.md +57 -0
- package/README.md +2 -0
- package/dist/advanced.js +4 -2
- package/dist/detach.js +78 -0
- package/dist/esm/advanced.js +2 -1
- package/dist/esm/detach.js +57 -0
- package/dist/esm/lib/builder/FlowChartBuilder.js +171 -61
- package/dist/esm/lib/contract/openapi.js +4 -5
- package/dist/esm/lib/contract/schema.js +115 -4
- package/dist/esm/lib/decide/decide.js +4 -5
- package/dist/esm/lib/decide/evidence.js +3 -2
- package/dist/esm/lib/detach/drivers/immediate.js +66 -0
- package/dist/esm/lib/detach/drivers/microtaskBatch.js +113 -0
- package/dist/esm/lib/detach/drivers/sendBeacon.js +78 -0
- package/dist/esm/lib/detach/drivers/setImmediate.js +81 -0
- package/dist/esm/lib/detach/drivers/setTimeout.js +69 -0
- package/dist/esm/lib/detach/drivers/workerThread.js +117 -0
- package/dist/esm/lib/detach/flush.js +91 -0
- package/dist/esm/lib/detach/handle.js +134 -0
- package/dist/esm/lib/detach/registry.js +97 -0
- package/dist/esm/lib/detach/runChild.js +40 -0
- package/dist/esm/lib/detach/spawn.js +86 -0
- package/dist/esm/lib/detach/types.js +37 -0
- package/dist/esm/lib/engine/errors/errorInfo.js +4 -4
- package/dist/esm/lib/engine/graph/StageNode.js +2 -2
- package/dist/esm/lib/engine/handlers/ChildrenExecutor.js +9 -8
- package/dist/esm/lib/engine/handlers/ContinuationResolver.js +12 -9
- package/dist/esm/lib/engine/handlers/DeciderHandler.js +7 -8
- package/dist/esm/lib/engine/handlers/ExtractorRunner.js +14 -8
- package/dist/esm/lib/engine/handlers/NodeResolver.js +5 -3
- package/dist/esm/lib/engine/handlers/RuntimeStructureManager.js +11 -14
- package/dist/esm/lib/engine/handlers/SelectorHandler.js +9 -9
- package/dist/esm/lib/engine/handlers/StageRunner.js +9 -11
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +13 -12
- package/dist/esm/lib/engine/handlers/SubflowInputMapper.js +4 -4
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +85 -96
- package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +18 -36
- package/dist/esm/lib/engine/narrative/NarrativeFlowRecorder.js +6 -5
- package/dist/esm/lib/engine/narrative/recorders/AdaptiveNarrativeFlowRecorder.js +7 -6
- package/dist/esm/lib/engine/narrative/recorders/ManifestFlowRecorder.js +10 -10
- package/dist/esm/lib/engine/narrative/recorders/MilestoneNarrativeFlowRecorder.js +5 -3
- package/dist/esm/lib/engine/narrative/recorders/ProgressiveNarrativeFlowRecorder.js +4 -3
- package/dist/esm/lib/engine/narrative/recorders/RLENarrativeFlowRecorder.js +4 -4
- package/dist/esm/lib/engine/narrative/recorders/SeparateNarrativeFlowRecorder.js +5 -6
- package/dist/esm/lib/engine/narrative/recorders/SilentNarrativeFlowRecorder.js +5 -6
- package/dist/esm/lib/engine/narrative/recorders/WindowedNarrativeFlowRecorder.js +5 -3
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +97 -71
- package/dist/esm/lib/memory/DiagnosticCollector.js +6 -8
- package/dist/esm/lib/memory/EventLog.js +5 -3
- package/dist/esm/lib/memory/SharedMemory.js +3 -2
- package/dist/esm/lib/memory/StageContext.js +44 -14
- package/dist/esm/lib/memory/TransactionBuffer.js +9 -8
- package/dist/esm/lib/memory/backtrack.js +3 -4
- package/dist/esm/lib/memory/commitLogUtils.js +2 -2
- package/dist/esm/lib/memory/utils.js +2 -3
- package/dist/esm/lib/pause/types.js +33 -14
- package/dist/esm/lib/reactive/createTypedScope.js +10 -8
- package/dist/esm/lib/reactive/types.js +3 -1
- package/dist/esm/lib/recorder/BoundaryStateTracker.js +263 -0
- package/dist/esm/lib/recorder/CompositeRecorder.js +3 -1
- package/dist/esm/lib/recorder/InOutRecorder.js +5 -6
- package/dist/esm/lib/recorder/KeyedRecorder.js +2 -4
- package/dist/esm/lib/recorder/QualityRecorder.js +15 -14
- package/dist/esm/lib/recorder/SequenceRecorder.js +11 -12
- package/dist/esm/lib/recorder/TopologyRecorder.js +36 -40
- package/dist/esm/lib/recorder/index.js +2 -1
- package/dist/esm/lib/recorder/qualityTrace.js +4 -5
- package/dist/esm/lib/runner/ExecutionRuntime.js +20 -4
- package/dist/esm/lib/runner/FlowChartExecutor.js +99 -55
- package/dist/esm/lib/runner/RunContext.js +5 -3
- package/dist/esm/lib/runner/RunnableChart.js +7 -9
- package/dist/esm/lib/runner/getSubtreeSnapshot.js +4 -5
- package/dist/esm/lib/schema/errors.js +4 -3
- package/dist/esm/lib/schema/validate.js +4 -5
- package/dist/esm/lib/scope/ScopeFacade.js +52 -35
- package/dist/esm/lib/scope/providers/baseStateCompatible.js +9 -9
- package/dist/esm/lib/scope/providers/guards.js +2 -2
- package/dist/esm/lib/scope/recorders/DebugRecorder.js +9 -7
- package/dist/esm/lib/scope/recorders/MetricRecorder.js +10 -8
- package/dist/esm/lib/scope/state/zod/defineScopeFromZod.js +2 -3
- package/dist/esm/lib/scope/state/zod/resolver.js +2 -3
- package/dist/esm/lib/scope/state/zod/scopeFactory.js +16 -20
- package/dist/esm/lib/scope/state/zod/utils/validateHelper.js +57 -14
- package/dist/esm/trace.js +4 -1
- package/dist/lib/builder/FlowChartBuilder.js +171 -61
- package/dist/lib/contract/openapi.js +4 -5
- package/dist/lib/contract/schema.js +115 -4
- package/dist/lib/decide/decide.js +4 -5
- package/dist/lib/decide/evidence.js +3 -2
- package/dist/lib/detach/drivers/immediate.js +70 -0
- package/dist/lib/detach/drivers/microtaskBatch.js +117 -0
- package/dist/lib/detach/drivers/sendBeacon.js +82 -0
- package/dist/lib/detach/drivers/setImmediate.js +85 -0
- package/dist/lib/detach/drivers/setTimeout.js +73 -0
- package/dist/lib/detach/drivers/workerThread.js +121 -0
- package/dist/lib/detach/flush.js +95 -0
- package/dist/lib/detach/handle.js +140 -0
- package/dist/lib/detach/registry.js +106 -0
- package/dist/lib/detach/runChild.js +67 -0
- package/dist/lib/detach/spawn.js +92 -0
- package/dist/lib/detach/types.js +38 -0
- package/dist/lib/engine/errors/errorInfo.js +4 -4
- package/dist/lib/engine/graph/StageNode.js +2 -2
- package/dist/lib/engine/handlers/ChildrenExecutor.js +9 -8
- package/dist/lib/engine/handlers/ContinuationResolver.js +12 -9
- package/dist/lib/engine/handlers/DeciderHandler.js +7 -8
- package/dist/lib/engine/handlers/ExtractorRunner.js +14 -8
- package/dist/lib/engine/handlers/NodeResolver.js +5 -3
- package/dist/lib/engine/handlers/RuntimeStructureManager.js +11 -14
- package/dist/lib/engine/handlers/SelectorHandler.js +9 -9
- package/dist/lib/engine/handlers/StageRunner.js +9 -11
- package/dist/lib/engine/handlers/SubflowExecutor.js +13 -12
- package/dist/lib/engine/handlers/SubflowInputMapper.js +4 -4
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +85 -96
- package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +18 -36
- package/dist/lib/engine/narrative/NarrativeFlowRecorder.js +6 -5
- package/dist/lib/engine/narrative/recorders/AdaptiveNarrativeFlowRecorder.js +7 -6
- package/dist/lib/engine/narrative/recorders/ManifestFlowRecorder.js +10 -10
- package/dist/lib/engine/narrative/recorders/MilestoneNarrativeFlowRecorder.js +5 -3
- package/dist/lib/engine/narrative/recorders/ProgressiveNarrativeFlowRecorder.js +4 -3
- package/dist/lib/engine/narrative/recorders/RLENarrativeFlowRecorder.js +4 -4
- package/dist/lib/engine/narrative/recorders/SeparateNarrativeFlowRecorder.js +5 -6
- package/dist/lib/engine/narrative/recorders/SilentNarrativeFlowRecorder.js +5 -6
- package/dist/lib/engine/narrative/recorders/WindowedNarrativeFlowRecorder.js +5 -3
- package/dist/lib/engine/traversal/FlowchartTraverser.js +97 -71
- package/dist/lib/memory/DiagnosticCollector.js +6 -8
- package/dist/lib/memory/EventLog.js +5 -3
- package/dist/lib/memory/SharedMemory.js +3 -2
- package/dist/lib/memory/StageContext.js +44 -14
- package/dist/lib/memory/TransactionBuffer.js +9 -8
- package/dist/lib/memory/backtrack.js +3 -4
- package/dist/lib/memory/commitLogUtils.js +2 -2
- package/dist/lib/memory/utils.js +2 -3
- package/dist/lib/pause/types.js +33 -14
- package/dist/lib/reactive/createTypedScope.js +10 -8
- package/dist/lib/reactive/types.js +3 -1
- package/dist/lib/recorder/BoundaryStateTracker.js +267 -0
- package/dist/lib/recorder/CompositeRecorder.js +3 -1
- package/dist/lib/recorder/InOutRecorder.js +5 -6
- package/dist/lib/recorder/KeyedRecorder.js +2 -4
- package/dist/lib/recorder/QualityRecorder.js +15 -14
- package/dist/lib/recorder/SequenceRecorder.js +11 -12
- package/dist/lib/recorder/TopologyRecorder.js +36 -40
- package/dist/lib/recorder/index.js +4 -2
- package/dist/lib/recorder/qualityTrace.js +4 -5
- package/dist/lib/runner/ExecutionRuntime.js +20 -4
- package/dist/lib/runner/FlowChartExecutor.js +99 -55
- package/dist/lib/runner/RunContext.js +5 -3
- package/dist/lib/runner/RunnableChart.js +7 -9
- package/dist/lib/runner/getSubtreeSnapshot.js +4 -5
- package/dist/lib/schema/errors.js +4 -3
- package/dist/lib/schema/validate.js +4 -5
- package/dist/lib/scope/ScopeFacade.js +52 -35
- package/dist/lib/scope/providers/baseStateCompatible.js +9 -9
- package/dist/lib/scope/providers/guards.js +2 -2
- package/dist/lib/scope/recorders/DebugRecorder.js +9 -7
- package/dist/lib/scope/recorders/MetricRecorder.js +10 -8
- package/dist/lib/scope/state/zod/defineScopeFromZod.js +2 -3
- package/dist/lib/scope/state/zod/resolver.js +2 -3
- package/dist/lib/scope/state/zod/scopeFactory.js +16 -20
- package/dist/lib/scope/state/zod/utils/validateHelper.js +57 -14
- package/dist/trace.js +6 -2
- package/dist/types/advanced.d.ts +1 -0
- package/dist/types/detach.d.ts +59 -0
- package/dist/types/lib/builder/FlowChartBuilder.d.ts +81 -0
- package/dist/types/lib/detach/drivers/immediate.d.ts +39 -0
- package/dist/types/lib/detach/drivers/microtaskBatch.d.ts +57 -0
- package/dist/types/lib/detach/drivers/sendBeacon.d.ts +38 -0
- package/dist/types/lib/detach/drivers/setImmediate.d.ts +32 -0
- package/dist/types/lib/detach/drivers/setTimeout.d.ts +34 -0
- package/dist/types/lib/detach/drivers/workerThread.d.ts +50 -0
- package/dist/types/lib/detach/flush.d.ts +62 -0
- package/dist/types/lib/detach/handle.d.ts +83 -0
- package/dist/types/lib/detach/registry.d.ts +82 -0
- package/dist/types/lib/detach/runChild.d.ts +41 -0
- package/dist/types/lib/detach/spawn.d.ts +64 -0
- package/dist/types/lib/detach/types.d.ts +200 -0
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +0 -1
- package/dist/types/lib/engine/types.d.ts +0 -1
- package/dist/types/lib/reactive/types.d.ts +4 -0
- package/dist/types/lib/recorder/BoundaryStateTracker.d.ts +215 -0
- package/dist/types/lib/recorder/index.d.ts +1 -0
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +28 -0
- package/dist/types/lib/scope/ScopeFacade.d.ts +4 -0
- package/dist/types/lib/scope/state/zod/utils/validateHelper.d.ts +13 -1
- package/dist/types/trace.d.ts +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* footprintjs/detach — Fire-and-forget child flowchart execution.
|
|
3
|
+
*
|
|
4
|
+
* A library of scheduling drivers that lets you detach work from the
|
|
5
|
+
* parent stage's hot path. The parent stage returns immediately; the
|
|
6
|
+
* child runs on whichever driver you pick (microtask, immediate, plus
|
|
7
|
+
* setImmediate / setTimeout / sendBeacon / worker-thread in v4.17.1+).
|
|
8
|
+
*
|
|
9
|
+
* Two shapes:
|
|
10
|
+
*
|
|
11
|
+
* - `detachAndJoinLater(driver, child, input)` — returns a `DetachHandle`
|
|
12
|
+
* you can `wait()` on (Promise) or read `.status` from (sync).
|
|
13
|
+
* - `detachAndForget(driver, child, input)` — discards the handle.
|
|
14
|
+
* Use for fire-and-forget telemetry where the caller never needs to
|
|
15
|
+
* know how the child finished.
|
|
16
|
+
*
|
|
17
|
+
* Two entry points:
|
|
18
|
+
*
|
|
19
|
+
* - `scope.$detachAndJoinLater(driver, child, input)` — from inside a
|
|
20
|
+
* stage. refIds are minted from the calling stage's runtimeStageId
|
|
21
|
+
* for diagnostic correlation.
|
|
22
|
+
* - `executor.detachAndJoinLater(driver, child, input)` — from outside
|
|
23
|
+
* any chart (consumer code). refIds use the synthetic `__executor__`
|
|
24
|
+
* prefix.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { microtaskBatchDriver } from 'footprintjs/detach';
|
|
29
|
+
* import { flowChart, FlowChartExecutor } from 'footprintjs';
|
|
30
|
+
*
|
|
31
|
+
* const telemetry = flowChart('telemetry', async (scope) => {
|
|
32
|
+
* await fetch('/log', { method: 'POST', body: JSON.stringify(scope.$getArgs()) });
|
|
33
|
+
* }, 'telemetry').build();
|
|
34
|
+
*
|
|
35
|
+
* const main = flowChart('process', async (scope) => {
|
|
36
|
+
* scope.result = await heavyWork();
|
|
37
|
+
* // Fire telemetry without blocking the parent.
|
|
38
|
+
* scope.$detachAndForget(microtaskBatchDriver, telemetry, { event: 'processed' });
|
|
39
|
+
* }, 'process').build();
|
|
40
|
+
*
|
|
41
|
+
* await new FlowChartExecutor(main).run();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export type { DetachDriver, DetachHandle, DetachPollResult, DetachWaitResult, DriverCapabilities, } from './lib/detach/types.js';
|
|
45
|
+
export { createImmediateDriver, immediateDriver } from './lib/detach/drivers/immediate.js';
|
|
46
|
+
export { createMicrotaskBatchDriver, microtaskBatchDriver } from './lib/detach/drivers/microtaskBatch.js';
|
|
47
|
+
export type { SendBeaconDriverOptions } from './lib/detach/drivers/sendBeacon.js';
|
|
48
|
+
export { createSendBeaconDriver } from './lib/detach/drivers/sendBeacon.js';
|
|
49
|
+
export { createSetImmediateDriver, setImmediateDriver } from './lib/detach/drivers/setImmediate.js';
|
|
50
|
+
export type { SetTimeoutDriverOptions } from './lib/detach/drivers/setTimeout.js';
|
|
51
|
+
export { createSetTimeoutDriver, setTimeoutDriver } from './lib/detach/drivers/setTimeout.js';
|
|
52
|
+
export type { WorkerThreadDriverOptions } from './lib/detach/drivers/workerThread.js';
|
|
53
|
+
export { createWorkerThreadDriver } from './lib/detach/drivers/workerThread.js';
|
|
54
|
+
export { asImpl, createHandle, HandleImpl } from './lib/detach/handle.js';
|
|
55
|
+
export type { ChildRunner } from './lib/detach/runChild.js';
|
|
56
|
+
export { defaultRunChild } from './lib/detach/runChild.js';
|
|
57
|
+
export { size as detachedCount, ids as listDetachedIds, lookup as lookupDetachedHandle, } from './lib/detach/registry.js';
|
|
58
|
+
export type { FlushOptions, FlushResult } from './lib/detach/flush.js';
|
|
59
|
+
export { flushAllDetached } from './lib/detach/flush.js';
|
|
@@ -146,6 +146,87 @@ export declare class FlowChartBuilder<TOut = any, TScope = any> {
|
|
|
146
146
|
* ```
|
|
147
147
|
*/
|
|
148
148
|
addPausableFunction(name: string, handler: PausableHandler<TScope>, id: string, description?: string): this;
|
|
149
|
+
/**
|
|
150
|
+
* Add a stage that fires a child flowchart on the given driver and
|
|
151
|
+
* DISCARDS the handle. Pure fire-and-forget — useful for telemetry
|
|
152
|
+
* exports, audit log shipping, cache warm-up.
|
|
153
|
+
*
|
|
154
|
+
* @param id Stable id for this stage (also the stageMap key).
|
|
155
|
+
* @param child The child flowchart to detach.
|
|
156
|
+
* @param options.driver The driver to schedule on (e.g. `microtaskBatchDriver`).
|
|
157
|
+
* @param options.inputMapper Maps the parent's scope to the child's input.
|
|
158
|
+
* Defaults to passing `undefined`.
|
|
159
|
+
* @param options.mountName Display name; defaults to `id`.
|
|
160
|
+
* @param options.description Stage description for narrative + tools.
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* ```ts
|
|
164
|
+
* import { microtaskBatchDriver } from 'footprintjs/detach';
|
|
165
|
+
*
|
|
166
|
+
* flowChart('process', processFn, 'process')
|
|
167
|
+
* .addDetachAndForget('telemetry', telemetryChart, {
|
|
168
|
+
* driver: microtaskBatchDriver,
|
|
169
|
+
* inputMapper: (scope) => ({ event: 'processed', orderId: scope.orderId }),
|
|
170
|
+
* })
|
|
171
|
+
* .addFunction('next', nextFn, 'next')
|
|
172
|
+
* .build();
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
addDetachAndForget(id: string, child: import('./types.js').FlowChart<any, any>, options: {
|
|
176
|
+
driver: import('../detach/types.js').DetachDriver;
|
|
177
|
+
inputMapper?: (scope: TScope) => unknown;
|
|
178
|
+
mountName?: string;
|
|
179
|
+
description?: string;
|
|
180
|
+
}): this;
|
|
181
|
+
/**
|
|
182
|
+
* Add a stage that fires a child flowchart on the given driver and
|
|
183
|
+
* delivers the resulting `DetachHandle` to a consumer-supplied
|
|
184
|
+
* `onHandle` callback. The handle CANNOT be stored in shared state
|
|
185
|
+
* — `StageContext.setValue` calls `structuredClone` which drops
|
|
186
|
+
* class prototypes (and therefore the handle's `.wait()` method).
|
|
187
|
+
*
|
|
188
|
+
* The callback pattern is the explicit alternative: keep handles in
|
|
189
|
+
* a closure-local array (or whatever shape suits) and have a
|
|
190
|
+
* downstream stage `await Promise.all(...)` over them.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* import { microtaskBatchDriver } from 'footprintjs/detach';
|
|
195
|
+
* import type { DetachHandle } from 'footprintjs/detach';
|
|
196
|
+
*
|
|
197
|
+
* const handles: DetachHandle[] = [];
|
|
198
|
+
*
|
|
199
|
+
* const chart = flowChart('seed', seedFn, 'seed')
|
|
200
|
+
* .addDetachAndJoinLater('eval-a', evalChart, {
|
|
201
|
+
* driver: microtaskBatchDriver,
|
|
202
|
+
* inputMapper: (scope) => scope.configA,
|
|
203
|
+
* onHandle: (h) => handles.push(h),
|
|
204
|
+
* })
|
|
205
|
+
* .addDetachAndJoinLater('eval-b', evalChart, {
|
|
206
|
+
* driver: microtaskBatchDriver,
|
|
207
|
+
* inputMapper: (scope) => scope.configB,
|
|
208
|
+
* onHandle: (h) => handles.push(h),
|
|
209
|
+
* })
|
|
210
|
+
* .addFunction('join', async (scope) => {
|
|
211
|
+
* const settled = await Promise.all(handles.map((h) => h.wait()));
|
|
212
|
+
* scope.results = settled;
|
|
213
|
+
* }, 'join')
|
|
214
|
+
* .build();
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* Note: putting `handles` in a module-level closure is fine for
|
|
218
|
+
* single-run scripts. For server code that runs the same chart
|
|
219
|
+
* concurrently across requests, allocate a new closure per run
|
|
220
|
+
* (e.g., wrap chart construction in a factory function) so handles
|
|
221
|
+
* from different runs don't bleed into each other.
|
|
222
|
+
*/
|
|
223
|
+
addDetachAndJoinLater(id: string, child: import('./types.js').FlowChart<any, any>, options: {
|
|
224
|
+
driver: import('../detach/types.js').DetachDriver;
|
|
225
|
+
onHandle: (handle: import('../detach/types.js').DetachHandle) => void;
|
|
226
|
+
inputMapper?: (scope: TScope) => unknown;
|
|
227
|
+
mountName?: string;
|
|
228
|
+
description?: string;
|
|
229
|
+
}): this;
|
|
149
230
|
addDeciderFunction(name: string, fn: StageFunction<any, TScope>, id: string, description?: string): DeciderList<TOut, TScope>;
|
|
150
231
|
addSelectorFunction(name: string, fn: StageFunction<any, TScope>, id: string, description?: string): SelectorFnList<TOut, TScope>;
|
|
151
232
|
addListOfFunction(children: SimplifiedParallelSpec<TOut, TScope>[], options?: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/drivers/immediate.ts — Run detached work synchronously inside `schedule()`.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Null-object driver for the "no actual deferral" case. Same
|
|
5
|
+
* intent as a `setTimeout(fn, 0)` shim that just calls `fn()`
|
|
6
|
+
* — keeps the API surface uniform so consumers can swap drivers
|
|
7
|
+
* without changing call sites.
|
|
8
|
+
* Role: Test fixture + opt-in for consumers who want fire-and-forget
|
|
9
|
+
* ergonomics (the handle API) without actually deferring. Useful
|
|
10
|
+
* for:
|
|
11
|
+
*
|
|
12
|
+
* - unit tests where deterministic, synchronous completion
|
|
13
|
+
* beats microtask gymnastics
|
|
14
|
+
* - very small detach payloads where the overhead of a
|
|
15
|
+
* microtask roundtrip exceeds the work itself
|
|
16
|
+
* - debugging — easier to step through with breakpoints
|
|
17
|
+
*
|
|
18
|
+
* Performance:
|
|
19
|
+
* - Sync runChild → handle becomes terminal before `schedule()` returns
|
|
20
|
+
* - Async runChild → handle marks running sync, terminal at runChild's
|
|
21
|
+
* resolution. The `wait()` Promise is the same one consumers use for
|
|
22
|
+
* any other driver; behaviour is uniform.
|
|
23
|
+
*
|
|
24
|
+
* Caveat — this is NOT a passive-recorder by default:
|
|
25
|
+
* When runChild is sync, the parent stage observes the work's side
|
|
26
|
+
* effects WITHIN its own slice. That's intentional for the test/debug
|
|
27
|
+
* use case but means consumers should NOT use `immediateDriver` for
|
|
28
|
+
* long-running work in production hot paths — pick `microtaskBatchDriver`
|
|
29
|
+
* for that.
|
|
30
|
+
*/
|
|
31
|
+
import { type ChildRunner } from '../runChild.js';
|
|
32
|
+
import type { DetachDriver } from '../types.js';
|
|
33
|
+
/**
|
|
34
|
+
* Build an immediate driver wired to a custom child runner. Most
|
|
35
|
+
* consumers want the default singleton `immediateDriver`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createImmediateDriver(runChild?: ChildRunner): DetachDriver;
|
|
38
|
+
/** Default singleton — most consumers use this. */
|
|
39
|
+
export declare const immediateDriver: DetachDriver;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/drivers/microtaskBatch.ts — Batch detached work into ONE microtask.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Producer-consumer with batched flush. Same shape as
|
|
5
|
+
* agentfootprint's `EventDispatcher` flush queue and the React
|
|
6
|
+
* reconciler's microtask scheduling — accumulate during the
|
|
7
|
+
* current sync slice, drain at the next microtask boundary.
|
|
8
|
+
* Role: Default driver for in-process detach. Cheapest scheduling
|
|
9
|
+
* primitive on V8/JSC: one `queueMicrotask` per batch
|
|
10
|
+
* regardless of how many work items, so the perf budget
|
|
11
|
+
* amortizes. Suitable for browser AND node AND edge runtimes
|
|
12
|
+
* (queueMicrotask is universal since 2018).
|
|
13
|
+
*
|
|
14
|
+
* Lifecycle:
|
|
15
|
+
*
|
|
16
|
+
* schedule(child, input, refId) ← driver entry
|
|
17
|
+
* └─ create handle (queued)
|
|
18
|
+
* └─ register in detachRegistry
|
|
19
|
+
* └─ push work item onto local queue
|
|
20
|
+
* └─ if no microtask scheduled yet → queueMicrotask(flush)
|
|
21
|
+
* └─ return handle (sync — passive recorder rule)
|
|
22
|
+
*
|
|
23
|
+
* flush() (microtask) ← deferred
|
|
24
|
+
* └─ swap out queue (drain races safely)
|
|
25
|
+
* └─ for each item: _markRunning, await runChild, _markDone/_markFailed
|
|
26
|
+
* └─ unregister handle from detachRegistry
|
|
27
|
+
*
|
|
28
|
+
* Why microtask (and not setImmediate / setTimeout):
|
|
29
|
+
* - Microtasks run BEFORE returning to the event loop — guarantees
|
|
30
|
+
* the work finishes within the current "tick" if the runtime allows
|
|
31
|
+
* - Lowest possible deferral cost (~50ns on modern V8)
|
|
32
|
+
* - Works in EVERY JS runtime (browser, node, deno, bun, edge)
|
|
33
|
+
* - Doesn't require any timer infrastructure → no GC pressure
|
|
34
|
+
*
|
|
35
|
+
* Re-entrancy:
|
|
36
|
+
* - If `runChild` calls `schedule()` for nested detach, the new item
|
|
37
|
+
* lands on the SAME queue. Because `scheduled` flips back to false
|
|
38
|
+
* at the start of `flush`, the new item triggers a fresh microtask.
|
|
39
|
+
* - Worst-case: O(n) microtasks for n nested levels. Acceptable —
|
|
40
|
+
* real-world detach trees are shallow.
|
|
41
|
+
*/
|
|
42
|
+
import { type ChildRunner } from '../runChild.js';
|
|
43
|
+
import type { DetachDriver } from '../types.js';
|
|
44
|
+
/**
|
|
45
|
+
* Build a microtask-batch driver wired to a custom child runner. Most
|
|
46
|
+
* consumers want the default singleton `microtaskBatchDriver` instead;
|
|
47
|
+
* this factory exists for tests and for advanced consumers who want to
|
|
48
|
+
* inject their own runner (e.g., a runner that wraps the child in a
|
|
49
|
+
* tracing context).
|
|
50
|
+
*/
|
|
51
|
+
export declare function createMicrotaskBatchDriver(runChild?: ChildRunner): DetachDriver;
|
|
52
|
+
/**
|
|
53
|
+
* Default singleton. Most consumers import this and pass it to
|
|
54
|
+
* `executor.detachAndJoinLater(child, input, { driver: microtaskBatchDriver })`
|
|
55
|
+
* (or rely on it being the executor's default driver, set in T5b).
|
|
56
|
+
*/
|
|
57
|
+
export declare const microtaskBatchDriver: DetachDriver;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/drivers/sendBeacon.ts — Browser-only driver that ships work
|
|
3
|
+
* via `navigator.sendBeacon` so it
|
|
4
|
+
* survives page-unload.
|
|
5
|
+
*
|
|
6
|
+
* Pattern: Strategy / Adapter — translates the consumer's child
|
|
7
|
+
* flowchart into a sendBeacon POST. The "child" is expected
|
|
8
|
+
* to produce a JSON-serializable payload via its
|
|
9
|
+
* `inputMapper`; the URL endpoint is set at driver creation.
|
|
10
|
+
* Role: The narrow but high-value driver for analytics / error
|
|
11
|
+
* reporting / page-leave telemetry — the one case where
|
|
12
|
+
* "fire-and-forget" must really mean "ships even if the
|
|
13
|
+
* user closes the tab right after."
|
|
14
|
+
*
|
|
15
|
+
* `navigator.sendBeacon` semantics:
|
|
16
|
+
* - Browser queues the POST in the OS network stack BEFORE returning
|
|
17
|
+
* control. Survives page-unload, navigation, refresh.
|
|
18
|
+
* - Limited to ~64 KB per call (per HTML5 spec).
|
|
19
|
+
* - Fire-and-forget — no response observable.
|
|
20
|
+
*
|
|
21
|
+
* Caveats:
|
|
22
|
+
* - Browser-only (`browserSafe: true, survivesUnload: true`).
|
|
23
|
+
* `validate()` throws helpfully if `navigator.sendBeacon` isn't a
|
|
24
|
+
* function (e.g., when imported in Node).
|
|
25
|
+
* - The driver does NOT run the child flowchart through a
|
|
26
|
+
* `FlowChartExecutor` — it serializes the input and POSTs. This
|
|
27
|
+
* is an intentional simplification: sendBeacon's semantics
|
|
28
|
+
* wouldn't survive an executor's async stages anyway.
|
|
29
|
+
*/
|
|
30
|
+
import type { DetachDriver } from '../types.js';
|
|
31
|
+
export interface SendBeaconDriverOptions {
|
|
32
|
+
/** Endpoint URL — required. e.g., `'https://telemetry.example.com/ingest'`. */
|
|
33
|
+
readonly url: string;
|
|
34
|
+
/** Custom serializer. Defaults to `JSON.stringify(input)` with
|
|
35
|
+
* `application/json` content type. */
|
|
36
|
+
readonly serialize?: (input: unknown) => Blob | string | FormData;
|
|
37
|
+
}
|
|
38
|
+
export declare function createSendBeaconDriver(opts: SendBeaconDriverOptions): DetachDriver;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/drivers/setImmediate.ts — Defer detached work to a Node.js
|
|
3
|
+
* `setImmediate` boundary.
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Same producer-consumer batch flush as `microtaskBatch`,
|
|
6
|
+
* but the deferral is `setImmediate` instead of
|
|
7
|
+
* `queueMicrotask`. Yields control back to the event loop
|
|
8
|
+
* BEFORE running — allows pending I/O callbacks to drain
|
|
9
|
+
* first, which microtasks would block.
|
|
10
|
+
* Role: Node-specific driver for "fire-and-forget after the
|
|
11
|
+
* current I/O tick." Use when the parent stage handles
|
|
12
|
+
* latency-sensitive work and you don't want detached work
|
|
13
|
+
* to compete for the synchronous slice.
|
|
14
|
+
*
|
|
15
|
+
* When to pick this over microtaskBatch:
|
|
16
|
+
* - You're shipping logs / metrics in a hot HTTP path and don't
|
|
17
|
+
* want them blocking the response from being flushed
|
|
18
|
+
* - The detached work itself is CPU-heavy enough that running it on
|
|
19
|
+
* the same microtask cycle would delay other microtasks
|
|
20
|
+
* - You explicitly want "next event-loop tick" semantics — useful
|
|
21
|
+
* when interacting with third-party libraries that expect at
|
|
22
|
+
* least one I/O tick between schedule and execution
|
|
23
|
+
*
|
|
24
|
+
* Capability:
|
|
25
|
+
* - `nodeSafe: true` — relies on Node's `setImmediate`, NOT
|
|
26
|
+
* available in browsers / Deno / Cloudflare Workers (use
|
|
27
|
+
* `setTimeoutDriver` for cross-runtime alternative)
|
|
28
|
+
*/
|
|
29
|
+
import { type ChildRunner } from '../runChild.js';
|
|
30
|
+
import type { DetachDriver } from '../types.js';
|
|
31
|
+
export declare function createSetImmediateDriver(runChild?: ChildRunner): DetachDriver;
|
|
32
|
+
export declare const setImmediateDriver: DetachDriver;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/drivers/setTimeout.ts — Defer detached work via `setTimeout(..., delayMs)`.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Producer-consumer batch flush; deferral mechanism is
|
|
5
|
+
* `setTimeout` with a configurable delay (default `0`).
|
|
6
|
+
* Role: Cross-runtime "next macrotask" driver. Works in browsers,
|
|
7
|
+
* Node.js, Deno, Cloudflare Workers, Bun, etc.
|
|
8
|
+
*
|
|
9
|
+
* When to pick this:
|
|
10
|
+
* - Consumer wants a SPECIFIC delay (e.g. "ship telemetry in 5
|
|
11
|
+
* seconds, batched") — pass `createSetTimeoutDriver({ delayMs: 5000 })`
|
|
12
|
+
* - Cross-runtime detach where `setImmediate` isn't available
|
|
13
|
+
* - Coalescing high-frequency events into a low-frequency flush
|
|
14
|
+
*
|
|
15
|
+
* Caveats:
|
|
16
|
+
* - Not for low-latency hot paths — minimum delay is ~4ms in
|
|
17
|
+
* browsers per the HTML5 spec, ~1ms in Node. Use
|
|
18
|
+
* `microtaskBatchDriver` for sub-ms scheduling.
|
|
19
|
+
* - Browser tab freezing / throttling can extend the delay
|
|
20
|
+
* significantly. Don't rely on precise timing.
|
|
21
|
+
*/
|
|
22
|
+
import { type ChildRunner } from '../runChild.js';
|
|
23
|
+
import type { DetachDriver } from '../types.js';
|
|
24
|
+
export interface SetTimeoutDriverOptions {
|
|
25
|
+
/** Milliseconds to wait before flushing the batch. Default 0
|
|
26
|
+
* (next macrotask). */
|
|
27
|
+
readonly delayMs?: number;
|
|
28
|
+
/** Custom `runChild`. Defaults to spawning a `FlowChartExecutor`. */
|
|
29
|
+
readonly runChild?: ChildRunner;
|
|
30
|
+
}
|
|
31
|
+
export declare function createSetTimeoutDriver(opts?: SetTimeoutDriverOptions): DetachDriver;
|
|
32
|
+
/** Default singleton — zero-delay (next macrotask). For configurable
|
|
33
|
+
* delays, use `createSetTimeoutDriver({ delayMs })`. */
|
|
34
|
+
export declare const setTimeoutDriver: DetachDriver;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/drivers/workerThread.ts — Run detached work in a Node.js
|
|
3
|
+
* Worker Thread (or browser Web Worker).
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Adapter — translates the consumer's child flowchart into
|
|
6
|
+
* a worker message + lifecycle handoff. The worker is owned
|
|
7
|
+
* by the driver instance; restarted on crash.
|
|
8
|
+
* Role: CPU-isolation driver — when detached work is genuinely
|
|
9
|
+
* expensive (heavy parsing, hashing, image processing) and
|
|
10
|
+
* you don't want it blocking the main thread's event loop
|
|
11
|
+
* even for a microtask burst.
|
|
12
|
+
*
|
|
13
|
+
* Caveats / IMPORTANT v1 limitations:
|
|
14
|
+
* - The worker entry point is a CONSUMER-PROVIDED file path / URL —
|
|
15
|
+
* this driver does NOT auto-spawn FlowChartExecutor in a worker.
|
|
16
|
+
* Workers can't `import('footprintjs')` portably without setup,
|
|
17
|
+
* and the worker file's lifecycle differs by runtime
|
|
18
|
+
* (Node Worker vs Web Worker vs Bun). Consumer writes the worker
|
|
19
|
+
* code; this driver just hands them a uniform `(input, handle)`
|
|
20
|
+
* API.
|
|
21
|
+
* - The "child flowchart" parameter is IGNORED in v1 (we only ship
|
|
22
|
+
* the input). The chart shape doesn't survive structuredClone +
|
|
23
|
+
* postMessage anyway. v2 may add a serialization protocol.
|
|
24
|
+
*
|
|
25
|
+
* Two ways to consume:
|
|
26
|
+
*
|
|
27
|
+
* 1. Node.js: pass a file path
|
|
28
|
+
* `createWorkerThreadDriver({ workerScript: '/path/to/worker.js' })`
|
|
29
|
+
*
|
|
30
|
+
* 2. Browser: pass a URL or pre-built Worker instance
|
|
31
|
+
* `createWorkerThreadDriver({ worker: new Worker(url) })`
|
|
32
|
+
*/
|
|
33
|
+
import type { DetachDriver } from '../types.js';
|
|
34
|
+
interface WorkerLike {
|
|
35
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
36
|
+
terminate?(): unknown;
|
|
37
|
+
on?(event: string, listener: (msg: unknown) => void): void;
|
|
38
|
+
addEventListener?(event: string, listener: (msg: unknown) => void): void;
|
|
39
|
+
}
|
|
40
|
+
export interface WorkerThreadDriverOptions {
|
|
41
|
+
/** Pre-constructed Worker instance. Pass either this OR
|
|
42
|
+
* `workerScript` — not both. */
|
|
43
|
+
readonly worker?: WorkerLike;
|
|
44
|
+
/** Path / URL to the worker script. Used only when `worker` is
|
|
45
|
+
* not provided; the driver constructs a Worker from this on demand
|
|
46
|
+
* (Node `worker_threads` API). */
|
|
47
|
+
readonly workerScript?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function createWorkerThreadDriver(opts: WorkerThreadDriverOptions): DetachDriver;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/flush.ts — Drain every in-flight detached handle to terminal.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Drain-loop with deadline. Same shape as a graceful HTTP
|
|
5
|
+
* server shutdown: snapshot the queue, await everything in
|
|
6
|
+
* flight, repeat until empty or deadline.
|
|
7
|
+
* Role: Graceful-shutdown hook for consumers who launched
|
|
8
|
+
* fire-and-forget work and want to make sure it actually
|
|
9
|
+
* flushed before exiting (server stop, test cleanup, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Why iterate (not single Promise.all over a snapshot):
|
|
12
|
+
* - A child stage can itself call `detachAndForget` while running —
|
|
13
|
+
* new handles arrive WHILE we're flushing. A single snapshot would
|
|
14
|
+
* miss them. Looping until `size() === 0` drains transitively.
|
|
15
|
+
*
|
|
16
|
+
* Why dedupe via `seen` Set:
|
|
17
|
+
* - Handles already terminal (but not yet `unregister`ed by their
|
|
18
|
+
* driver's finally-block) can re-appear in subsequent snapshots.
|
|
19
|
+
* Without dedupe, the `done` counter would double-count them.
|
|
20
|
+
*
|
|
21
|
+
* Why `Promise.allSettled` (not `Promise.all`):
|
|
22
|
+
* - One handle's rejection must NOT abort the rest. A failed child
|
|
23
|
+
* is normal (it's why `wait()` rejects); we still want to drain
|
|
24
|
+
* the siblings.
|
|
25
|
+
*/
|
|
26
|
+
export interface FlushResult {
|
|
27
|
+
/** Handles whose `wait()` we EXPLICITLY awaited and saw fulfilled.
|
|
28
|
+
* Best-effort count — a child that completes inside another's
|
|
29
|
+
* `wait()` may finish (and unregister) before we get a chance to
|
|
30
|
+
* await it directly. The DRAIN is still guaranteed (registry empty
|
|
31
|
+
* on return); only the COUNT is approximate. */
|
|
32
|
+
readonly done: number;
|
|
33
|
+
/** Handles whose `wait()` rejected. Same best-effort semantics. */
|
|
34
|
+
readonly failed: number;
|
|
35
|
+
/** Handles still in-flight when the deadline expired. `0` indicates
|
|
36
|
+
* a successful (complete) drain — registry was empty on return. */
|
|
37
|
+
readonly pending: number;
|
|
38
|
+
}
|
|
39
|
+
export interface FlushOptions {
|
|
40
|
+
/** Max wall-clock to spend draining, in milliseconds. Default 30s. */
|
|
41
|
+
readonly timeoutMs?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Wait for every in-flight detached handle to reach a terminal state.
|
|
45
|
+
* Returns counts for diagnostics. PROCESS-WIDE — drains every driver
|
|
46
|
+
* across every executor. For per-executor scoping, consumers should
|
|
47
|
+
* collect their own handles from `executor.detachAndJoinLater(...)`
|
|
48
|
+
* calls and await `Promise.allSettled([...].map(h => h.wait()))`
|
|
49
|
+
* themselves.
|
|
50
|
+
*
|
|
51
|
+
* @example Graceful server shutdown
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { flushAllDetached } from 'footprintjs/detach';
|
|
54
|
+
*
|
|
55
|
+
* process.on('SIGTERM', async () => {
|
|
56
|
+
* const stats = await flushAllDetached({ timeoutMs: 10_000 });
|
|
57
|
+
* console.log(`Drained ${stats.done} done, ${stats.failed} failed, ${stats.pending} pending.`);
|
|
58
|
+
* process.exit(stats.pending === 0 ? 0 : 1);
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function flushAllDetached(opts?: FlushOptions): Promise<FlushResult>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/handle.ts — DetachHandle implementation.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Object-as-state-machine. Mutable status field; transitions
|
|
5
|
+
* are one-way and irreversible (queued → running → done/failed).
|
|
6
|
+
* Role: Backs the consumer-facing `DetachHandle` interface. The
|
|
7
|
+
* public surface is the interface (defined in `types.ts`);
|
|
8
|
+
* this class is the runtime impl.
|
|
9
|
+
*
|
|
10
|
+
* Internal vs public split:
|
|
11
|
+
* - PUBLIC (in `types.ts`) — read-only properties + `wait()`
|
|
12
|
+
* - INTERNAL (this file) — `_markRunning` / `_markDone` /
|
|
13
|
+
* `_markFailed` mutators called by drivers
|
|
14
|
+
*
|
|
15
|
+
* The class implements `DetachHandle` (which has only readonly fields
|
|
16
|
+
* exposed). Drivers cast to `HandleImpl` via the `asImpl()` helper to
|
|
17
|
+
* call the mutators — a controlled escape from readonly. Consumers
|
|
18
|
+
* cannot do this (they only see the interface).
|
|
19
|
+
*
|
|
20
|
+
* Promise caching contract:
|
|
21
|
+
* - First `wait()` call:
|
|
22
|
+
* - if status terminal → returns IMMEDIATELY-resolved Promise
|
|
23
|
+
* - if not terminal → returns NEW Promise; resolvers stored
|
|
24
|
+
* for use by `_markDone` / `_markFailed`
|
|
25
|
+
* - Subsequent `wait()` calls → returns the SAME cached Promise
|
|
26
|
+
* - The resolved/rejected value is the SAME on every call (no
|
|
27
|
+
* re-running, no duplicated work)
|
|
28
|
+
*
|
|
29
|
+
* Concurrency notes:
|
|
30
|
+
* - All transitions are sync. JavaScript is single-threaded so no
|
|
31
|
+
* atomics or locks needed.
|
|
32
|
+
* - State transitions out of terminal states are forbidden — calling
|
|
33
|
+
* `_markDone` after `_markFailed` (or vice-versa) is a no-op
|
|
34
|
+
* (defensive: prevents driver bugs from corrupting state).
|
|
35
|
+
*/
|
|
36
|
+
import type { DetachHandle, DetachWaitResult } from './types.js';
|
|
37
|
+
/**
|
|
38
|
+
* Internal handle implementation. Drivers call the `_mark*` methods
|
|
39
|
+
* to drive state transitions; consumers see only the readonly
|
|
40
|
+
* `DetachHandle` interface.
|
|
41
|
+
*/
|
|
42
|
+
export declare class HandleImpl implements DetachHandle {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
status: DetachHandle['status'];
|
|
45
|
+
result: unknown;
|
|
46
|
+
error: Error | undefined;
|
|
47
|
+
private waitPromise;
|
|
48
|
+
private resolveWait;
|
|
49
|
+
private rejectWait;
|
|
50
|
+
constructor(id: string);
|
|
51
|
+
/**
|
|
52
|
+
* Public — opt-in async join. Returns a cached Promise.
|
|
53
|
+
* See `DetachHandle.wait()` docstring for contract.
|
|
54
|
+
*/
|
|
55
|
+
wait(): Promise<DetachWaitResult>;
|
|
56
|
+
/** Transition queued → running. No-op if already past 'queued'. */
|
|
57
|
+
_markRunning(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Transition to terminal 'done' with the given result. No-op if
|
|
60
|
+
* already terminal (defensive: prevents driver bugs from corrupting
|
|
61
|
+
* state).
|
|
62
|
+
*/
|
|
63
|
+
_markDone(result: unknown): void;
|
|
64
|
+
/**
|
|
65
|
+
* Transition to terminal 'failed' with the given error. No-op if
|
|
66
|
+
* already terminal.
|
|
67
|
+
*/
|
|
68
|
+
_markFailed(error: Error): void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Type-narrowing helper — cast a public `DetachHandle` to its
|
|
72
|
+
* implementation. Drivers (only) use this to call internal mutators.
|
|
73
|
+
*
|
|
74
|
+
* Throws if the handle isn't actually a `HandleImpl` — defends
|
|
75
|
+
* against consumers passing a hand-rolled object that satisfies the
|
|
76
|
+
* interface shape but lacks the mutators.
|
|
77
|
+
*/
|
|
78
|
+
export declare function asImpl(handle: DetachHandle): HandleImpl;
|
|
79
|
+
/**
|
|
80
|
+
* Driver-facing factory. Drivers MUST use this to create handles
|
|
81
|
+
* (NOT construct `HandleImpl` directly — keeps the impl type private).
|
|
82
|
+
*/
|
|
83
|
+
export declare function createHandle(id: string): DetachHandle;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detach/registry.ts — Process-singleton handle registry.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: Registry (GoF). Same shape as the cache strategy registry
|
|
5
|
+
* in agentfootprint v2.6 — a Map keyed by stable string id.
|
|
6
|
+
* Role: Glue between drivers and executors. When a driver schedules
|
|
7
|
+
* work it `register`s the handle here; later (during executor
|
|
8
|
+
* disposal, or for diagnostics) consumers `lookup` by refId.
|
|
9
|
+
*
|
|
10
|
+
* Why a singleton?
|
|
11
|
+
* - refIds are minted per detach call and are unique across the
|
|
12
|
+
* process lifetime (driver name + monotonic counter)
|
|
13
|
+
* - handles need to be cleanable from MULTIPLE call sites (executor
|
|
14
|
+
* disposal, driver-internal flush, test cleanup) without each one
|
|
15
|
+
* having to thread a Registry instance through ten layers
|
|
16
|
+
* - one-source-of-truth simplifies "is this handle still alive?"
|
|
17
|
+
* queries during debugging
|
|
18
|
+
*
|
|
19
|
+
* Why NOT a class instance per executor?
|
|
20
|
+
* - drivers (e.g., `microtaskBatchDriver`) are PROCESS-wide (one queue
|
|
21
|
+
* per driver, shared by every executor). Tying registry to executor
|
|
22
|
+
* would force per-executor driver instances, multiplying the queue
|
|
23
|
+
* count and breaking the batch-amortization the drivers exist for.
|
|
24
|
+
*
|
|
25
|
+
* Cleanup contract:
|
|
26
|
+
* - Drivers call `register(handle)` synchronously inside `schedule()`
|
|
27
|
+
* - Drivers (or executor disposal) call `unregister(refId)` once the
|
|
28
|
+
* handle is terminal AND the consumer has had a chance to observe it
|
|
29
|
+
* - `_resetForTests()` clears every entry — tests only
|
|
30
|
+
*
|
|
31
|
+
* Capacity:
|
|
32
|
+
* - No upper bound. The handle objects are tiny (~6 fields). A long-
|
|
33
|
+
* running process that detaches a million units WITHOUT cleanup
|
|
34
|
+
* would leak ~50 MB — acceptable for v1, since drivers ARE the
|
|
35
|
+
* cleanup site. If real-world programs hit the limit, add a
|
|
36
|
+
* sliding-window cap with telemetry hook (mirrors
|
|
37
|
+
* `LIVE_STATUS_LOG_CAP` in agentfootprint).
|
|
38
|
+
*/
|
|
39
|
+
import type { DetachHandle } from './types.js';
|
|
40
|
+
/**
|
|
41
|
+
* Register a freshly-minted handle. Drivers MUST call this synchronously
|
|
42
|
+
* inside `schedule()` so the handle is observable from the moment it
|
|
43
|
+
* exists.
|
|
44
|
+
*
|
|
45
|
+
* Replacing an existing registration is treated as a programming error
|
|
46
|
+
* (refIds are supposed to be unique). We don't throw — silent overwrite
|
|
47
|
+
* could mask a bug, but throwing inside a driver's hot path could cascade
|
|
48
|
+
* into the parent stage. Compromise: warn in dev mode, overwrite always.
|
|
49
|
+
*/
|
|
50
|
+
export declare function register(handle: DetachHandle): void;
|
|
51
|
+
/**
|
|
52
|
+
* Look up a handle by refId. Returns `undefined` for unknown ids — the
|
|
53
|
+
* caller decides whether that's an error or just a stale reference.
|
|
54
|
+
*
|
|
55
|
+
* Used by:
|
|
56
|
+
* - Executor disposal (find handles to mark cancelled / drain)
|
|
57
|
+
* - Driver-internal flush (correlate work-queue entries → handles)
|
|
58
|
+
* - Diagnostic tooling (dump handle state for a refId in a log line)
|
|
59
|
+
*/
|
|
60
|
+
export declare function lookup(refId: string): DetachHandle | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Drop a handle from the registry. Idempotent — calling on an already-
|
|
63
|
+
* removed refId is a no-op (matches `Map.delete` semantics; useful when
|
|
64
|
+
* cleanup may race between executor disposal and the driver's own
|
|
65
|
+
* post-terminal cleanup).
|
|
66
|
+
*/
|
|
67
|
+
export declare function unregister(refId: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Diagnostic — total live handles. Use sparingly; calling this on hot
|
|
70
|
+
* paths defeats the registry's "cheap insert/lookup" goal.
|
|
71
|
+
*/
|
|
72
|
+
export declare function size(): number;
|
|
73
|
+
/**
|
|
74
|
+
* Diagnostic — every live refId. Use for "what's still in flight?"
|
|
75
|
+
* dumps during executor disposal or oncall debugging.
|
|
76
|
+
*/
|
|
77
|
+
export declare function ids(): readonly string[];
|
|
78
|
+
/**
|
|
79
|
+
* Test-only — wipe every entry. NEVER call from production code; that
|
|
80
|
+
* would orphan in-flight work without a chance to drain.
|
|
81
|
+
*/
|
|
82
|
+
export declare function _resetForTests(): void;
|