@swirls/looms 0.0.1
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/LICENSE +190 -0
- package/README.md +9 -0
- package/dist/actor/cell.d.ts +27 -0
- package/dist/actor/cell.js +41 -0
- package/dist/actor/host.d.ts +27 -0
- package/dist/actor/host.js +82 -0
- package/dist/actor/index.d.ts +3 -0
- package/dist/actor/index.js +3 -0
- package/dist/actor/routing.d.ts +15 -0
- package/dist/actor/routing.js +80 -0
- package/dist/agent/definitions-store.d.ts +11 -0
- package/dist/agent/definitions-store.js +10 -0
- package/dist/agent/definitions.d.ts +152 -0
- package/dist/agent/definitions.js +74 -0
- package/dist/agent/effects.d.ts +425 -0
- package/dist/agent/effects.js +318 -0
- package/dist/agent/events.d.ts +170 -0
- package/dist/agent/events.js +61 -0
- package/dist/agent/index.d.ts +13 -0
- package/dist/agent/index.js +13 -0
- package/dist/agent/llm.d.ts +35 -0
- package/dist/agent/llm.js +48 -0
- package/dist/agent/module.d.ts +1037 -0
- package/dist/agent/module.js +27 -0
- package/dist/agent/projections.d.ts +17 -0
- package/dist/agent/projections.js +81 -0
- package/dist/agent/scope.d.ts +171 -0
- package/dist/agent/scope.js +7 -0
- package/dist/agent/signals.d.ts +17 -0
- package/dist/agent/signals.js +19 -0
- package/dist/agent/stop-when.d.ts +12 -0
- package/dist/agent/stop-when.js +6 -0
- package/dist/agent/threads.d.ts +34 -0
- package/dist/agent/threads.js +407 -0
- package/dist/agent/tool-schema.d.ts +21 -0
- package/dist/agent/tool-schema.js +67 -0
- package/dist/agent/types.d.ts +87 -0
- package/dist/agent/types.js +32 -0
- package/dist/ai-vercel/index.d.ts +2 -0
- package/dist/ai-vercel/index.js +2 -0
- package/dist/ai-vercel/messages.d.ts +9 -0
- package/dist/ai-vercel/messages.js +68 -0
- package/dist/ai-vercel/vercel-llm.d.ts +8 -0
- package/dist/ai-vercel/vercel-llm.js +98 -0
- package/dist/approval/effects.d.ts +133 -0
- package/dist/approval/effects.js +13 -0
- package/dist/approval/events.d.ts +52 -0
- package/dist/approval/events.js +28 -0
- package/dist/approval/index.d.ts +6 -0
- package/dist/approval/index.js +6 -0
- package/dist/approval/module.d.ts +342 -0
- package/dist/approval/module.js +10 -0
- package/dist/approval/projections.d.ts +42 -0
- package/dist/approval/projections.js +57 -0
- package/dist/approval/scope.d.ts +45 -0
- package/dist/approval/scope.js +7 -0
- package/dist/approval/signals.d.ts +19 -0
- package/dist/approval/signals.js +34 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +6 -0
- package/dist/cli/commands.d.ts +13 -0
- package/dist/cli/commands.js +133 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/client/client.d.ts +69 -0
- package/dist/client/client.js +179 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/sse.d.ts +10 -0
- package/dist/client/sse.js +103 -0
- package/dist/cloudflare/alarm-scheduler.d.ts +9 -0
- package/dist/cloudflare/alarm-scheduler.js +13 -0
- package/dist/cloudflare/durable-object.d.ts +24 -0
- package/dist/cloudflare/durable-object.js +43 -0
- package/dist/cloudflare/event-store.d.ts +10 -0
- package/dist/cloudflare/event-store.js +18 -0
- package/dist/cloudflare/index.d.ts +4 -0
- package/dist/cloudflare/index.js +4 -0
- package/dist/cloudflare/router.d.ts +12 -0
- package/dist/cloudflare/router.js +26 -0
- package/dist/core/bun-sqlite.d.ts +13 -0
- package/dist/core/bun-sqlite.js +19 -0
- package/dist/core/catalog.d.ts +49 -0
- package/dist/core/catalog.js +39 -0
- package/dist/core/coalesce.d.ts +21 -0
- package/dist/core/coalesce.js +178 -0
- package/dist/core/compose.d.ts +59 -0
- package/dist/core/compose.js +97 -0
- package/dist/core/cursor.d.ts +16 -0
- package/dist/core/cursor.js +46 -0
- package/dist/core/effects.d.ts +110 -0
- package/dist/core/effects.js +99 -0
- package/dist/core/envelope.d.ts +79 -0
- package/dist/core/envelope.js +68 -0
- package/dist/core/event-codec.d.ts +8 -0
- package/dist/core/event-codec.js +82 -0
- package/dist/core/event-index.d.ts +28 -0
- package/dist/core/event-index.js +86 -0
- package/dist/core/fold.d.ts +19 -0
- package/dist/core/fold.js +362 -0
- package/dist/core/ids.d.ts +11 -0
- package/dist/core/ids.js +47 -0
- package/dist/core/index.d.ts +26 -0
- package/dist/core/index.js +25 -0
- package/dist/core/match.d.ts +8 -0
- package/dist/core/match.js +58 -0
- package/dist/core/module-scope.d.ts +99 -0
- package/dist/core/module-scope.js +72 -0
- package/dist/core/module.d.ts +74 -0
- package/dist/core/module.js +3 -0
- package/dist/core/projection.d.ts +83 -0
- package/dist/core/projection.js +241 -0
- package/dist/core/protocol.d.ts +99 -0
- package/dist/core/protocol.js +101 -0
- package/dist/core/replay.d.ts +13 -0
- package/dist/core/replay.js +34 -0
- package/dist/core/schema.d.ts +35 -0
- package/dist/core/schema.js +130 -0
- package/dist/core/serialize.d.ts +14 -0
- package/dist/core/serialize.js +42 -0
- package/dist/core/snapshot-store.d.ts +39 -0
- package/dist/core/snapshot-store.js +53 -0
- package/dist/core/snapshots.d.ts +26 -0
- package/dist/core/snapshots.js +100 -0
- package/dist/core/sqlite-store.d.ts +31 -0
- package/dist/core/sqlite-store.js +328 -0
- package/dist/core/state.d.ts +43 -0
- package/dist/core/state.js +31 -0
- package/dist/core/store.d.ts +146 -0
- package/dist/core/store.js +229 -0
- package/dist/core/thread.d.ts +41 -0
- package/dist/core/thread.js +13 -0
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.js +42 -0
- package/dist/core/validate-event.d.ts +19 -0
- package/dist/core/validate-event.js +42 -0
- package/dist/debugger/components/debugger-split.d.ts +23 -0
- package/dist/debugger/components/debugger-split.js +25 -0
- package/dist/debugger/components/event-inspector.d.ts +5 -0
- package/dist/debugger/components/event-inspector.js +38 -0
- package/dist/debugger/components/event-stream.d.ts +13 -0
- package/dist/debugger/components/event-stream.js +119 -0
- package/dist/debugger/components/json-view.d.ts +5 -0
- package/dist/debugger/components/json-view.js +5 -0
- package/dist/debugger/components/run-tree.d.ts +14 -0
- package/dist/debugger/components/run-tree.js +38 -0
- package/dist/debugger/components/status-dot.d.ts +3 -0
- package/dist/debugger/components/status-dot.js +5 -0
- package/dist/debugger/contracts.d.ts +22 -0
- package/dist/debugger/contracts.js +1 -0
- package/dist/debugger/events/catalog.d.ts +6 -0
- package/dist/debugger/events/catalog.js +27 -0
- package/dist/debugger/events/domain-summarize.d.ts +3 -0
- package/dist/debugger/events/domain-summarize.js +107 -0
- package/dist/debugger/events/family.d.ts +8 -0
- package/dist/debugger/events/family.js +44 -0
- package/dist/debugger/events/protocol-summarize.d.ts +3 -0
- package/dist/debugger/events/protocol-summarize.js +70 -0
- package/dist/debugger/index.d.ts +17 -0
- package/dist/debugger/index.js +16 -0
- package/dist/debugger/lib/cn.d.ts +5 -0
- package/dist/debugger/lib/cn.js +17 -0
- package/dist/debugger/lib/event-counts.d.ts +5 -0
- package/dist/debugger/lib/event-counts.js +23 -0
- package/dist/debugger/lib/event-meta.d.ts +8 -0
- package/dist/debugger/lib/event-meta.js +20 -0
- package/dist/debugger/lib/project-run.d.ts +9 -0
- package/dist/debugger/lib/project-run.js +14 -0
- package/dist/debugger/lib/status.d.ts +3 -0
- package/dist/debugger/lib/status.js +31 -0
- package/dist/debugger/lib/visible-events.d.ts +8 -0
- package/dist/debugger/lib/visible-events.js +21 -0
- package/dist/debugger/styles.css +71 -0
- package/dist/debugger/ui/checkbox.d.ts +4 -0
- package/dist/debugger/ui/checkbox.js +9 -0
- package/dist/debugger/ui/input.d.ts +3 -0
- package/dist/debugger/ui/input.js +7 -0
- package/dist/debugger/ui/scroll-area.d.ts +9 -0
- package/dist/debugger/ui/scroll-area.js +10 -0
- package/dist/debugger/ui/tabs.d.ts +7 -0
- package/dist/debugger/ui/tabs.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/projectors/index-model.d.ts +56 -0
- package/dist/projectors/index-model.js +117 -0
- package/dist/projectors/index.d.ts +4 -0
- package/dist/projectors/index.js +3 -0
- package/dist/projectors/memory.d.ts +5 -0
- package/dist/projectors/memory.js +77 -0
- package/dist/projectors/postgres.d.ts +9 -0
- package/dist/projectors/postgres.js +108 -0
- package/dist/projectors/projector.d.ts +11 -0
- package/dist/projectors/projector.js +1 -0
- package/dist/projectors/schema.d.ts +5 -0
- package/dist/projectors/schema.js +43 -0
- package/dist/projectors/sqlite.d.ts +9 -0
- package/dist/projectors/sqlite.js +103 -0
- package/dist/projectors/with-projectors.d.ts +17 -0
- package/dist/projectors/with-projectors.js +55 -0
- package/dist/react/derived.d.ts +19 -0
- package/dist/react/derived.js +50 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +5 -0
- package/dist/react/materialize.d.ts +3 -0
- package/dist/react/materialize.js +86 -0
- package/dist/react/notify.d.ts +18 -0
- package/dist/react/notify.js +80 -0
- package/dist/react/react.d.ts +61 -0
- package/dist/react/react.js +187 -0
- package/dist/react/register.d.ts +31 -0
- package/dist/react/register.js +1 -0
- package/dist/react/sse.d.ts +1 -0
- package/dist/react/sse.js +1 -0
- package/dist/react/store.d.ts +56 -0
- package/dist/react/store.js +340 -0
- package/dist/react/tables.d.ts +21 -0
- package/dist/react/tables.js +6 -0
- package/dist/runtime/dispatch-effect.d.ts +3 -0
- package/dist/runtime/dispatch-effect.js +113 -0
- package/dist/runtime/events-api.d.ts +4 -0
- package/dist/runtime/events-api.js +84 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/looms.d.ts +74 -0
- package/dist/runtime/looms.js +109 -0
- package/dist/runtime/runtime-helpers.d.ts +51 -0
- package/dist/runtime/runtime-helpers.js +230 -0
- package/dist/runtime/runtime.d.ts +91 -0
- package/dist/runtime/runtime.js +531 -0
- package/dist/runtime/server.d.ts +17 -0
- package/dist/runtime/server.js +226 -0
- package/dist/runtime/sse.d.ts +15 -0
- package/dist/runtime/sse.js +96 -0
- package/dist/runtime/wake-scheduler.d.ts +8 -0
- package/dist/runtime/wake-scheduler.js +38 -0
- package/dist/s2/config.d.ts +27 -0
- package/dist/s2/config.js +54 -0
- package/dist/s2/index.d.ts +6 -0
- package/dist/s2/index.js +6 -0
- package/dist/s2/lite.d.ts +26 -0
- package/dist/s2/lite.js +150 -0
- package/dist/s2/projector.d.ts +8 -0
- package/dist/s2/projector.js +28 -0
- package/dist/s2/serialize.d.ts +1 -0
- package/dist/s2/serialize.js +1 -0
- package/dist/s2/snapshot-store.d.ts +17 -0
- package/dist/s2/snapshot-store.js +320 -0
- package/dist/s2/store.d.ts +35 -0
- package/dist/s2/store.js +337 -0
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.js +63 -0
- package/dist/workflow/definitions-store.d.ts +11 -0
- package/dist/workflow/definitions-store.js +8 -0
- package/dist/workflow/definitions.d.ts +82 -0
- package/dist/workflow/definitions.js +32 -0
- package/dist/workflow/effects.d.ts +305 -0
- package/dist/workflow/effects.js +208 -0
- package/dist/workflow/events.d.ts +60 -0
- package/dist/workflow/events.js +39 -0
- package/dist/workflow/index.d.ts +8 -0
- package/dist/workflow/index.js +8 -0
- package/dist/workflow/module.d.ts +926 -0
- package/dist/workflow/module.js +19 -0
- package/dist/workflow/projections.d.ts +30 -0
- package/dist/workflow/projections.js +43 -0
- package/dist/workflow/scope.d.ts +61 -0
- package/dist/workflow/scope.js +7 -0
- package/dist/workflow/threads.d.ts +171 -0
- package/dist/workflow/threads.js +287 -0
- package/package.json +183 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer, Option, Queue, Ref, Stream } from 'effect';
|
|
2
|
+
import { withAssignedSeq } from './envelope.js';
|
|
3
|
+
export class EventStoreError extends Data.TaggedError('EventStoreError') {
|
|
4
|
+
conflict;
|
|
5
|
+
cause;
|
|
6
|
+
constructor(text, cause, options) {
|
|
7
|
+
super({ message: text, cause, conflict: options?.conflict });
|
|
8
|
+
this.name = 'EventStoreError';
|
|
9
|
+
this.cause = cause;
|
|
10
|
+
this.conflict = options?.conflict;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class EventStoreConflictError extends Data.TaggedError('EventStoreConflictError') {
|
|
14
|
+
conflict = true;
|
|
15
|
+
constructor(runId, expectedTail, actualTail, cause) {
|
|
16
|
+
super({
|
|
17
|
+
runId,
|
|
18
|
+
expectedTail,
|
|
19
|
+
actualTail,
|
|
20
|
+
message: `Conflict appending to ${runId}: expected tail ${expectedTail}, got ${actualTail}`,
|
|
21
|
+
cause,
|
|
22
|
+
});
|
|
23
|
+
this.name = 'EventStoreConflictError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class EventStoreTruncationError extends Data.TaggedError('EventStoreTruncationError') {
|
|
27
|
+
constructor(runId, readCount, tail) {
|
|
28
|
+
super({
|
|
29
|
+
runId,
|
|
30
|
+
readCount,
|
|
31
|
+
tail,
|
|
32
|
+
message: `store.read returned ${readCount} events but tail is ${tail}; refusing to dispatch from a truncated log`,
|
|
33
|
+
});
|
|
34
|
+
this.name = 'EventStoreTruncationError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class EventStoreTrimmedError extends Data.TaggedError('EventStoreTrimmedError') {
|
|
38
|
+
constructor(runId, head, requestedSeq) {
|
|
39
|
+
super({
|
|
40
|
+
runId,
|
|
41
|
+
head,
|
|
42
|
+
requestedSeq,
|
|
43
|
+
message: `store head is ${head} but requested seq ${requestedSeq} for run "${runId}"; history was trimmed and no covering snapshot is available`,
|
|
44
|
+
});
|
|
45
|
+
this.name = 'EventStoreTrimmedError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class EventStoreFencedError extends Data.TaggedError('EventStoreFencedError') {
|
|
49
|
+
constructor(runId, token) {
|
|
50
|
+
super({
|
|
51
|
+
runId,
|
|
52
|
+
token,
|
|
53
|
+
message: `Fencing token mismatch for run "${runId}"; token "${token}" is no longer current`,
|
|
54
|
+
});
|
|
55
|
+
this.name = 'EventStoreFencedError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function isEventStoreError(err) {
|
|
59
|
+
return (err instanceof EventStoreError ||
|
|
60
|
+
err instanceof EventStoreConflictError ||
|
|
61
|
+
err instanceof EventStoreTruncationError ||
|
|
62
|
+
err instanceof EventStoreTrimmedError ||
|
|
63
|
+
err instanceof EventStoreFencedError);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Pure Effect-native stream pagination primitive.
|
|
67
|
+
*
|
|
68
|
+
* Lazily pulls pages using `fetchPage` until:
|
|
69
|
+
* 1. `fetchPage` returns null or an empty record list,
|
|
70
|
+
* 2. `nextCursor` is null/undefined (signaling stream exhaustion / tail reached),
|
|
71
|
+
* 3. Or the caller's optional `limit` is satisfied (avoiding subsequent page fetches).
|
|
72
|
+
*/
|
|
73
|
+
export function paginatePages(options, fetchPage) {
|
|
74
|
+
const baseStream = Stream.paginate(options.initialCursor, (cursor) => Effect.gen(function* () {
|
|
75
|
+
const page = yield* fetchPage(cursor);
|
|
76
|
+
if (!page || page.records.length === 0 || page.nextCursor == null) {
|
|
77
|
+
return [page?.records ?? [], Option.none()];
|
|
78
|
+
}
|
|
79
|
+
return [page.records, Option.some(page.nextCursor)];
|
|
80
|
+
}));
|
|
81
|
+
return options.limit !== undefined ? baseStream.pipe(Stream.take(options.limit)) : baseStream;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Utility to read an EventStore as a Stream.Stream, using `store.readStream` if implemented
|
|
85
|
+
* or falling back to `Stream.fromIterableEffect(store.read(...))`.
|
|
86
|
+
*/
|
|
87
|
+
export function readEventStream(store, runId, options) {
|
|
88
|
+
if (store.readStream) {
|
|
89
|
+
return store.readStream(runId, options);
|
|
90
|
+
}
|
|
91
|
+
return Stream.fromIterableEffect(store.read(runId, options));
|
|
92
|
+
}
|
|
93
|
+
export class EventStoreTag extends Context.Service()('looms/EventStore') {
|
|
94
|
+
}
|
|
95
|
+
export const makeMemoryEventStore = Effect.gen(function* () {
|
|
96
|
+
const logs = yield* Ref.make(new Map());
|
|
97
|
+
const getOrCreate = (map, runId) => {
|
|
98
|
+
const existing = map.get(runId);
|
|
99
|
+
if (existing) {
|
|
100
|
+
return existing;
|
|
101
|
+
}
|
|
102
|
+
const created = { events: [], waiters: [], head: 1, tail: 0 };
|
|
103
|
+
map.set(runId, created);
|
|
104
|
+
return created;
|
|
105
|
+
};
|
|
106
|
+
const service = {
|
|
107
|
+
append: (runId, events, options) => Effect.gen(function* () {
|
|
108
|
+
const sequences = [];
|
|
109
|
+
yield* Ref.update(logs, (map) => {
|
|
110
|
+
const next = new Map(map);
|
|
111
|
+
const log = getOrCreate(next, runId);
|
|
112
|
+
if (options?.expectedTail !== undefined && log.tail !== options.expectedTail) {
|
|
113
|
+
throw new EventStoreConflictError(runId, options.expectedTail, log.tail);
|
|
114
|
+
}
|
|
115
|
+
if (options?.fencingToken !== undefined && log.fenceToken !== options.fencingToken) {
|
|
116
|
+
throw new EventStoreFencedError(runId, options.fencingToken);
|
|
117
|
+
}
|
|
118
|
+
let nextTail = log.tail;
|
|
119
|
+
let fenceToken = log.fenceToken;
|
|
120
|
+
if (options?.fence !== undefined) {
|
|
121
|
+
nextTail += 1;
|
|
122
|
+
fenceToken = options.fence === '' ? undefined : options.fence;
|
|
123
|
+
}
|
|
124
|
+
const appended = [];
|
|
125
|
+
for (const partial of events) {
|
|
126
|
+
const seq = nextTail + appended.length + 1;
|
|
127
|
+
const event = withAssignedSeq(partial, runId, seq);
|
|
128
|
+
appended.push(event);
|
|
129
|
+
sequences.push(seq);
|
|
130
|
+
}
|
|
131
|
+
nextTail += appended.length;
|
|
132
|
+
const updated = {
|
|
133
|
+
events: [...log.events, ...appended],
|
|
134
|
+
waiters: log.waiters,
|
|
135
|
+
head: log.head,
|
|
136
|
+
tail: nextTail,
|
|
137
|
+
fenceToken,
|
|
138
|
+
};
|
|
139
|
+
next.set(runId, updated);
|
|
140
|
+
for (const event of appended) {
|
|
141
|
+
for (const waiter of log.waiters) {
|
|
142
|
+
waiter(event);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return next;
|
|
146
|
+
}).pipe(Effect.catchDefect((cause) => Effect.fail(cause instanceof EventStoreConflictError ||
|
|
147
|
+
cause instanceof EventStoreFencedError ||
|
|
148
|
+
cause instanceof EventStoreError
|
|
149
|
+
? cause
|
|
150
|
+
: new EventStoreError('append failed', cause))));
|
|
151
|
+
const tail = yield* service.tail(runId);
|
|
152
|
+
return { sequences, tail };
|
|
153
|
+
}),
|
|
154
|
+
read: (runId, options) => Effect.gen(function* () {
|
|
155
|
+
const map = yield* Ref.get(logs);
|
|
156
|
+
const log = map.get(runId);
|
|
157
|
+
if (!log) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const fromSeq = Math.max(options?.fromSeq ?? 1, log.head);
|
|
161
|
+
const sliced = log.events.filter((e) => e.seq >= fromSeq);
|
|
162
|
+
return options?.limit !== undefined ? sliced.slice(0, options.limit) : sliced;
|
|
163
|
+
}),
|
|
164
|
+
readStream: (runId, options) => Stream.fromIterableEffect(service.read(runId, options)),
|
|
165
|
+
tail: (runId) => Effect.gen(function* () {
|
|
166
|
+
const map = yield* Ref.get(logs);
|
|
167
|
+
return map.get(runId)?.tail ?? 0;
|
|
168
|
+
}),
|
|
169
|
+
bounds: (runId) => Effect.gen(function* () {
|
|
170
|
+
const map = yield* Ref.get(logs);
|
|
171
|
+
const log = map.get(runId);
|
|
172
|
+
if (!log) {
|
|
173
|
+
return { head: 1, tail: 0 };
|
|
174
|
+
}
|
|
175
|
+
return { head: log.head, tail: log.tail };
|
|
176
|
+
}),
|
|
177
|
+
trim: (runId, beforeSeq) => Effect.gen(function* () {
|
|
178
|
+
yield* Ref.update(logs, (map) => {
|
|
179
|
+
const log = map.get(runId);
|
|
180
|
+
if (!log) {
|
|
181
|
+
return map;
|
|
182
|
+
}
|
|
183
|
+
const next = new Map(map);
|
|
184
|
+
const retained = log.events.filter((e) => e.seq >= beforeSeq);
|
|
185
|
+
next.set(runId, {
|
|
186
|
+
...log,
|
|
187
|
+
events: retained,
|
|
188
|
+
head: Math.max(log.head, beforeSeq),
|
|
189
|
+
});
|
|
190
|
+
return next;
|
|
191
|
+
});
|
|
192
|
+
}),
|
|
193
|
+
subscribe: (runId, options) => Stream.callback((queue) => Effect.gen(function* () {
|
|
194
|
+
const fromSeq = options?.fromSeq ?? 1;
|
|
195
|
+
const map = yield* Ref.get(logs);
|
|
196
|
+
const log = getOrCreate(map, runId);
|
|
197
|
+
yield* Ref.update(logs, (m) => {
|
|
198
|
+
const n = new Map(m);
|
|
199
|
+
if (!n.has(runId)) {
|
|
200
|
+
n.set(runId, log);
|
|
201
|
+
}
|
|
202
|
+
return n;
|
|
203
|
+
});
|
|
204
|
+
for (const event of log.events) {
|
|
205
|
+
if (event.seq >= fromSeq) {
|
|
206
|
+
Queue.offerUnsafe(queue, event);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const waiter = (event) => {
|
|
210
|
+
if (event.seq >= fromSeq) {
|
|
211
|
+
Queue.offerUnsafe(queue, event);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
log.waiters.push(waiter);
|
|
215
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => {
|
|
216
|
+
const idx = log.waiters.indexOf(waiter);
|
|
217
|
+
if (idx >= 0) {
|
|
218
|
+
log.waiters.splice(idx, 1);
|
|
219
|
+
}
|
|
220
|
+
}));
|
|
221
|
+
})),
|
|
222
|
+
listRuns: () => Effect.gen(function* () {
|
|
223
|
+
const map = yield* Ref.get(logs);
|
|
224
|
+
return [...map.keys()];
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
return service;
|
|
228
|
+
});
|
|
229
|
+
export const MemoryEventStoreLive = Layer.effect(EventStoreTag, makeMemoryEventStore);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { RuntimeEffect } from './effects';
|
|
2
|
+
import type { EventEnvelope } from './envelope';
|
|
3
|
+
import type { InferDefinedSchema, InferSchemaOutput, SchemaInput } from './schema';
|
|
4
|
+
import type { ThreadStatus, JsonValue } from './types';
|
|
5
|
+
export interface StartContext<TInput = JsonValue> {
|
|
6
|
+
readonly runId: string;
|
|
7
|
+
readonly threadId: string;
|
|
8
|
+
readonly parentThreadId: string | null;
|
|
9
|
+
readonly definitionName: string;
|
|
10
|
+
readonly input: TInput;
|
|
11
|
+
}
|
|
12
|
+
export interface ThreadContext {
|
|
13
|
+
readonly runId: string;
|
|
14
|
+
readonly threadId: string;
|
|
15
|
+
readonly parentThreadId: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface ThreadDefinition<S = any> {
|
|
18
|
+
readonly kind: string;
|
|
19
|
+
readonly shape?: unknown;
|
|
20
|
+
readonly input?: SchemaInput;
|
|
21
|
+
initialState(ctx: StartContext): S;
|
|
22
|
+
step(state: S, event: EventEnvelope, ctx: ThreadContext): S;
|
|
23
|
+
effects(state: S, ctx: ThreadContext): RuntimeEffect[];
|
|
24
|
+
}
|
|
25
|
+
export declare function defineThread<TShape, TInputSchema extends SchemaInput | undefined = undefined>(def: {
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly shape: TShape;
|
|
28
|
+
readonly input?: TInputSchema;
|
|
29
|
+
initialState(ctx: StartContext<InferDefinedSchema<TInputSchema>>): InferSchemaOutput<TShape>;
|
|
30
|
+
step(state: InferSchemaOutput<TShape>, event: EventEnvelope, ctx: ThreadContext): InferSchemaOutput<TShape>;
|
|
31
|
+
effects?(state: InferSchemaOutput<TShape>, ctx: ThreadContext): RuntimeEffect[];
|
|
32
|
+
}): ThreadDefinition<InferSchemaOutput<TShape>>;
|
|
33
|
+
export declare function defineThread<S = JsonValue, TInputSchema extends SchemaInput | undefined = undefined>(def: {
|
|
34
|
+
readonly kind: string;
|
|
35
|
+
readonly shape?: unknown;
|
|
36
|
+
readonly input?: TInputSchema;
|
|
37
|
+
initialState(ctx: StartContext<InferDefinedSchema<TInputSchema>>): S;
|
|
38
|
+
step(state: S, event: EventEnvelope, ctx: ThreadContext): S;
|
|
39
|
+
effects?(state: S, ctx: ThreadContext): RuntimeEffect[];
|
|
40
|
+
}): ThreadDefinition<S>;
|
|
41
|
+
export declare function isTerminalStatus(status: ThreadStatus): boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function defineThread(def) {
|
|
2
|
+
return {
|
|
3
|
+
kind: def.kind,
|
|
4
|
+
shape: def.shape,
|
|
5
|
+
input: def.input,
|
|
6
|
+
initialState: (ctx) => def.initialState(ctx),
|
|
7
|
+
step: (state, event, ctx) => def.step(state, event, ctx),
|
|
8
|
+
effects: def.effects ? (state, ctx) => def.effects(state, ctx) : () => [],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function isTerminalStatus(status) {
|
|
12
|
+
return status === 'completed' || status === 'failed' || status === 'cancelled';
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** JSON-compatible values used in event payloads and effect I/O. */
|
|
2
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
3
|
+
export type JsonArray = JsonValue[] | readonly JsonValue[];
|
|
4
|
+
export type JsonObject = {
|
|
5
|
+
readonly [key: string]: JsonValue;
|
|
6
|
+
};
|
|
7
|
+
export type JsonValue = JsonPrimitive | JsonArray | JsonObject;
|
|
8
|
+
/** Structs that JSON-serialize to event payloads. */
|
|
9
|
+
export interface JsonPayload {
|
|
10
|
+
readonly [key: string]: JsonValue | JsonPayload | readonly (JsonValue | JsonPayload)[] | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Recursively remove keys with `undefined` values from an object,
|
|
14
|
+
* matching wire JSON serialization behavior where undefined properties drop.
|
|
15
|
+
*/
|
|
16
|
+
export declare function cleanUndefined<T>(value: T): T;
|
|
17
|
+
/** Mark a JSON-serializable struct as a log payload, stripping undefined fields. */
|
|
18
|
+
export declare function asJson<T>(payload: T): JsonValue;
|
|
19
|
+
/** Recover a typed struct previously written as JSON. */
|
|
20
|
+
export declare function fromJsonStruct<T>(value: JsonValue): T;
|
|
21
|
+
export declare function isJsonObject(value: unknown): value is {
|
|
22
|
+
[key: string]: JsonValue;
|
|
23
|
+
};
|
|
24
|
+
export declare function isJsonString(value: unknown): value is string;
|
|
25
|
+
export declare function isJsonNumber(value: unknown): value is number;
|
|
26
|
+
export type ThreadStatus = 'running' | 'waiting' | 'completed' | 'failed' | 'cancelled';
|
|
27
|
+
export type RunStatus = 'running' | 'completed' | 'failed' | 'cancelled';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Predicate } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Recursively remove keys with `undefined` values from an object,
|
|
4
|
+
* matching wire JSON serialization behavior where undefined properties drop.
|
|
5
|
+
*/
|
|
6
|
+
export function cleanUndefined(value) {
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
// SAFETY: Mapping recursively over an array preserves array structure.
|
|
9
|
+
return value.map(cleanUndefined);
|
|
10
|
+
}
|
|
11
|
+
if (value === null || !Predicate.isObject(value)) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [k, v] of Object.entries(value)) {
|
|
16
|
+
if (v !== undefined) {
|
|
17
|
+
// SAFETY: cleanUndefined produces JSON-compatible values from JSON-like inputs.
|
|
18
|
+
result[k] = cleanUndefined(v);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// SAFETY: Rebuilding an object without undefined keys preserves its record shape.
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
/** Mark a JSON-serializable struct as a log payload, stripping undefined fields. */
|
|
25
|
+
export function asJson(payload) {
|
|
26
|
+
// SAFETY: cleanUndefined ensures undefined keys drop as they would across the wire.
|
|
27
|
+
return cleanUndefined(payload);
|
|
28
|
+
}
|
|
29
|
+
/** Recover a typed struct previously written as JSON. */
|
|
30
|
+
export function fromJsonStruct(value) {
|
|
31
|
+
// SAFETY: value is a JSON struct previously produced as T.
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function isJsonObject(value) {
|
|
35
|
+
return Predicate.isObject(value) && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
export function isJsonString(value) {
|
|
38
|
+
return Predicate.isString(value);
|
|
39
|
+
}
|
|
40
|
+
export function isJsonNumber(value) {
|
|
41
|
+
return Predicate.isNumber(value) && Number.isFinite(value);
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import type { EventCatalog } from './catalog';
|
|
4
|
+
import type { EventInput } from './envelope';
|
|
5
|
+
declare const InvalidEventError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
6
|
+
readonly _tag: "InvalidEventError";
|
|
7
|
+
} & Readonly<A>;
|
|
8
|
+
export declare class InvalidEventError extends InvalidEventError_base<{
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
12
|
+
}> {
|
|
13
|
+
readonly type: string;
|
|
14
|
+
readonly issues: ReadonlyArray<StandardSchemaV1.Issue>;
|
|
15
|
+
constructor(type: string, text: string, issues?: ReadonlyArray<StandardSchemaV1.Issue>);
|
|
16
|
+
}
|
|
17
|
+
export declare function validateEventInput(catalogs: readonly EventCatalog[], input: EventInput): Effect.Effect<EventInput, InvalidEventError>;
|
|
18
|
+
export declare function validateEventsEffect(catalogs: readonly EventCatalog[], events: readonly EventInput[]): Effect.Effect<EventInput[], InvalidEventError>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Data, Effect } from 'effect';
|
|
2
|
+
import { validateInputEffect } from './schema.js';
|
|
3
|
+
import { cleanUndefined } from './types.js';
|
|
4
|
+
export class InvalidEventError extends Data.TaggedError('InvalidEventError') {
|
|
5
|
+
type;
|
|
6
|
+
issues;
|
|
7
|
+
constructor(type, text, issues = []) {
|
|
8
|
+
super({ type, message: text, issues });
|
|
9
|
+
this.name = 'InvalidEventError';
|
|
10
|
+
this.type = type;
|
|
11
|
+
this.issues = issues;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function findCatalogAndEntry(catalogs, type) {
|
|
15
|
+
for (const catalog of catalogs) {
|
|
16
|
+
const prefix = `${catalog.namespace}.`;
|
|
17
|
+
if (type.startsWith(prefix)) {
|
|
18
|
+
const key = type.slice(prefix.length);
|
|
19
|
+
const entries = catalog.entries;
|
|
20
|
+
const entry = entries[key];
|
|
21
|
+
if (entry !== undefined) {
|
|
22
|
+
return { catalog, key, entry };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function validateEventInput(catalogs, input) {
|
|
29
|
+
const match = findCatalogAndEntry(catalogs, input.type);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return Effect.succeed(input);
|
|
32
|
+
}
|
|
33
|
+
const cleanedPayload = cleanUndefined(input.payload);
|
|
34
|
+
// SAFETY: match.entry is a catalog entry (Schema, StandardSchema, or payload marker).
|
|
35
|
+
return validateInputEffect(match.entry, cleanedPayload).pipe(Effect.map((validatedPayload) => ({
|
|
36
|
+
...input,
|
|
37
|
+
payload: validatedPayload,
|
|
38
|
+
})), Effect.mapError((err) => new InvalidEventError(input.type, `Invalid payload for event "${input.type}": ${err.message}`, err.issues)));
|
|
39
|
+
}
|
|
40
|
+
export function validateEventsEffect(catalogs, events) {
|
|
41
|
+
return Effect.forEach(events, (event) => validateEventInput(catalogs, event));
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ThreadTree } from '@swirls/looms/core';
|
|
3
|
+
import type { DebuggerEvent, EventStreamCatalog, ReplayLoader } from '../contracts';
|
|
4
|
+
export declare function DebuggerSplit<TEvent extends DebuggerEvent>({ runId, runStatus, tree, events, eventCounts, startedAt, selectedThread, onSelectThread, selectedSeq, onSelectSeq, catalog, loadReplayStep, renderInspector, orientation, showTreeHeading, className, treeClassName, streamClassName, }: {
|
|
5
|
+
runId: string;
|
|
6
|
+
runStatus: string;
|
|
7
|
+
tree: ThreadTree;
|
|
8
|
+
events: readonly TEvent[];
|
|
9
|
+
eventCounts: Map<string, number>;
|
|
10
|
+
startedAt?: number;
|
|
11
|
+
selectedThread?: string;
|
|
12
|
+
onSelectThread: (threadId: string | undefined) => void;
|
|
13
|
+
selectedSeq?: number;
|
|
14
|
+
onSelectSeq: (seq: number | undefined) => void;
|
|
15
|
+
catalog?: EventStreamCatalog<TEvent>;
|
|
16
|
+
loadReplayStep?: ReplayLoader;
|
|
17
|
+
renderInspector?: (selected: TEvent) => ReactNode;
|
|
18
|
+
orientation?: 'horizontal' | 'vertical';
|
|
19
|
+
showTreeHeading?: boolean;
|
|
20
|
+
className?: string;
|
|
21
|
+
treeClassName?: string;
|
|
22
|
+
streamClassName?: string;
|
|
23
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from '../lib/cn.js';
|
|
3
|
+
import { EventStream } from './event-stream.js';
|
|
4
|
+
import { RunTree } from './run-tree.js';
|
|
5
|
+
function findThreadName(node, threadId) {
|
|
6
|
+
if (!node || !threadId) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (node.threadId === threadId) {
|
|
10
|
+
return node.definitionName;
|
|
11
|
+
}
|
|
12
|
+
for (const child of node.children) {
|
|
13
|
+
const name = findThreadName(child, threadId);
|
|
14
|
+
if (name) {
|
|
15
|
+
return name;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
export function DebuggerSplit({ runId, runStatus, tree, events, eventCounts, startedAt, selectedThread, onSelectThread, selectedSeq, onSelectSeq, catalog, loadReplayStep, renderInspector, orientation = 'horizontal', showTreeHeading = true, className, treeClassName, streamClassName, }) {
|
|
21
|
+
const vertical = orientation === 'vertical';
|
|
22
|
+
return (_jsxs("div", { className: cn('grid h-full min-h-0', vertical
|
|
23
|
+
? 'grid-rows-[minmax(0,38%)_minmax(0,1fr)]'
|
|
24
|
+
: 'grid-cols-1 min-[36rem]:grid-cols-[11rem_minmax(0,1fr)]', className), children: [_jsx("section", { "aria-label": "Run tree", className: cn('border-border min-h-0 overflow-auto border-b py-2', !vertical && 'min-[36rem]:border-r min-[36rem]:border-b-0', treeClassName), children: _jsx(RunTree, { showHeading: showTreeHeading, runId: runId, runStatus: runStatus, tree: tree, eventCounts: eventCounts, totalEventCount: events.length, selectedThread: selectedThread, onSelectThread: onSelectThread }) }), _jsx("section", { className: cn('min-h-0 overflow-hidden', streamClassName), children: _jsx(EventStream, { events: events, startedAt: startedAt, threadId: selectedThread, threadLabel: findThreadName(tree.root, selectedThread), selectedSeq: selectedSeq, onSelectSeq: onSelectSeq, catalog: catalog, loadReplayStep: loadReplayStep, renderInspector: renderInspector }) })] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { asJson } from '@swirls/looms/core';
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs.js';
|
|
5
|
+
import { JsonView } from './json-view.js';
|
|
6
|
+
export function EventInspector({ event, loadReplayStep, }) {
|
|
7
|
+
const [step, setStep] = useState(null);
|
|
8
|
+
const canReplay = loadReplayStep !== undefined;
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!loadReplayStep) {
|
|
11
|
+
setStep(null);
|
|
12
|
+
return () => undefined;
|
|
13
|
+
}
|
|
14
|
+
let cancelled = false;
|
|
15
|
+
void loadReplayStep({ runId: event.runId, seq: event.seq }).then((next) => {
|
|
16
|
+
if (!cancelled) {
|
|
17
|
+
setStep(next);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return () => {
|
|
21
|
+
cancelled = true;
|
|
22
|
+
};
|
|
23
|
+
}, [event.runId, event.seq, loadReplayStep]);
|
|
24
|
+
return (_jsxs("div", { className: "border-border flex max-h-[45%] min-h-0 shrink-0 flex-col border-t", children: [_jsxs("div", { className: "text-muted-foreground flex items-center gap-2 px-3 py-1.5 text-[11px]", children: [_jsx("span", { className: "font-mono", children: event.seq }), _jsx("span", { className: "text-foreground truncate font-mono", children: event.type })] }), _jsxs(Tabs, { defaultValue: "payload", className: "min-h-0 flex-1 px-3 pb-2", children: [_jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "payload", children: "Payload" }), _jsx(TabsTrigger, { value: "envelope", children: "Envelope" }), canReplay ? _jsx(TabsTrigger, { value: "before", children: "Before" }) : null, canReplay ? _jsx(TabsTrigger, { value: "after", children: "After" }) : null, canReplay ? _jsx(TabsTrigger, { value: "effects", children: "Effects" }) : null] }), _jsx(TabsContent, { value: "payload", className: "overflow-auto", children: _jsx(JsonView, { value: event.payload }) }), _jsx(TabsContent, { value: "envelope", className: "overflow-auto", children: _jsx(JsonView, { value: asJson({
|
|
25
|
+
id: event.id,
|
|
26
|
+
runId: event.runId,
|
|
27
|
+
seq: event.seq,
|
|
28
|
+
ts: event.ts,
|
|
29
|
+
type: event.type,
|
|
30
|
+
threadId: event.threadId,
|
|
31
|
+
parentThreadId: event.parentThreadId ?? null,
|
|
32
|
+
causationId: event.causationId ?? null,
|
|
33
|
+
correlationId: event.correlationId ?? null,
|
|
34
|
+
effectId: event.effectId ?? null,
|
|
35
|
+
origin: event.origin,
|
|
36
|
+
ephemeral: event.ephemeral ?? false,
|
|
37
|
+
}) }) }), canReplay ? (_jsx(TabsContent, { value: "before", className: "overflow-auto", children: step ? (_jsx(JsonView, { value: asJson(step.before) })) : (_jsx("p", { className: "text-muted-foreground text-xs", children: "Loading\u2026" })) })) : null, canReplay ? (_jsx(TabsContent, { value: "after", className: "overflow-auto", children: step ? (_jsx(JsonView, { value: asJson(step.after) })) : (_jsx("p", { className: "text-muted-foreground text-xs", children: "Loading\u2026" })) })) : null, canReplay ? (_jsx(TabsContent, { value: "effects", className: "overflow-auto", children: step ? (_jsx(JsonView, { value: asJson(step.effects) })) : (_jsx("p", { className: "text-muted-foreground text-xs", children: "Loading\u2026" })) })) : null] })] }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { DebuggerEvent, EventStreamCatalog, ReplayLoader } from '../contracts';
|
|
3
|
+
export declare function EventStream<TEvent extends DebuggerEvent>({ events, startedAt, threadId, threadLabel, selectedSeq, onSelectSeq, catalog, loadReplayStep, renderInspector, }: {
|
|
4
|
+
events: readonly TEvent[];
|
|
5
|
+
startedAt?: number;
|
|
6
|
+
threadId?: string;
|
|
7
|
+
threadLabel?: string;
|
|
8
|
+
selectedSeq?: number;
|
|
9
|
+
onSelectSeq: (seq: number | undefined) => void;
|
|
10
|
+
catalog?: EventStreamCatalog<TEvent>;
|
|
11
|
+
loadReplayStep?: ReplayLoader;
|
|
12
|
+
renderInspector?: (selected: TEvent) => ReactNode;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { Check, Search, SlidersHorizontal } from 'lucide-react';
|
|
4
|
+
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
|
5
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { defaultEventCatalog } from '../events/catalog.js';
|
|
7
|
+
import { cn, shortId } from '../lib/cn.js';
|
|
8
|
+
import { getEventMeta } from '../lib/event-meta.js';
|
|
9
|
+
import { useVisibleEvents } from '../lib/visible-events.js';
|
|
10
|
+
import { Checkbox } from '../ui/checkbox.js';
|
|
11
|
+
import { Input } from '../ui/input.js';
|
|
12
|
+
import { ScrollArea } from '../ui/scroll-area.js';
|
|
13
|
+
import { EventInspector } from './event-inspector.js';
|
|
14
|
+
const ROW_ESTIMATE = 28;
|
|
15
|
+
const FOLLOW_THRESHOLD = 24;
|
|
16
|
+
export function EventStream({ events, startedAt, threadId, threadLabel, selectedSeq, onSelectSeq, catalog, loadReplayStep, renderInspector, }) {
|
|
17
|
+
const eventCatalog = catalog ?? defaultEventCatalog;
|
|
18
|
+
const [query, setQuery] = useState('');
|
|
19
|
+
const [follow, setFollow] = useState(true);
|
|
20
|
+
const [families, setFamilies] = useState(() => new Set(eventCatalog.families));
|
|
21
|
+
const parentRef = useRef(null);
|
|
22
|
+
const followLock = useRef(false);
|
|
23
|
+
const selected = useMemo(() => {
|
|
24
|
+
if (selectedSeq === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const idx = selectedSeq - 1;
|
|
28
|
+
if (idx >= 0 && idx < events.length && events[idx]?.seq === selectedSeq) {
|
|
29
|
+
return events[idx];
|
|
30
|
+
}
|
|
31
|
+
return events.find((event) => event.seq === selectedSeq);
|
|
32
|
+
}, [events, selectedSeq]);
|
|
33
|
+
const visible = useVisibleEvents({
|
|
34
|
+
events,
|
|
35
|
+
query,
|
|
36
|
+
families,
|
|
37
|
+
threadId,
|
|
38
|
+
catalog: eventCatalog,
|
|
39
|
+
});
|
|
40
|
+
const virtualizer = useVirtualizer({
|
|
41
|
+
count: visible.length,
|
|
42
|
+
getScrollElement: () => parentRef.current,
|
|
43
|
+
estimateSize: () => ROW_ESTIMATE,
|
|
44
|
+
overscan: 16,
|
|
45
|
+
paddingStart: 4,
|
|
46
|
+
paddingEnd: 4,
|
|
47
|
+
getItemKey: (index) => visible[index]?.id ?? index,
|
|
48
|
+
// Avoid flushSync during ref measurement / follow-scroll under high event rates.
|
|
49
|
+
useFlushSync: false,
|
|
50
|
+
});
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!follow || visible.length === 0) {
|
|
53
|
+
return () => undefined;
|
|
54
|
+
}
|
|
55
|
+
followLock.current = true;
|
|
56
|
+
let timer;
|
|
57
|
+
const rafId = requestAnimationFrame(() => {
|
|
58
|
+
virtualizer.scrollToIndex(visible.length - 1, { align: 'end' });
|
|
59
|
+
timer = setTimeout(() => {
|
|
60
|
+
followLock.current = false;
|
|
61
|
+
}, 50);
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelAnimationFrame(rafId);
|
|
65
|
+
if (timer !== undefined) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
followLock.current = false;
|
|
69
|
+
};
|
|
70
|
+
}, [follow, visible.length, virtualizer]);
|
|
71
|
+
const onScroll = useCallback(() => {
|
|
72
|
+
if (followLock.current) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const el = parentRef.current;
|
|
76
|
+
if (!el) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < FOLLOW_THRESHOLD;
|
|
80
|
+
if (follow && !atBottom) {
|
|
81
|
+
setFollow(false);
|
|
82
|
+
}
|
|
83
|
+
else if (!follow && atBottom) {
|
|
84
|
+
setFollow(true);
|
|
85
|
+
}
|
|
86
|
+
}, [follow]);
|
|
87
|
+
const toggleFamily = useCallback((family) => {
|
|
88
|
+
setFamilies((prev) => {
|
|
89
|
+
const next = new Set(prev);
|
|
90
|
+
if (next.has(family)) {
|
|
91
|
+
next.delete(family);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
next.add(family);
|
|
95
|
+
}
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
}, []);
|
|
99
|
+
const hiddenFamilyCount = eventCatalog.families.length - families.size;
|
|
100
|
+
return (_jsxs("div", { className: "debugger-event-stream flex h-full min-h-0 flex-col", children: [_jsxs("div", { className: "border-border flex min-h-10 items-center gap-1.5 border-b px-2 py-1.5", children: [_jsxs("div", { className: "relative min-w-0 flex-1", children: [_jsx(Search, { "aria-hidden": true, className: "text-muted-foreground pointer-events-none absolute top-1/2 left-2 size-3 -translate-y-1/2" }), _jsx(Input, { value: query, onChange: (event) => setQuery(event.target.value), placeholder: "Search events", "aria-label": "Search events", className: "h-7 w-full pl-7 text-xs" })] }), _jsxs(DropdownMenuPrimitive.Root, { children: [_jsx(DropdownMenuPrimitive.Trigger, { asChild: true, children: _jsxs("button", { type: "button", className: "border-input text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:ring-ring/50 relative inline-flex size-7 shrink-0 items-center justify-center rounded-md border outline-none focus-visible:ring-[3px]", "aria-label": "Filter event families", children: [_jsx(SlidersHorizontal, { className: "size-3.5" }), hiddenFamilyCount > 0 ? (_jsx("span", { className: "bg-foreground text-background absolute -top-1 -right-1 flex size-3.5 items-center justify-center rounded-full font-mono text-[8px]", children: hiddenFamilyCount })) : null] }) }), _jsx(DropdownMenuPrimitive.Portal, { children: _jsxs(DropdownMenuPrimitive.Content, { align: "end", sideOffset: 5, className: "bg-background text-foreground border-border z-50 min-w-36 rounded-md border p-1 shadow-lg", children: [_jsx(DropdownMenuPrimitive.Label, { className: "text-muted-foreground px-2 py-1 text-[10px] font-medium tracking-wide uppercase", children: "Event families" }), eventCatalog.families.map((family) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { checked: families.has(family), onCheckedChange: () => toggleFamily(family), onSelect: (event) => event.preventDefault(), className: "focus:bg-accent relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-7 font-mono text-xs outline-none select-none", children: [_jsx("span", { className: "absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "size-3" }) }) }), family] }, family)))] }) })] }), _jsxs("label", { className: "text-muted-foreground hover:text-foreground flex shrink-0 cursor-pointer items-center gap-1.5 px-1 text-[10px]", children: [_jsx(Checkbox, { checked: follow, onCheckedChange: (checked) => setFollow(checked === true), "aria-label": "Follow newest events" }), _jsx("span", { className: "debugger-follow-label", children: "Follow" })] })] }), threadId ? (_jsxs("div", { className: "border-border text-muted-foreground flex items-center gap-1.5 border-b px-2 py-1 font-mono text-[10px]", children: [_jsx("span", { className: "text-[9px] tracking-wide uppercase", children: "Thread" }), _jsx("span", { className: "text-foreground truncate", title: threadId, children: threadLabel ?? shortId(threadId) })] })) : null, _jsx(ScrollArea, { viewportRef: parentRef, className: "min-h-32 flex-1", onViewportScroll: onScroll, children: _jsx("div", { className: "relative w-full px-1", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map((row) => {
|
|
101
|
+
const event = visible[row.index];
|
|
102
|
+
if (!event) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
return (_jsx("div", { "data-index": row.index, ref: virtualizer.measureElement, className: "absolute top-0 left-0 w-full", style: { transform: `translateY(${row.start}px)` }, children: _jsx(EventRow, { event: event, catalog: eventCatalog, startedAt: startedAt, selected: selectedSeq === event.seq, onSelectSeq: onSelectSeq }) }, event.id));
|
|
106
|
+
}) }) }), selected
|
|
107
|
+
? (renderInspector?.(selected) ?? (_jsx(EventInspector, { event: selected, loadReplayStep: loadReplayStep })))
|
|
108
|
+
: null] }));
|
|
109
|
+
}
|
|
110
|
+
// SAFETY: memoized EventRow preserves generic EventRowProps contract.
|
|
111
|
+
const EventRow = memo(function EventRow({ event, catalog, startedAt, selected, onSelectSeq, }) {
|
|
112
|
+
const meta = getEventMeta(event, catalog);
|
|
113
|
+
const offset = startedAt !== undefined ? Math.max(0, event.ts - startedAt) : undefined;
|
|
114
|
+
const handleClick = useCallback(() => {
|
|
115
|
+
onSelectSeq(selected ? undefined : event.seq);
|
|
116
|
+
}, [onSelectSeq, selected, event.seq]);
|
|
117
|
+
return (_jsxs("button", { type: "button", onClick: handleClick, title: [meta.summary.title, meta.summary.detail].filter(Boolean).join(' — '), className: cn('flex w-full items-start gap-1.5 rounded-sm px-1.5 py-1 text-left font-mono text-[11px]', selected ? 'bg-accent' : 'hover:bg-accent/40', event.ephemeral && 'opacity-50'), children: [_jsx("span", { className: cn('mt-1.5 size-1.5 shrink-0 rounded-full', catalog.familyClass(meta.family)) }), _jsx("span", { className: "text-muted-foreground w-6 shrink-0 text-right tabular-nums", children: event.seq }), _jsx("span", { className: "debugger-event-offset text-muted-foreground w-10 shrink-0 text-right tabular-nums", children: offset !== undefined ? `+${offset}` : '' }), _jsxs("span", { className: "min-w-0 flex-1 truncate whitespace-nowrap", children: [_jsx("span", { className: "text-foreground", children: meta.summary.title }), meta.summary.detail ? (_jsx("span", { className: "text-muted-foreground ml-1", children: meta.summary.detail })) : null] }), event.causationId ? (_jsxs("span", { className: "debugger-event-causation text-muted-foreground shrink-0", children: ["\u2190 ", shortId(event.causationId)] })) : null, event.threadId ? (_jsx("span", { className: "debugger-event-thread text-muted-foreground shrink-0", children: shortId(event.threadId) })) : null] }));
|
|
118
|
+
// SAFETY: memoized EventRow preserves generic EventRowProps contract.
|
|
119
|
+
});
|