yaml-flow 8.6.3 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/adapters/firebase-storage.js +3 -0
- package/browser/adapters/firestore-storage.js +3 -0
- package/browser/adapters/localstorage-storage.js +4 -0
- package/browser/asset-integrity.json +20 -4
- package/browser/server-runtime-controlface.js +8 -0
- package/examples/ARCHITECTURE.md +5 -32
- package/examples/board/demo-shell-with-server.html +2 -2
- package/examples/board/doc.html +2 -2
- package/examples/board/server/board-server.js +4 -2
- package/examples/board/server/chat-flow/flow-steps.json +10 -5
- package/examples/board/test/server-http-test.js +93 -0
- package/examples/board-firestore/README.md +81 -0
- package/examples/board-firestore/browser/board-runtime.js +263 -0
- package/examples/board-firestore/firestore.indexes.json +29 -0
- package/examples/board-firestore/package.json +14 -0
- package/examples/board-firestore/server/adapters/firestore-archive-factory.js +59 -0
- package/examples/board-firestore/server/adapters/firestore-blob-storage.js +82 -0
- package/examples/board-firestore/server/adapters/firestore-board-adapter.js +127 -0
- package/examples/board-firestore/server/adapters/firestore-journal-storage.js +54 -0
- package/examples/board-firestore/server/adapters/firestore-kv-storage.js +47 -0
- package/examples/board-firestore/server/adapters/firestore-lock.js +62 -0
- package/examples/board-firestore/server/adapters/firestore-queue-storage.js +186 -0
- package/examples/board-firestore/server/adapters/firestore-scratch-storage.js +50 -0
- package/examples/board-firestore/server/worker.js +146 -0
- package/lib/{artifacts-store-lib-BR-Samty.d.cts → artifacts-store-lib-D9nMkVcE.d.cts} +1 -1
- package/lib/{artifacts-store-lib-DT7XlWUL.d.ts → artifacts-store-lib-DSSMqVL2.d.ts} +1 -1
- package/lib/artifacts-store-public.d.cts +2 -2
- package/lib/artifacts-store-public.d.ts +2 -2
- package/lib/board-live-cards-mcp.cjs +1 -1
- package/lib/board-live-cards-mcp.d.cts +51 -3
- package/lib/board-live-cards-mcp.d.ts +51 -3
- package/lib/board-live-cards-mcp.js +1 -1
- package/lib/board-live-cards-node.cjs +8 -8
- package/lib/board-live-cards-node.d.cts +13 -9
- package/lib/board-live-cards-node.d.ts +13 -9
- package/lib/board-live-cards-node.js +8 -8
- package/lib/{board-live-cards-public-BMUIPOrc.d.ts → board-live-cards-public-D-DJek3X.d.ts} +1 -1
- package/lib/{board-live-cards-public-wkNmBIRC.d.cts → board-live-cards-public-DQzPe7A9.d.cts} +1 -1
- package/lib/board-live-cards-public-async-3hUuHxDx.d.ts +55 -0
- package/lib/board-live-cards-public-async-CgMCYYft.d.cts +55 -0
- package/lib/board-live-cards-public.d.cts +1 -1
- package/lib/board-live-cards-public.d.ts +1 -1
- package/lib/board-live-cards-server-runtime.cjs +1 -1
- package/lib/board-live-cards-server-runtime.d.cts +10 -6
- package/lib/board-live-cards-server-runtime.d.ts +10 -6
- package/lib/board-live-cards-server-runtime.js +1 -1
- package/lib/board-platform-adapter-async-DOfEq_HC.d.cts +129 -0
- package/lib/board-platform-adapter-async-JZPCzZnH.d.ts +129 -0
- package/lib/board-worker-adapter.cjs +3 -3
- package/lib/board-worker-adapter.js +3 -3
- package/lib/card-store-public.d.cts +1 -1
- package/lib/card-store-public.d.ts +1 -1
- package/lib/{chat-storage-lib-BIUbE-fM.d.cts → chat-storage-lib-B9Q34Dyv.d.cts} +1 -1
- package/lib/{chat-storage-lib-BlG-sobS.d.ts → chat-storage-lib-DB9iSai2.d.ts} +1 -1
- package/lib/chat-store-public.d.cts +2 -2
- package/lib/chat-store-public.d.ts +2 -2
- package/lib/chunk-272IYUKT.cjs +2 -0
- package/lib/chunk-5XHOHTLZ.cjs +2 -0
- package/lib/chunk-6APH25VI.js +2 -0
- package/lib/chunk-7FGPOGRV.cjs +2 -0
- package/lib/chunk-7ICPAABP.cjs +7 -0
- package/lib/chunk-CPAXTVBQ.cjs +2 -0
- package/lib/chunk-DDYSXP2A.cjs +3 -0
- package/lib/chunk-EGRHWZRV.js +2 -0
- package/lib/chunk-GL2OHR2E.cjs +2 -0
- package/lib/chunk-IPLSRN6P.cjs +4 -0
- package/lib/chunk-J5J6BG7B.js +2 -0
- package/lib/chunk-KAWQPLIE.cjs +2 -0
- package/lib/chunk-LPXVVMQT.cjs +2 -0
- package/lib/chunk-M3OU6IS5.cjs +2 -0
- package/lib/chunk-M6STQR5F.cjs +2 -0
- package/lib/chunk-NJJ7WEDT.cjs +2 -0
- package/lib/chunk-NKIQRCOM.cjs +2 -0
- package/lib/chunk-NM6O35RY.cjs +2 -0
- package/lib/chunk-NTICU4OK.js +2 -0
- package/lib/chunk-O7NOHKVR.js +2 -0
- package/lib/chunk-PRHQBGPT.js +2 -0
- package/lib/chunk-S44QZUDX.js +2 -0
- package/lib/chunk-SGV7PU4H.js +2 -0
- package/lib/chunk-TSN3RTXT.js +4 -0
- package/lib/chunk-VXJHBWK3.js +2 -0
- package/lib/chunk-WHDEBJLT.js +7 -0
- package/lib/chunk-XYN5D3GL.js +2 -0
- package/lib/chunk-YGALANRO.js +2 -0
- package/lib/chunk-ZJ5M5COT.js +2 -0
- package/lib/chunk-ZXQR7GHT.js +3 -0
- package/lib/cloud-storage.cjs +1 -1
- package/lib/cloud-storage.d.cts +5 -3
- package/lib/cloud-storage.d.ts +5 -3
- package/lib/cloud-storage.js +1 -1
- package/lib/firebase-storage/index.cjs +3 -0
- package/lib/firebase-storage/index.d.cts +57 -0
- package/lib/firebase-storage/index.d.ts +57 -0
- package/lib/firebase-storage/index.js +3 -0
- package/lib/firestore-storage/index.cjs +3 -0
- package/lib/firestore-storage/index.d.cts +98 -0
- package/lib/firestore-storage/index.d.ts +98 -0
- package/lib/firestore-storage/index.js +3 -0
- package/lib/localstorage-storage/index.cjs +2 -0
- package/lib/localstorage-storage/index.d.cts +39 -0
- package/lib/localstorage-storage/index.d.ts +39 -0
- package/lib/localstorage-storage/index.js +2 -0
- package/lib/mcp-tool-registries-BBObLYga.d.ts +41 -0
- package/lib/mcp-tool-registries-W3TRj6O5.d.cts +41 -0
- package/lib/queue-lane-registry-PaZuFpwp.d.cts +30 -0
- package/lib/queue-lane-registry-PaZuFpwp.d.ts +30 -0
- package/lib/server-jobs-queue-runner/index.cjs +2 -0
- package/lib/server-jobs-queue-runner/index.d.cts +22 -0
- package/lib/server-jobs-queue-runner/index.d.ts +22 -0
- package/lib/server-jobs-queue-runner/index.js +2 -0
- package/lib/server-runtime/index.cjs +1 -1
- package/lib/server-runtime/index.d.cts +11 -17
- package/lib/server-runtime/index.d.ts +11 -17
- package/lib/server-runtime/index.js +1 -1
- package/lib/server-runtime-agentface/index.cjs +2 -0
- package/lib/server-runtime-agentface/index.d.cts +53 -0
- package/lib/server-runtime-agentface/index.d.ts +53 -0
- package/lib/server-runtime-agentface/index.js +2 -0
- package/lib/server-runtime-controlface/index.cjs +2 -0
- package/lib/server-runtime-controlface/index.d.cts +80 -0
- package/lib/server-runtime-controlface/index.d.ts +80 -0
- package/lib/server-runtime-controlface/index.js +2 -0
- package/lib/server-runtime-core/index.cjs +2 -0
- package/lib/server-runtime-core/index.d.cts +376 -0
- package/lib/server-runtime-core/index.d.ts +376 -0
- package/lib/server-runtime-core/index.js +2 -0
- package/lib/server-runtime-watchers/index.cjs +2 -0
- package/lib/server-runtime-watchers/index.d.cts +127 -0
- package/lib/server-runtime-watchers/index.d.ts +127 -0
- package/lib/server-runtime-watchers/index.js +2 -0
- package/lib/server-runtime-webhooks/index.cjs +2 -0
- package/lib/server-runtime-webhooks/index.d.cts +34 -0
- package/lib/server-runtime-webhooks/index.d.ts +34 -0
- package/lib/server-runtime-webhooks/index.js +2 -0
- package/lib/storage-async-interface-BRR4eBjx.d.cts +81 -0
- package/lib/storage-async-interface-DhlOVPSp.d.ts +81 -0
- package/lib/{queue-lane-registry-BPKWWgd4.d.cts → types-BzQY45dH.d.cts} +8 -34
- package/lib/{queue-lane-registry-Be6c0ftj.d.ts → types-CF2xUcZW.d.ts} +8 -34
- package/package.json +46 -2
- package/examples/board-local/demo-shell-localstorage.html +0 -843
- package/lib/board-live-cards-public-async-DKZqbJVU.d.ts +0 -256
- package/lib/board-live-cards-public-async-dMWNbWq6.d.cts +0 -256
- package/lib/chunk-GJJMEAVN.cjs +0 -2
- package/lib/chunk-GLIX37VG.cjs +0 -8
- package/lib/chunk-LRVAVWAG.js +0 -8
- package/lib/chunk-MLVTJASJ.js +0 -2
- package/lib/chunk-SCWHDI3I.js +0 -2
- package/lib/chunk-WOALA3V5.cjs +0 -2
package/examples/ARCHITECTURE.md
CHANGED
|
@@ -72,39 +72,13 @@ Include them with a `<script>` tag. Each sets a global on `window`.
|
|
|
72
72
|
|
|
73
73
|
### compute-jsonata.js → `window.jsonataSync`
|
|
74
74
|
|
|
75
|
-
Vendored jsonata engine for in-browser card computation.
|
|
76
|
-
**Must be loaded before** `board-livecards-localstorage.js`.
|
|
75
|
+
Vendored jsonata engine for browser bundles that need in-browser card computation.
|
|
77
76
|
|
|
78
77
|
```html
|
|
79
78
|
<script src="compute-jsonata.js"></script>
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
No public API — just sets `window.jsonataSync` for
|
|
83
|
-
|
|
84
|
-
---
|
|
85
|
-
|
|
86
|
-
### board-livecards-localstorage.js → `window.BoardLiveCardsLocalStorage`
|
|
87
|
-
|
|
88
|
-
**Drop-in full board engine for the browser** — no server required.
|
|
89
|
-
|
|
90
|
-
Bundles: board engine + card computation + localStorage adapter.
|
|
91
|
-
Ideal for demos, offline use, or prototyping before wiring up a server.
|
|
92
|
-
|
|
93
|
-
```html
|
|
94
|
-
<script src="compute-jsonata.js"></script>
|
|
95
|
-
<script src="board-livecards-localstorage.js"></script>
|
|
96
|
-
<script>
|
|
97
|
-
const app = BoardLiveCardsLocalStorage.create('my-board', {
|
|
98
|
-
cards: [ /* card JSON objects */ ],
|
|
99
|
-
taskExecutor: async (ref, args) => { /* your async task handler */ },
|
|
100
|
-
});
|
|
101
|
-
await app.bootstrap();
|
|
102
|
-
const state = app.getState();
|
|
103
|
-
// → feed to live-cards.js via LiveCard.init(...)
|
|
104
|
-
</script>
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Key exports: `create()`, `selectLiveCardModel()`, `selectAllLiveCardModels()`
|
|
81
|
+
No public API — just sets `window.jsonataSync` for other bundles to pick up.
|
|
108
82
|
|
|
109
83
|
---
|
|
110
84
|
|
|
@@ -181,8 +155,7 @@ charts fall back to tables and markdown renders as escaped plain text.
|
|
|
181
155
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
182
156
|
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
183
157
|
|
|
184
|
-
<!-- 2. yaml-flow bundles
|
|
185
|
-
<script src="
|
|
186
|
-
<script src="
|
|
187
|
-
<script src="live-cards.js"></script> <!-- sets window.LiveCard -->
|
|
158
|
+
<!-- 2. yaml-flow bundles -->
|
|
159
|
+
<script src="board-livecards-client.js"></script>
|
|
160
|
+
<script src="live-cards.js"></script>
|
|
188
161
|
```
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
20
20
|
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
21
21
|
<script src="https://cdn.jsdelivr.net/npm/leader-line/leader-line.min.js"></script>
|
|
22
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.
|
|
23
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.7.0/browser/live-cards.js"></script>
|
|
23
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.7.0/browser/board-livecards-client.js"></script>
|
|
24
24
|
</head>
|
|
25
25
|
<body class="bg-light">
|
|
26
26
|
<div class="container-fluid py-3">
|
package/examples/board/doc.html
CHANGED
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
38
38
|
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
|
39
39
|
<script src="https://cdn.jsdelivr.net/npm/leader-line/leader-line.min.js"></script>
|
|
40
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.
|
|
41
|
-
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.
|
|
40
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.7.0/browser/live-cards.js"></script>
|
|
41
|
+
<script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.7.0/browser/board-livecards-client.js"></script>
|
|
42
42
|
</head>
|
|
43
43
|
<body class="bg-light">
|
|
44
44
|
<div class="container-fluid py-3">
|
|
@@ -11,8 +11,10 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
11
11
|
import {
|
|
12
12
|
createMultiBoardServerRuntime,
|
|
13
13
|
createSingleBoardServerRuntime,
|
|
14
|
+
} from 'yaml-flow/server-runtime-controlface';
|
|
15
|
+
import {
|
|
14
16
|
createHostedBoardQueueLaneRegistry,
|
|
15
|
-
} from 'yaml-flow/
|
|
17
|
+
} from 'yaml-flow/server-jobs-queue-runner';
|
|
16
18
|
import {
|
|
17
19
|
createHostedAsyncBoardPlatformAdapter,
|
|
18
20
|
} from 'yaml-flow/cloud-storage';
|
|
@@ -316,7 +318,7 @@ function makeBoardWorkerCallbackTransport(serverUrl, boardApiBasePath, transport
|
|
|
316
318
|
const normalizedServerUrl = typeof serverUrl === 'string' ? serverUrl.trim().replace(/\/+$/, '') : '';
|
|
317
319
|
const normalizedApiBasePath = typeof boardApiBasePath === 'string' ? boardApiBasePath.trim().replace(/\/+$/, '') : '';
|
|
318
320
|
if (!normalizedServerUrl || !normalizedApiBasePath) return undefined;
|
|
319
|
-
return createHttpBoardCallbackTransport(`${normalizedServerUrl}${normalizedApiBasePath}/
|
|
321
|
+
return createHttpBoardCallbackTransport(`${normalizedServerUrl}${normalizedApiBasePath}/mcp-webhooks`);
|
|
320
322
|
}
|
|
321
323
|
|
|
322
324
|
async function readJsonRequest(req) {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"open_turn": {
|
|
41
41
|
"description": "Resolve the current user turn from flow-provided chat messages and parse any chat envelope options",
|
|
42
|
-
"produces_data": ["boardId", "cardId", "boardSetupRoot", "boardBaseRef", "boardRuntimeDir", "runtimeStatusDir", "cardsDir", "projectRoot", "chatFlowRoot", "chatMessages", "userText", "lastChatEntryId", "turnId", "probe", "chatHandlerMode", "chatCopilotTimeoutMs", "probeAttachmentFileIdx", "serverUrl"],
|
|
42
|
+
"produces_data": ["boardId", "cardId", "boardSetupRoot", "boardBaseRef", "boardRuntimeDir", "runtimeStatusDir", "cardsDir", "projectRoot", "chatFlowRoot", "chatMessages", "userText", "lastChatEntryId", "turnId", "probe", "chatHandlerMode", "chatCopilotTimeoutMs", "probeAttachmentFileIdx", "probeGenerateAttachment", "replyFiles", "serverUrl"],
|
|
43
43
|
"input_validations": [
|
|
44
44
|
"$type(cardId) = \"string\" and $length(cardId) > 0",
|
|
45
45
|
"$type(chatMessages) = \"array\"",
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"data._legacyEnvelope = data._hasLegacyEnvelope ? $eval(data._rawMessageText) : null",
|
|
73
73
|
"data._legacyPrompt = ($type(data._legacyEnvelope.prompt) = \"string\" and $length($trim(data._legacyEnvelope.prompt)) > 0) ? data._legacyEnvelope.prompt : (($type(data._legacyEnvelope.text) = \"string\" and $length($trim(data._legacyEnvelope.text)) > 0) ? data._legacyEnvelope.text : (($type(data._legacyEnvelope.userText) = \"string\" and $length($trim(data._legacyEnvelope.userText)) > 0) ? data._legacyEnvelope.userText : (($type(data._legacyEnvelope.query) = \"string\" and $length($trim(data._legacyEnvelope.query)) > 0) ? data._legacyEnvelope.query : null)))",
|
|
74
74
|
"data.userText = data._isEchoProbe ? $trim($substring(data._rawMessageText, data._markerLen, $length(data._rawMessageText) - (data._markerLen * 2))) : (($type(data._legacyPrompt) = \"string\" and $length($trim(data._legacyPrompt)) > 0) ? $trim(data._legacyPrompt) : data._rawMessageText)",
|
|
75
|
+
"data._attachMarker = \"[attach]\"",
|
|
76
|
+
"data.probeGenerateAttachment = $lowercase($substring(data.userText, 0, $length(data._attachMarker))) = data._attachMarker",
|
|
77
|
+
"data.userText = data.probeGenerateAttachment ? $trim($substring(data.userText, $length(data._attachMarker))) : data.userText",
|
|
78
|
+
"data.replyFiles = []",
|
|
75
79
|
"data.probe = data._isEchoProbe ? true : (data._legacyEnvelope.probe = true or data._legacyEnvelope.probe = 1 or ($type(data._legacyEnvelope.probe) = \"string\" and $count($filter([\"1\", \"true\", \"yes\", \"on\"], function($value) { $value = $lowercase($trim(data._legacyEnvelope.probe)) })) > 0))",
|
|
76
80
|
"data.chatHandlerMode = data._isEchoProbe ? \"echo-probe\" : (($type(data._legacyEnvelope.chatHandlerMode) = \"string\" and $length($trim(data._legacyEnvelope.chatHandlerMode)) > 0) ? $lowercase($trim(data._legacyEnvelope.chatHandlerMode)) : (data.probe ? \"probe\" : \"copilot\"))",
|
|
77
81
|
"data.chatCopilotTimeoutMs = ($parsePositiveInt := function($value) { ($type($value) = \"number\" and $value > 0) ? $floor($value) : (($type($value) = \"string\" and $count($match($trim($value), /^[0-9]+$/)) > 0 and $number($trim($value)) > 0) ? $floor($number($trim($value))) : null) }; $legacyTimeout := $parsePositiveInt(data._legacyEnvelope.chatTimeoutMs); $legacyCopilotTimeout := $parsePositiveInt(data._legacyEnvelope.chatCopilotTimeoutMs); $fallbackTimeout := $parsePositiveInt(expects_data.chatCopilotTimeoutMs); $legacyTimeout ? $legacyTimeout : ($legacyCopilotTimeout ? $legacyCopilotTimeout : ($fallbackTimeout ? $fallbackTimeout : 300000)))",
|
|
@@ -208,8 +212,8 @@
|
|
|
208
212
|
},
|
|
209
213
|
"probe_assistant": {
|
|
210
214
|
"description": "Compute the deterministic echo assistant reply for echo-probe and test flows",
|
|
211
|
-
"expects_data": ["userText"],
|
|
212
|
-
"produces_data": ["replyText"],
|
|
215
|
+
"expects_data": ["userText", "probeGenerateAttachment"],
|
|
216
|
+
"produces_data": ["replyText", "replyFiles"],
|
|
213
217
|
"input_validations": [
|
|
214
218
|
"$type(userText) = \"string\""
|
|
215
219
|
],
|
|
@@ -217,6 +221,7 @@
|
|
|
217
221
|
"type": "compute-jsonata",
|
|
218
222
|
"expr": [
|
|
219
223
|
"data.replyText = \"Echo: \" & expects_data.userText",
|
|
224
|
+
"data.replyFiles = expects_data.probeGenerateAttachment ? [{ 'file_name': 'probe-generated.txt', 'content_type': 'text/plain', 'text': data.replyText }] : []",
|
|
220
225
|
"result = $contains($lowercase(expects_data.userText), \"simulate-failure\") ? \"failure\" : \"success\""
|
|
221
226
|
]
|
|
222
227
|
},
|
|
@@ -228,7 +233,7 @@
|
|
|
228
233
|
},
|
|
229
234
|
"write_reply": {
|
|
230
235
|
"description": "Append the assistant reply via MCP stage-ai-response-and-any-attachments",
|
|
231
|
-
"expects_data": ["serverUrl", "boardId", "cardId", "replyText", "turnId"],
|
|
236
|
+
"expects_data": ["serverUrl", "boardId", "cardId", "replyText", "replyFiles", "turnId"],
|
|
232
237
|
"produces_data": ["replyId"],
|
|
233
238
|
"input_validations": [
|
|
234
239
|
"$type(serverUrl) = \"string\" and $length(serverUrl) > 0",
|
|
@@ -246,7 +251,7 @@
|
|
|
246
251
|
"meta": "chat-handler",
|
|
247
252
|
"argsMassaging": {
|
|
248
253
|
"urlTemplate": "serverUrl & '/api/boards/' & boardId & '/mcp'",
|
|
249
|
-
"bodyTemplate": "{ 'tool': 'stage-ai-response-and-any-attachments', 'args': { 'card_id': cardId, 'text': replyText, 'turn_id': turnId } }"
|
|
254
|
+
"bodyTemplate": "{ 'tool': 'stage-ai-response-and-any-attachments', 'args': { 'card_id': cardId, 'text': replyText, 'turn_id': turnId, 'files': ($type(replyFiles) = 'array' ? replyFiles : []) } }"
|
|
250
255
|
},
|
|
251
256
|
"outputTransforms": {
|
|
252
257
|
"dataTemplate": "{ 'replyId': output.data.id }"
|
|
@@ -45,6 +45,7 @@ function isCopilotAvailable() {
|
|
|
45
45
|
|
|
46
46
|
const skipT3a = cliArgs.includes('--skip-t3a') || !isCopilotAvailable();
|
|
47
47
|
const skipT3b = cliArgs.includes('--skip-t3b');
|
|
48
|
+
const skipT3d = cliArgs.includes('--skip-t3d');
|
|
48
49
|
const RUN_ID = `run-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
49
50
|
|
|
50
51
|
const BOARD_ID = 'live';
|
|
@@ -952,6 +953,98 @@ try {
|
|
|
952
953
|
console.log('[T3b] ok: upload protocol and ordered probe lifecycle observed with attachment-derived assistant reply');
|
|
953
954
|
}
|
|
954
955
|
|
|
956
|
+
// ── T3d: probe-echo chat with one AI-generated attachment ──
|
|
957
|
+
if (skipT3d) {
|
|
958
|
+
console.log('\n=== T3d: skipped (--skip-t3d) ===');
|
|
959
|
+
} else {
|
|
960
|
+
console.log('\n=== T3d: probe-echo chat with AI-generated attachment ===');
|
|
961
|
+
|
|
962
|
+
// Ensure chat SSE subscription is active (T3 may have been skipped)
|
|
963
|
+
let t3dOwnedSseClient = false;
|
|
964
|
+
if (!chatSseClient) {
|
|
965
|
+
chatSseClientId = `chat-proto-t3d-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
966
|
+
chatSseClient = startSseClient(`${BASE}/sse?clientId=${encodeURIComponent(chatSseClientId)}`, (payload) => {
|
|
967
|
+
captureChatEvents(payload, CHAT_CARD_ID);
|
|
968
|
+
});
|
|
969
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
970
|
+
const t3dSubRes = await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/chats/subscribe-sse`, { clientId: chatSseClientId });
|
|
971
|
+
assert(t3dSubRes.status === 200, `T3d chat subscribe returned ${t3dSubRes.status}`);
|
|
972
|
+
t3dOwnedSseClient = true;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const t2dBeforeChats = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats?all-turns=true`);
|
|
976
|
+
assert(t2dBeforeChats.status === 200, `T3d pre chats returned ${t2dBeforeChats.status}`);
|
|
977
|
+
const t2dBeforeMessages = Array.isArray(t2dBeforeChats.data?.messages) ? t2dBeforeChats.data.messages : [];
|
|
978
|
+
const t2dBeforeCount = t2dBeforeMessages.length;
|
|
979
|
+
|
|
980
|
+
const t2dBeforeCard = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}`);
|
|
981
|
+
assert(t2dBeforeCard.status === 200, `T3d pre card returned ${t2dBeforeCard.status}`);
|
|
982
|
+
const t2dBeforeFiles = Array.isArray(t2dBeforeCard.data?.card_data?.files)
|
|
983
|
+
? t2dBeforeCard.data.card_data.files
|
|
984
|
+
: [];
|
|
985
|
+
|
|
986
|
+
const t3dTurnId = randomTurnId();
|
|
987
|
+
const t2dPrompt = `probe generated attachment validation ${Date.now()}`;
|
|
988
|
+
const t2dEventStart = NS.chatEvents.length;
|
|
989
|
+
const t2dSendRes = await httpJson('POST', `${BASE}/cards/${CHAT_CARD_ID}/actions`, {
|
|
990
|
+
actionType: 'chat-send',
|
|
991
|
+
payload: {
|
|
992
|
+
text: `${ECHO_PROBE_MARKER}[attach] ${t2dPrompt}${ECHO_PROBE_MARKER}`,
|
|
993
|
+
'turn-id': t3dTurnId,
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
assert(t2dSendRes.status === 200, `T3d chat-send returned ${t2dSendRes.status}`);
|
|
997
|
+
|
|
998
|
+
const t2dLifecycle = await waitForChatPredicate((events) => {
|
|
999
|
+
return matchOrderedProbeLifecycle(events.slice(t2dEventStart), {
|
|
1000
|
+
beforeCount: t2dBeforeCount,
|
|
1001
|
+
beforeProcessing: false,
|
|
1002
|
+
prompt: t2dPrompt,
|
|
1003
|
+
assistantText: `Echo: ${t2dPrompt}`,
|
|
1004
|
+
inProgressText: PROBE_IN_PROGRESS_TEXT,
|
|
1005
|
+
});
|
|
1006
|
+
}, 60_000, 'T3d ordered lifecycle');
|
|
1007
|
+
assert(!!t2dLifecycle, 'T3d ordered lifecycle not observed');
|
|
1008
|
+
|
|
1009
|
+
const t2dAfter = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}/chats?all-turns=true`);
|
|
1010
|
+
assert(t2dAfter.status === 200, `T3d post chats returned ${t2dAfter.status}`);
|
|
1011
|
+
const t2dAfterMessages = Array.isArray(t2dAfter.data?.messages) ? t2dAfter.data.messages : [];
|
|
1012
|
+
const t2dNewMessages = t2dAfterMessages.slice(t2dBeforeCount);
|
|
1013
|
+
assert(t2dNewMessages.length >= 4, `T3d expected at least 4 chat messages after send, got ${t2dNewMessages.length}`);
|
|
1014
|
+
|
|
1015
|
+
const t2dUser = t2dNewMessages.find((m) => m?.role === 'user');
|
|
1016
|
+
const t2dInProgress = t2dNewMessages.find((m) => m?.role === 'system' && String(m?.text || '').trim().toLowerCase() === PROBE_IN_PROGRESS_TEXT);
|
|
1017
|
+
const t2dAiGenerated = t2dNewMessages.find((m) => m?.role === 'system' && /^AI generated:/i.test(String(m?.text || '')));
|
|
1018
|
+
const t2dAssistantMsg = t2dNewMessages.find((m) => m?.role === 'assistant');
|
|
1019
|
+
|
|
1020
|
+
assert(!!t2dUser && typeof t2dUser.id === 'string', 'T3d missing user chat message');
|
|
1021
|
+
assert(!!t2dInProgress && typeof t2dInProgress.id === 'string', 'T3d missing in-progress system chat message');
|
|
1022
|
+
assert(!!t2dAiGenerated && typeof t2dAiGenerated.id === 'string', 'T3d missing AI-generated attachment system chat message');
|
|
1023
|
+
assert(/#\d+\s*$/.test(String(t2dAiGenerated?.text || '')), 'T3d AI-generated system message should include merged file index');
|
|
1024
|
+
assert(String(t2dAiGenerated?.turn || '') === t3dTurnId, 'T3d AI-generated system turn id mismatch');
|
|
1025
|
+
assert(!!t2dAssistantMsg && typeof t2dAssistantMsg.id === 'string', 'T3d missing assistant chat message');
|
|
1026
|
+
assert(String(t2dAssistantMsg?.text || '').includes(`Echo: ${t2dPrompt}`), 'T3d assistant content mismatch');
|
|
1027
|
+
assert(String(t2dAssistantMsg?.turn || '') === t3dTurnId, 'T3d assistant turn id mismatch');
|
|
1028
|
+
|
|
1029
|
+
const t2dFileIndexMatch = /#(\d+)\s*$/.exec(String(t2dAiGenerated?.text || ''));
|
|
1030
|
+
assert(!!t2dFileIndexMatch, 'T3d AI-generated message missing file index');
|
|
1031
|
+
const t2dFileIndex = Number.parseInt(t2dFileIndexMatch[1], 10);
|
|
1032
|
+
assert(Number.isInteger(t2dFileIndex) && t2dFileIndex >= 0, 'T3d AI-generated message file index should be non-negative');
|
|
1033
|
+
|
|
1034
|
+
const t2dAfterCard = await httpGet(`${BASE}/cards/${CHAT_CARD_ID}`);
|
|
1035
|
+
assert(t2dAfterCard.status === 200, `T3d post card returned ${t2dAfterCard.status}`);
|
|
1036
|
+
const t2dAfterFiles = Array.isArray(t2dAfterCard.data?.card_data?.files)
|
|
1037
|
+
? t2dAfterCard.data.card_data.files
|
|
1038
|
+
: [];
|
|
1039
|
+
assert(t2dAfterFiles.length === t2dBeforeFiles.length + 1, `T3d expected exactly one new stored file, got ${t2dAfterFiles.length - t2dBeforeFiles.length}`);
|
|
1040
|
+
const t2dStoredFile = t2dAfterFiles[t2dFileIndex];
|
|
1041
|
+
assert(!!t2dStoredFile, `T3d stored file missing at merged index ${t2dFileIndex}`);
|
|
1042
|
+
assert(t2dStoredFile?.chat === true, 'T3d generated file should be marked as chat-origin');
|
|
1043
|
+
assert(String(t2dStoredFile?.stored_name || '').length > 0, 'T3d generated file stored_name missing');
|
|
1044
|
+
assert(!Object.prototype.hasOwnProperty.call(t2dStoredFile || {}, 'path'), 'T3d stored file metadata should not expose path');
|
|
1045
|
+
console.log('[T3d] ok: probe staged one AI-generated attachment and appended the final reply through the shared flow');
|
|
1046
|
+
}
|
|
1047
|
+
|
|
955
1048
|
if (skipT4) {
|
|
956
1049
|
console.log('\n=== T4: skipped (--skip-t4) ===');
|
|
957
1050
|
} else {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# board-firestore
|
|
2
|
+
|
|
3
|
+
Example of a [yaml-flow](https://www.npmjs.com/package/yaml-flow) board backed by **Cloud Firestore** (Firebase).
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Browser SPA Node.js Worker (server/worker.js)
|
|
9
|
+
────────────── ──────────────────────────────────
|
|
10
|
+
Firebase JS SDK ──> Firestore <── Firebase Admin SDK
|
|
11
|
+
│
|
|
12
|
+
board cards,
|
|
13
|
+
queue messages,
|
|
14
|
+
blob storage
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Worker** — runs queue lanes (board-worker, chat-agent, process-accumulated) and exposes an HTTP control-plane on port 7900 for the browser SPA.
|
|
18
|
+
- **Browser** — uses the `ServerRuntimeControlface` IIFE (`browser/server-runtime-controlface.js`) with a Firestore JS SDK adapter to read card state and trigger board operations.
|
|
19
|
+
|
|
20
|
+
## Firestore data layout
|
|
21
|
+
|
|
22
|
+
All data lives under `boards/{boardId}/`:
|
|
23
|
+
|
|
24
|
+
| Subcollection | Purpose |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `kv-{namespace}/` | `AsyncKVStorage` (card state, config, etc.) |
|
|
27
|
+
| `cards/` | Card store (KV, keyed by card ID) |
|
|
28
|
+
| `runtime-out/` | Computed outputs store |
|
|
29
|
+
| `journal/` | Append-only board journal |
|
|
30
|
+
| `worker-queue/` | Board worker task queue |
|
|
31
|
+
| `chat-queue/` | Chat agent dispatch queue |
|
|
32
|
+
| `process-queue/` | processAccumulated trigger queue |
|
|
33
|
+
| `blobs-{namespace}/` | Blob/artifact storage |
|
|
34
|
+
| `scratch/` | Ephemeral scratch storage |
|
|
35
|
+
| `archive-stream-{name}/` | Named archive streams |
|
|
36
|
+
| `archive-blob-{name}/` | Named archive blob collections |
|
|
37
|
+
| `locks/board-lock` | Distributed lock document |
|
|
38
|
+
|
|
39
|
+
> **Required Firestore composite index** for each queue collection:
|
|
40
|
+
> Fields: `dead` (ASC), `visibleAfter` (ASC)
|
|
41
|
+
|
|
42
|
+
## Setup
|
|
43
|
+
|
|
44
|
+
1. Create a Firebase project and enable Firestore.
|
|
45
|
+
2. Generate a service account key and save it as `server/service-account.json` (or set `GOOGLE_APPLICATION_CREDENTIALS` env var).
|
|
46
|
+
3. Install dependencies:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
4. Start the worker:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
FIREBASE_PROJECT_ID=your-project npm run worker
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The worker listens on `http://localhost:7900` by default.
|
|
59
|
+
|
|
60
|
+
## Browser usage
|
|
61
|
+
|
|
62
|
+
Include the IIFE bundle in your HTML:
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<script src="/browser/server-runtime-controlface.js"></script>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then use `browser/board-runtime.js` as a reference for constructing the runtime with the Firebase JS SDK.
|
|
69
|
+
|
|
70
|
+
## Firestore security rules (development)
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
rules_version = '2';
|
|
74
|
+
service cloud.firestore {
|
|
75
|
+
match /databases/{database}/documents {
|
|
76
|
+
match /boards/{boardId}/{document=**} {
|
|
77
|
+
allow read, write: if request.auth != null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* board-firestore/browser/board-runtime.js
|
|
3
|
+
*
|
|
4
|
+
* Shows how to instantiate the ServerRuntimeControlface IIFE in a browser
|
|
5
|
+
* backed by the Firebase JS SDK (v9 modular) instead of the Admin SDK.
|
|
6
|
+
*
|
|
7
|
+
* This file is NOT bundled — it is a plain ES module that your build tool
|
|
8
|
+
* (Vite, webpack, etc.) would process. Import it from your SPA's main entry.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* Browser <──Firestore JS SDK──> Cloud Firestore
|
|
12
|
+
* │ │
|
|
13
|
+
* │ ServerRuntimeControlface │
|
|
14
|
+
* └── createSingleBoardServerRuntime() reads/writes Firestore directly
|
|
15
|
+
*
|
|
16
|
+
* The worker (server/worker.js) runs the queue lanes and can process AI card
|
|
17
|
+
* requests without exposing any service account credentials to the browser.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { initializeApp } from 'firebase/app';
|
|
21
|
+
import {
|
|
22
|
+
getFirestore,
|
|
23
|
+
doc,
|
|
24
|
+
getDoc,
|
|
25
|
+
setDoc,
|
|
26
|
+
deleteDoc,
|
|
27
|
+
collection,
|
|
28
|
+
query,
|
|
29
|
+
where,
|
|
30
|
+
orderBy,
|
|
31
|
+
getDocs,
|
|
32
|
+
} from 'firebase/firestore';
|
|
33
|
+
|
|
34
|
+
// ── IMPORTANT: ServerRuntimeControlface is loaded as a browser IIFE ───────────
|
|
35
|
+
// Add this to your HTML: <script src="/browser/server-runtime-controlface.js"></script>
|
|
36
|
+
// Then window.ServerRuntimeControlface is available.
|
|
37
|
+
const { createSingleBoardServerRuntime } = window.ServerRuntimeControlface;
|
|
38
|
+
|
|
39
|
+
// ── Firebase config — replace with your project values ────────────────────────
|
|
40
|
+
const firebaseConfig = {
|
|
41
|
+
apiKey: 'YOUR_API_KEY',
|
|
42
|
+
authDomain: 'YOUR_PROJECT.firebaseapp.com',
|
|
43
|
+
projectId: 'YOUR_PROJECT_ID',
|
|
44
|
+
storageBucket: 'YOUR_PROJECT.appspot.com',
|
|
45
|
+
appId: 'YOUR_APP_ID',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const app = initializeApp(firebaseConfig);
|
|
49
|
+
const db = getFirestore(app);
|
|
50
|
+
|
|
51
|
+
const BOARD_ID = 'default';
|
|
52
|
+
|
|
53
|
+
// ── Browser-compatible base64url helpers ───────────────────────────────────────
|
|
54
|
+
function encodeDocId(key) {
|
|
55
|
+
return btoa(encodeURIComponent(key).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))))
|
|
56
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function makeRef(kind, value) {
|
|
60
|
+
const json = JSON.stringify({ kind, value });
|
|
61
|
+
const b64 = btoa(unescape(encodeURIComponent(json)))
|
|
62
|
+
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
63
|
+
return `b64:${b64}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Minimal Firestore KV adapter for the browser (Firebase JS SDK) ─────────────
|
|
67
|
+
function createBrowserKvStorage(colRef) {
|
|
68
|
+
return {
|
|
69
|
+
async read(key) {
|
|
70
|
+
const snap = await getDoc(doc(colRef, encodeDocId(key)));
|
|
71
|
+
if (!snap.exists()) return null;
|
|
72
|
+
return snap.data()?.value ?? null;
|
|
73
|
+
},
|
|
74
|
+
async write(key, value) {
|
|
75
|
+
await setDoc(doc(colRef, encodeDocId(key)), { k: key, value });
|
|
76
|
+
},
|
|
77
|
+
async delete(key) {
|
|
78
|
+
await deleteDoc(doc(colRef, encodeDocId(key)));
|
|
79
|
+
},
|
|
80
|
+
async listKeys(prefix = '') {
|
|
81
|
+
let q = colRef;
|
|
82
|
+
if (prefix) {
|
|
83
|
+
q = query(colRef, where('k', '>=', prefix), where('k', '<', prefix + '\uf8ff'), orderBy('k'));
|
|
84
|
+
} else {
|
|
85
|
+
q = query(colRef, orderBy('k'));
|
|
86
|
+
}
|
|
87
|
+
const snap = await getDocs(q);
|
|
88
|
+
return snap.docs.map((d) => d.data().k ?? d.id);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Minimal Firestore blob adapter for the browser ────────────────────────────
|
|
94
|
+
function createBrowserBlobStorage(colRef) {
|
|
95
|
+
return {
|
|
96
|
+
async read(key) {
|
|
97
|
+
const snap = await getDoc(doc(colRef, encodeDocId(key)));
|
|
98
|
+
if (!snap.exists()) return null;
|
|
99
|
+
return snap.data()?.content ?? null;
|
|
100
|
+
},
|
|
101
|
+
async write(key, content) {
|
|
102
|
+
await setDoc(doc(colRef, encodeDocId(key)), { k: key, content });
|
|
103
|
+
},
|
|
104
|
+
async exists(key) {
|
|
105
|
+
const snap = await getDoc(doc(colRef, encodeDocId(key)));
|
|
106
|
+
return snap.exists();
|
|
107
|
+
},
|
|
108
|
+
async remove(key) {
|
|
109
|
+
await deleteDoc(doc(colRef, encodeDocId(key)));
|
|
110
|
+
},
|
|
111
|
+
async listKeys(prefix = '') {
|
|
112
|
+
let q = prefix
|
|
113
|
+
? query(colRef, where('k', '>=', prefix), where('k', '<', prefix + '\uf8ff'), orderBy('k'))
|
|
114
|
+
: query(colRef, orderBy('k'));
|
|
115
|
+
const snap = await getDocs(q);
|
|
116
|
+
return snap.docs.map((d) => d.data().k ?? d.id);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Board platform adapter shim for browser use ────────────────────────────────
|
|
122
|
+
// NOTE: The browser adapter is read-heavy (renders current card state) and
|
|
123
|
+
// delegates writes to the worker. The runtime will route execution dispatch
|
|
124
|
+
// to the worker via HTTP if you configure invocationAdapter with an http:post ref.
|
|
125
|
+
function createBrowserFirestoreAdapter() {
|
|
126
|
+
const boardDoc = (name) => collection(db, 'boards', BOARD_ID, name);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
kvStorage: (namespace) => createBrowserKvStorage(boardDoc(`kv-${namespace || 'root'}`)),
|
|
130
|
+
kvStorageForRef: (ref) => createBrowserKvStorage(boardDoc('kv-root')),
|
|
131
|
+
blobStorage: (namespace) => createBrowserBlobStorage(boardDoc(`blobs-${namespace || 'root'}`)),
|
|
132
|
+
scratchStorage: () => ({
|
|
133
|
+
...createBrowserBlobStorage(boardDoc('scratch')),
|
|
134
|
+
getUniqueKey: (prefix = 'scratch-') => `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
135
|
+
create: async (data, prefix = 'scratch-') => {
|
|
136
|
+
const key = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
137
|
+
await createBrowserBlobStorage(boardDoc('scratch')).write(key, data);
|
|
138
|
+
return key;
|
|
139
|
+
},
|
|
140
|
+
keyRef: (key) => ({ kind: 'firestore-blob', value: key }),
|
|
141
|
+
config: {
|
|
142
|
+
get: async (k) => {
|
|
143
|
+
const v = await createBrowserBlobStorage(boardDoc('scratch')).read(`__config__/${k}`);
|
|
144
|
+
if (v == null) return null;
|
|
145
|
+
try { return JSON.parse(v); } catch { return v; }
|
|
146
|
+
},
|
|
147
|
+
set: async (k, v) =>
|
|
148
|
+
createBrowserBlobStorage(boardDoc('scratch')).write(`__config__/${k}`, JSON.stringify(v)),
|
|
149
|
+
},
|
|
150
|
+
}),
|
|
151
|
+
scratchStorageForRef: () => ({ read: async () => null, write: async () => {}, exists: async () => false, remove: async () => {}, listKeys: async () => [], getUniqueKey: () => `${Date.now()}`, create: async () => `${Date.now()}`, keyRef: () => ({ kind: 'unknown', value: '' }), config: { get: async () => null, set: async () => {} } }),
|
|
152
|
+
journalStorage: () => ({
|
|
153
|
+
append: async (payload) => {
|
|
154
|
+
const id = `${String(Date.now()).padStart(13, '0')}-${Math.random().toString(36).slice(2, 10)}`;
|
|
155
|
+
await setDoc(doc(db, 'boards', BOARD_ID, 'journal', id), { id, createdAt: new Date().toISOString(), payload });
|
|
156
|
+
return { id, payload };
|
|
157
|
+
},
|
|
158
|
+
readAll: async () => {
|
|
159
|
+
const snap = await getDocs(query(collection(db, 'boards', BOARD_ID, 'journal'), orderBy('id')));
|
|
160
|
+
return snap.docs.map((d) => ({ id: d.data().id, payload: d.data().payload }));
|
|
161
|
+
},
|
|
162
|
+
readAfter: async (cursor) => {
|
|
163
|
+
let q = cursor
|
|
164
|
+
? query(collection(db, 'boards', BOARD_ID, 'journal'), where('id', '>', cursor), orderBy('id'))
|
|
165
|
+
: query(collection(db, 'boards', BOARD_ID, 'journal'), orderBy('id'));
|
|
166
|
+
const snap = await getDocs(q);
|
|
167
|
+
const entries = snap.docs.map((d) => ({ id: d.data().id, payload: d.data().payload }));
|
|
168
|
+
return { entries, newCursor: entries.at(-1)?.id ?? cursor };
|
|
169
|
+
},
|
|
170
|
+
}),
|
|
171
|
+
archiveFactory: () => ({
|
|
172
|
+
stream: (name) => ({
|
|
173
|
+
append: async (payload) => {
|
|
174
|
+
const id = `${String(Date.now()).padStart(13, '0')}-${Math.random().toString(36).slice(2, 10)}`;
|
|
175
|
+
await setDoc(doc(db, 'boards', BOARD_ID, `archive-stream-${name}`, id), { id, createdAt: new Date().toISOString(), payload });
|
|
176
|
+
return { id, payload };
|
|
177
|
+
},
|
|
178
|
+
readAll: async () => {
|
|
179
|
+
const snap = await getDocs(query(collection(db, 'boards', BOARD_ID, `archive-stream-${name}`), orderBy('id')));
|
|
180
|
+
return snap.docs.map((d) => ({ id: d.data().id, payload: d.data().payload }));
|
|
181
|
+
},
|
|
182
|
+
readAfter: async (cursor) => {
|
|
183
|
+
const q = cursor
|
|
184
|
+
? query(collection(db, 'boards', BOARD_ID, `archive-stream-${name}`), where('id', '>', cursor), orderBy('id'))
|
|
185
|
+
: query(collection(db, 'boards', BOARD_ID, `archive-stream-${name}`), orderBy('id'));
|
|
186
|
+
const snap = await getDocs(q);
|
|
187
|
+
const entries = snap.docs.map((d) => ({ id: d.data().id, payload: d.data().payload }));
|
|
188
|
+
return { entries, newCursor: entries.at(-1)?.id ?? cursor };
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
blob: (name) => createBrowserBlobStorage(collection(db, 'boards', BOARD_ID, `archive-blob-${name}`)),
|
|
192
|
+
listStreams: async () => [], // Not available in JS SDK — use Admin SDK or maintain an index
|
|
193
|
+
listBlobs: async () => [],
|
|
194
|
+
config: {
|
|
195
|
+
get: async (k) => { const s = await getDoc(doc(db, 'boards', BOARD_ID, 'archive-config', 'main')); return s.data()?.[k] ?? null; },
|
|
196
|
+
set: async (k, v) => setDoc(doc(db, 'boards', BOARD_ID, 'archive-config', 'main'), { [k]: v }, { merge: true }),
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
archiveFactoryForRef: () => null,
|
|
200
|
+
|
|
201
|
+
// Queue storage: browser-side is read-only; the worker handles leasing
|
|
202
|
+
// Use null for browser-only apps; the worker processes queued tasks.
|
|
203
|
+
boardWorkerStore: () => null,
|
|
204
|
+
chatAgentStore: () => null,
|
|
205
|
+
processAccumulatedStore: () => null,
|
|
206
|
+
|
|
207
|
+
// Lock: no-op in browser (worker holds the real lock)
|
|
208
|
+
lock: { tryAcquire: async () => async () => {} },
|
|
209
|
+
|
|
210
|
+
hashFn: (value) => {
|
|
211
|
+
// Browser-safe hash (djb2 — for display only, not crypto security)
|
|
212
|
+
const str = JSON.stringify(value);
|
|
213
|
+
let hash = 5381;
|
|
214
|
+
for (let i = 0; i < str.length; i++) hash = ((hash << 5) + hash) ^ str.charCodeAt(i);
|
|
215
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
genId: () => `${String(Date.now()).padStart(13, '0')}-${Math.random().toString(36).slice(2, 10)}`,
|
|
219
|
+
|
|
220
|
+
dispatchExecution: async (_ref, _args) => ({ dispatched: false, error: 'browser-only' }),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Bootstrap the in-browser runtime ──────────────────────────────────────────
|
|
225
|
+
export function createBoardRuntime() {
|
|
226
|
+
const boardAdapter = createBrowserFirestoreAdapter();
|
|
227
|
+
|
|
228
|
+
const runtime = createSingleBoardServerRuntime({
|
|
229
|
+
boardId: BOARD_ID,
|
|
230
|
+
boards: [
|
|
231
|
+
{
|
|
232
|
+
label: `Board — ${BOARD_ID}`,
|
|
233
|
+
boardAdapter,
|
|
234
|
+
baseRef: { kind: 'firestore', value: `boards/${BOARD_ID}` },
|
|
235
|
+
cardStoreRef: makeRef('firestore', `boards/${BOARD_ID}/cards`),
|
|
236
|
+
outputsStoreRef: makeRef('firestore', `boards/${BOARD_ID}/runtime-out`),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
invocationAdapter: {
|
|
240
|
+
// Dispatch executions to the worker via HTTP
|
|
241
|
+
async invoke(ref, args) {
|
|
242
|
+
const workerUrl = window.__BOARD_WORKER_URL__ ?? 'http://localhost:7900';
|
|
243
|
+
const res = await fetch(`${workerUrl}/api/board/invoke`, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: { 'Content-Type': 'application/json' },
|
|
246
|
+
body: JSON.stringify({ ref, args }),
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok) return { dispatched: false, error: `worker HTTP ${res.status}` };
|
|
249
|
+
return res.json();
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return runtime;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Usage example ──────────────────────────────────────────────────────────────
|
|
258
|
+
// const runtime = createBoardRuntime();
|
|
259
|
+
// const boardStatus = await runtime.handleRuntimeApi(fakeReq, fakeRes, url);
|
|
260
|
+
//
|
|
261
|
+
// Or call runtime methods directly:
|
|
262
|
+
// await runtime.processAccumulatedLane(); // trigger a card computation pass
|
|
263
|
+
// const cards = await runtime.cardStore?.list(); // read current card state
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"indexes": [
|
|
3
|
+
{
|
|
4
|
+
"collectionGroup": "worker-queue",
|
|
5
|
+
"queryScope": "COLLECTION",
|
|
6
|
+
"fields": [
|
|
7
|
+
{ "fieldPath": "dead", "order": "ASCENDING" },
|
|
8
|
+
{ "fieldPath": "visibleAfter", "order": "ASCENDING" }
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"collectionGroup": "chat-queue",
|
|
13
|
+
"queryScope": "COLLECTION",
|
|
14
|
+
"fields": [
|
|
15
|
+
{ "fieldPath": "dead", "order": "ASCENDING" },
|
|
16
|
+
{ "fieldPath": "visibleAfter", "order": "ASCENDING" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"collectionGroup": "process-queue",
|
|
21
|
+
"queryScope": "COLLECTION",
|
|
22
|
+
"fields": [
|
|
23
|
+
{ "fieldPath": "dead", "order": "ASCENDING" },
|
|
24
|
+
{ "fieldPath": "visibleAfter", "order": "ASCENDING" }
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"fieldOverrides": []
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "board-firestore",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"worker": "node server/worker.js",
|
|
8
|
+
"agents": "node server/agents.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"firebase-admin": "^12.0.0",
|
|
12
|
+
"yaml-flow": "*"
|
|
13
|
+
}
|
|
14
|
+
}
|