yaml-flow 8.6.4 → 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.
Files changed (146) hide show
  1. package/browser/adapters/firebase-storage.js +3 -0
  2. package/browser/adapters/firestore-storage.js +3 -0
  3. package/browser/adapters/localstorage-storage.js +4 -0
  4. package/browser/asset-integrity.json +20 -4
  5. package/browser/server-runtime-controlface.js +8 -0
  6. package/examples/ARCHITECTURE.md +5 -32
  7. package/examples/board/demo-shell-with-server.html +2 -2
  8. package/examples/board/doc.html +2 -2
  9. package/examples/board/server/board-server.js +4 -2
  10. package/examples/board-firestore/README.md +81 -0
  11. package/examples/board-firestore/browser/board-runtime.js +263 -0
  12. package/examples/board-firestore/firestore.indexes.json +29 -0
  13. package/examples/board-firestore/package.json +14 -0
  14. package/examples/board-firestore/server/adapters/firestore-archive-factory.js +59 -0
  15. package/examples/board-firestore/server/adapters/firestore-blob-storage.js +82 -0
  16. package/examples/board-firestore/server/adapters/firestore-board-adapter.js +127 -0
  17. package/examples/board-firestore/server/adapters/firestore-journal-storage.js +54 -0
  18. package/examples/board-firestore/server/adapters/firestore-kv-storage.js +47 -0
  19. package/examples/board-firestore/server/adapters/firestore-lock.js +62 -0
  20. package/examples/board-firestore/server/adapters/firestore-queue-storage.js +186 -0
  21. package/examples/board-firestore/server/adapters/firestore-scratch-storage.js +50 -0
  22. package/examples/board-firestore/server/worker.js +146 -0
  23. package/lib/{artifacts-store-lib-BR-Samty.d.cts → artifacts-store-lib-D9nMkVcE.d.cts} +1 -1
  24. package/lib/{artifacts-store-lib-DT7XlWUL.d.ts → artifacts-store-lib-DSSMqVL2.d.ts} +1 -1
  25. package/lib/artifacts-store-public.d.cts +2 -2
  26. package/lib/artifacts-store-public.d.ts +2 -2
  27. package/lib/board-live-cards-mcp.cjs +1 -1
  28. package/lib/board-live-cards-mcp.d.cts +50 -3
  29. package/lib/board-live-cards-mcp.d.ts +50 -3
  30. package/lib/board-live-cards-mcp.js +1 -1
  31. package/lib/board-live-cards-node.cjs +8 -8
  32. package/lib/board-live-cards-node.d.cts +13 -9
  33. package/lib/board-live-cards-node.d.ts +13 -9
  34. package/lib/board-live-cards-node.js +8 -8
  35. package/lib/{board-live-cards-public-BMUIPOrc.d.ts → board-live-cards-public-D-DJek3X.d.ts} +1 -1
  36. package/lib/{board-live-cards-public-wkNmBIRC.d.cts → board-live-cards-public-DQzPe7A9.d.cts} +1 -1
  37. package/lib/board-live-cards-public-async-3hUuHxDx.d.ts +55 -0
  38. package/lib/board-live-cards-public-async-CgMCYYft.d.cts +55 -0
  39. package/lib/board-live-cards-public.d.cts +1 -1
  40. package/lib/board-live-cards-public.d.ts +1 -1
  41. package/lib/board-live-cards-server-runtime.cjs +1 -1
  42. package/lib/board-live-cards-server-runtime.d.cts +10 -6
  43. package/lib/board-live-cards-server-runtime.d.ts +10 -6
  44. package/lib/board-live-cards-server-runtime.js +1 -1
  45. package/lib/board-platform-adapter-async-DOfEq_HC.d.cts +129 -0
  46. package/lib/board-platform-adapter-async-JZPCzZnH.d.ts +129 -0
  47. package/lib/board-worker-adapter.cjs +3 -3
  48. package/lib/board-worker-adapter.js +3 -3
  49. package/lib/card-store-public.d.cts +1 -1
  50. package/lib/card-store-public.d.ts +1 -1
  51. package/lib/{chat-storage-lib-BIUbE-fM.d.cts → chat-storage-lib-B9Q34Dyv.d.cts} +1 -1
  52. package/lib/{chat-storage-lib-BlG-sobS.d.ts → chat-storage-lib-DB9iSai2.d.ts} +1 -1
  53. package/lib/chat-store-public.d.cts +2 -2
  54. package/lib/chat-store-public.d.ts +2 -2
  55. package/lib/chunk-272IYUKT.cjs +2 -0
  56. package/lib/chunk-5XHOHTLZ.cjs +2 -0
  57. package/lib/chunk-6APH25VI.js +2 -0
  58. package/lib/chunk-7FGPOGRV.cjs +2 -0
  59. package/lib/chunk-7ICPAABP.cjs +7 -0
  60. package/lib/chunk-CPAXTVBQ.cjs +2 -0
  61. package/lib/chunk-DDYSXP2A.cjs +3 -0
  62. package/lib/chunk-EGRHWZRV.js +2 -0
  63. package/lib/chunk-GL2OHR2E.cjs +2 -0
  64. package/lib/chunk-IPLSRN6P.cjs +4 -0
  65. package/lib/chunk-J5J6BG7B.js +2 -0
  66. package/lib/chunk-KAWQPLIE.cjs +2 -0
  67. package/lib/chunk-LPXVVMQT.cjs +2 -0
  68. package/lib/chunk-M3OU6IS5.cjs +2 -0
  69. package/lib/chunk-M6STQR5F.cjs +2 -0
  70. package/lib/chunk-NJJ7WEDT.cjs +2 -0
  71. package/lib/chunk-NKIQRCOM.cjs +2 -0
  72. package/lib/chunk-NM6O35RY.cjs +2 -0
  73. package/lib/chunk-NTICU4OK.js +2 -0
  74. package/lib/chunk-O7NOHKVR.js +2 -0
  75. package/lib/chunk-PRHQBGPT.js +2 -0
  76. package/lib/chunk-S44QZUDX.js +2 -0
  77. package/lib/chunk-SGV7PU4H.js +2 -0
  78. package/lib/chunk-TSN3RTXT.js +4 -0
  79. package/lib/chunk-VXJHBWK3.js +2 -0
  80. package/lib/chunk-WHDEBJLT.js +7 -0
  81. package/lib/chunk-XYN5D3GL.js +2 -0
  82. package/lib/chunk-YGALANRO.js +2 -0
  83. package/lib/chunk-ZJ5M5COT.js +2 -0
  84. package/lib/chunk-ZXQR7GHT.js +3 -0
  85. package/lib/cloud-storage.cjs +1 -1
  86. package/lib/cloud-storage.d.cts +5 -3
  87. package/lib/cloud-storage.d.ts +5 -3
  88. package/lib/cloud-storage.js +1 -1
  89. package/lib/firebase-storage/index.cjs +3 -0
  90. package/lib/firebase-storage/index.d.cts +57 -0
  91. package/lib/firebase-storage/index.d.ts +57 -0
  92. package/lib/firebase-storage/index.js +3 -0
  93. package/lib/firestore-storage/index.cjs +3 -0
  94. package/lib/firestore-storage/index.d.cts +98 -0
  95. package/lib/firestore-storage/index.d.ts +98 -0
  96. package/lib/firestore-storage/index.js +3 -0
  97. package/lib/localstorage-storage/index.cjs +2 -0
  98. package/lib/localstorage-storage/index.d.cts +39 -0
  99. package/lib/localstorage-storage/index.d.ts +39 -0
  100. package/lib/localstorage-storage/index.js +2 -0
  101. package/lib/mcp-tool-registries-BBObLYga.d.ts +41 -0
  102. package/lib/mcp-tool-registries-W3TRj6O5.d.cts +41 -0
  103. package/lib/queue-lane-registry-PaZuFpwp.d.cts +30 -0
  104. package/lib/queue-lane-registry-PaZuFpwp.d.ts +30 -0
  105. package/lib/server-jobs-queue-runner/index.cjs +2 -0
  106. package/lib/server-jobs-queue-runner/index.d.cts +22 -0
  107. package/lib/server-jobs-queue-runner/index.d.ts +22 -0
  108. package/lib/server-jobs-queue-runner/index.js +2 -0
  109. package/lib/server-runtime/index.cjs +1 -1
  110. package/lib/server-runtime/index.d.cts +11 -17
  111. package/lib/server-runtime/index.d.ts +11 -17
  112. package/lib/server-runtime/index.js +1 -1
  113. package/lib/server-runtime-agentface/index.cjs +2 -0
  114. package/lib/server-runtime-agentface/index.d.cts +53 -0
  115. package/lib/server-runtime-agentface/index.d.ts +53 -0
  116. package/lib/server-runtime-agentface/index.js +2 -0
  117. package/lib/server-runtime-controlface/index.cjs +2 -0
  118. package/lib/server-runtime-controlface/index.d.cts +80 -0
  119. package/lib/server-runtime-controlface/index.d.ts +80 -0
  120. package/lib/server-runtime-controlface/index.js +2 -0
  121. package/lib/server-runtime-core/index.cjs +2 -0
  122. package/lib/server-runtime-core/index.d.cts +376 -0
  123. package/lib/server-runtime-core/index.d.ts +376 -0
  124. package/lib/server-runtime-core/index.js +2 -0
  125. package/lib/server-runtime-watchers/index.cjs +2 -0
  126. package/lib/server-runtime-watchers/index.d.cts +127 -0
  127. package/lib/server-runtime-watchers/index.d.ts +127 -0
  128. package/lib/server-runtime-watchers/index.js +2 -0
  129. package/lib/server-runtime-webhooks/index.cjs +2 -0
  130. package/lib/server-runtime-webhooks/index.d.cts +34 -0
  131. package/lib/server-runtime-webhooks/index.d.ts +34 -0
  132. package/lib/server-runtime-webhooks/index.js +2 -0
  133. package/lib/storage-async-interface-BRR4eBjx.d.cts +81 -0
  134. package/lib/storage-async-interface-DhlOVPSp.d.ts +81 -0
  135. package/lib/{queue-lane-registry-BPKWWgd4.d.cts → types-BzQY45dH.d.cts} +8 -34
  136. package/lib/{queue-lane-registry-Be6c0ftj.d.ts → types-CF2xUcZW.d.ts} +8 -34
  137. package/package.json +46 -2
  138. package/examples/board-local/demo-shell-localstorage.html +0 -843
  139. package/lib/board-live-cards-public-async-DKZqbJVU.d.ts +0 -256
  140. package/lib/board-live-cards-public-async-dMWNbWq6.d.cts +0 -256
  141. package/lib/chunk-KXWT3CY6.cjs +0 -8
  142. package/lib/chunk-MLVTJASJ.js +0 -2
  143. package/lib/chunk-OJLA6NLU.js +0 -8
  144. package/lib/chunk-R5L5WUKN.js +0 -2
  145. package/lib/chunk-WOALA3V5.cjs +0 -2
  146. package/lib/chunk-YEB5QHGE.cjs +0 -2
@@ -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 the other bundles to pick up.
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 — order matters -->
185
- <script src="compute-jsonata.js"></script> <!-- sets window.jsonataSync -->
186
- <script src="board-livecards-localstorage.js"></script> <!-- or board-livecards-client.js -->
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.6.4/browser/live-cards.js"></script>
23
- <script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.6.4/browser/board-livecards-client.js"></script>
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">
@@ -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.6.4/browser/live-cards.js"></script>
41
- <script src="https://cdn.jsdelivr.net/npm/yaml-flow@8.6.4/browser/board-livecards-client.js"></script>
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/board-live-cards-server-runtime';
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}/callback/board-worker`);
321
+ return createHttpBoardCallbackTransport(`${normalizedServerUrl}${normalizedApiBasePath}/mcp-webhooks`);
320
322
  }
321
323
 
322
324
  async function readJsonRequest(req) {
@@ -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
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * firestore-archive-factory.js
3
+ *
4
+ * AsyncArchiveFactory backed by Firestore.
5
+ * Each named stream is a subcollection: boards/{boardId}/archive-stream-{name}
6
+ * Each named blob collection: boards/{boardId}/archive-blob-{name}
7
+ *
8
+ * NOTE: listStreams / listBlobs use Admin SDK's listCollections() — not available
9
+ * in the browser Firestore JS SDK. In the browser, skip or maintain an index manually.
10
+ *
11
+ * @param {import('@google-cloud/firestore').Firestore} db
12
+ * @param {string} boardId
13
+ * @returns {import('yaml-flow/cloud-storage').AsyncArchiveFactory}
14
+ */
15
+
16
+ import { createFirestoreJournalStorage } from './firestore-journal-storage.js';
17
+ import { createFirestoreBlobStorage } from './firestore-blob-storage.js';
18
+
19
+ export function createFirestoreArchiveFactory(db, boardId) {
20
+ const boardDoc = db.collection('boards').doc(boardId);
21
+
22
+ return {
23
+ stream(name) {
24
+ return createFirestoreJournalStorage(boardDoc.collection(`archive-stream-${name}`));
25
+ },
26
+
27
+ blob(name) {
28
+ return createFirestoreBlobStorage(boardDoc.collection(`archive-blob-${name}`));
29
+ },
30
+
31
+ async listStreams(prefix = '') {
32
+ const cols = await boardDoc.listCollections();
33
+ const tag = `archive-stream-${prefix}`;
34
+ return cols
35
+ .map((c) => c.id)
36
+ .filter((id) => id.startsWith(tag))
37
+ .map((id) => id.slice('archive-stream-'.length));
38
+ },
39
+
40
+ async listBlobs(prefix = '') {
41
+ const cols = await boardDoc.listCollections();
42
+ const tag = `archive-blob-${prefix}`;
43
+ return cols
44
+ .map((c) => c.id)
45
+ .filter((id) => id.startsWith(tag))
46
+ .map((id) => id.slice('archive-blob-'.length));
47
+ },
48
+
49
+ config: {
50
+ async get(k) {
51
+ const snap = await boardDoc.collection('archive-config').doc('main').get();
52
+ return snap.data()?.[k] ?? null;
53
+ },
54
+ async set(k, v) {
55
+ await boardDoc.collection('archive-config').doc('main').set({ [k]: v }, { merge: true });
56
+ },
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * firestore-blob-storage.js
3
+ *
4
+ * AsyncBlobStorage backed by Firestore for text/small blobs.
5
+ * Document shape: { k, content, contentType?, bytesBase64? }
6
+ *
7
+ * Binary blobs (readBytes/writeBytes) are stored as base64-encoded strings.
8
+ * For production use with files > 1 MB, replace writeBytes/readBytes with
9
+ * Firebase Storage (Cloud Storage for Firebase) and store the GCS path here.
10
+ *
11
+ * @param {import('@google-cloud/firestore').CollectionReference} col
12
+ * @returns {import('yaml-flow/cloud-storage').AsyncBlobStorage}
13
+ */
14
+
15
+ function encodeDocId(key) {
16
+ return Buffer.from(key).toString('base64url');
17
+ }
18
+
19
+ function decodeDocId(docId) {
20
+ return Buffer.from(docId, 'base64url').toString();
21
+ }
22
+
23
+ export function createFirestoreBlobStorage(col) {
24
+ return {
25
+ async read(key) {
26
+ const snap = await col.doc(encodeDocId(key)).get();
27
+ if (!snap.exists) return null;
28
+ return snap.data()?.content ?? null;
29
+ },
30
+
31
+ async write(key, content) {
32
+ await col.doc(encodeDocId(key)).set({ k: key, content });
33
+ },
34
+
35
+ async exists(key) {
36
+ const snap = await col.doc(encodeDocId(key)).get();
37
+ return snap.exists;
38
+ },
39
+
40
+ async remove(key) {
41
+ await col.doc(encodeDocId(key)).delete();
42
+ },
43
+
44
+ async readBytes(key) {
45
+ const snap = await col.doc(encodeDocId(key)).get();
46
+ if (!snap.exists) return null;
47
+ const data = snap.data();
48
+ if (data?.bytesBase64) return Buffer.from(data.bytesBase64, 'base64');
49
+ if (data?.content) return Buffer.from(data.content);
50
+ return null;
51
+ },
52
+
53
+ async writeBytes(key, bytes) {
54
+ // NOTE: Firestore 1 MB doc limit. For larger files use Firebase Storage.
55
+ const bytesBase64 = Buffer.from(bytes).toString('base64');
56
+ await col.doc(encodeDocId(key)).set({ k: key, bytesBase64 });
57
+ },
58
+
59
+ async listKeys(prefix = '') {
60
+ let q = col;
61
+ if (prefix) {
62
+ q = col.where('k', '>=', prefix).where('k', '<', prefix + '\uf8ff');
63
+ }
64
+ const snap = await q.orderBy('k').get();
65
+ return snap.docs.map((d) => d.data().k ?? decodeDocId(d.id));
66
+ },
67
+
68
+ async stat(key) {
69
+ const snap = await col.doc(encodeDocId(key)).get();
70
+ if (!snap.exists) return null;
71
+ const data = snap.data();
72
+ const size = data?.bytesBase64
73
+ ? Math.floor((data.bytesBase64.length * 3) / 4)
74
+ : (data?.content?.length ?? 0);
75
+ return { key, size, contentType: data?.contentType ?? 'application/octet-stream' };
76
+ },
77
+
78
+ keyRef(key) {
79
+ return { kind: 'firestore-blob', value: `${col.path}/${encodeDocId(key)}` };
80
+ },
81
+ };
82
+ }