@amodalai/runtime 0.2.0 → 0.2.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/dist/src/__fixtures__/README.md +4 -0
- package/dist/src/{agent/user-context-fetcher.test.d.ts → __fixtures__/e2e.test.d.ts} +1 -1
- package/dist/src/__fixtures__/e2e.test.js +211 -0
- package/dist/src/__fixtures__/e2e.test.js.map +1 -0
- package/dist/src/__fixtures__/smoke-agent/automations/delivery-callback-test.json +9 -0
- package/dist/src/__fixtures__/smoke-agent/connections/mock-mcp/spec.json +1 -1
- package/dist/src/__fixtures__/smoke.test.js +715 -29
- package/dist/src/__fixtures__/smoke.test.js.map +1 -1
- package/dist/src/__fixtures__/test-env.d.ts +27 -0
- package/dist/src/__fixtures__/test-env.js +64 -0
- package/dist/src/__fixtures__/test-env.js.map +1 -0
- package/dist/src/__fixtures__/test-helpers.d.ts +30 -0
- package/dist/src/__fixtures__/test-helpers.js +120 -0
- package/dist/src/__fixtures__/test-helpers.js.map +1 -0
- package/dist/src/agent/agent-types.d.ts +22 -0
- package/dist/src/agent/agent-types.js.map +1 -1
- package/dist/src/agent/automation-bridge.d.ts +9 -0
- package/dist/src/agent/automation-bridge.js +26 -0
- package/dist/src/agent/automation-bridge.js.map +1 -1
- package/dist/src/agent/automation-bridge.test.js +63 -0
- package/dist/src/agent/automation-bridge.test.js.map +1 -1
- package/dist/src/agent/local-server.d.ts +0 -7
- package/dist/src/agent/local-server.js +274 -87
- package/dist/src/agent/local-server.js.map +1 -1
- package/dist/src/agent/local-server.test.js +14 -11
- package/dist/src/agent/local-server.test.js.map +1 -1
- package/dist/src/agent/loop-types.d.ts +81 -7
- package/dist/src/agent/loop-types.js +4 -0
- package/dist/src/agent/loop-types.js.map +1 -1
- package/dist/src/agent/loop.js +16 -3
- package/dist/src/agent/loop.js.map +1 -1
- package/dist/src/agent/loop.test.js +572 -10
- package/dist/src/agent/loop.test.js.map +1 -1
- package/dist/src/agent/page-builder.js +20 -17
- package/dist/src/agent/page-builder.js.map +1 -1
- package/dist/src/agent/proactive/delivery-router.d.ts +68 -0
- package/dist/src/agent/proactive/delivery-router.js +337 -0
- package/dist/src/agent/proactive/delivery-router.js.map +1 -0
- package/dist/src/agent/proactive/delivery-router.test.d.ts +6 -0
- package/dist/src/agent/proactive/delivery-router.test.js +455 -0
- package/dist/src/agent/proactive/delivery-router.test.js.map +1 -0
- package/dist/src/agent/proactive/proactive-runner.d.ts +23 -1
- package/dist/src/agent/proactive/proactive-runner.js +42 -10
- package/dist/src/agent/proactive/proactive-runner.js.map +1 -1
- package/dist/src/agent/proactive/proactive-runner.test.js +0 -3
- package/dist/src/agent/proactive/proactive-runner.test.js.map +1 -1
- package/dist/src/agent/routes/admin-chat-abort.test.d.ts +6 -0
- package/dist/src/agent/routes/admin-chat-abort.test.js +206 -0
- package/dist/src/agent/routes/admin-chat-abort.test.js.map +1 -0
- package/dist/src/agent/routes/admin-chat.js +0 -3
- package/dist/src/agent/routes/admin-chat.js.map +1 -1
- package/dist/src/agent/routes/files.js +46 -52
- package/dist/src/agent/routes/files.js.map +1 -1
- package/dist/src/agent/routes/inspect.js +4 -6
- package/dist/src/agent/routes/inspect.js.map +1 -1
- package/dist/src/agent/routes/task.test.js +0 -3
- package/dist/src/agent/routes/task.test.js.map +1 -1
- package/dist/src/agent/snapshot-server.js +37 -3
- package/dist/src/agent/snapshot-server.js.map +1 -1
- package/dist/src/agent/states/compacting.js +5 -3
- package/dist/src/agent/states/compacting.js.map +1 -1
- package/dist/src/agent/states/confirming.js +3 -0
- package/dist/src/agent/states/confirming.js.map +1 -1
- package/dist/src/agent/states/dispatching.js +45 -2
- package/dist/src/agent/states/dispatching.js.map +1 -1
- package/dist/src/agent/states/executing.js +225 -81
- package/dist/src/agent/states/executing.js.map +1 -1
- package/dist/src/agent/states/streaming.js +14 -0
- package/dist/src/agent/states/streaming.js.map +1 -1
- package/dist/src/agent/states/thinking.d.ts +1 -1
- package/dist/src/agent/states/thinking.js +246 -29
- package/dist/src/agent/states/thinking.js.map +1 -1
- package/dist/src/agent/token-estimate.d.ts +20 -6
- package/dist/src/agent/token-estimate.js +24 -3
- package/dist/src/agent/token-estimate.js.map +1 -1
- package/dist/src/agent/token-estimate.test.d.ts +6 -0
- package/dist/src/agent/token-estimate.test.js +44 -0
- package/dist/src/agent/token-estimate.test.js.map +1 -0
- package/dist/src/agent/tool-executor-local.test.js +0 -1
- package/dist/src/agent/tool-executor-local.test.js.map +1 -1
- package/dist/src/agent/tool-harness-template.js +0 -1
- package/dist/src/agent/tool-harness-template.js.map +1 -1
- package/dist/src/api/create-agent.js +1 -5
- package/dist/src/api/create-agent.js.map +1 -1
- package/dist/src/api/types.d.ts +1 -5
- package/dist/src/channels/bootstrap.d.ts +59 -0
- package/dist/src/channels/bootstrap.js +84 -0
- package/dist/src/channels/bootstrap.js.map +1 -0
- package/dist/src/channels/channel-session-mapper.d.ts +42 -0
- package/dist/src/channels/channel-session-mapper.js +91 -0
- package/dist/src/channels/channel-session-mapper.js.map +1 -0
- package/dist/src/channels/dedup-cache.d.ts +17 -0
- package/dist/src/channels/dedup-cache.js +51 -0
- package/dist/src/channels/dedup-cache.js.map +1 -0
- package/dist/src/channels/dedup-cache.test.d.ts +6 -0
- package/dist/src/channels/dedup-cache.test.js +51 -0
- package/dist/src/channels/dedup-cache.test.js.map +1 -0
- package/dist/src/channels/errors.d.ts +28 -0
- package/dist/src/channels/errors.js +38 -0
- package/dist/src/channels/errors.js.map +1 -0
- package/dist/src/channels/in-memory-session-mapper.d.ts +34 -0
- package/dist/src/channels/in-memory-session-mapper.js +50 -0
- package/dist/src/channels/in-memory-session-mapper.js.map +1 -0
- package/dist/src/channels/plugin-loader.d.ts +20 -0
- package/dist/src/channels/plugin-loader.js +136 -0
- package/dist/src/channels/plugin-loader.js.map +1 -0
- package/dist/src/channels/plugin-loader.test.d.ts +6 -0
- package/dist/src/channels/plugin-loader.test.js +113 -0
- package/dist/src/channels/plugin-loader.test.js.map +1 -0
- package/dist/src/channels/routes.d.ts +29 -0
- package/dist/src/channels/routes.js +165 -0
- package/dist/src/channels/routes.js.map +1 -0
- package/dist/src/config.d.ts +0 -2
- package/dist/src/config.js +0 -1
- package/dist/src/config.js.map +1 -1
- package/dist/src/config.test.js +0 -2
- package/dist/src/config.test.js.map +1 -1
- package/dist/src/context/compiler.js +11 -34
- package/dist/src/context/compiler.js.map +1 -1
- package/dist/src/context/compiler.test.js +7 -60
- package/dist/src/context/compiler.test.js.map +1 -1
- package/dist/src/context/types.d.ts +0 -4
- package/dist/src/env-ref.d.ts +13 -0
- package/dist/src/env-ref.js +31 -0
- package/dist/src/env-ref.js.map +1 -0
- package/dist/src/env-ref.test.d.ts +6 -0
- package/dist/src/env-ref.test.js +34 -0
- package/dist/src/env-ref.test.js.map +1 -0
- package/dist/src/errors.d.ts +15 -0
- package/dist/src/errors.js +22 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/errors.test.js +2 -2
- package/dist/src/errors.test.js.map +1 -1
- package/dist/src/events/event-bus.d.ts +54 -0
- package/dist/src/events/event-bus.js +84 -0
- package/dist/src/events/event-bus.js.map +1 -0
- package/dist/src/events/event-bus.test.d.ts +6 -0
- package/dist/src/events/event-bus.test.js +112 -0
- package/dist/src/events/event-bus.test.js.map +1 -0
- package/dist/src/events/events-route.d.ts +36 -0
- package/dist/src/events/events-route.js +80 -0
- package/dist/src/events/events-route.js.map +1 -0
- package/dist/src/events/events-route.test.d.ts +6 -0
- package/dist/src/events/events-route.test.js +134 -0
- package/dist/src/events/events-route.test.js.map +1 -0
- package/dist/src/events/store-event-wrapper.d.ts +19 -0
- package/dist/src/events/store-event-wrapper.js +57 -0
- package/dist/src/events/store-event-wrapper.js.map +1 -0
- package/dist/src/events/store-event-wrapper.test.d.ts +6 -0
- package/dist/src/events/store-event-wrapper.test.js +91 -0
- package/dist/src/events/store-event-wrapper.test.js.map +1 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.js +10 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/middleware/auth.d.ts +0 -2
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/providers/search-provider.d.ts +64 -0
- package/dist/src/providers/search-provider.js +174 -0
- package/dist/src/providers/search-provider.js.map +1 -0
- package/dist/src/providers/types.d.ts +8 -0
- package/dist/src/routes/ai-stream.d.ts +18 -4
- package/dist/src/routes/ai-stream.js +10 -2
- package/dist/src/routes/ai-stream.js.map +1 -1
- package/dist/src/routes/chat-stream.d.ts +9 -1
- package/dist/src/routes/chat-stream.js +3 -1
- package/dist/src/routes/chat-stream.js.map +1 -1
- package/dist/src/routes/chat.d.ts +6 -0
- package/dist/src/routes/chat.js +2 -1
- package/dist/src/routes/chat.js.map +1 -1
- package/dist/src/routes/session-resolver.d.ts +15 -2
- package/dist/src/routes/session-resolver.js +22 -25
- package/dist/src/routes/session-resolver.js.map +1 -1
- package/dist/src/routes/session-resolver.test.js +117 -20
- package/dist/src/routes/session-resolver.test.js.map +1 -1
- package/dist/src/server.d.ts +35 -1
- package/dist/src/server.js +33 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/session/drizzle-session-store.d.ts +57 -0
- package/dist/src/session/drizzle-session-store.js +204 -0
- package/dist/src/session/drizzle-session-store.js.map +1 -0
- package/dist/src/session/manager.d.ts +6 -3
- package/dist/src/session/manager.js +46 -19
- package/dist/src/session/manager.js.map +1 -1
- package/dist/src/session/manager.test.js +12 -18
- package/dist/src/session/manager.test.js.map +1 -1
- package/dist/src/session/pglite-session-store.d.ts +23 -0
- package/dist/src/session/pglite-session-store.js +86 -0
- package/dist/src/session/pglite-session-store.js.map +1 -0
- package/dist/src/session/postgres-session-store.d.ts +44 -0
- package/dist/src/session/postgres-session-store.js +153 -0
- package/dist/src/session/postgres-session-store.js.map +1 -0
- package/dist/src/session/session-builder.d.ts +0 -5
- package/dist/src/session/session-builder.js +22 -6
- package/dist/src/session/session-builder.js.map +1 -1
- package/dist/src/session/session-builder.test.js +3 -8
- package/dist/src/session/session-builder.test.js.map +1 -1
- package/dist/src/session/session-store-selector.d.ts +49 -0
- package/dist/src/session/session-store-selector.js +60 -0
- package/dist/src/session/session-store-selector.js.map +1 -0
- package/dist/src/session/session-store-selector.test.d.ts +6 -0
- package/dist/src/session/session-store-selector.test.js +79 -0
- package/dist/src/session/session-store-selector.test.js.map +1 -0
- package/dist/src/session/store.d.ts +146 -32
- package/dist/src/session/store.js +126 -138
- package/dist/src/session/store.js.map +1 -1
- package/dist/src/session/store.test.js +385 -107
- package/dist/src/session/store.test.js.map +1 -1
- package/dist/src/session/tool-context-factory.d.ts +3 -7
- package/dist/src/session/tool-context-factory.js +1 -3
- package/dist/src/session/tool-context-factory.js.map +1 -1
- package/dist/src/session/tool-context-factory.test.js +1 -6
- package/dist/src/session/tool-context-factory.test.js.map +1 -1
- package/dist/src/session/types.d.ts +13 -10
- package/dist/src/stores/schema.d.ts +111 -34
- package/dist/src/stores/schema.js +21 -4
- package/dist/src/stores/schema.js.map +1 -1
- package/dist/src/tools/admin-file-tools.d.ts +29 -0
- package/dist/src/tools/admin-file-tools.js +527 -13
- package/dist/src/tools/admin-file-tools.js.map +1 -1
- package/dist/src/tools/admin-file-tools.test.js +380 -9
- package/dist/src/tools/admin-file-tools.test.js.map +1 -1
- package/dist/src/tools/custom-tool-adapter.js +0 -1
- package/dist/src/tools/custom-tool-adapter.js.map +1 -1
- package/dist/src/tools/custom-tool-adapter.test.js +0 -2
- package/dist/src/tools/custom-tool-adapter.test.js.map +1 -1
- package/dist/src/tools/dispatch-tool.d.ts +4 -4
- package/dist/src/tools/fetch-url-tool.d.ts +23 -0
- package/dist/src/tools/fetch-url-tool.js +333 -0
- package/dist/src/tools/fetch-url-tool.js.map +1 -0
- package/dist/src/tools/fetch-url-tool.test.d.ts +6 -0
- package/dist/src/tools/fetch-url-tool.test.js +227 -0
- package/dist/src/tools/fetch-url-tool.test.js.map +1 -0
- package/dist/src/tools/mcp-tool-adapter.test.js +0 -2
- package/dist/src/tools/mcp-tool-adapter.test.js.map +1 -1
- package/dist/src/tools/registry.test.js +0 -2
- package/dist/src/tools/registry.test.js.map +1 -1
- package/dist/src/tools/request-tool.test.js +0 -2
- package/dist/src/tools/request-tool.test.js.map +1 -1
- package/dist/src/tools/store-tools.test.js +0 -2
- package/dist/src/tools/store-tools.test.js.map +1 -1
- package/dist/src/tools/types.d.ts +20 -7
- package/dist/src/tools/web-search-tool.d.ts +31 -0
- package/dist/src/tools/web-search-tool.js +170 -0
- package/dist/src/tools/web-search-tool.js.map +1 -0
- package/dist/src/tools/web-search-tool.test.d.ts +6 -0
- package/dist/src/tools/web-search-tool.test.js +152 -0
- package/dist/src/tools/web-search-tool.test.js.map +1 -0
- package/dist/src/tools/web-tools-shared.d.ts +21 -0
- package/dist/src/tools/web-tools-shared.js +32 -0
- package/dist/src/tools/web-tools-shared.js.map +1 -0
- package/dist/src/types.d.ts +20 -4
- package/dist/src/types.js +13 -2
- package/dist/src/types.js.map +1 -1
- package/dist/src/types.test.js +0 -3
- package/dist/src/types.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -3
- package/dist/src/agent/session-store.d.ts +0 -71
- package/dist/src/agent/session-store.js +0 -151
- package/dist/src/agent/session-store.js.map +0 -1
- package/dist/src/agent/user-context-fetcher.d.ts +0 -25
- package/dist/src/agent/user-context-fetcher.js +0 -79
- package/dist/src/agent/user-context-fetcher.js.map +0 -1
- package/dist/src/agent/user-context-fetcher.test.js +0 -121
- package/dist/src/agent/user-context-fetcher.test.js.map +0 -1
- package/dist/src/session/admin-file-tools.d.ts +0 -136
- package/dist/src/session/admin-file-tools.js +0 -240
- package/dist/src/session/admin-file-tools.js.map +0 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Runtime event bus.
|
|
8
|
+
*
|
|
9
|
+
* Centralized pub/sub for server-level state changes (session created,
|
|
10
|
+
* automation triggered, store updated, manifest reloaded). Clients
|
|
11
|
+
* subscribe via the `/api/events` SSE endpoint and use these events to
|
|
12
|
+
* drive live UI updates instead of polling.
|
|
13
|
+
*
|
|
14
|
+
* Each event gets a monotonic sequence number assigned at emit time.
|
|
15
|
+
* A ring buffer of recent events lets reconnecting clients catch up
|
|
16
|
+
* via the `Last-Event-ID` SSE header without missing any state changes.
|
|
17
|
+
*/
|
|
18
|
+
import type { RuntimeEvent, RuntimeEventPayload } from '@amodalai/types';
|
|
19
|
+
export type RuntimeEventListener = (event: RuntimeEvent) => void;
|
|
20
|
+
export interface EventBusOptions {
|
|
21
|
+
/** Max events retained for reconnect-and-resume. Default 200. */
|
|
22
|
+
bufferSize?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Called when a listener throws. The bus never rethrows listener errors
|
|
25
|
+
* (one bad subscriber shouldn't break the broadcast to others), but a
|
|
26
|
+
* throwing listener is a bug worth surfacing. Wire this to a logger.
|
|
27
|
+
*/
|
|
28
|
+
onListenerError?: (err: unknown, event: RuntimeEvent) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare class RuntimeEventBus {
|
|
31
|
+
private listeners;
|
|
32
|
+
private seq;
|
|
33
|
+
private readonly bufferSize;
|
|
34
|
+
private readonly onListenerError;
|
|
35
|
+
private buffer;
|
|
36
|
+
constructor(options?: EventBusOptions);
|
|
37
|
+
/**
|
|
38
|
+
* Emit an event. The bus assigns `seq` and `timestamp`.
|
|
39
|
+
* Returns the fully-populated event.
|
|
40
|
+
*/
|
|
41
|
+
emit(payload: RuntimeEventPayload): RuntimeEvent;
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to events. Returns an unsubscribe function.
|
|
44
|
+
*
|
|
45
|
+
* If `sinceSeq` is provided, buffered events with `seq > sinceSeq`
|
|
46
|
+
* are delivered synchronously before the listener starts receiving
|
|
47
|
+
* live events. Used for reconnect-and-resume.
|
|
48
|
+
*/
|
|
49
|
+
subscribe(listener: RuntimeEventListener, sinceSeq?: number): () => void;
|
|
50
|
+
/** Count of currently subscribed listeners (for observability) */
|
|
51
|
+
get listenerCount(): number;
|
|
52
|
+
/** Current sequence number (for tests + observability) */
|
|
53
|
+
get currentSeq(): number;
|
|
54
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/** Maximum number of events kept in the replay buffer */
|
|
7
|
+
const DEFAULT_BUFFER_SIZE = 200;
|
|
8
|
+
export class RuntimeEventBus {
|
|
9
|
+
listeners = new Set();
|
|
10
|
+
seq = 0;
|
|
11
|
+
bufferSize;
|
|
12
|
+
onListenerError;
|
|
13
|
+
buffer = [];
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE;
|
|
16
|
+
this.onListenerError = options.onListenerError;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Emit an event. The bus assigns `seq` and `timestamp`.
|
|
20
|
+
* Returns the fully-populated event.
|
|
21
|
+
*/
|
|
22
|
+
emit(payload) {
|
|
23
|
+
this.seq += 1;
|
|
24
|
+
// The payload's discriminant `type` field carries through; we only
|
|
25
|
+
// add `seq` and `timestamp`. Shape-preserving by construction.
|
|
26
|
+
const event = {
|
|
27
|
+
...payload,
|
|
28
|
+
seq: this.seq,
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
};
|
|
31
|
+
// Append to ring buffer
|
|
32
|
+
this.buffer.push(event);
|
|
33
|
+
if (this.buffer.length > this.bufferSize) {
|
|
34
|
+
this.buffer.shift();
|
|
35
|
+
}
|
|
36
|
+
// Fan out to listeners. Errors in one listener must not break the
|
|
37
|
+
// broadcast to others — so we catch, surface via `onListenerError`
|
|
38
|
+
// (for observability), and continue. Not a silent swallow: the
|
|
39
|
+
// caller sees every failure via the injected callback.
|
|
40
|
+
for (const listener of this.listeners) {
|
|
41
|
+
try {
|
|
42
|
+
listener(event);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
this.onListenerError?.(err, event);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return event;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Subscribe to events. Returns an unsubscribe function.
|
|
52
|
+
*
|
|
53
|
+
* If `sinceSeq` is provided, buffered events with `seq > sinceSeq`
|
|
54
|
+
* are delivered synchronously before the listener starts receiving
|
|
55
|
+
* live events. Used for reconnect-and-resume.
|
|
56
|
+
*/
|
|
57
|
+
subscribe(listener, sinceSeq) {
|
|
58
|
+
if (sinceSeq !== undefined) {
|
|
59
|
+
for (const event of this.buffer) {
|
|
60
|
+
if (event.seq > sinceSeq) {
|
|
61
|
+
try {
|
|
62
|
+
listener(event);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
this.onListenerError?.(err, event);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.listeners.add(listener);
|
|
71
|
+
return () => {
|
|
72
|
+
this.listeners.delete(listener);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Count of currently subscribed listeners (for observability) */
|
|
76
|
+
get listenerCount() {
|
|
77
|
+
return this.listeners.size;
|
|
78
|
+
}
|
|
79
|
+
/** Current sequence number (for tests + observability) */
|
|
80
|
+
get currentSeq() {
|
|
81
|
+
return this.seq;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=event-bus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bus.js","sourceRoot":"","sources":["../../../src/events/event-bus.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAoBH,yDAAyD;AACzD,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAehC,MAAM,OAAO,eAAe;IAClB,SAAS,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC5C,GAAG,GAAG,CAAC,CAAC;IACC,UAAU,CAAS;IACnB,eAAe,CAAqC;IAC7D,MAAM,GAAmB,EAAE,CAAC;IAEpC,YAAY,UAA2B,EAAE;QACvC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,mBAAmB,CAAC;QAC5D,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IACjD,CAAC;IAED;;;OAGG;IACH,IAAI,CAAC,OAA4B;QAC/B,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;QACd,mEAAmE;QACnE,+DAA+D;QAC/D,MAAM,KAAK,GAAG;YACZ,GAAG,OAAO;YACV,GAAG,EAAE,IAAI,CAAC,GAAG;YACb,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpB,CAAC;QAElB,wBAAwB;QACxB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;QAED,kEAAkE;QAClE,mEAAmE;QACnE,+DAA+D;QAC/D,uDAAuD;QACvD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC;gBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;YAClB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,QAA8B,EAAE,QAAiB;QACzD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChC,IAAI,KAAK,CAAC,GAAG,GAAG,QAAQ,EAAE,CAAC;oBACzB,IAAI,CAAC;wBACH,QAAQ,CAAC,KAAK,CAAC,CAAC;oBAClB,CAAC;oBAAC,OAAO,GAAG,EAAE,CAAC;wBACb,IAAI,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;oBACrC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC,CAAC;IACJ,CAAC;IAED,kEAAkE;IAClE,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IAC7B,CAAC;IAED,0DAA0D;IAC1D,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,GAAG,CAAC;IAClB,CAAC;CACF"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
import { RuntimeEventBus } from './event-bus.js';
|
|
8
|
+
describe('RuntimeEventBus', () => {
|
|
9
|
+
it('assigns monotonic sequence numbers starting at 1', () => {
|
|
10
|
+
const bus = new RuntimeEventBus();
|
|
11
|
+
const a = bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
|
|
12
|
+
const b = bus.emit({ type: 'session_updated', sessionId: 's1', appId: 'local' });
|
|
13
|
+
const c = bus.emit({ type: 'manifest_changed' });
|
|
14
|
+
expect(a.seq).toBe(1);
|
|
15
|
+
expect(b.seq).toBe(2);
|
|
16
|
+
expect(c.seq).toBe(3);
|
|
17
|
+
});
|
|
18
|
+
it('stamps ISO timestamps on every event', () => {
|
|
19
|
+
const bus = new RuntimeEventBus();
|
|
20
|
+
const event = bus.emit({ type: 'manifest_changed' });
|
|
21
|
+
expect(event.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
22
|
+
});
|
|
23
|
+
it('delivers events to all subscribed listeners', () => {
|
|
24
|
+
const bus = new RuntimeEventBus();
|
|
25
|
+
const listener1 = vi.fn();
|
|
26
|
+
const listener2 = vi.fn();
|
|
27
|
+
bus.subscribe(listener1);
|
|
28
|
+
bus.subscribe(listener2);
|
|
29
|
+
bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
|
|
30
|
+
expect(listener1).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(listener2).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
it('unsubscribe stops delivery to that listener', () => {
|
|
34
|
+
const bus = new RuntimeEventBus();
|
|
35
|
+
const listener = vi.fn();
|
|
36
|
+
const unsub = bus.subscribe(listener);
|
|
37
|
+
bus.emit({ type: 'manifest_changed' });
|
|
38
|
+
unsub();
|
|
39
|
+
bus.emit({ type: 'manifest_changed' });
|
|
40
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
it('replays buffered events when sinceSeq is provided', () => {
|
|
43
|
+
const bus = new RuntimeEventBus();
|
|
44
|
+
bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' }); // seq 1
|
|
45
|
+
bus.emit({ type: 'session_created', sessionId: 's2', appId: 'local' }); // seq 2
|
|
46
|
+
bus.emit({ type: 'session_created', sessionId: 's3', appId: 'local' }); // seq 3
|
|
47
|
+
const listener = vi.fn();
|
|
48
|
+
bus.subscribe(listener, 1);
|
|
49
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
50
|
+
expect(listener.mock.calls[0]?.[0].seq).toBe(2);
|
|
51
|
+
expect(listener.mock.calls[1]?.[0].seq).toBe(3);
|
|
52
|
+
});
|
|
53
|
+
it('ring buffer evicts oldest events past capacity', () => {
|
|
54
|
+
const bus = new RuntimeEventBus({ bufferSize: 3 });
|
|
55
|
+
for (let i = 0; i < 5; i++) {
|
|
56
|
+
bus.emit({ type: 'manifest_changed' });
|
|
57
|
+
}
|
|
58
|
+
const listener = vi.fn();
|
|
59
|
+
bus.subscribe(listener, 0);
|
|
60
|
+
expect(listener).toHaveBeenCalledTimes(3);
|
|
61
|
+
expect(listener.mock.calls[0]?.[0].seq).toBe(3);
|
|
62
|
+
expect(listener.mock.calls[2]?.[0].seq).toBe(5);
|
|
63
|
+
});
|
|
64
|
+
it('a throwing listener does not block other listeners', () => {
|
|
65
|
+
const bus = new RuntimeEventBus();
|
|
66
|
+
const bad = vi.fn(() => { throw new Error('oops'); });
|
|
67
|
+
const good = vi.fn();
|
|
68
|
+
bus.subscribe(bad);
|
|
69
|
+
bus.subscribe(good);
|
|
70
|
+
bus.emit({ type: 'manifest_changed' });
|
|
71
|
+
expect(bad).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(good).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
it('surfaces listener errors via onListenerError callback', () => {
|
|
75
|
+
const onListenerError = vi.fn();
|
|
76
|
+
const bus = new RuntimeEventBus({ onListenerError });
|
|
77
|
+
bus.subscribe(() => { throw new Error('oops'); });
|
|
78
|
+
bus.emit({ type: 'manifest_changed' });
|
|
79
|
+
expect(onListenerError).toHaveBeenCalledTimes(1);
|
|
80
|
+
const [err, event] = onListenerError.mock.calls[0] ?? [];
|
|
81
|
+
expect(err).toBeInstanceOf(Error);
|
|
82
|
+
expect(err.message).toBe('oops');
|
|
83
|
+
expect(event.type).toBe('manifest_changed');
|
|
84
|
+
});
|
|
85
|
+
it('listenerCount tracks active subscriptions', () => {
|
|
86
|
+
const bus = new RuntimeEventBus();
|
|
87
|
+
expect(bus.listenerCount).toBe(0);
|
|
88
|
+
const unsub1 = bus.subscribe(() => { });
|
|
89
|
+
const unsub2 = bus.subscribe(() => { });
|
|
90
|
+
expect(bus.listenerCount).toBe(2);
|
|
91
|
+
unsub1();
|
|
92
|
+
expect(bus.listenerCount).toBe(1);
|
|
93
|
+
unsub2();
|
|
94
|
+
expect(bus.listenerCount).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
it('preserves event data fields through emit', () => {
|
|
97
|
+
const bus = new RuntimeEventBus();
|
|
98
|
+
const event = bus.emit({
|
|
99
|
+
type: 'store_updated',
|
|
100
|
+
storeName: 'leads',
|
|
101
|
+
operation: 'put',
|
|
102
|
+
count: 5,
|
|
103
|
+
});
|
|
104
|
+
expect(event.type).toBe('store_updated');
|
|
105
|
+
if (event.type === 'store_updated') {
|
|
106
|
+
expect(event.storeName).toBe('leads');
|
|
107
|
+
expect(event.operation).toBe('put');
|
|
108
|
+
expect(event.count).toBe(5);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=event-bus.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-bus.test.js","sourceRoot":"","sources":["../../../src/events/event-bus.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAC,MAAM,QAAQ,CAAC;AAChD,OAAO,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAE/C,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC;QAC/E,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC;QAC/E,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QAC/C,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QACnD,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,sCAAsC,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1B,MAAM,SAAS,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1B,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACzB,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAEzB,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC;QAErE,MAAM,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAEtC,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QACrC,KAAK,EAAE,CAAC;QACR,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QAErC,MAAM,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC,CAAC,QAAQ;QAC9E,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC,CAAC,QAAQ;QAC9E,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,iBAAiB,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC,CAAC,CAAC,QAAQ;QAE9E,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAE3B,MAAM,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,GAAG,GAAG,IAAI,eAAe,CAAC,EAAC,UAAU,EAAE,CAAC,EAAC,CAAC,CAAC;QACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QACvC,CAAC;QAED,MAAM,QAAQ,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACzB,GAAG,CAAC,SAAS,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAE3B,MAAM,CAAC,QAAQ,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAChD,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,MAAM,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QACrB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAEpB,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QAErC,MAAM,CAAC,GAAG,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACrC,MAAM,CAAC,IAAI,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,eAAe,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,IAAI,eAAe,CAAC,EAAC,eAAe,EAAC,CAAC,CAAC;QACnD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAElD,GAAG,CAAC,IAAI,CAAC,EAAC,IAAI,EAAE,kBAAkB,EAAC,CAAC,CAAC;QAErC,MAAM,CAAC,eAAe,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACzD,MAAM,CAAC,GAAG,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,CAAE,KAAwB,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,EAAE,CAAC;QACT,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,GAAG,GAAG,IAAI,eAAe,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC;YACrB,IAAI,EAAE,eAAe;YACrB,SAAS,EAAE,OAAO;YAClB,SAAS,EAAE,KAAK;YAChB,KAAK,EAAE,CAAC;SACT,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACzC,IAAI,KAAK,CAAC,IAAI,KAAK,eAAe,EAAE,CAAC;YACnC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* SSE route for the runtime event bus.
|
|
8
|
+
*
|
|
9
|
+
* GET /api/events
|
|
10
|
+
*
|
|
11
|
+
* Clients open a single EventSource and receive every server-emitted
|
|
12
|
+
* runtime event (session_created, automation_triggered, store_updated,
|
|
13
|
+
* etc.) without polling.
|
|
14
|
+
*
|
|
15
|
+
* Reconnect-and-resume: clients may send a `Last-Event-ID` header
|
|
16
|
+
* (EventSource does this automatically on reconnect). The bus replays
|
|
17
|
+
* any buffered events with seq > Last-Event-ID before streaming live.
|
|
18
|
+
*
|
|
19
|
+
* Heartbeats: a comment line is sent every 15s so proxies don't close
|
|
20
|
+
* the connection during quiet periods.
|
|
21
|
+
*/
|
|
22
|
+
import { Router } from 'express';
|
|
23
|
+
import type { RuntimeEventBus } from './event-bus.js';
|
|
24
|
+
/** Minimal logger surface — matches the runtime's Logger interface. */
|
|
25
|
+
interface EventsRouterLogger {
|
|
26
|
+
debug(event: string, data?: Record<string, unknown>): void;
|
|
27
|
+
}
|
|
28
|
+
export interface EventsRouterOptions {
|
|
29
|
+
bus: RuntimeEventBus;
|
|
30
|
+
/** Heartbeat interval in ms. Default 15000. */
|
|
31
|
+
heartbeatMs?: number;
|
|
32
|
+
/** Optional logger for connect/disconnect observability */
|
|
33
|
+
logger?: EventsRouterLogger;
|
|
34
|
+
}
|
|
35
|
+
export declare function createEventsRouter(options: EventsRouterOptions): Router;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* SSE route for the runtime event bus.
|
|
8
|
+
*
|
|
9
|
+
* GET /api/events
|
|
10
|
+
*
|
|
11
|
+
* Clients open a single EventSource and receive every server-emitted
|
|
12
|
+
* runtime event (session_created, automation_triggered, store_updated,
|
|
13
|
+
* etc.) without polling.
|
|
14
|
+
*
|
|
15
|
+
* Reconnect-and-resume: clients may send a `Last-Event-ID` header
|
|
16
|
+
* (EventSource does this automatically on reconnect). The bus replays
|
|
17
|
+
* any buffered events with seq > Last-Event-ID before streaming live.
|
|
18
|
+
*
|
|
19
|
+
* Heartbeats: a comment line is sent every 15s so proxies don't close
|
|
20
|
+
* the connection during quiet periods.
|
|
21
|
+
*/
|
|
22
|
+
import { Router } from 'express';
|
|
23
|
+
const HEARTBEAT_DEFAULT_MS = 15_000;
|
|
24
|
+
export function createEventsRouter(options) {
|
|
25
|
+
const router = Router();
|
|
26
|
+
const { bus, logger } = options;
|
|
27
|
+
const heartbeatMs = options.heartbeatMs ?? HEARTBEAT_DEFAULT_MS;
|
|
28
|
+
router.get('/api/events', (req, res) => {
|
|
29
|
+
// Parse optional Last-Event-ID for reconnect-and-resume
|
|
30
|
+
const lastIdHeader = req.header('Last-Event-ID');
|
|
31
|
+
let sinceSeq;
|
|
32
|
+
if (lastIdHeader) {
|
|
33
|
+
const parsed = Number.parseInt(lastIdHeader, 10);
|
|
34
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
35
|
+
sinceSeq = parsed;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
logger?.debug('events_client_connected', {
|
|
39
|
+
sinceSeq,
|
|
40
|
+
listenerCount: bus.listenerCount + 1,
|
|
41
|
+
});
|
|
42
|
+
// SSE response headers
|
|
43
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
44
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
45
|
+
res.setHeader('Connection', 'keep-alive');
|
|
46
|
+
// Disable nginx response buffering for SSE
|
|
47
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
48
|
+
res.flushHeaders();
|
|
49
|
+
// Subscribe to the bus. `subscribe` replays buffered events > sinceSeq
|
|
50
|
+
// synchronously, then hands live events to the listener.
|
|
51
|
+
const unsubscribe = bus.subscribe((event) => {
|
|
52
|
+
// SSE frame: id + event + data. `id:` lets EventSource resume.
|
|
53
|
+
res.write(`id: ${String(event.seq)}\n`);
|
|
54
|
+
res.write(`event: ${event.type}\n`);
|
|
55
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
56
|
+
}, sinceSeq);
|
|
57
|
+
// Heartbeat: SSE comment line every N seconds. Keeps proxies from
|
|
58
|
+
// closing an idle connection and lets the client detect drops.
|
|
59
|
+
const heartbeat = setInterval(() => {
|
|
60
|
+
res.write(':\n\n');
|
|
61
|
+
}, heartbeatMs);
|
|
62
|
+
// Clean up on disconnect
|
|
63
|
+
let cleanedUp = false;
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
if (cleanedUp)
|
|
66
|
+
return;
|
|
67
|
+
cleanedUp = true;
|
|
68
|
+
clearInterval(heartbeat);
|
|
69
|
+
unsubscribe();
|
|
70
|
+
logger?.debug('events_client_disconnected', {
|
|
71
|
+
listenerCount: bus.listenerCount,
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
req.on('close', cleanup);
|
|
75
|
+
req.on('error', cleanup);
|
|
76
|
+
res.on('close', cleanup);
|
|
77
|
+
});
|
|
78
|
+
return router;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=events-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events-route.js","sourceRoot":"","sources":["../../../src/events/events-route.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAC,MAAM,EAAC,MAAM,SAAS,CAAC;AAiB/B,MAAM,oBAAoB,GAAG,MAAM,CAAC;AAEpC,MAAM,UAAU,kBAAkB,CAAC,OAA4B;IAC7D,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,MAAM,EAAC,GAAG,EAAE,MAAM,EAAC,GAAG,OAAO,CAAC;IAC9B,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,oBAAoB,CAAC;IAEhE,MAAM,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;QACxD,wDAAwD;QACxD,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;QACjD,IAAI,QAA4B,CAAC;QACjC,IAAI,YAAY,EAAE,CAAC;YACjB,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;YACjD,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAC3C,QAAQ,GAAG,MAAM,CAAC;YACpB,CAAC;QACH,CAAC;QAED,MAAM,EAAE,KAAK,CAAC,yBAAyB,EAAE;YACvC,QAAQ;YACR,aAAa,EAAE,GAAG,CAAC,aAAa,GAAG,CAAC;SACrC,CAAC,CAAC;QAEH,uBAAuB;QACvB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;QACnD,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,wBAAwB,CAAC,CAAC;QACzD,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QAC1C,2CAA2C;QAC3C,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,IAAI,CAAC,CAAC;QACzC,GAAG,CAAC,YAAY,EAAE,CAAC;QAEnB,uEAAuE;QACvE,yDAAyD;QACzD,MAAM,WAAW,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC1C,+DAA+D;YAC/D,GAAG,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACxC,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;YACpC,GAAG,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAClD,CAAC,EAAE,QAAQ,CAAC,CAAC;QAEb,kEAAkE;QAClE,+DAA+D;QAC/D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE;YACjC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC,EAAE,WAAW,CAAC,CAAC;QAEhB,yBAAyB;QACzB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,MAAM,OAAO,GAAG,GAAS,EAAE;YACzB,IAAI,SAAS;gBAAE,OAAO;YACtB,SAAS,GAAG,IAAI,CAAC;YACjB,aAAa,CAAC,SAAS,CAAC,CAAC;YACzB,WAAW,EAAE,CAAC;YACd,MAAM,EAAE,KAAK,CAAC,4BAA4B,EAAE;gBAC1C,aAAa,EAAE,GAAG,CAAC,aAAa;aACjC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACzB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import { RuntimeEventBus } from './event-bus.js';
|
|
9
|
+
import { createEventsRouter } from './events-route.js';
|
|
10
|
+
function startServer(bus) {
|
|
11
|
+
const app = express();
|
|
12
|
+
app.use(createEventsRouter({ bus, heartbeatMs: 60_000 }));
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const server = app.listen(0, () => {
|
|
15
|
+
const address = server.address();
|
|
16
|
+
const port = address && typeof address === 'object' ? address.port : 0;
|
|
17
|
+
resolve({ port, server, bus });
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function closeServer(server) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
server.close(() => resolve());
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function collectEvents(url, expected, opts = {}) {
|
|
27
|
+
const headers = { Accept: 'text/event-stream' };
|
|
28
|
+
if (opts.lastEventId)
|
|
29
|
+
headers['Last-Event-ID'] = opts.lastEventId;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? 2000);
|
|
32
|
+
const res = await fetch(url, { headers, signal: controller.signal });
|
|
33
|
+
if (!res.body)
|
|
34
|
+
throw new Error('no response body');
|
|
35
|
+
const reader = res.body.getReader();
|
|
36
|
+
const decoder = new TextDecoder();
|
|
37
|
+
const collected = [];
|
|
38
|
+
let buffer = '';
|
|
39
|
+
try {
|
|
40
|
+
while (collected.length < expected) {
|
|
41
|
+
const { done, value } = await reader.read();
|
|
42
|
+
if (done)
|
|
43
|
+
break;
|
|
44
|
+
buffer += decoder.decode(value, { stream: true });
|
|
45
|
+
let idx;
|
|
46
|
+
while ((idx = buffer.indexOf('\n\n')) !== -1) {
|
|
47
|
+
const frame = buffer.slice(0, idx);
|
|
48
|
+
buffer = buffer.slice(idx + 2);
|
|
49
|
+
const dataLine = frame.split('\n').find((l) => l.startsWith('data:'));
|
|
50
|
+
if (dataLine) {
|
|
51
|
+
const json = dataLine.slice(5).trim();
|
|
52
|
+
collected.push(JSON.parse(json));
|
|
53
|
+
if (collected.length >= expected)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
controller.abort();
|
|
62
|
+
reader.cancel().catch(() => { });
|
|
63
|
+
}
|
|
64
|
+
return collected;
|
|
65
|
+
}
|
|
66
|
+
describe('events SSE route', () => {
|
|
67
|
+
let harness = null;
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
if (harness) {
|
|
70
|
+
await closeServer(harness.server);
|
|
71
|
+
harness = null;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
it('streams emitted events to connected clients', async () => {
|
|
75
|
+
const bus = new RuntimeEventBus();
|
|
76
|
+
harness = await startServer(bus);
|
|
77
|
+
const url = `http://127.0.0.1:${String(harness.port)}/api/events`;
|
|
78
|
+
const eventsPromise = collectEvents(url, 2, { timeoutMs: 2000 });
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
|
|
81
|
+
bus.emit({ type: 'manifest_changed' });
|
|
82
|
+
}, 50);
|
|
83
|
+
const events = await eventsPromise;
|
|
84
|
+
expect(events).toHaveLength(2);
|
|
85
|
+
expect(events[0]?.['type']).toBe('session_created');
|
|
86
|
+
expect(events[0]?.['seq']).toBe(1);
|
|
87
|
+
expect(events[1]?.['type']).toBe('manifest_changed');
|
|
88
|
+
expect(events[1]?.['seq']).toBe(2);
|
|
89
|
+
});
|
|
90
|
+
it('replays buffered events matching Last-Event-ID', async () => {
|
|
91
|
+
const bus = new RuntimeEventBus();
|
|
92
|
+
harness = await startServer(bus);
|
|
93
|
+
const url = `http://127.0.0.1:${String(harness.port)}/api/events`;
|
|
94
|
+
bus.emit({ type: 'session_created', sessionId: 's1', appId: 'local' });
|
|
95
|
+
bus.emit({ type: 'session_created', sessionId: 's2', appId: 'local' });
|
|
96
|
+
bus.emit({ type: 'session_created', sessionId: 's3', appId: 'local' });
|
|
97
|
+
const events = await collectEvents(url, 2, { lastEventId: '1', timeoutMs: 2000 });
|
|
98
|
+
expect(events).toHaveLength(2);
|
|
99
|
+
expect(events[0]?.['seq']).toBe(2);
|
|
100
|
+
expect(events[1]?.['seq']).toBe(3);
|
|
101
|
+
});
|
|
102
|
+
it('includes id and event headers in SSE frames', async () => {
|
|
103
|
+
const bus = new RuntimeEventBus();
|
|
104
|
+
harness = await startServer(bus);
|
|
105
|
+
const url = `http://127.0.0.1:${String(harness.port)}/api/events`;
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
108
|
+
const res = await fetch(url, {
|
|
109
|
+
headers: { Accept: 'text/event-stream' },
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
});
|
|
112
|
+
if (!res.body)
|
|
113
|
+
throw new Error('no response body');
|
|
114
|
+
const reader = res.body.getReader();
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
bus.emit({ type: 'manifest_changed' });
|
|
118
|
+
}, 50);
|
|
119
|
+
let raw = '';
|
|
120
|
+
while (!raw.includes('\n\n')) {
|
|
121
|
+
const { value, done } = await reader.read();
|
|
122
|
+
if (done)
|
|
123
|
+
break;
|
|
124
|
+
raw += decoder.decode(value, { stream: true });
|
|
125
|
+
}
|
|
126
|
+
clearTimeout(timeout);
|
|
127
|
+
controller.abort();
|
|
128
|
+
reader.cancel().catch(() => { });
|
|
129
|
+
expect(raw).toContain('id: 1');
|
|
130
|
+
expect(raw).toContain('event: manifest_changed');
|
|
131
|
+
expect(raw).toContain('data: {"type":"manifest_changed"');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
//# sourceMappingURL=events-route.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events-route.test.js","sourceRoot":"","sources":["../../../src/events/events-route.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Wraps a `StoreBackend` so every successful write emits a `store_updated`
|
|
8
|
+
* event to the runtime event bus. Read operations pass through unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Instrumenting at the backend level (rather than each call site: tools,
|
|
11
|
+
* REST routes, admin file tools, task execution) means we cover every
|
|
12
|
+
* write path through one seam.
|
|
13
|
+
*/
|
|
14
|
+
import type { StoreBackend, RuntimeEventPayload } from '@amodalai/types';
|
|
15
|
+
interface StoreEventSink {
|
|
16
|
+
emit: (payload: RuntimeEventPayload) => unknown;
|
|
17
|
+
}
|
|
18
|
+
export declare function wrapStoreBackendWithEvents(inner: StoreBackend, bus: StoreEventSink): StoreBackend;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Amodal Labs, Inc.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
export function wrapStoreBackendWithEvents(inner, bus) {
|
|
7
|
+
return {
|
|
8
|
+
initialize(stores) {
|
|
9
|
+
return inner.initialize(stores);
|
|
10
|
+
},
|
|
11
|
+
get(appId, store, key) {
|
|
12
|
+
return inner.get(appId, store, key);
|
|
13
|
+
},
|
|
14
|
+
list(appId, store, opts) {
|
|
15
|
+
return inner.list(appId, store, opts);
|
|
16
|
+
},
|
|
17
|
+
history(appId, store, key) {
|
|
18
|
+
return inner.history(appId, store, key);
|
|
19
|
+
},
|
|
20
|
+
async put(appId, store, key, payload, meta) {
|
|
21
|
+
const result = await inner.put(appId, store, key, payload, meta);
|
|
22
|
+
bus.emit({
|
|
23
|
+
type: 'store_updated',
|
|
24
|
+
storeName: store,
|
|
25
|
+
operation: 'put',
|
|
26
|
+
});
|
|
27
|
+
return result;
|
|
28
|
+
},
|
|
29
|
+
async delete(appId, store, key) {
|
|
30
|
+
const result = await inner.delete(appId, store, key);
|
|
31
|
+
if (result) {
|
|
32
|
+
bus.emit({
|
|
33
|
+
type: 'store_updated',
|
|
34
|
+
storeName: store,
|
|
35
|
+
operation: 'delete',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
},
|
|
40
|
+
async purgeExpired(appId, store) {
|
|
41
|
+
const count = await inner.purgeExpired(appId, store);
|
|
42
|
+
if (count > 0) {
|
|
43
|
+
bus.emit({
|
|
44
|
+
type: 'store_updated',
|
|
45
|
+
storeName: store ?? '*',
|
|
46
|
+
operation: 'delete',
|
|
47
|
+
count,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return count;
|
|
51
|
+
},
|
|
52
|
+
close() {
|
|
53
|
+
return inner.close();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=store-event-wrapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-event-wrapper.js","sourceRoot":"","sources":["../../../src/events/store-event-wrapper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0BH,MAAM,UAAU,0BAA0B,CACxC,KAAmB,EACnB,GAAmB;IAEnB,OAAO;QACL,UAAU,CAAC,MAAqB;YAC9B,OAAO,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAClC,CAAC;QAED,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW;YAC3C,OAAO,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QACtC,CAAC;QAED,IAAI,CAAC,KAAa,EAAE,KAAa,EAAE,IAAuB;YACxD,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QACxC,CAAC;QAED,OAAO,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW;YAC/C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QAC1C,CAAC;QAED,KAAK,CAAC,GAAG,CACP,KAAa,EACb,KAAa,EACb,GAAW,EACX,OAAgC,EAChC,IAAgC;YAEhC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC;YACjE,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,eAAe;gBACrB,SAAS,EAAE,KAAK;gBAChB,SAAS,EAAE,KAAK;aACjB,CAAC,CAAC;YACH,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW;YACpD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;YACrD,IAAI,MAAM,EAAE,CAAC;gBACX,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,SAAS,EAAE,KAAK;oBAChB,SAAS,EAAE,QAAQ;iBACpB,CAAC,CAAC;YACL,CAAC;YACD,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,KAAK,CAAC,YAAY,CAAC,KAAa,EAAE,KAAc;YAC9C,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;YACrD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACd,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,eAAe;oBACrB,SAAS,EAAE,KAAK,IAAI,GAAG;oBACvB,SAAS,EAAE,QAAQ;oBACnB,KAAK;iBACN,CAAC,CAAC;YACL,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,KAAK;YACH,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC"}
|