@ersbeth/picoflow 2.3.2 → 3.0.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/README.md +34 -5
- package/SKILL.md +130 -16
- package/dist/index.d.ts +767 -0
- package/dist/index.js +544 -0
- package/dist/inspect.d.ts +252 -0
- package/dist/inspect.js +300 -0
- package/dist/shared.d.ts +361 -0
- package/dist/shared.js +1496 -0
- package/dist/shared2.d.ts +93 -0
- package/dist/shared2.js +59 -0
- package/dist/shared3.d.ts +54 -0
- package/dist/shared3.js +17 -0
- package/dist/{types/converters/solid.d.ts → solid.d.ts} +10 -8
- package/dist/solid.js +63 -0
- package/package.json +37 -25
- package/dist/picoflow.js +0 -1283
- package/dist/types/converters/index.d.ts +0 -1
- package/dist/types/core/api/base/flowConfig.d.ts +0 -17
- package/dist/types/core/api/base/flowDisposable.d.ts +0 -40
- package/dist/types/core/api/base/flowErrors.d.ts +0 -1
- package/dist/types/core/api/base/flowObservable.d.ts +0 -26
- package/dist/types/core/api/base/flowPrimitiveOptions.d.ts +0 -14
- package/dist/types/core/api/base/flowSubscribable.d.ts +0 -78
- package/dist/types/core/api/base/flowTracker.d.ts +0 -7
- package/dist/types/core/api/base/index.d.ts +0 -7
- package/dist/types/core/api/index.d.ts +0 -2
- package/dist/types/core/api/nodes/async/flowConstantAsync.d.ts +0 -32
- package/dist/types/core/api/nodes/async/flowDerivationAsync.d.ts +0 -37
- package/dist/types/core/api/nodes/async/flowStateAsync.d.ts +0 -42
- package/dist/types/core/api/nodes/async/flowWritableDerivationAsync.d.ts +0 -31
- package/dist/types/core/api/nodes/async/index.d.ts +0 -4
- package/dist/types/core/api/nodes/collections/flowArray.d.ts +0 -138
- package/dist/types/core/api/nodes/collections/flowMap.d.ts +0 -100
- package/dist/types/core/api/nodes/collections/index.d.ts +0 -2
- package/dist/types/core/api/nodes/flowEffect.d.ts +0 -28
- package/dist/types/core/api/nodes/flowSignal.d.ts +0 -26
- package/dist/types/core/api/nodes/flowValue.d.ts +0 -35
- package/dist/types/core/api/nodes/index.d.ts +0 -7
- package/dist/types/core/api/nodes/sync/flowConstant.d.ts +0 -30
- package/dist/types/core/api/nodes/sync/flowDerivation.d.ts +0 -37
- package/dist/types/core/api/nodes/sync/flowState.d.ts +0 -40
- package/dist/types/core/api/nodes/sync/flowWritableDerivation.d.ts +0 -29
- package/dist/types/core/api/nodes/sync/index.d.ts +0 -4
- package/dist/types/core/api/nodes/utils.d.ts +0 -22
- package/dist/types/core/base/dependenciesRegistry.d.ts +0 -1
- package/dist/types/core/base/dependentsRegistry.d.ts +0 -1
- package/dist/types/core/base/disposable.d.ts +0 -15
- package/dist/types/core/base/errors.d.ts +0 -31
- package/dist/types/core/base/executionStack.d.ts +0 -31
- package/dist/types/core/base/index.d.ts +0 -8
- package/dist/types/core/base/node.d.ts +0 -27
- package/dist/types/core/base/observable.d.ts +0 -34
- package/dist/types/core/base/observer.d.ts +0 -33
- package/dist/types/core/nodes/actionNode.d.ts +0 -1
- package/dist/types/core/nodes/arrayNode.d.ts +0 -1
- package/dist/types/core/nodes/effectNode.d.ts +0 -1
- package/dist/types/core/nodes/index.d.ts +0 -8
- package/dist/types/core/nodes/mapNode.d.ts +0 -1
- package/dist/types/core/nodes/signalNode.d.ts +0 -1
- package/dist/types/core/nodes/valueAsyncNode.d.ts +0 -1
- package/dist/types/core/nodes/valueNode.d.ts +0 -1
- package/dist/types/core/nodes/valueSyncNode.d.ts +0 -1
- package/dist/types/core/schedulers/asyncResolver.d.ts +0 -1
- package/dist/types/core/schedulers/asyncScheduler.d.ts +0 -1
- package/dist/types/core/schedulers/index.d.ts +0 -4
- package/dist/types/core/schedulers/pendingError.d.ts +0 -1
- package/dist/types/core/schedulers/scheduler.d.ts +0 -1
- package/dist/types/core/schedulers/syncResolver.d.ts +0 -1
- package/dist/types/core/schedulers/syncScheduler.d.ts +0 -1
- package/dist/types/index.d.ts +0 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/// <reference lib="esnext.disposable" preserve="true" />
|
|
2
|
+
import { d as FlowEffect, h as ObserverStatus, m as ObservableStatus, n as FlowValue } from "./shared.js";
|
|
3
|
+
import { t as FlowSignal } from "./shared2.js";
|
|
4
|
+
//#region src/core/base/disposable.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Which primitive a node came from. Declared with the engine that carries it, and surfaced to
|
|
7
|
+
* consumers by the inspection layer (#50, A2-DX-2).
|
|
8
|
+
*
|
|
9
|
+
* `unknown` is what a core built on its own reports — the kind is a property of the primitive a
|
|
10
|
+
* facade makes of it, not of the engine.
|
|
11
|
+
*
|
|
12
|
+
* @public
|
|
13
|
+
*/
|
|
14
|
+
type FlowNodeKind = "constant" | "constantAsync" | "derivation" | "derivationAsync" | "state" | "stateAsync" | "writableDerivation" | "writableDerivationAsync" | "array" | "map" | "action" | "signal" | "effect" | "unknown";
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/core/base/traceHub.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* The moments in a node's life a trace can record (#50, A2-DX-2).
|
|
19
|
+
*
|
|
20
|
+
* Between them they spell out one propagation: a write notifies its dependents, each is queued for
|
|
21
|
+
* the coming flush, the flush executes them, each settles, and the effects run last.
|
|
22
|
+
*
|
|
23
|
+
* Declared here, where the engine emits them, and re-exported by the inspection layer rather than
|
|
24
|
+
* aliased there: an alias would leave the name TypeDoc documents pointing at a declaration outside
|
|
25
|
+
* every entry point, and an internal-tagged one would be stripped from the published declarations
|
|
26
|
+
* while the public alias still referenced it. The same shape as `ObserverStatus`, declared in
|
|
27
|
+
* `observer.ts` and re-exported by the API layer, prefix included since this is a public name.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
type FlowTraceEventType = "notify" | "queue-pending" | "queue-effect" | "execute" | "resolve" | "reject" | "effect-run" | "cycle-error";
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core/inspect/snapshot.d.ts
|
|
34
|
+
/**
|
|
35
|
+
* How a node is named in a dump, and everything needed to recognise it — the shape used both for a
|
|
36
|
+
* node itself and for each of its neighbours.
|
|
37
|
+
*
|
|
38
|
+
* @public
|
|
39
|
+
*/
|
|
40
|
+
interface FlowNodeRef {
|
|
41
|
+
/**
|
|
42
|
+
* Identity of this node, assigned at creation and never reused. Two dumps taken at different
|
|
43
|
+
* moments describe the same node under the same `id`, which is what lets a diff of them mean
|
|
44
|
+
* something.
|
|
45
|
+
*/
|
|
46
|
+
id: number;
|
|
47
|
+
/** Which primitive this is — the factory it came from, or `"effect"` for a subscription. */
|
|
48
|
+
kind: FlowNodeKind;
|
|
49
|
+
/** The `name` option given at creation, absent when none was. */
|
|
50
|
+
name?: string;
|
|
51
|
+
/** How to display this node: its `name` when it has one, `kind#id` otherwise. */
|
|
52
|
+
label: string;
|
|
53
|
+
/** The node's current status, always reported — including `"disposed"`. */
|
|
54
|
+
status: ObservableStatus | ObserverStatus;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A node and its edges at one instant.
|
|
58
|
+
*
|
|
59
|
+
* Edges are a snapshot, not a fixture: a computation rebuilds its dependency set from scratch on
|
|
60
|
+
* every run, so this describes the graph as of each node's last computation.
|
|
61
|
+
*
|
|
62
|
+
* @public
|
|
63
|
+
*/
|
|
64
|
+
interface FlowNodeSnapshot extends FlowNodeRef {
|
|
65
|
+
/** The sources this node currently reads. */
|
|
66
|
+
dependencies: FlowNodeRef[];
|
|
67
|
+
/** The observers currently registered on this node. */
|
|
68
|
+
dependents: FlowNodeRef[];
|
|
69
|
+
/**
|
|
70
|
+
* The node's value, rendered as a short string — present only when `status` is `"resolved"`,
|
|
71
|
+
* mirroring the discrimination of the public `value` snapshot. A pending, dirty or disposed node
|
|
72
|
+
* has no value worth reporting, and a stale cached one would be actively misleading.
|
|
73
|
+
*/
|
|
74
|
+
value?: string;
|
|
75
|
+
/** The error's message — present only when `status` is `"error"`. */
|
|
76
|
+
error?: string;
|
|
77
|
+
/** Present and `true` only on a derivation created with `eager: true`. */
|
|
78
|
+
eager?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Describes one primitive and its immediate neighbours, without touching the reactive system (#50,
|
|
82
|
+
* A2-DX-2).
|
|
83
|
+
*
|
|
84
|
+
* Strictly passive: it reads `status` and the passive value snapshot, and iterates the dependency
|
|
85
|
+
* registries. It never calls `get()`, `pick()`, `watch()` or `subscribe()`, so it computes nothing,
|
|
86
|
+
* registers no dependency and moves no node — inspecting a graph cannot change what that graph then
|
|
87
|
+
* does. Inspecting a disposed primitive is fine too, and reports `status: "disposed"`.
|
|
88
|
+
*
|
|
89
|
+
* One consequence is worth stating plainly: a lazy derivation that has never been read has never
|
|
90
|
+
* computed, so it has **no dependencies yet** and reports `status: "dirty"` with no value. That is
|
|
91
|
+
* the truth about the graph, not a gap in the report — exercise the application first, or create the
|
|
92
|
+
* derivations you mean to watch with `eager: true`.
|
|
93
|
+
*
|
|
94
|
+
* @param target - The primitive to describe: a value, a signal, or the effect `subscribe()` returned
|
|
95
|
+
* @returns A serialisable description of that node
|
|
96
|
+
*
|
|
97
|
+
* @public
|
|
98
|
+
*/
|
|
99
|
+
export declare function inspect(target: FlowValue<unknown> | FlowEffect | FlowSignal): FlowNodeSnapshot;
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/core/inspect/graph.d.ts
|
|
102
|
+
/**
|
|
103
|
+
* Options accepted by {@link graphSnapshot}.
|
|
104
|
+
*
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export interface FlowGraphOptions {
|
|
108
|
+
/**
|
|
109
|
+
* Include nodes already disposed. Off by default: a disposed node is inert and its edges are
|
|
110
|
+
* gone, so it is noise in an ordinary dump — but seeing one still listed means something holds a
|
|
111
|
+
* reference to it, which is worth knowing when hunting a retained object.
|
|
112
|
+
*/
|
|
113
|
+
includeDisposed?: boolean;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Counts that describe the graph at a glance.
|
|
117
|
+
*
|
|
118
|
+
* @public
|
|
119
|
+
*/
|
|
120
|
+
export interface FlowGraphSummary {
|
|
121
|
+
/** How many nodes the snapshot covers — the length of {@link FlowGraphSnapshot.nodes}. */
|
|
122
|
+
total: number;
|
|
123
|
+
/** How many nodes of each {@link FlowNodeKind} — keys are absent rather than zero. */
|
|
124
|
+
byKind: Record<string, number>;
|
|
125
|
+
/** How many nodes in each status — keys are absent rather than zero. */
|
|
126
|
+
byStatus: Record<string, number>;
|
|
127
|
+
/**
|
|
128
|
+
* Nodes with no edge at all, in either direction.
|
|
129
|
+
*
|
|
130
|
+
* The number to watch: a primitive nothing reads and that reads nothing is either a lazy
|
|
131
|
+
* derivation not yet exercised, or a node that outlived its purpose and was never disposed. A
|
|
132
|
+
* count that grows as an application runs is the visible form of a leak.
|
|
133
|
+
*/
|
|
134
|
+
orphans: number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The whole graph at one instant.
|
|
138
|
+
*
|
|
139
|
+
* Every field is a plain number, string or array, so the snapshot survives `JSON.stringify` intact —
|
|
140
|
+
* it can be logged, sent to a bug report, or handed to a tool that compares two of them by node id.
|
|
141
|
+
*
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
144
|
+
export interface FlowGraphSnapshot {
|
|
145
|
+
/** Every node the snapshot covers, in creation order. */
|
|
146
|
+
nodes: FlowNodeSnapshot[];
|
|
147
|
+
/** Counts over {@link FlowGraphSnapshot.nodes}, for reading the shape without walking it. */
|
|
148
|
+
summary: FlowGraphSummary;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Describes every reactive node alive in this process, without touching the reactive system (#50,
|
|
152
|
+
* A2-DX-2).
|
|
153
|
+
*
|
|
154
|
+
* There is no way to name where to start, and that is the point: the graph has no roots, and the
|
|
155
|
+
* nodes worth finding — the ones leaked because nothing disposed them — are exactly the ones no
|
|
156
|
+
* handle points at any more. So the dump comes from a census kept as nodes are created, held weakly:
|
|
157
|
+
* a node the garbage collector has taken is gone from it, and one that lingers shows up as an orphan.
|
|
158
|
+
*
|
|
159
|
+
* Passivity is the same contract as {@link inspect}: statuses are read, registries iterated, nothing
|
|
160
|
+
* computed. In particular a lazy derivation never read has no dependencies yet and appears as an
|
|
161
|
+
* orphan — see {@link inspect} for what to do about it.
|
|
162
|
+
*
|
|
163
|
+
* @param options - See {@link FlowGraphOptions}
|
|
164
|
+
* @returns Every node, in creation order, with a {@link FlowGraphSummary}
|
|
165
|
+
*
|
|
166
|
+
* @public
|
|
167
|
+
*/
|
|
168
|
+
export declare function graphSnapshot(options?: FlowGraphOptions): FlowGraphSnapshot;
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region src/core/inspect/format.d.ts
|
|
171
|
+
/**
|
|
172
|
+
* Renders a graph as readable text (#50, A2-DX-2).
|
|
173
|
+
*
|
|
174
|
+
* The intended use is a developer with the browser console open, mid-session, asking what the graph
|
|
175
|
+
* looks like right now: `console.log(formatGraph())`. It returns a string rather than printing one,
|
|
176
|
+
* so the same call works in a terminal, in a test, or pasted into a bug report — and so the library
|
|
177
|
+
* never writes to anyone's console on its own.
|
|
178
|
+
*
|
|
179
|
+
* One block per node: a line naming it, its kind and its status, followed by its value when it has
|
|
180
|
+
* one (`= …`) or its error (`! …`), then an indented line of edges — `←` for what it reads, `→` for
|
|
181
|
+
* what reads it. Nodes with no edges at all are gathered at the end under `orphans`, where a leak
|
|
182
|
+
* shows up as a list that keeps growing.
|
|
183
|
+
*
|
|
184
|
+
* @param graph - A snapshot to render; taken with {@link graphSnapshot} when omitted
|
|
185
|
+
* @returns The rendered listing
|
|
186
|
+
*
|
|
187
|
+
* @public
|
|
188
|
+
*/
|
|
189
|
+
export declare function formatGraph(graph?: FlowGraphSnapshot): string;
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/core/inspect/trace.d.ts
|
|
192
|
+
/**
|
|
193
|
+
* One recorded moment: what happened, to which node, and where it falls in the order.
|
|
194
|
+
*
|
|
195
|
+
* @public
|
|
196
|
+
*/
|
|
197
|
+
export interface FlowTraceEvent {
|
|
198
|
+
/** Position in the trace, counted from the moment tracing started. Never reused. */
|
|
199
|
+
seq: number;
|
|
200
|
+
/** What the engine did — see {@link FlowTraceEventType} for the vocabulary. */
|
|
201
|
+
type: FlowTraceEventType;
|
|
202
|
+
/** The node it happened to, described as of the moment it was recorded. */
|
|
203
|
+
node: FlowNodeRef;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A running trace. Stop it with `dispose()` — a trace left on keeps recording for the life of the
|
|
207
|
+
* process, which is fine while debugging and waste afterwards.
|
|
208
|
+
*
|
|
209
|
+
* @public
|
|
210
|
+
*/
|
|
211
|
+
export interface FlowTrace {
|
|
212
|
+
/** What has been recorded, oldest first, up to the capacity given at creation. */
|
|
213
|
+
readonly events: readonly FlowTraceEvent[];
|
|
214
|
+
/** Forgets everything recorded so far, without stopping the trace. */
|
|
215
|
+
clear(): void;
|
|
216
|
+
/** Stops recording. */
|
|
217
|
+
dispose(): void;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Options accepted by {@link enableTrace}.
|
|
221
|
+
*
|
|
222
|
+
* @public
|
|
223
|
+
*/
|
|
224
|
+
export interface FlowTraceOptions {
|
|
225
|
+
/**
|
|
226
|
+
* How many events to keep. Older ones are dropped as newer arrive, so memory is bounded however
|
|
227
|
+
* long the trace runs — and what remains is the end of the story, which is the part that
|
|
228
|
+
* explains the symptom. Defaults to 500.
|
|
229
|
+
*/
|
|
230
|
+
capacity?: number;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Records what the engine does, so that a question about *time* can be answered (#50, A2-DX-2).
|
|
234
|
+
*
|
|
235
|
+
* `inspect()` and `graphSnapshot()` describe the graph's shape at an instant; some questions are not
|
|
236
|
+
* about shape. "I wrote to `$count` and my effect never ran" is answered by reading the sequence: a
|
|
237
|
+
* `notify` on `$count` with no `queue-effect` behind it means the edge was never there, while a
|
|
238
|
+
* `queue-effect` with no `effect-run` means the flush never reached it. A feedback loop reads as an
|
|
239
|
+
* alternation of `notify` and `effect-run` ending in `cycle-error`, which is how the nodes involved
|
|
240
|
+
* get named — the error alone names neither.
|
|
241
|
+
*
|
|
242
|
+
* Tracing is off until this is called, and costs one boolean check per engine event while it is.
|
|
243
|
+
* Recording does not change what the engine does: events are observed as they pass, never injected.
|
|
244
|
+
*
|
|
245
|
+
* @param options - See {@link FlowTraceOptions}
|
|
246
|
+
* @returns The running trace; call `dispose()` on it to stop
|
|
247
|
+
*
|
|
248
|
+
* @public
|
|
249
|
+
*/
|
|
250
|
+
export declare function enableTrace(options?: FlowTraceOptions): FlowTrace;
|
|
251
|
+
//#endregion
|
|
252
|
+
export type { FlowNodeKind, FlowNodeRef, FlowNodeSnapshot, FlowTraceEventType };
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { a as ReadableNode, c as Observable, d as TraceHub, f as LiveNodeRegistry, h as PicoFlowError, l as Observer, o as EffectNode, s as Node, t as ValueCore } from "./shared.js";
|
|
2
|
+
import { t as SignalNode } from "./shared3.js";
|
|
3
|
+
|
|
4
|
+
//#region src/core/inspect/renderValue.ts
|
|
5
|
+
/** Longest rendering kept; anything past this is cut and marked with an ellipsis. */
|
|
6
|
+
const MAX_LENGTH = 80;
|
|
7
|
+
/** How deep the renderer descends before collapsing a nested value to `{…}` / `[…]`. */
|
|
8
|
+
const MAX_DEPTH = 2;
|
|
9
|
+
/** How many entries of an array, map or set are rendered before the rest is elided. */
|
|
10
|
+
const MAX_ENTRIES = 10;
|
|
11
|
+
/**
|
|
12
|
+
* Renders a value for an inspection dump: short, safe, and never a hazard of its own (#50).
|
|
13
|
+
*
|
|
14
|
+
* A dump must survive whatever a graph happens to hold — a megabyte of data, a cyclic object, a
|
|
15
|
+
* getter that throws — so this is deliberately not `JSON.stringify`. Depth and entry count are
|
|
16
|
+
* bounded (which is also what makes a cycle harmless, without the false "[Circular]" a seen-set
|
|
17
|
+
* reports for a merely repeated object), the result is truncated, and anything unexpected degrades
|
|
18
|
+
* to a placeholder rather than throwing inside the debugging tool.
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
function renderValue(data) {
|
|
22
|
+
let text;
|
|
23
|
+
try {
|
|
24
|
+
text = render(data, MAX_DEPTH);
|
|
25
|
+
} catch {
|
|
26
|
+
text = "[unrenderable]";
|
|
27
|
+
}
|
|
28
|
+
return text.length > MAX_LENGTH ? `${text.slice(0, 79)}…` : text;
|
|
29
|
+
}
|
|
30
|
+
function render(data, depth) {
|
|
31
|
+
switch (typeof data) {
|
|
32
|
+
case "string": return JSON.stringify(data);
|
|
33
|
+
case "number":
|
|
34
|
+
case "boolean": return String(data);
|
|
35
|
+
case "undefined": return "undefined";
|
|
36
|
+
case "bigint": return `${data}n`;
|
|
37
|
+
case "symbol": return data.toString();
|
|
38
|
+
case "function": return data.name ? `[Function ${data.name}]` : "[Function]";
|
|
39
|
+
}
|
|
40
|
+
if (data === null) return "null";
|
|
41
|
+
if (data instanceof Error) return `${data.name}: ${data.message}`;
|
|
42
|
+
if (data instanceof Date) return data.toISOString();
|
|
43
|
+
const isArray = Array.isArray(data);
|
|
44
|
+
const isMap = data instanceof Map;
|
|
45
|
+
const isSet = data instanceof Set;
|
|
46
|
+
if (depth === 0) return isArray ? "[…]" : "{…}";
|
|
47
|
+
if (isArray) return `[${list(data, (item) => render(item, depth - 1))}]`;
|
|
48
|
+
if (isSet) return `Set(${data.size}) {${list([...data], (item) => render(item, depth - 1))}}`;
|
|
49
|
+
if (isMap) {
|
|
50
|
+
const entries = list([...data], ([key, value]) => `${render(key, depth - 1)} => ${render(value, depth - 1)}`);
|
|
51
|
+
return `Map(${data.size}) {${entries}}`;
|
|
52
|
+
}
|
|
53
|
+
return `{${list(Object.entries(data), ([key, value]) => `${key}: ${render(value, depth - 1)}`)}}`;
|
|
54
|
+
}
|
|
55
|
+
/** Joins at most {@link MAX_ENTRIES} rendered items, marking the elided tail. */
|
|
56
|
+
function list(items, renderItem) {
|
|
57
|
+
const shown = items.slice(0, MAX_ENTRIES).map(renderItem);
|
|
58
|
+
if (items.length > MAX_ENTRIES) shown.push(`…${items.length - MAX_ENTRIES} more`);
|
|
59
|
+
return shown.join(", ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/core/inspect/snapshot.ts
|
|
64
|
+
/**
|
|
65
|
+
* Describes one primitive and its immediate neighbours, without touching the reactive system (#50,
|
|
66
|
+
* A2-DX-2).
|
|
67
|
+
*
|
|
68
|
+
* Strictly passive: it reads `status` and the passive value snapshot, and iterates the dependency
|
|
69
|
+
* registries. It never calls `get()`, `pick()`, `watch()` or `subscribe()`, so it computes nothing,
|
|
70
|
+
* registers no dependency and moves no node — inspecting a graph cannot change what that graph then
|
|
71
|
+
* does. Inspecting a disposed primitive is fine too, and reports `status: "disposed"`.
|
|
72
|
+
*
|
|
73
|
+
* One consequence is worth stating plainly: a lazy derivation that has never been read has never
|
|
74
|
+
* computed, so it has **no dependencies yet** and reports `status: "dirty"` with no value. That is
|
|
75
|
+
* the truth about the graph, not a gap in the report — exercise the application first, or create the
|
|
76
|
+
* derivations you mean to watch with `eager: true`.
|
|
77
|
+
*
|
|
78
|
+
* @param target - The primitive to describe: a value, a signal, or the effect `subscribe()` returned
|
|
79
|
+
* @returns A serialisable description of that node
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
function inspect(target) {
|
|
84
|
+
return snapshotOf(participantOf(target));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolves a public handle to the node that actually sits in the graph.
|
|
88
|
+
*
|
|
89
|
+
* A value primitive is a facade owning a {@link ValueCore}, so the map built at construction is the
|
|
90
|
+
* way across; a signal and an effect are their own graph node. Nothing else is accepted — in
|
|
91
|
+
* particular not the tracker a compute function receives, even though reading it would be harmless:
|
|
92
|
+
* introspection stays outside the reactive system, and an entry point that takes a tracker would
|
|
93
|
+
* suggest otherwise.
|
|
94
|
+
*/
|
|
95
|
+
function participantOf(target) {
|
|
96
|
+
if (target instanceof ReadableNode) return target._core;
|
|
97
|
+
if (target instanceof EffectNode || target instanceof SignalNode) return target;
|
|
98
|
+
throw new PicoFlowError("[PicoFlow] inspect() expects a primitive, a signal or an effect — not the tracker a compute function receives");
|
|
99
|
+
}
|
|
100
|
+
/** Builds the full description of a graph participant. @internal */
|
|
101
|
+
function snapshotOf(participant) {
|
|
102
|
+
const snapshot = {
|
|
103
|
+
...refOf(participant),
|
|
104
|
+
dependencies: [],
|
|
105
|
+
dependents: []
|
|
106
|
+
};
|
|
107
|
+
if (participant instanceof Node || participant instanceof Observer) participant._forEachDependency((dependency) => {
|
|
108
|
+
snapshot.dependencies.push(refOf(dependency));
|
|
109
|
+
});
|
|
110
|
+
if (participant instanceof Observable) participant._forEachDependent((dependent) => {
|
|
111
|
+
snapshot.dependents.push(refOf(dependent));
|
|
112
|
+
});
|
|
113
|
+
if (participant instanceof ValueCore) {
|
|
114
|
+
const value = participant.snapshot();
|
|
115
|
+
if (value.status === "resolved") snapshot.value = renderValue(value.data);
|
|
116
|
+
if (value.status === "error") snapshot.error = value.error.message;
|
|
117
|
+
if (participant._isEager) snapshot.eager = true;
|
|
118
|
+
}
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
/** Builds the short form: what a node is called, and where it stands. @internal */
|
|
122
|
+
function refOf(participant) {
|
|
123
|
+
const kind = participant._kind;
|
|
124
|
+
const name = participant._inspectName;
|
|
125
|
+
const status = statusOf(participant);
|
|
126
|
+
const ref = {
|
|
127
|
+
id: participant._id,
|
|
128
|
+
kind,
|
|
129
|
+
label: name ?? `${kind}#${participant._id}`,
|
|
130
|
+
status
|
|
131
|
+
};
|
|
132
|
+
if (name !== void 0) ref.name = name;
|
|
133
|
+
return ref;
|
|
134
|
+
}
|
|
135
|
+
/** `status` is the one reading every participant offers, and it never computes nor throws. */
|
|
136
|
+
function statusOf(participant) {
|
|
137
|
+
if (participant instanceof Observable) return participant.status;
|
|
138
|
+
return participant.status;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/core/inspect/graph.ts
|
|
143
|
+
/**
|
|
144
|
+
* Describes every reactive node alive in this process, without touching the reactive system (#50,
|
|
145
|
+
* A2-DX-2).
|
|
146
|
+
*
|
|
147
|
+
* There is no way to name where to start, and that is the point: the graph has no roots, and the
|
|
148
|
+
* nodes worth finding — the ones leaked because nothing disposed them — are exactly the ones no
|
|
149
|
+
* handle points at any more. So the dump comes from a census kept as nodes are created, held weakly:
|
|
150
|
+
* a node the garbage collector has taken is gone from it, and one that lingers shows up as an orphan.
|
|
151
|
+
*
|
|
152
|
+
* Passivity is the same contract as {@link inspect}: statuses are read, registries iterated, nothing
|
|
153
|
+
* computed. In particular a lazy derivation never read has no dependencies yet and appears as an
|
|
154
|
+
* orphan — see {@link inspect} for what to do about it.
|
|
155
|
+
*
|
|
156
|
+
* @param options - See {@link FlowGraphOptions}
|
|
157
|
+
* @returns Every node, in creation order, with a {@link FlowGraphSummary}
|
|
158
|
+
*
|
|
159
|
+
* @public
|
|
160
|
+
*/
|
|
161
|
+
function graphSnapshot(options) {
|
|
162
|
+
const includeDisposed = options?.includeDisposed ?? false;
|
|
163
|
+
const nodes = [];
|
|
164
|
+
LiveNodeRegistry.forEach((participant) => {
|
|
165
|
+
const snapshot = snapshotOf(participant);
|
|
166
|
+
if (!includeDisposed && snapshot.status === "disposed") return;
|
|
167
|
+
nodes.push(snapshot);
|
|
168
|
+
});
|
|
169
|
+
const byKind = {};
|
|
170
|
+
const byStatus = {};
|
|
171
|
+
let orphans = 0;
|
|
172
|
+
for (const node of nodes) {
|
|
173
|
+
byKind[node.kind] = (byKind[node.kind] ?? 0) + 1;
|
|
174
|
+
byStatus[node.status] = (byStatus[node.status] ?? 0) + 1;
|
|
175
|
+
if (node.dependencies.length === 0 && node.dependents.length === 0) orphans++;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
nodes,
|
|
179
|
+
summary: {
|
|
180
|
+
total: nodes.length,
|
|
181
|
+
byKind,
|
|
182
|
+
byStatus,
|
|
183
|
+
orphans
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/core/inspect/format.ts
|
|
190
|
+
/** Widest a column is padded to; a longer label simply pushes its own line out. */
|
|
191
|
+
const MAX_COLUMN = 24;
|
|
192
|
+
/**
|
|
193
|
+
* Renders a graph as readable text (#50, A2-DX-2).
|
|
194
|
+
*
|
|
195
|
+
* The intended use is a developer with the browser console open, mid-session, asking what the graph
|
|
196
|
+
* looks like right now: `console.log(formatGraph())`. It returns a string rather than printing one,
|
|
197
|
+
* so the same call works in a terminal, in a test, or pasted into a bug report — and so the library
|
|
198
|
+
* never writes to anyone's console on its own.
|
|
199
|
+
*
|
|
200
|
+
* One block per node: a line naming it, its kind and its status, followed by its value when it has
|
|
201
|
+
* one (`= …`) or its error (`! …`), then an indented line of edges — `←` for what it reads, `→` for
|
|
202
|
+
* what reads it. Nodes with no edges at all are gathered at the end under `orphans`, where a leak
|
|
203
|
+
* shows up as a list that keeps growing.
|
|
204
|
+
*
|
|
205
|
+
* @param graph - A snapshot to render; taken with {@link graphSnapshot} when omitted
|
|
206
|
+
* @returns The rendered listing
|
|
207
|
+
*
|
|
208
|
+
* @public
|
|
209
|
+
*/
|
|
210
|
+
function formatGraph(graph = graphSnapshot()) {
|
|
211
|
+
const { nodes, summary } = graph;
|
|
212
|
+
const counts = Object.entries(summary.byStatus).map(([status, count]) => `${status}:${count}`);
|
|
213
|
+
if (summary.orphans > 0) counts.push(`orphans:${summary.orphans}`);
|
|
214
|
+
const header = `picoflow graph — ${summary.total} node${summary.total === 1 ? "" : "s"}${counts.length > 0 ? ` · ${counts.join(" · ")}` : ""}`;
|
|
215
|
+
if (nodes.length === 0) return header;
|
|
216
|
+
const connected = nodes.filter((node) => node.dependencies.length > 0 || node.dependents.length > 0);
|
|
217
|
+
const orphans = nodes.filter((node) => node.dependencies.length === 0 && node.dependents.length === 0);
|
|
218
|
+
const labelWidth = width(nodes, (node) => node.label);
|
|
219
|
+
const kindWidth = width(nodes, (node) => node.kind);
|
|
220
|
+
const lines = [header, ""];
|
|
221
|
+
for (const node of connected) lines.push(...block(node, labelWidth, kindWidth));
|
|
222
|
+
if (orphans.length > 0) {
|
|
223
|
+
if (connected.length > 0) lines.push("");
|
|
224
|
+
lines.push("orphans — no edges:");
|
|
225
|
+
for (const node of orphans) lines.push(...block(node, labelWidth, kindWidth));
|
|
226
|
+
}
|
|
227
|
+
return lines.join("\n");
|
|
228
|
+
}
|
|
229
|
+
/** The node's own line, plus its edge line when it has any. */
|
|
230
|
+
function block(node, labelWidth, kindWidth) {
|
|
231
|
+
const status = node.eager ? `${node.status} (eager)` : node.status;
|
|
232
|
+
let line = `${pad(node.label, labelWidth)} ${pad(node.kind, kindWidth)} ${pad(status, 8)}`;
|
|
233
|
+
if (node.value !== void 0) line += ` = ${node.value}`;
|
|
234
|
+
if (node.error !== void 0) line += ` ! ${node.error}`;
|
|
235
|
+
const lines = [line.trimEnd()];
|
|
236
|
+
const edges = [];
|
|
237
|
+
if (node.dependencies.length > 0) edges.push(`← ${node.dependencies.map((ref) => ref.label).join(", ")}`);
|
|
238
|
+
if (node.dependents.length > 0) edges.push(`→ ${node.dependents.map((ref) => ref.label).join(", ")}`);
|
|
239
|
+
if (edges.length > 0) lines.push(` ${edges.join(" ")}`);
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
242
|
+
function width(nodes, of) {
|
|
243
|
+
return Math.min(MAX_COLUMN, Math.max(...nodes.map((node) => of(node).length)));
|
|
244
|
+
}
|
|
245
|
+
function pad(text, to) {
|
|
246
|
+
return text.length >= to ? text : text + " ".repeat(to - text.length);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/core/inspect/trace.ts
|
|
251
|
+
/**
|
|
252
|
+
* Records what the engine does, so that a question about *time* can be answered (#50, A2-DX-2).
|
|
253
|
+
*
|
|
254
|
+
* `inspect()` and `graphSnapshot()` describe the graph's shape at an instant; some questions are not
|
|
255
|
+
* about shape. "I wrote to `$count` and my effect never ran" is answered by reading the sequence: a
|
|
256
|
+
* `notify` on `$count` with no `queue-effect` behind it means the edge was never there, while a
|
|
257
|
+
* `queue-effect` with no `effect-run` means the flush never reached it. A feedback loop reads as an
|
|
258
|
+
* alternation of `notify` and `effect-run` ending in `cycle-error`, which is how the nodes involved
|
|
259
|
+
* get named — the error alone names neither.
|
|
260
|
+
*
|
|
261
|
+
* Tracing is off until this is called, and costs one boolean check per engine event while it is.
|
|
262
|
+
* Recording does not change what the engine does: events are observed as they pass, never injected.
|
|
263
|
+
*
|
|
264
|
+
* @param options - See {@link FlowTraceOptions}
|
|
265
|
+
* @returns The running trace; call `dispose()` on it to stop
|
|
266
|
+
*
|
|
267
|
+
* @public
|
|
268
|
+
*/
|
|
269
|
+
function enableTrace(options) {
|
|
270
|
+
const capacity = Math.max(1, Math.trunc(options?.capacity ?? 500));
|
|
271
|
+
let buffer = [];
|
|
272
|
+
let next = 0;
|
|
273
|
+
let seq = 0;
|
|
274
|
+
const listener = (type, node) => {
|
|
275
|
+
const event = {
|
|
276
|
+
seq: seq++,
|
|
277
|
+
type,
|
|
278
|
+
node: refOf(node)
|
|
279
|
+
};
|
|
280
|
+
if (buffer.length < capacity) buffer.push(event);
|
|
281
|
+
else buffer[next] = event;
|
|
282
|
+
next = (next + 1) % capacity;
|
|
283
|
+
};
|
|
284
|
+
TraceHub.listen(listener);
|
|
285
|
+
return {
|
|
286
|
+
get events() {
|
|
287
|
+
return buffer.length < capacity ? [...buffer] : [...buffer.slice(next), ...buffer.slice(0, next)];
|
|
288
|
+
},
|
|
289
|
+
clear() {
|
|
290
|
+
buffer = [];
|
|
291
|
+
next = 0;
|
|
292
|
+
},
|
|
293
|
+
dispose() {
|
|
294
|
+
TraceHub.unlisten(listener);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
export { enableTrace, formatGraph, graphSnapshot, inspect };
|