yaml-flow 8.6.4 → 8.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +22 -6
- package/browser/live-cards.schema.json +10 -1
- 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/test/server-http-test.js +73 -79
- 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 +5 -5
- package/lib/board-live-cards-node.d.cts +16 -11
- package/lib/board-live-cards-node.d.ts +16 -11
- package/lib/board-live-cards-node.js +5 -5
- package/lib/{board-live-cards-public-BMUIPOrc.d.ts → board-live-cards-public-JNRKfBZy.d.ts} +1 -1
- package/lib/{board-live-cards-public-wkNmBIRC.d.cts → board-live-cards-public-LlVUQPL2.d.cts} +1 -1
- package/lib/board-live-cards-public-async-Di9QB141.d.cts +55 -0
- package/lib/board-live-cards-public-async-fwd1QI82.d.ts +55 -0
- package/lib/board-live-cards-public.cjs +1 -1
- 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-public.js +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-livegraph-runtime/index.cjs +1 -1
- package/lib/board-livegraph-runtime/index.js +1 -1
- package/lib/board-platform-adapter-async-BfHmHdx2.d.cts +129 -0
- package/lib/board-platform-adapter-async-DYahVzIK.d.ts +129 -0
- package/lib/board-worker-adapter.cjs +3 -3
- package/lib/board-worker-adapter.js +3 -3
- package/lib/card-compute/index.cjs +1 -1
- package/lib/card-compute/index.js +1 -1
- package/lib/card-store-public.d.cts +1 -1
- package/lib/card-store-public.d.ts +1 -1
- package/lib/card-validation.cjs +1 -1
- package/lib/card-validation.js +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-3KC6LBOG.js +3 -0
- package/lib/chunk-5XHOHTLZ.cjs +2 -0
- package/lib/chunk-6APH25VI.js +2 -0
- package/lib/chunk-76C7N4YT.js +3 -0
- package/lib/chunk-7FGPOGRV.cjs +2 -0
- package/lib/chunk-7ICPAABP.cjs +7 -0
- package/lib/chunk-ASR44K7H.cjs +3 -0
- package/lib/chunk-CPAXTVBQ.cjs +2 -0
- package/lib/chunk-EGRHWZRV.js +2 -0
- package/lib/chunk-EZENHAVZ.cjs +2 -0
- package/lib/chunk-FO4KNVU7.cjs +2 -0
- package/lib/chunk-GL2OHR2E.cjs +2 -0
- package/lib/chunk-HWYMZK3N.cjs +3 -0
- package/lib/chunk-IPLSRN6P.cjs +4 -0
- package/lib/{chunk-H5HBXPOI.cjs → chunk-J6EGN6S4.cjs} +3 -3
- package/lib/chunk-JH37NJGP.js +3 -0
- package/lib/chunk-JJL5VOQZ.cjs +3 -0
- package/lib/chunk-KAWQPLIE.cjs +2 -0
- package/lib/chunk-LPXVVMQT.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-PBOQ4HYB.cjs +2 -0
- package/lib/{chunk-VMW4Z6EF.js → chunk-PRKRXAVN.js} +3 -3
- package/lib/chunk-QJVR3FWQ.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-YBYXCFAI.js +2 -0
- package/lib/chunk-YGALANRO.js +2 -0
- package/lib/chunk-ZCNN6XPV.js +2 -0
- package/lib/chunk-ZJ5M5COT.js +2 -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/continuous-event-graph/index.cjs +1 -1
- package/lib/continuous-event-graph/index.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 +111 -0
- package/lib/firestore-storage/index.d.ts +111 -0
- package/lib/firestore-storage/index.js +3 -0
- package/lib/index.cjs +2 -2
- package/lib/index.js +1 -1
- 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 +29 -0
- package/lib/server-runtime-controlface/index.d.ts +29 -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 +378 -0
- package/lib/server-runtime-core/index.d.ts +378 -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-Ba8H5_Wo.d.cts} +10 -34
- package/lib/{queue-lane-registry-Be6c0ftj.d.ts → types-SO5OZm4s.d.ts} +10 -34
- package/package.json +46 -2
- package/schema/live-cards.schema.json +10 -1
- 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-KXWT3CY6.cjs +0 -8
- package/lib/chunk-MLVTJASJ.js +0 -2
- package/lib/chunk-N6P2JW4W.js +0 -3
- package/lib/chunk-NMZ6XNLB.cjs +0 -3
- package/lib/chunk-OEFTOO47.cjs +0 -3
- package/lib/chunk-OJLA6NLU.js +0 -8
- package/lib/chunk-R5L5WUKN.js +0 -2
- package/lib/chunk-VLBB3D6B.js +0 -3
- package/lib/chunk-WOALA3V5.cjs +0 -2
- package/lib/chunk-YEB5QHGE.cjs +0 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* firestore-board-adapter.js
|
|
3
|
+
*
|
|
4
|
+
* Composes all Firestore-backed storage primitives into an AsyncBoardPlatformAdapter
|
|
5
|
+
* using createHostedAsyncBoardPlatformAdapter from yaml-flow/cloud-storage.
|
|
6
|
+
*
|
|
7
|
+
* Firestore collection layout under boards/{boardId}/:
|
|
8
|
+
* kv-{namespace}/ AsyncKVStorage per namespace
|
|
9
|
+
* journal/ single board journal
|
|
10
|
+
* worker-queue/ board worker task queue
|
|
11
|
+
* chat-queue/ chat agent dispatch queue
|
|
12
|
+
* process-queue/ processAccumulated trigger queue
|
|
13
|
+
* blobs-{namespace}/ AsyncBlobStorage per namespace
|
|
14
|
+
* scratch/ ephemeral scratch storage
|
|
15
|
+
* archive-stream-{name}/ archive journal streams
|
|
16
|
+
* archive-blob-{name}/ archive blob collections
|
|
17
|
+
* archive-config/ archive config KV
|
|
18
|
+
* locks/board-lock AtomicRelayLock document
|
|
19
|
+
*
|
|
20
|
+
* @param {import('@google-cloud/firestore').Firestore} db
|
|
21
|
+
* @param {string} boardId
|
|
22
|
+
* @param {{ holderId?: string, requestProcessAccumulated?: () => void }} [options]
|
|
23
|
+
* @returns {import('yaml-flow/cloud-storage').AsyncBoardPlatformAdapter}
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
27
|
+
import { createHostedAsyncBoardPlatformAdapter } from 'yaml-flow/cloud-storage';
|
|
28
|
+
|
|
29
|
+
import { createFirestoreKvStorage } from './firestore-kv-storage.js';
|
|
30
|
+
import { createFirestoreJournalStorage } from './firestore-journal-storage.js';
|
|
31
|
+
import { createFirestoreQueueStorage } from './firestore-queue-storage.js';
|
|
32
|
+
import { createFirestoreBlobStorage } from './firestore-blob-storage.js';
|
|
33
|
+
import { createFirestoreScratchStorage } from './firestore-scratch-storage.js';
|
|
34
|
+
import { createFirestoreArchiveFactory } from './firestore-archive-factory.js';
|
|
35
|
+
import { createFirestoreLock } from './firestore-lock.js';
|
|
36
|
+
|
|
37
|
+
function parseKindValueRef(ref) {
|
|
38
|
+
const PREFIX = 'b64:';
|
|
39
|
+
if (!ref.startsWith(PREFIX)) return { kind: 'unknown', value: ref };
|
|
40
|
+
const b64 = ref.slice(PREFIX.length)
|
|
41
|
+
.replace(/-/g, '+').replace(/_/g, '/')
|
|
42
|
+
+ '='.repeat((4 - (ref.length - PREFIX.length) % 4) % 4);
|
|
43
|
+
return JSON.parse(Buffer.from(b64, 'base64').toString());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function lexicalId() {
|
|
47
|
+
const ts = String(Date.now()).padStart(13, '0');
|
|
48
|
+
const rand = Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
49
|
+
return `${ts}-${rand}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createFirestoreBoardAdapter(db, boardId, options = {}) {
|
|
53
|
+
const boardDoc = db.collection('boards').doc(boardId);
|
|
54
|
+
|
|
55
|
+
return createHostedAsyncBoardPlatformAdapter({
|
|
56
|
+
boardId,
|
|
57
|
+
|
|
58
|
+
// ── KV storage ────────────────────────────────────────────────────────
|
|
59
|
+
kvStorage(namespace) {
|
|
60
|
+
return createFirestoreKvStorage(boardDoc.collection(`kv-${namespace || 'root'}`));
|
|
61
|
+
},
|
|
62
|
+
kvStorageForRef(ref) {
|
|
63
|
+
const parsed = parseKindValueRef(ref);
|
|
64
|
+
// ref.value is treated as a full Firestore collection path when kind='firestore'
|
|
65
|
+
const col = parsed.kind === 'firestore'
|
|
66
|
+
? db.collection(parsed.value)
|
|
67
|
+
: boardDoc.collection(`kv-ref-${Buffer.from(ref).toString('base64url').slice(0, 16)}`);
|
|
68
|
+
return createFirestoreKvStorage(col);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// ── Blob storage ──────────────────────────────────────────────────────
|
|
72
|
+
blobStorage(namespace) {
|
|
73
|
+
return createFirestoreBlobStorage(boardDoc.collection(`blobs-${namespace || 'root'}`));
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// ── Scratch storage ───────────────────────────────────────────────────
|
|
77
|
+
scratchStorage() {
|
|
78
|
+
return createFirestoreScratchStorage(boardDoc.collection('scratch'));
|
|
79
|
+
},
|
|
80
|
+
scratchStorageForRef(ref) {
|
|
81
|
+
const parsed = parseKindValueRef(ref);
|
|
82
|
+
const col = parsed.kind === 'firestore'
|
|
83
|
+
? db.collection(parsed.value)
|
|
84
|
+
: boardDoc.collection('scratch');
|
|
85
|
+
return createFirestoreScratchStorage(col);
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// ── Archive factory ───────────────────────────────────────────────────
|
|
89
|
+
archiveFactory() {
|
|
90
|
+
return createFirestoreArchiveFactory(db, boardId);
|
|
91
|
+
},
|
|
92
|
+
archiveFactoryForRef(ref) {
|
|
93
|
+
const parsed = parseKindValueRef(ref);
|
|
94
|
+
const altBoardId = parsed.kind === 'firestore' ? parsed.value : `${boardId}-archive`;
|
|
95
|
+
return createFirestoreArchiveFactory(db, altBoardId);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// ── Journal storage ───────────────────────────────────────────────────
|
|
99
|
+
journalStorage() {
|
|
100
|
+
return createFirestoreJournalStorage(boardDoc.collection('journal'));
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// ── Queue storage ─────────────────────────────────────────────────────
|
|
104
|
+
queueStorage: createFirestoreQueueStorage(boardDoc.collection('worker-queue')),
|
|
105
|
+
chatAgentQueueStorage: createFirestoreQueueStorage(boardDoc.collection('chat-queue')),
|
|
106
|
+
processAccumulatedQueueStorage: createFirestoreQueueStorage(boardDoc.collection('process-queue')),
|
|
107
|
+
|
|
108
|
+
// ── Lock ──────────────────────────────────────────────────────────────
|
|
109
|
+
lock: createFirestoreLock(
|
|
110
|
+
boardDoc.collection('locks').doc('board-lock'),
|
|
111
|
+
{ holderId: options.holderId ?? randomUUID() },
|
|
112
|
+
),
|
|
113
|
+
|
|
114
|
+
// ── Utilities ─────────────────────────────────────────────────────────
|
|
115
|
+
hashFn(value) {
|
|
116
|
+
return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16);
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
genId() {
|
|
120
|
+
return lexicalId();
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
requestProcessAccumulated: options.requestProcessAccumulated,
|
|
124
|
+
publishBoardChangeNotifications: options.publishBoardChangeNotifications,
|
|
125
|
+
onWarn: (msg) => console.warn(`[firestore-board-adapter:${boardId}] ${msg}`),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* firestore-journal-storage.js
|
|
3
|
+
*
|
|
4
|
+
* AsyncJournalStorage backed by a Firestore CollectionReference.
|
|
5
|
+
* Each entry is stored as: { id, createdAt, payload }
|
|
6
|
+
* IDs are lexicographically sortable (timestamp prefix) so ORDER BY id
|
|
7
|
+
* gives insertion order.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('@google-cloud/firestore').CollectionReference} col
|
|
10
|
+
* @returns {import('yaml-flow/cloud-storage').AsyncJournalStorage}
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function lexicalId() {
|
|
14
|
+
const ts = String(Date.now()).padStart(13, '0');
|
|
15
|
+
const rand = Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
16
|
+
return `${ts}-${rand}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createFirestoreJournalStorage(col) {
|
|
20
|
+
return {
|
|
21
|
+
async append(payload) {
|
|
22
|
+
const id = lexicalId();
|
|
23
|
+
const doc = { id, createdAt: new Date().toISOString(), payload };
|
|
24
|
+
await col.doc(id).set(doc);
|
|
25
|
+
return { id, payload };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
async readAll() {
|
|
29
|
+
const snap = await col.orderBy('id').get();
|
|
30
|
+
return snap.docs.map((d) => ({ id: d.data().id, payload: d.data().payload }));
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async readAfter(cursor) {
|
|
34
|
+
let q = col.orderBy('id');
|
|
35
|
+
if (cursor) q = col.where('id', '>', cursor).orderBy('id');
|
|
36
|
+
const snap = await q.get();
|
|
37
|
+
const entries = snap.docs.map((d) => ({ id: d.data().id, payload: d.data().payload }));
|
|
38
|
+
return {
|
|
39
|
+
entries,
|
|
40
|
+
newCursor: entries.length > 0 ? entries[entries.length - 1].id : cursor,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async clear() {
|
|
45
|
+
const snap = await col.get();
|
|
46
|
+
const batchSize = 500;
|
|
47
|
+
for (let i = 0; i < snap.docs.length; i += batchSize) {
|
|
48
|
+
const batch = col.firestore.batch();
|
|
49
|
+
for (const doc of snap.docs.slice(i, i + batchSize)) batch.delete(doc.ref);
|
|
50
|
+
await batch.commit();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* firestore-kv-storage.js
|
|
3
|
+
*
|
|
4
|
+
* AsyncKVStorage backed by a Firestore CollectionReference.
|
|
5
|
+
* Each key is stored as a document with fields: { k, value }
|
|
6
|
+
* where k = original key string, value = arbitrary JSON value.
|
|
7
|
+
*
|
|
8
|
+
* Document ID: base64url(key) — avoids '/' and other Firestore-forbidden chars.
|
|
9
|
+
*
|
|
10
|
+
* @param {import('@google-cloud/firestore').CollectionReference} col
|
|
11
|
+
* @returns {import('yaml-flow/cloud-storage').AsyncKVStorage}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function encodeDocId(key) {
|
|
15
|
+
return Buffer.from(key).toString('base64url');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function decodeDocId(docId) {
|
|
19
|
+
return Buffer.from(docId, 'base64url').toString();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createFirestoreKvStorage(col) {
|
|
23
|
+
return {
|
|
24
|
+
async read(key) {
|
|
25
|
+
const snap = await col.doc(encodeDocId(key)).get();
|
|
26
|
+
if (!snap.exists) return null;
|
|
27
|
+
return snap.data()?.value ?? null;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async write(key, value) {
|
|
31
|
+
await col.doc(encodeDocId(key)).set({ k: key, value });
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async delete(key) {
|
|
35
|
+
await col.doc(encodeDocId(key)).delete();
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async listKeys(prefix = '') {
|
|
39
|
+
let q = col;
|
|
40
|
+
if (prefix) {
|
|
41
|
+
q = col.where('k', '>=', prefix).where('k', '<', prefix + '\uf8ff');
|
|
42
|
+
}
|
|
43
|
+
const snap = await q.orderBy('k').get();
|
|
44
|
+
return snap.docs.map((d) => d.data().k ?? decodeDocId(d.id));
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|