@prometheus-ags/entity-graph-core 3.2.0 → 4.0.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/CHANGELOG.md +62 -0
- package/dist/{chunk-BE3BMRKF.mjs → chunk-H7NBREZV.mjs} +17 -5
- package/dist/chunk-H7NBREZV.mjs.map +1 -0
- package/dist/devtools.mjs +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +7 -19
- package/dist/chunk-BE3BMRKF.mjs.map +0 -1
- package/dist/devtools.cjs +0 -2571
- package/dist/devtools.cjs.map +0 -1
- package/dist/devtools.d.cts +0 -531
- package/dist/graph-ClW3HD8B.d.cts +0 -990
- package/dist/index.cjs +0 -4199
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -2383
package/dist/index.cjs
DELETED
|
@@ -1,4199 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var vanilla = require('zustand/vanilla');
|
|
4
|
-
var middleware = require('zustand/middleware');
|
|
5
|
-
var immer = require('zustand/middleware/immer');
|
|
6
|
-
|
|
7
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
-
}) : x)(function(x) {
|
|
10
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
// src/errors.ts
|
|
15
|
-
var TerminalError = class _TerminalError extends Error {
|
|
16
|
-
kind = "terminal";
|
|
17
|
-
status;
|
|
18
|
-
constructor(message, opts = {}) {
|
|
19
|
-
super(message, { cause: opts.cause });
|
|
20
|
-
this.name = "TerminalError";
|
|
21
|
-
this.status = opts.status;
|
|
22
|
-
if (typeof Error.captureStackTrace === "function") {
|
|
23
|
-
Error.captureStackTrace(this, _TerminalError);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
var TransientError = class _TransientError extends Error {
|
|
28
|
-
kind = "transient";
|
|
29
|
-
status;
|
|
30
|
-
constructor(message, opts = {}) {
|
|
31
|
-
super(message, { cause: opts.cause });
|
|
32
|
-
this.name = "TransientError";
|
|
33
|
-
this.status = opts.status;
|
|
34
|
-
if (typeof Error.captureStackTrace === "function") {
|
|
35
|
-
Error.captureStackTrace(this, _TransientError);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
function toEntityError(err) {
|
|
40
|
-
if (err instanceof TerminalError) return err;
|
|
41
|
-
if (err instanceof TransientError) return err;
|
|
42
|
-
if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
|
|
43
|
-
return new TerminalError("Aborted", { cause: err });
|
|
44
|
-
}
|
|
45
|
-
if (err && typeof err === "object" && "status" in err) {
|
|
46
|
-
const status = Number(err.status);
|
|
47
|
-
const message2 = err && typeof err === "object" && "message" in err ? String(err.message ?? `HTTP ${status}`) : `HTTP ${status}`;
|
|
48
|
-
if (Number.isFinite(status) && status >= 400 && status < 500) {
|
|
49
|
-
return new TerminalError(message2, { status, cause: err });
|
|
50
|
-
}
|
|
51
|
-
if (Number.isFinite(status) && status >= 500) {
|
|
52
|
-
return new TransientError(message2, { status, cause: err });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
-
return new TransientError(message, { cause: err });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/transport/registry.ts
|
|
60
|
-
var transports = /* @__PURE__ */ new Map();
|
|
61
|
-
function registerEntityTransport(type, transport) {
|
|
62
|
-
transports.set(type, transport);
|
|
63
|
-
}
|
|
64
|
-
function getEntityTransport(type) {
|
|
65
|
-
const t = transports.get(type);
|
|
66
|
-
if (!t) {
|
|
67
|
-
throw new Error(
|
|
68
|
-
`[entity-management] No transport registered for entity type "${type}". Call registerEntityTransport("${type}", ...) at app boot.`
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
return t;
|
|
72
|
-
}
|
|
73
|
-
function __resetEntityTransports() {
|
|
74
|
-
transports.clear();
|
|
75
|
-
}
|
|
76
|
-
function getRegisteredEntityTypes() {
|
|
77
|
-
return Array.from(transports.keys());
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// src/active-store.ts
|
|
81
|
-
var moduleActiveStore = null;
|
|
82
|
-
var asyncScope = null;
|
|
83
|
-
var asyncScopeAttempted = false;
|
|
84
|
-
function isNodeLike() {
|
|
85
|
-
return typeof process !== "undefined" && typeof process.versions?.node === "string";
|
|
86
|
-
}
|
|
87
|
-
function resolveAsyncScope() {
|
|
88
|
-
if (asyncScopeAttempted) return asyncScope;
|
|
89
|
-
asyncScopeAttempted = true;
|
|
90
|
-
if (!isNodeLike()) return null;
|
|
91
|
-
try {
|
|
92
|
-
const req = typeof module !== "undefined" && typeof module.require === "function" ? module.require : typeof __require === "function" ? __require : null;
|
|
93
|
-
if (!req) return null;
|
|
94
|
-
const { AsyncLocalStorage } = req("node:async_hooks");
|
|
95
|
-
asyncScope = new AsyncLocalStorage();
|
|
96
|
-
} catch {
|
|
97
|
-
asyncScope = null;
|
|
98
|
-
}
|
|
99
|
-
return asyncScope;
|
|
100
|
-
}
|
|
101
|
-
async function prepareGraphStoreScope() {
|
|
102
|
-
if (asyncScope) return true;
|
|
103
|
-
if (!isNodeLike()) {
|
|
104
|
-
asyncScopeAttempted = true;
|
|
105
|
-
return false;
|
|
106
|
-
}
|
|
107
|
-
if (resolveAsyncScope()) return true;
|
|
108
|
-
try {
|
|
109
|
-
const specifier = "node:async_hooks";
|
|
110
|
-
const mod = await import(
|
|
111
|
-
/* @vite-ignore */
|
|
112
|
-
specifier
|
|
113
|
-
);
|
|
114
|
-
asyncScope = new mod.AsyncLocalStorage();
|
|
115
|
-
asyncScopeAttempted = true;
|
|
116
|
-
return true;
|
|
117
|
-
} catch {
|
|
118
|
-
asyncScopeAttempted = true;
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function setActiveGraphStore(store) {
|
|
123
|
-
const previous = moduleActiveStore;
|
|
124
|
-
moduleActiveStore = store;
|
|
125
|
-
return () => {
|
|
126
|
-
moduleActiveStore = previous;
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
var warnedUnscopedFallback = false;
|
|
130
|
-
function warnUnscopedRequestFallback() {
|
|
131
|
-
if (warnedUnscopedFallback || !isNodeLike()) return;
|
|
132
|
-
warnedUnscopedFallback = true;
|
|
133
|
-
console.warn(
|
|
134
|
-
"[entity-graph-core] runWithGraphStore() could not load node:async_hooks, so it fell back to a module-level store. Concurrent requests will NOT be isolated. Await prepareGraphStoreScope() once during server startup."
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
function runWithGraphStore(store, fn) {
|
|
138
|
-
const scope = resolveAsyncScope();
|
|
139
|
-
if (!scope) {
|
|
140
|
-
warnUnscopedRequestFallback();
|
|
141
|
-
const restore = setActiveGraphStore(store);
|
|
142
|
-
try {
|
|
143
|
-
return fn();
|
|
144
|
-
} finally {
|
|
145
|
-
restore();
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return scope.run(store, fn);
|
|
149
|
-
}
|
|
150
|
-
function resolveActiveGraphStore(fallback) {
|
|
151
|
-
const scoped = resolveAsyncScope()?.getStore();
|
|
152
|
-
if (scoped) return scoped;
|
|
153
|
-
return moduleActiveStore ?? fallback;
|
|
154
|
-
}
|
|
155
|
-
function __resetActiveGraphStore() {
|
|
156
|
-
moduleActiveStore = null;
|
|
157
|
-
asyncScope = null;
|
|
158
|
-
asyncScopeAttempted = false;
|
|
159
|
-
warnedUnscopedFallback = false;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// src/view/prisma-compile.ts
|
|
163
|
-
function nestWhereField(parts, leaf) {
|
|
164
|
-
if (parts.length === 0) return {};
|
|
165
|
-
if (parts.length === 1) return { [parts[0]]: leaf };
|
|
166
|
-
return { [parts[0]]: nestWhereField(parts.slice(1), leaf) };
|
|
167
|
-
}
|
|
168
|
-
function clauseToPrismaLeaf(c) {
|
|
169
|
-
switch (c.op) {
|
|
170
|
-
case "eq":
|
|
171
|
-
return { equals: c.value };
|
|
172
|
-
case "neq":
|
|
173
|
-
return { not: c.value };
|
|
174
|
-
case "gt":
|
|
175
|
-
return { gt: c.value };
|
|
176
|
-
case "gte":
|
|
177
|
-
return { gte: c.value };
|
|
178
|
-
case "lt":
|
|
179
|
-
return { lt: c.value };
|
|
180
|
-
case "lte":
|
|
181
|
-
return { lte: c.value };
|
|
182
|
-
case "contains":
|
|
183
|
-
return { contains: c.value, mode: "insensitive" };
|
|
184
|
-
case "startsWith":
|
|
185
|
-
return { startsWith: c.value, mode: "insensitive" };
|
|
186
|
-
case "endsWith":
|
|
187
|
-
return { endsWith: c.value, mode: "insensitive" };
|
|
188
|
-
case "in":
|
|
189
|
-
return { in: c.value };
|
|
190
|
-
case "nin":
|
|
191
|
-
return { notIn: c.value };
|
|
192
|
-
case "arrayContains":
|
|
193
|
-
return { has: c.value };
|
|
194
|
-
case "between":
|
|
195
|
-
case "arrayOverlaps":
|
|
196
|
-
case "matches":
|
|
197
|
-
case "custom":
|
|
198
|
-
default:
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
function clauseToPrismaEntry(c) {
|
|
203
|
-
const parts = c.field.split(".").filter(Boolean);
|
|
204
|
-
if (parts.length === 0) return null;
|
|
205
|
-
if (c.op === "isNull") {
|
|
206
|
-
const equalsNull = c.value === void 0 || c.value === true;
|
|
207
|
-
return nestWhereField(parts, equalsNull ? null : { not: null });
|
|
208
|
-
}
|
|
209
|
-
if (c.op === "isNotNull") {
|
|
210
|
-
return nestWhereField(parts, { not: null });
|
|
211
|
-
}
|
|
212
|
-
const leaf = clauseToPrismaLeaf(c);
|
|
213
|
-
if (leaf === null) return null;
|
|
214
|
-
return nestWhereField(parts, leaf);
|
|
215
|
-
}
|
|
216
|
-
function groupToPrismaWhere(g) {
|
|
217
|
-
const parts = [];
|
|
218
|
-
for (const item of g.clauses) {
|
|
219
|
-
if ("logic" in item) {
|
|
220
|
-
const nested = groupToPrismaWhere(item);
|
|
221
|
-
if (Object.keys(nested).length > 0) parts.push(nested);
|
|
222
|
-
} else {
|
|
223
|
-
const entry = clauseToPrismaEntry(item);
|
|
224
|
-
if (entry) parts.push(entry);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
if (parts.length === 0) return {};
|
|
228
|
-
if (parts.length === 1) return parts[0];
|
|
229
|
-
return g.logic === "or" ? { OR: parts } : { AND: parts };
|
|
230
|
-
}
|
|
231
|
-
function toPrismaWhere(filter) {
|
|
232
|
-
if (Array.isArray(filter)) {
|
|
233
|
-
const parts = [];
|
|
234
|
-
for (const item of filter) {
|
|
235
|
-
const entry = clauseToPrismaEntry(item);
|
|
236
|
-
if (entry) parts.push(entry);
|
|
237
|
-
}
|
|
238
|
-
if (parts.length === 0) return {};
|
|
239
|
-
if (parts.length === 1) return parts[0];
|
|
240
|
-
return { AND: parts };
|
|
241
|
-
}
|
|
242
|
-
return groupToPrismaWhere(filter);
|
|
243
|
-
}
|
|
244
|
-
function toPrismaOrderBy(sort) {
|
|
245
|
-
return sort.map((s) => ({ [s.field]: s.direction }));
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/view/types.ts
|
|
249
|
-
function toRestParams(view) {
|
|
250
|
-
const params = {};
|
|
251
|
-
if (view.filter) {
|
|
252
|
-
const clauses = flattenClauses(view.filter);
|
|
253
|
-
for (const c of clauses) {
|
|
254
|
-
if (c.op === "custom") continue;
|
|
255
|
-
const key = c.op === "eq" ? c.field : `${c.field}[${c.op}]`;
|
|
256
|
-
params[key] = Array.isArray(c.value) ? c.value.join(",") : String(c.value ?? "");
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
if (view.sort) params["sort"] = view.sort.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
|
|
260
|
-
if (view.search?.query) params["q"] = view.search.query;
|
|
261
|
-
return params;
|
|
262
|
-
}
|
|
263
|
-
function toSQLClauses(view) {
|
|
264
|
-
const params = [];
|
|
265
|
-
let paramIdx = 1;
|
|
266
|
-
function clauseToSQL(c) {
|
|
267
|
-
const col = `"${c.field}"`;
|
|
268
|
-
switch (c.op) {
|
|
269
|
-
case "eq":
|
|
270
|
-
params.push(c.value);
|
|
271
|
-
return `${col} = $${paramIdx++}`;
|
|
272
|
-
case "neq":
|
|
273
|
-
params.push(c.value);
|
|
274
|
-
return `${col} != $${paramIdx++}`;
|
|
275
|
-
case "gt":
|
|
276
|
-
params.push(c.value);
|
|
277
|
-
return `${col} > $${paramIdx++}`;
|
|
278
|
-
case "gte":
|
|
279
|
-
params.push(c.value);
|
|
280
|
-
return `${col} >= $${paramIdx++}`;
|
|
281
|
-
case "lt":
|
|
282
|
-
params.push(c.value);
|
|
283
|
-
return `${col} < $${paramIdx++}`;
|
|
284
|
-
case "lte":
|
|
285
|
-
params.push(c.value);
|
|
286
|
-
return `${col} <= $${paramIdx++}`;
|
|
287
|
-
case "in":
|
|
288
|
-
params.push(c.value);
|
|
289
|
-
return `${col} = ANY($${paramIdx++})`;
|
|
290
|
-
case "nin":
|
|
291
|
-
params.push(c.value);
|
|
292
|
-
return `${col} != ALL($${paramIdx++})`;
|
|
293
|
-
case "isNull":
|
|
294
|
-
return `${col} IS NULL`;
|
|
295
|
-
case "isNotNull":
|
|
296
|
-
return `${col} IS NOT NULL`;
|
|
297
|
-
case "contains":
|
|
298
|
-
params.push(`%${c.value}%`);
|
|
299
|
-
return `${col} ILIKE $${paramIdx++}`;
|
|
300
|
-
case "startsWith":
|
|
301
|
-
params.push(`${c.value}%`);
|
|
302
|
-
return `${col} ILIKE $${paramIdx++}`;
|
|
303
|
-
case "between": {
|
|
304
|
-
const [lo, hi] = c.value;
|
|
305
|
-
params.push(lo, hi);
|
|
306
|
-
return `${col} BETWEEN $${paramIdx++} AND $${paramIdx++}`;
|
|
307
|
-
}
|
|
308
|
-
case "arrayContains":
|
|
309
|
-
params.push(c.value);
|
|
310
|
-
return `$${paramIdx++} = ANY(${col})`;
|
|
311
|
-
default:
|
|
312
|
-
return "TRUE";
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function groupToSQL(g) {
|
|
316
|
-
const parts = g.clauses.map((c) => "logic" in c ? `(${groupToSQL(c)})` : clauseToSQL(c));
|
|
317
|
-
return parts.join(` ${g.logic.toUpperCase()} `);
|
|
318
|
-
}
|
|
319
|
-
let where = "TRUE";
|
|
320
|
-
if (view.filter) {
|
|
321
|
-
if (Array.isArray(view.filter)) where = view.filter.map(clauseToSQL).join(" AND ") || "TRUE";
|
|
322
|
-
else where = groupToSQL(view.filter) || "TRUE";
|
|
323
|
-
}
|
|
324
|
-
if (view.search?.query) {
|
|
325
|
-
params.push(`%${view.search.query}%`);
|
|
326
|
-
where += ` AND (${view.search.fields.map((f) => `"${f}"`).join(" || ' ' || ")}) ILIKE $${paramIdx++}`;
|
|
327
|
-
}
|
|
328
|
-
const orderBy = view.sort ? view.sort.map((s) => `"${s.field}" ${s.direction.toUpperCase()}${s.nulls ? ` NULLS ${s.nulls.toUpperCase()}` : ""}`).join(", ") : "";
|
|
329
|
-
return { where, orderBy, params };
|
|
330
|
-
}
|
|
331
|
-
function toGraphQLVariables(view) {
|
|
332
|
-
const result = {};
|
|
333
|
-
if (view.filter) {
|
|
334
|
-
const clauses = flattenClauses(view.filter);
|
|
335
|
-
const where = {};
|
|
336
|
-
for (const c of clauses) {
|
|
337
|
-
if (c.op === "custom") continue;
|
|
338
|
-
where[c.field] = { [`_${c.op}`]: c.value };
|
|
339
|
-
}
|
|
340
|
-
if (Object.keys(where).length) result.where = where;
|
|
341
|
-
}
|
|
342
|
-
if (view.sort) result.orderBy = view.sort.map((s) => ({ [s.field]: s.direction === "desc" ? "desc_nulls_last" : "asc_nulls_last" }));
|
|
343
|
-
if (view.search?.query) result.search = view.search.query;
|
|
344
|
-
return result;
|
|
345
|
-
}
|
|
346
|
-
function flattenClauses(filter) {
|
|
347
|
-
if (Array.isArray(filter)) return filter;
|
|
348
|
-
function walk(g) {
|
|
349
|
-
return g.clauses.flatMap((c) => "logic" in c ? walk(c) : [c]);
|
|
350
|
-
}
|
|
351
|
-
return walk(filter);
|
|
352
|
-
}
|
|
353
|
-
function hasCustomPredicates(filter) {
|
|
354
|
-
return flattenClauses(filter).some((c) => c.op === "custom");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// src/transport/rest.ts
|
|
358
|
-
function makeRestTransport(opts) {
|
|
359
|
-
const {
|
|
360
|
-
supabase,
|
|
361
|
-
table,
|
|
362
|
-
select = "*",
|
|
363
|
-
identify = (row) => String(row.id),
|
|
364
|
-
authoritative = false,
|
|
365
|
-
staleTime,
|
|
366
|
-
searchColumns = [],
|
|
367
|
-
defaultLimit = 200
|
|
368
|
-
} = opts;
|
|
369
|
-
return {
|
|
370
|
-
identify,
|
|
371
|
-
authoritative,
|
|
372
|
-
staleTime,
|
|
373
|
-
async list(q) {
|
|
374
|
-
const limit = q.limit ?? defaultLimit;
|
|
375
|
-
const offset = typeof q.cursor === "number" ? q.cursor : 0;
|
|
376
|
-
let builder = supabase.from(table).select(select, { count: offset === 0 ? "exact" : void 0 });
|
|
377
|
-
if (q.filter) builder = applyFilter(builder, q.filter);
|
|
378
|
-
if (q.search && searchColumns.length > 0) {
|
|
379
|
-
const escaped = q.search.replace(/[%,()]/g, "");
|
|
380
|
-
const orClause = searchColumns.map((c) => `${c}.ilike.*${escaped}*`).join(",");
|
|
381
|
-
builder = builder.or(orClause);
|
|
382
|
-
}
|
|
383
|
-
if (q.sort) {
|
|
384
|
-
for (const s of q.sort) {
|
|
385
|
-
builder = builder.order(s.field, {
|
|
386
|
-
ascending: s.direction === "asc",
|
|
387
|
-
nullsFirst: s.nulls === "first"
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
builder = builder.range(offset, offset + limit - 1);
|
|
392
|
-
if (q.signal && builder.abortSignal) builder = builder.abortSignal(q.signal);
|
|
393
|
-
const result = await builder;
|
|
394
|
-
if (result.error) {
|
|
395
|
-
throw mapPostgrestError(result.error, result.status);
|
|
396
|
-
}
|
|
397
|
-
const rows = result.data ?? [];
|
|
398
|
-
const total = typeof result.count === "number" ? result.count : null;
|
|
399
|
-
const nextCursor = rows.length === limit ? offset + limit : null;
|
|
400
|
-
return { rows, total, nextCursor };
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
function applyFilter(builder, filter) {
|
|
405
|
-
if (!Array.isArray(filter) && filter.logic === "or") {
|
|
406
|
-
const parts = filter.clauses.filter((c) => !("logic" in c)).map((c) => clauseToOrFragment(c)).filter((s) => !!s);
|
|
407
|
-
if (parts.length > 0) return builder.or(parts.join(","));
|
|
408
|
-
return builder;
|
|
409
|
-
}
|
|
410
|
-
const clauses = flattenClauses(filter);
|
|
411
|
-
let b = builder;
|
|
412
|
-
for (const c of clauses) {
|
|
413
|
-
b = applyClause(b, c);
|
|
414
|
-
}
|
|
415
|
-
return b;
|
|
416
|
-
}
|
|
417
|
-
function applyClause(builder, c) {
|
|
418
|
-
switch (c.op) {
|
|
419
|
-
case "eq":
|
|
420
|
-
return builder.eq(c.field, c.value);
|
|
421
|
-
case "neq":
|
|
422
|
-
return builder.neq(c.field, c.value);
|
|
423
|
-
case "gt":
|
|
424
|
-
return builder.gt(c.field, c.value);
|
|
425
|
-
case "gte":
|
|
426
|
-
return builder.gte(c.field, c.value);
|
|
427
|
-
case "lt":
|
|
428
|
-
return builder.lt(c.field, c.value);
|
|
429
|
-
case "lte":
|
|
430
|
-
return builder.lte(c.field, c.value);
|
|
431
|
-
case "in":
|
|
432
|
-
return builder.in(c.field, c.value);
|
|
433
|
-
case "isNull":
|
|
434
|
-
return builder.is(c.field, null);
|
|
435
|
-
case "isNotNull":
|
|
436
|
-
return builder.neq(c.field, null);
|
|
437
|
-
case "contains":
|
|
438
|
-
return builder.ilike(c.field, `%${String(c.value)}%`);
|
|
439
|
-
case "startsWith":
|
|
440
|
-
return builder.ilike(c.field, `${String(c.value)}%`);
|
|
441
|
-
case "endsWith":
|
|
442
|
-
return builder.ilike(c.field, `%${String(c.value)}`);
|
|
443
|
-
case "matches":
|
|
444
|
-
return builder.ilike(c.field, String(c.value));
|
|
445
|
-
case "custom":
|
|
446
|
-
return builder;
|
|
447
|
-
// local-only predicate; skip on server
|
|
448
|
-
default:
|
|
449
|
-
return builder;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
function clauseToOrFragment(c) {
|
|
453
|
-
const v = encodeURIComponent(String(c.value ?? ""));
|
|
454
|
-
switch (c.op) {
|
|
455
|
-
case "eq":
|
|
456
|
-
return `${c.field}.eq.${v}`;
|
|
457
|
-
case "neq":
|
|
458
|
-
return `${c.field}.neq.${v}`;
|
|
459
|
-
case "gt":
|
|
460
|
-
return `${c.field}.gt.${v}`;
|
|
461
|
-
case "gte":
|
|
462
|
-
return `${c.field}.gte.${v}`;
|
|
463
|
-
case "lt":
|
|
464
|
-
return `${c.field}.lt.${v}`;
|
|
465
|
-
case "lte":
|
|
466
|
-
return `${c.field}.lte.${v}`;
|
|
467
|
-
case "isNull":
|
|
468
|
-
return `${c.field}.is.null`;
|
|
469
|
-
case "isNotNull":
|
|
470
|
-
return `${c.field}.not.is.null`;
|
|
471
|
-
case "contains":
|
|
472
|
-
return `${c.field}.ilike.*${v}*`;
|
|
473
|
-
case "startsWith":
|
|
474
|
-
return `${c.field}.ilike.${v}*`;
|
|
475
|
-
case "endsWith":
|
|
476
|
-
return `${c.field}.ilike.*${v}`;
|
|
477
|
-
default:
|
|
478
|
-
return null;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
function mapPostgrestError(error, status) {
|
|
482
|
-
if (error instanceof TerminalError || error instanceof TransientError) return error;
|
|
483
|
-
const message = error && typeof error === "object" && "message" in error ? String(error.message ?? "Unknown REST error") : String(error);
|
|
484
|
-
if (typeof status === "number") {
|
|
485
|
-
if (status >= 400 && status < 500) {
|
|
486
|
-
return new TerminalError(message, { status, cause: error });
|
|
487
|
-
}
|
|
488
|
-
if (status >= 500) {
|
|
489
|
-
return new TransientError(message, { status, cause: error });
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
|
|
493
|
-
if (code === "42P01" || code === "PGRST116" || code === "PGRST106") {
|
|
494
|
-
return new TerminalError(message, { cause: error });
|
|
495
|
-
}
|
|
496
|
-
return toEntityError(error);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// src/merge/lww.ts
|
|
500
|
-
var lwwStrategy = (prev, next) => {
|
|
501
|
-
return prev ? { ...prev, ...next } : { ...next };
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
// src/merge/registry.ts
|
|
505
|
-
var byType = /* @__PURE__ */ new Map();
|
|
506
|
-
var globalDefault = null;
|
|
507
|
-
function registerMergeStrategy(type, strategy) {
|
|
508
|
-
byType.set(type, strategy);
|
|
509
|
-
}
|
|
510
|
-
function setDefaultMergeStrategy(strategy) {
|
|
511
|
-
globalDefault = strategy;
|
|
512
|
-
}
|
|
513
|
-
function getMergeStrategy(type) {
|
|
514
|
-
return byType.get(type) ?? globalDefault ?? lwwStrategy;
|
|
515
|
-
}
|
|
516
|
-
function hasMergeStrategy(type) {
|
|
517
|
-
return byType.has(type) || globalDefault !== null;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// src/view/evaluator.ts
|
|
521
|
-
function matchesFilter(entity, filter) {
|
|
522
|
-
if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
|
|
523
|
-
return matchesGroup(entity, filter);
|
|
524
|
-
}
|
|
525
|
-
function matchesGroup(entity, group) {
|
|
526
|
-
const { logic, clauses } = group;
|
|
527
|
-
if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
528
|
-
return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
|
|
529
|
-
}
|
|
530
|
-
function matchesClause(entity, clause) {
|
|
531
|
-
const { field, op, value, predicate } = clause;
|
|
532
|
-
const fv = getNestedValue(entity, field);
|
|
533
|
-
switch (op) {
|
|
534
|
-
case "eq":
|
|
535
|
-
return fv === value;
|
|
536
|
-
case "neq":
|
|
537
|
-
return fv !== value;
|
|
538
|
-
case "gt":
|
|
539
|
-
return fv > value;
|
|
540
|
-
case "gte":
|
|
541
|
-
return fv >= value;
|
|
542
|
-
case "lt":
|
|
543
|
-
return fv < value;
|
|
544
|
-
case "lte":
|
|
545
|
-
return fv <= value;
|
|
546
|
-
case "in":
|
|
547
|
-
return Array.isArray(value) && value.includes(fv);
|
|
548
|
-
case "nin":
|
|
549
|
-
return Array.isArray(value) && !value.includes(fv);
|
|
550
|
-
case "isNull":
|
|
551
|
-
return fv == null;
|
|
552
|
-
case "isNotNull":
|
|
553
|
-
return fv != null;
|
|
554
|
-
case "contains":
|
|
555
|
-
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
|
|
556
|
-
case "startsWith":
|
|
557
|
-
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
|
|
558
|
-
case "endsWith":
|
|
559
|
-
return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
|
|
560
|
-
case "between": {
|
|
561
|
-
const [lo, hi] = value;
|
|
562
|
-
return fv >= lo && fv <= hi;
|
|
563
|
-
}
|
|
564
|
-
case "arrayContains":
|
|
565
|
-
return Array.isArray(fv) && fv.includes(value);
|
|
566
|
-
case "arrayOverlaps":
|
|
567
|
-
return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
|
|
568
|
-
case "matches":
|
|
569
|
-
return typeof fv === "string" && new RegExp(value).test(fv);
|
|
570
|
-
case "custom":
|
|
571
|
-
return predicate ? predicate(fv, entity) : true;
|
|
572
|
-
default:
|
|
573
|
-
return true;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
function matchesSearch(entity, query, fields) {
|
|
577
|
-
if (!query.trim()) return true;
|
|
578
|
-
const lq = query.toLowerCase();
|
|
579
|
-
return fields.some((field) => {
|
|
580
|
-
const v = getNestedValue(entity, field);
|
|
581
|
-
return typeof v === "string" && v.toLowerCase().includes(lq);
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
function compareEntities(a, b, sort) {
|
|
585
|
-
for (const clause of sort) {
|
|
586
|
-
const r = compareByClause(a, b, clause);
|
|
587
|
-
if (r !== 0) return r;
|
|
588
|
-
}
|
|
589
|
-
return 0;
|
|
590
|
-
}
|
|
591
|
-
function compareByClause(a, b, clause) {
|
|
592
|
-
const { field, direction, nulls = "last", comparator } = clause;
|
|
593
|
-
const av = getNestedValue(a, field);
|
|
594
|
-
const bv = getNestedValue(b, field);
|
|
595
|
-
const aNull = av == null;
|
|
596
|
-
const bNull = bv == null;
|
|
597
|
-
if (aNull && bNull) return 0;
|
|
598
|
-
if (aNull) return nulls === "first" ? -1 : 1;
|
|
599
|
-
if (bNull) return nulls === "first" ? 1 : -1;
|
|
600
|
-
let cmp;
|
|
601
|
-
if (comparator) cmp = comparator(av, bv);
|
|
602
|
-
else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
|
|
603
|
-
else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
|
|
604
|
-
else cmp = String(av).localeCompare(String(bv));
|
|
605
|
-
return direction === "desc" ? -cmp : cmp;
|
|
606
|
-
}
|
|
607
|
-
function findInsertionIndex(entity, sortedIds, getEntity, sort) {
|
|
608
|
-
let lo = 0;
|
|
609
|
-
let hi = sortedIds.length;
|
|
610
|
-
while (lo < hi) {
|
|
611
|
-
const mid = lo + hi >>> 1;
|
|
612
|
-
const me = getEntity(sortedIds[mid]);
|
|
613
|
-
if (!me) {
|
|
614
|
-
lo = mid + 1;
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
if (compareEntities(entity, me, sort) <= 0) hi = mid;
|
|
618
|
-
else lo = mid + 1;
|
|
619
|
-
}
|
|
620
|
-
return lo;
|
|
621
|
-
}
|
|
622
|
-
function applyView(ids, getEntity, filter, sort, search) {
|
|
623
|
-
let entries = [];
|
|
624
|
-
for (const id of ids) {
|
|
625
|
-
const entity = getEntity(id);
|
|
626
|
-
if (!entity) continue;
|
|
627
|
-
entries.push({ id, entity });
|
|
628
|
-
}
|
|
629
|
-
if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
|
|
630
|
-
if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
|
|
631
|
-
if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
|
|
632
|
-
return entries.map((e) => e.id);
|
|
633
|
-
}
|
|
634
|
-
function checkCompleteness(loadedCount, total, hasNextPage) {
|
|
635
|
-
if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
|
|
636
|
-
if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
|
|
637
|
-
return { isComplete: true, reason: "no-more-pages" };
|
|
638
|
-
}
|
|
639
|
-
function getNestedValue(obj, path) {
|
|
640
|
-
const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
641
|
-
let current = obj;
|
|
642
|
-
for (const part of parts) {
|
|
643
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
644
|
-
current = current[part];
|
|
645
|
-
}
|
|
646
|
-
return current;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// src/graph.ts
|
|
650
|
-
var EMPTY_IDS = [];
|
|
651
|
-
var EMPTY_ENTITY_STATE = {
|
|
652
|
-
isFetching: false,
|
|
653
|
-
lastFetched: null,
|
|
654
|
-
error: null,
|
|
655
|
-
stale: false
|
|
656
|
-
};
|
|
657
|
-
var EMPTY_SYNC_METADATA = {
|
|
658
|
-
synced: true,
|
|
659
|
-
origin: "server",
|
|
660
|
-
updatedAt: null
|
|
661
|
-
};
|
|
662
|
-
var EMPTY_LIST_STATE = {
|
|
663
|
-
ids: EMPTY_IDS,
|
|
664
|
-
total: null,
|
|
665
|
-
nextCursor: null,
|
|
666
|
-
prevCursor: null,
|
|
667
|
-
hasNextPage: false,
|
|
668
|
-
hasPrevPage: false,
|
|
669
|
-
isFetching: false,
|
|
670
|
-
isFetchingMore: false,
|
|
671
|
-
error: null,
|
|
672
|
-
lastError: null,
|
|
673
|
-
lastFetched: null,
|
|
674
|
-
stale: false,
|
|
675
|
-
currentPage: null,
|
|
676
|
-
pageSize: null
|
|
677
|
-
};
|
|
678
|
-
function defaultEntityState() {
|
|
679
|
-
return { ...EMPTY_ENTITY_STATE };
|
|
680
|
-
}
|
|
681
|
-
function defaultSyncMetadata() {
|
|
682
|
-
return { ...EMPTY_SYNC_METADATA };
|
|
683
|
-
}
|
|
684
|
-
function defaultListState() {
|
|
685
|
-
return { ...EMPTY_LIST_STATE, ids: [] };
|
|
686
|
-
}
|
|
687
|
-
function ek(type, id) {
|
|
688
|
-
return `${type}:${id}`;
|
|
689
|
-
}
|
|
690
|
-
var snapshotCache = /* @__PURE__ */ new Map();
|
|
691
|
-
function readCachedEntitySnapshot(type, id, base, patch, metadata) {
|
|
692
|
-
const key = ek(type, id);
|
|
693
|
-
const cached = snapshotCache.get(key);
|
|
694
|
-
if (cached && cached.base === base && cached.patch === patch && cached.metadata === metadata) {
|
|
695
|
-
return cached.snapshot;
|
|
696
|
-
}
|
|
697
|
-
const snapshot = {
|
|
698
|
-
...patch ? { ...base, ...patch } : base,
|
|
699
|
-
$synced: metadata.synced,
|
|
700
|
-
$origin: metadata.origin,
|
|
701
|
-
$updatedAt: metadata.updatedAt
|
|
702
|
-
};
|
|
703
|
-
snapshotCache.set(key, { base, patch, metadata, snapshot });
|
|
704
|
-
return snapshot;
|
|
705
|
-
}
|
|
706
|
-
function createGraphStore() {
|
|
707
|
-
return vanilla.createStore()(
|
|
708
|
-
middleware.subscribeWithSelector(
|
|
709
|
-
immer.immer((set, get) => ({
|
|
710
|
-
entities: {},
|
|
711
|
-
patches: {},
|
|
712
|
-
entityStates: {},
|
|
713
|
-
syncMetadata: {},
|
|
714
|
-
lists: {},
|
|
715
|
-
upsertEntity: (type, id, data) => set((s) => {
|
|
716
|
-
if (!s.entities[type]) s.entities[type] = {};
|
|
717
|
-
const key = ek(type, id);
|
|
718
|
-
const strategy = getMergeStrategy(type);
|
|
719
|
-
const ctx = { type, id, origin: s.syncMetadata[key]?.origin ?? "server", updatedAt: Date.now() };
|
|
720
|
-
s.entities[type][id] = strategy(s.entities[type][id], data, ctx);
|
|
721
|
-
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
722
|
-
}),
|
|
723
|
-
upsertEntities: (type, entries) => set((s) => {
|
|
724
|
-
if (!s.entities[type]) s.entities[type] = {};
|
|
725
|
-
const strategy = getMergeStrategy(type);
|
|
726
|
-
for (const { id, data } of entries) {
|
|
727
|
-
const key = ek(type, id);
|
|
728
|
-
const ctx = { type, id, origin: s.syncMetadata[key]?.origin ?? "server", updatedAt: Date.now() };
|
|
729
|
-
s.entities[type][id] = strategy(s.entities[type][id], data, ctx);
|
|
730
|
-
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
731
|
-
}
|
|
732
|
-
}),
|
|
733
|
-
ingestFetchedList: (type, entries, options = {}) => set((s) => {
|
|
734
|
-
const fetchedAt = Date.now();
|
|
735
|
-
const ids = entries.map(({ id }) => id);
|
|
736
|
-
const batches = [
|
|
737
|
-
{ type, entries },
|
|
738
|
-
...options.sideBatches ?? []
|
|
739
|
-
];
|
|
740
|
-
const origins = /* @__PURE__ */ new Map();
|
|
741
|
-
for (const batch of batches) {
|
|
742
|
-
if (!s.entities[batch.type]) s.entities[batch.type] = {};
|
|
743
|
-
for (const { id } of batch.entries) {
|
|
744
|
-
const key = ek(batch.type, id);
|
|
745
|
-
if (!origins.has(key)) {
|
|
746
|
-
origins.set(key, s.syncMetadata[key]?.origin ?? "server");
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
for (const batch of batches) {
|
|
751
|
-
const strategy = getMergeStrategy(batch.type);
|
|
752
|
-
for (const { id, data } of batch.entries) {
|
|
753
|
-
const key = ek(batch.type, id);
|
|
754
|
-
const ctx = {
|
|
755
|
-
type: batch.type,
|
|
756
|
-
id,
|
|
757
|
-
origin: origins.get(key) ?? "server",
|
|
758
|
-
updatedAt: fetchedAt
|
|
759
|
-
};
|
|
760
|
-
s.entities[batch.type][id] = strategy(s.entities[batch.type][id], data, ctx);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
const completed = /* @__PURE__ */ new Set();
|
|
764
|
-
for (const batch of batches) {
|
|
765
|
-
for (const { id } of batch.entries) {
|
|
766
|
-
const key = ek(batch.type, id);
|
|
767
|
-
if (completed.has(key)) continue;
|
|
768
|
-
completed.add(key);
|
|
769
|
-
if (!s.entityStates[key]) s.entityStates[key] = defaultEntityState();
|
|
770
|
-
s.entityStates[key].lastFetched = fetchedAt;
|
|
771
|
-
s.entityStates[key].isFetching = false;
|
|
772
|
-
s.entityStates[key].error = null;
|
|
773
|
-
s.entityStates[key].stale = false;
|
|
774
|
-
s.syncMetadata[key] = {
|
|
775
|
-
...s.syncMetadata[key] ?? defaultSyncMetadata(),
|
|
776
|
-
synced: true,
|
|
777
|
-
origin: "server",
|
|
778
|
-
updatedAt: fetchedAt
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
const readDraftEntity = (id) => {
|
|
783
|
-
const base = s.entities[type]?.[id];
|
|
784
|
-
if (!base) return null;
|
|
785
|
-
const patch = s.patches[type]?.[id];
|
|
786
|
-
const metadata = s.syncMetadata[ek(type, id)] ?? defaultSyncMetadata();
|
|
787
|
-
return {
|
|
788
|
-
...base,
|
|
789
|
-
...patch ?? {},
|
|
790
|
-
$synced: metadata.synced,
|
|
791
|
-
$origin: metadata.origin,
|
|
792
|
-
$updatedAt: metadata.updatedAt
|
|
793
|
-
};
|
|
794
|
-
};
|
|
795
|
-
for (const target of options.lists ?? []) {
|
|
796
|
-
const existing = s.lists[target.key] ?? defaultListState();
|
|
797
|
-
const targetIds = target.ids ?? ids;
|
|
798
|
-
const nextIds = target.mode === "append" ? Array.from(/* @__PURE__ */ new Set([...existing.ids, ...targetIds])) : targetIds;
|
|
799
|
-
s.lists[target.key] = {
|
|
800
|
-
...existing,
|
|
801
|
-
...target.meta ?? {},
|
|
802
|
-
ids: nextIds,
|
|
803
|
-
isFetching: false,
|
|
804
|
-
isFetchingMore: false,
|
|
805
|
-
error: null,
|
|
806
|
-
lastError: null,
|
|
807
|
-
stale: false,
|
|
808
|
-
lastFetched: fetchedAt
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
for (const projection of options.projections ?? []) {
|
|
812
|
-
const existing = s.lists[projection.key] ?? defaultListState();
|
|
813
|
-
const nextIds = [...existing.ids];
|
|
814
|
-
const { filter, sort, search } = projection.view;
|
|
815
|
-
for (const id of new Set(ids)) {
|
|
816
|
-
if (nextIds.includes(id)) continue;
|
|
817
|
-
const entity = readDraftEntity(id);
|
|
818
|
-
if (!entity) continue;
|
|
819
|
-
const matches = (!filter || matchesFilter(entity, filter)) && (!search?.query || matchesSearch(entity, search.query, search.fields));
|
|
820
|
-
if (!matches) continue;
|
|
821
|
-
if (sort && sort.length > 0) {
|
|
822
|
-
const index = findInsertionIndex(entity, nextIds, readDraftEntity, sort);
|
|
823
|
-
nextIds.splice(index, 0, id);
|
|
824
|
-
} else {
|
|
825
|
-
nextIds.unshift(id);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
s.lists[projection.key] = projection.completeFetch ? {
|
|
829
|
-
...existing,
|
|
830
|
-
ids: nextIds,
|
|
831
|
-
isFetching: false,
|
|
832
|
-
isFetchingMore: false,
|
|
833
|
-
error: null,
|
|
834
|
-
lastError: null,
|
|
835
|
-
stale: false,
|
|
836
|
-
lastFetched: fetchedAt
|
|
837
|
-
} : { ...existing, ids: nextIds };
|
|
838
|
-
}
|
|
839
|
-
for (const key of options.finishListFetches ?? []) {
|
|
840
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
841
|
-
s.lists[key].isFetching = false;
|
|
842
|
-
}
|
|
843
|
-
}),
|
|
844
|
-
replaceEntity: (type, id, data) => set((s) => {
|
|
845
|
-
if (!s.entities[type]) s.entities[type] = {};
|
|
846
|
-
s.entities[type][id] = data;
|
|
847
|
-
const key = ek(type, id);
|
|
848
|
-
if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
|
|
849
|
-
}),
|
|
850
|
-
removeEntity: (type, id) => set((s) => {
|
|
851
|
-
delete s.entities[type]?.[id];
|
|
852
|
-
delete s.patches[type]?.[id];
|
|
853
|
-
delete s.entityStates[ek(type, id)];
|
|
854
|
-
delete s.syncMetadata[ek(type, id)];
|
|
855
|
-
}),
|
|
856
|
-
patchEntity: (type, id, patch) => set((s) => {
|
|
857
|
-
if (!s.patches[type]) s.patches[type] = {};
|
|
858
|
-
s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
|
|
859
|
-
}),
|
|
860
|
-
unpatchEntity: (type, id, keys) => set((s) => {
|
|
861
|
-
const p = s.patches[type]?.[id];
|
|
862
|
-
if (!p) return;
|
|
863
|
-
for (const k of keys) delete p[k];
|
|
864
|
-
}),
|
|
865
|
-
clearPatch: (type, id) => set((s) => {
|
|
866
|
-
delete s.patches[type]?.[id];
|
|
867
|
-
}),
|
|
868
|
-
setEntityFetching: (type, id, fetching) => set((s) => {
|
|
869
|
-
const k = ek(type, id);
|
|
870
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
871
|
-
s.entityStates[k].isFetching = fetching;
|
|
872
|
-
}),
|
|
873
|
-
setEntityError: (type, id, error) => set((s) => {
|
|
874
|
-
const k = ek(type, id);
|
|
875
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
876
|
-
s.entityStates[k].error = error;
|
|
877
|
-
s.entityStates[k].isFetching = false;
|
|
878
|
-
}),
|
|
879
|
-
setEntityFetched: (type, id) => set((s) => {
|
|
880
|
-
const k = ek(type, id);
|
|
881
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
882
|
-
s.entityStates[k].lastFetched = Date.now();
|
|
883
|
-
s.entityStates[k].isFetching = false;
|
|
884
|
-
s.entityStates[k].error = null;
|
|
885
|
-
s.entityStates[k].stale = false;
|
|
886
|
-
s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
|
|
887
|
-
}),
|
|
888
|
-
setEntityStale: (type, id, stale) => set((s) => {
|
|
889
|
-
const k = ek(type, id);
|
|
890
|
-
if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
|
|
891
|
-
s.entityStates[k].stale = stale;
|
|
892
|
-
}),
|
|
893
|
-
setEntitySyncMetadata: (type, id, metadata) => set((s) => {
|
|
894
|
-
const k = ek(type, id);
|
|
895
|
-
s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
|
|
896
|
-
}),
|
|
897
|
-
clearEntitySyncMetadata: (type, id) => set((s) => {
|
|
898
|
-
delete s.syncMetadata[ek(type, id)];
|
|
899
|
-
}),
|
|
900
|
-
setListResult: (key, ids, meta) => set((s) => {
|
|
901
|
-
const ex = s.lists[key] ?? defaultListState();
|
|
902
|
-
s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
|
|
903
|
-
}),
|
|
904
|
-
appendListResult: (key, ids, meta) => set((s) => {
|
|
905
|
-
const ex = s.lists[key] ?? defaultListState();
|
|
906
|
-
s.lists[key] = { ...ex, ...meta, ids: Array.from(/* @__PURE__ */ new Set([...ex.ids, ...ids])), isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
|
|
907
|
-
}),
|
|
908
|
-
prependListResult: (key, ids, meta) => set((s) => {
|
|
909
|
-
const ex = s.lists[key] ?? defaultListState();
|
|
910
|
-
s.lists[key] = { ...ex, ...meta ?? {}, ids: Array.from(/* @__PURE__ */ new Set([...ids, ...ex.ids])), isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
|
|
911
|
-
}),
|
|
912
|
-
removeIdFromAllLists: (_type, id) => set((s) => {
|
|
913
|
-
for (const key of Object.keys(s.lists)) {
|
|
914
|
-
const list = s.lists[key];
|
|
915
|
-
const idx = list.ids.indexOf(id);
|
|
916
|
-
if (idx !== -1) {
|
|
917
|
-
list.ids.splice(idx, 1);
|
|
918
|
-
if (list.total !== null) list.total -= 1;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}),
|
|
922
|
-
insertIdInList: (key, id, position) => set((s) => {
|
|
923
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
924
|
-
const ids = s.lists[key].ids;
|
|
925
|
-
const ex = ids.indexOf(id);
|
|
926
|
-
if (ex !== -1) ids.splice(ex, 1);
|
|
927
|
-
if (position === "start") ids.unshift(id);
|
|
928
|
-
else if (position === "end") ids.push(id);
|
|
929
|
-
else ids.splice(position, 0, id);
|
|
930
|
-
}),
|
|
931
|
-
setListFetching: (key, fetching) => set((s) => {
|
|
932
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
933
|
-
s.lists[key].isFetching = fetching;
|
|
934
|
-
}),
|
|
935
|
-
setListFetchingMore: (key, fetchingMore) => set((s) => {
|
|
936
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
937
|
-
s.lists[key].isFetchingMore = fetchingMore;
|
|
938
|
-
}),
|
|
939
|
-
setListError: (key, error, typed) => set((s) => {
|
|
940
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
941
|
-
s.lists[key].error = error;
|
|
942
|
-
s.lists[key].lastError = typed ?? (error === null ? null : s.lists[key].lastError);
|
|
943
|
-
s.lists[key].isFetching = false;
|
|
944
|
-
s.lists[key].isFetchingMore = false;
|
|
945
|
-
s.lists[key].lastFetched = Date.now();
|
|
946
|
-
s.lists[key].stale = false;
|
|
947
|
-
}),
|
|
948
|
-
setListStale: (key, stale) => set((s) => {
|
|
949
|
-
if (!s.lists[key]) s.lists[key] = defaultListState();
|
|
950
|
-
s.lists[key].stale = stale;
|
|
951
|
-
}),
|
|
952
|
-
invalidateEntity: (type, id) => set((s) => {
|
|
953
|
-
if (id) {
|
|
954
|
-
const k = ek(type, id);
|
|
955
|
-
if (s.entityStates[k]) s.entityStates[k].stale = true;
|
|
956
|
-
} else {
|
|
957
|
-
for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
|
|
958
|
-
}
|
|
959
|
-
}),
|
|
960
|
-
invalidateLists: (matcher) => set((s) => {
|
|
961
|
-
const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
|
|
962
|
-
for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
|
|
963
|
-
}),
|
|
964
|
-
invalidateType: (type) => {
|
|
965
|
-
get().invalidateEntity(type);
|
|
966
|
-
get().invalidateLists(type);
|
|
967
|
-
},
|
|
968
|
-
readEntity: (type, id) => {
|
|
969
|
-
const s = get();
|
|
970
|
-
const base = s.entities[type]?.[id];
|
|
971
|
-
if (!base) return null;
|
|
972
|
-
const patch = s.patches[type]?.[id];
|
|
973
|
-
return patch ? { ...base, ...patch } : base;
|
|
974
|
-
},
|
|
975
|
-
readEntitySnapshot: (type, id) => {
|
|
976
|
-
const s = get();
|
|
977
|
-
const base = s.entities[type]?.[id];
|
|
978
|
-
if (!base) {
|
|
979
|
-
snapshotCache.delete(ek(type, id));
|
|
980
|
-
return null;
|
|
981
|
-
}
|
|
982
|
-
const patch = s.patches[type]?.[id];
|
|
983
|
-
const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
|
|
984
|
-
return readCachedEntitySnapshot(
|
|
985
|
-
type,
|
|
986
|
-
id,
|
|
987
|
-
base,
|
|
988
|
-
patch,
|
|
989
|
-
metadata
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
}))
|
|
993
|
-
)
|
|
994
|
-
);
|
|
995
|
-
}
|
|
996
|
-
var graphStore = createGraphStore();
|
|
997
|
-
var useGraphStore = graphStore;
|
|
998
|
-
|
|
999
|
-
// src/object-path.ts
|
|
1000
|
-
function isObject(value) {
|
|
1001
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1002
|
-
}
|
|
1003
|
-
function getValueAtPath(source, path) {
|
|
1004
|
-
if (!path) return source;
|
|
1005
|
-
const segments = path.split(".").filter(Boolean);
|
|
1006
|
-
let current = source;
|
|
1007
|
-
for (const segment of segments) {
|
|
1008
|
-
if (!isObject(current) && !Array.isArray(current)) return void 0;
|
|
1009
|
-
current = current[segment];
|
|
1010
|
-
}
|
|
1011
|
-
return current;
|
|
1012
|
-
}
|
|
1013
|
-
function setValueAtPath(source, path, value) {
|
|
1014
|
-
const segments = path.split(".").filter(Boolean);
|
|
1015
|
-
if (segments.length === 0) return source;
|
|
1016
|
-
const clone2 = structuredClone(source);
|
|
1017
|
-
let current = clone2;
|
|
1018
|
-
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
1019
|
-
const segment = segments[index];
|
|
1020
|
-
const next = current[segment];
|
|
1021
|
-
if (!isObject(next)) current[segment] = {};
|
|
1022
|
-
current = current[segment];
|
|
1023
|
-
}
|
|
1024
|
-
current[segments[segments.length - 1]] = value;
|
|
1025
|
-
return clone2;
|
|
1026
|
-
}
|
|
1027
|
-
function collectDirtyPaths(current, original, prefix = "", acc = /* @__PURE__ */ new Set()) {
|
|
1028
|
-
if (isObject(current) && isObject(original)) {
|
|
1029
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(original)]);
|
|
1030
|
-
for (const key of keys) {
|
|
1031
|
-
const nextPrefix = prefix ? `${prefix}.${key}` : key;
|
|
1032
|
-
collectDirtyPaths(current[key], original[key], nextPrefix, acc);
|
|
1033
|
-
}
|
|
1034
|
-
return acc;
|
|
1035
|
-
}
|
|
1036
|
-
if (JSON.stringify(current) !== JSON.stringify(original) && prefix) acc.add(prefix);
|
|
1037
|
-
return acc;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
// src/merge/loro.ts
|
|
1041
|
-
var ROOT_MAP = "entity";
|
|
1042
|
-
var LORO_MODULE_SPECIFIER = "loro-crdt";
|
|
1043
|
-
var docs = /* @__PURE__ */ new Map();
|
|
1044
|
-
function ek2(type, id) {
|
|
1045
|
-
return `${type}:${id}`;
|
|
1046
|
-
}
|
|
1047
|
-
async function createLoroMergeStrategy(loadLoro) {
|
|
1048
|
-
let mod;
|
|
1049
|
-
try {
|
|
1050
|
-
const loaded = loadLoro ? await loadLoro() : await import(
|
|
1051
|
-
/* @vite-ignore */
|
|
1052
|
-
LORO_MODULE_SPECIFIER
|
|
1053
|
-
);
|
|
1054
|
-
if (typeof loaded.LoroDoc !== "function") {
|
|
1055
|
-
throw new TypeError("loro-crdt did not export a LoroDoc constructor");
|
|
1056
|
-
}
|
|
1057
|
-
mod = loaded;
|
|
1058
|
-
} catch (cause) {
|
|
1059
|
-
throw new Error(
|
|
1060
|
-
"[merge/loro] createLoroMergeStrategy requires the optional peer dependency 'loro-crdt'. Install it with `pnpm add loro-crdt`.",
|
|
1061
|
-
{ cause }
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
const { LoroDoc } = mod;
|
|
1065
|
-
return (prev, next, ctx) => {
|
|
1066
|
-
const key = ek2(ctx.type, ctx.id);
|
|
1067
|
-
let doc = docs.get(key);
|
|
1068
|
-
if (!doc) {
|
|
1069
|
-
doc = new LoroDoc();
|
|
1070
|
-
docs.set(key, doc);
|
|
1071
|
-
if (prev) {
|
|
1072
|
-
const seed = doc.getMap(ROOT_MAP);
|
|
1073
|
-
for (const [k, v] of Object.entries(prev)) seed.set(k, v);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
const map = doc.getMap(ROOT_MAP);
|
|
1077
|
-
for (const [k, v] of Object.entries(next)) map.set(k, v);
|
|
1078
|
-
const root = doc.toJSON()[ROOT_MAP];
|
|
1079
|
-
return root && typeof root === "object" ? root : { ...next };
|
|
1080
|
-
};
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// src/graph-query.ts
|
|
1084
|
-
function queryOnce(opts) {
|
|
1085
|
-
const store = useGraphStore.getState();
|
|
1086
|
-
const ids = resolveCandidateIds(store, opts);
|
|
1087
|
-
let rows = ids.map((id) => store.readEntitySnapshot(opts.type, id)).filter((row) => row !== null);
|
|
1088
|
-
if (opts.where) rows = rows.filter(opts.where);
|
|
1089
|
-
if (opts.sort) rows = [...rows].sort(opts.sort);
|
|
1090
|
-
const projected = rows.map((row) => applySelection(projectRow(row, opts.include, store), opts.select));
|
|
1091
|
-
if (opts.id) return projected[0] ?? null;
|
|
1092
|
-
return projected;
|
|
1093
|
-
}
|
|
1094
|
-
var selectGraph = queryOnce;
|
|
1095
|
-
function resolveCandidateIds(store, opts) {
|
|
1096
|
-
if (opts.id) return [opts.id];
|
|
1097
|
-
if (opts.ids) return opts.ids;
|
|
1098
|
-
if (opts.listKey) return store.lists[opts.listKey]?.ids ?? [];
|
|
1099
|
-
return Object.keys(store.entities[opts.type] ?? {});
|
|
1100
|
-
}
|
|
1101
|
-
function projectRow(row, include, store) {
|
|
1102
|
-
if (!include) return row;
|
|
1103
|
-
const projected = { ...row };
|
|
1104
|
-
for (const [key, relation] of Object.entries(include)) {
|
|
1105
|
-
const related = resolveRelation(row, relation, store);
|
|
1106
|
-
projected[key] = related;
|
|
1107
|
-
}
|
|
1108
|
-
return projected;
|
|
1109
|
-
}
|
|
1110
|
-
function resolveRelation(entity, relation, store) {
|
|
1111
|
-
const include = relation.include;
|
|
1112
|
-
switch (relation.via.kind) {
|
|
1113
|
-
case "field": {
|
|
1114
|
-
const relatedId = entity[relation.via.field];
|
|
1115
|
-
if (typeof relatedId !== "string") return null;
|
|
1116
|
-
const related = store.readEntitySnapshot(relation.type, relatedId);
|
|
1117
|
-
return related ? projectRow(related, include, store) : null;
|
|
1118
|
-
}
|
|
1119
|
-
case "array": {
|
|
1120
|
-
const ids = entity[relation.via.field];
|
|
1121
|
-
if (!Array.isArray(ids)) return [];
|
|
1122
|
-
return ids.map((id) => typeof id === "string" ? store.readEntitySnapshot(relation.type, id) : null).filter((row) => row !== null).map((row) => projectRow(row, include, store));
|
|
1123
|
-
}
|
|
1124
|
-
case "list": {
|
|
1125
|
-
const key = typeof relation.via.key === "function" ? relation.via.key(entity) : relation.via.key;
|
|
1126
|
-
if (!key) return [];
|
|
1127
|
-
const ids = store.lists[key]?.ids ?? [];
|
|
1128
|
-
return ids.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
|
|
1129
|
-
}
|
|
1130
|
-
case "resolver": {
|
|
1131
|
-
const resolved = relation.via.resolve(entity, store);
|
|
1132
|
-
if (Array.isArray(resolved)) {
|
|
1133
|
-
return resolved.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
|
|
1134
|
-
}
|
|
1135
|
-
if (typeof resolved !== "string") return null;
|
|
1136
|
-
const related = store.readEntitySnapshot(relation.type, resolved);
|
|
1137
|
-
return related ? projectRow(related, include, store) : null;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
function applySelection(row, select) {
|
|
1142
|
-
if (!select) return row;
|
|
1143
|
-
if (typeof select === "function") {
|
|
1144
|
-
const result = select(row);
|
|
1145
|
-
return result && typeof result === "object" ? result : { value: result };
|
|
1146
|
-
}
|
|
1147
|
-
const picked = {};
|
|
1148
|
-
for (const key of select) {
|
|
1149
|
-
if (key in row) picked[key] = row[key];
|
|
1150
|
-
}
|
|
1151
|
-
return picked;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
// src/legacy-devtools.ts
|
|
1155
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
1156
|
-
function publish(event) {
|
|
1157
|
-
if (listeners.size === 0) return;
|
|
1158
|
-
for (const listener of [...listeners]) {
|
|
1159
|
-
try {
|
|
1160
|
-
listener(event);
|
|
1161
|
-
} catch {
|
|
1162
|
-
console.warn("[engine] devtools subscriber threw");
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
function subscribeLegacyDevtoolsEvent(listener) {
|
|
1167
|
-
listeners.add(listener);
|
|
1168
|
-
return () => listeners.delete(listener);
|
|
1169
|
-
}
|
|
1170
|
-
function emitLegacyDevtoolsEvent(event) {
|
|
1171
|
-
publish(event);
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
// src/graph-actions.ts
|
|
1175
|
-
var graphActionListeners = /* @__PURE__ */ new Set();
|
|
1176
|
-
var graphActionReplayers = /* @__PURE__ */ new Map();
|
|
1177
|
-
function createGraphTransaction(storeApi = graphStore) {
|
|
1178
|
-
const baseline = cloneGraphData(storeApi.getState());
|
|
1179
|
-
let closed = false;
|
|
1180
|
-
const tx = {
|
|
1181
|
-
upsertEntity(type, id, data) {
|
|
1182
|
-
storeApi.getState().upsertEntity(type, id, data);
|
|
1183
|
-
if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
|
|
1184
|
-
emitLegacyDevtoolsEvent({ kind: "upsert", type, id, data, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1185
|
-
}
|
|
1186
|
-
return tx;
|
|
1187
|
-
},
|
|
1188
|
-
replaceEntity(type, id, data) {
|
|
1189
|
-
storeApi.getState().replaceEntity(type, id, data);
|
|
1190
|
-
return tx;
|
|
1191
|
-
},
|
|
1192
|
-
removeEntity(type, id) {
|
|
1193
|
-
storeApi.getState().removeEntity(type, id);
|
|
1194
|
-
return tx;
|
|
1195
|
-
},
|
|
1196
|
-
patchEntity(type, id, patch) {
|
|
1197
|
-
storeApi.getState().patchEntity(type, id, patch);
|
|
1198
|
-
if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
|
|
1199
|
-
emitLegacyDevtoolsEvent({ kind: "patch", type, id, patch, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1200
|
-
}
|
|
1201
|
-
return tx;
|
|
1202
|
-
},
|
|
1203
|
-
clearPatch(type, id) {
|
|
1204
|
-
storeApi.getState().clearPatch(type, id);
|
|
1205
|
-
if (typeof process === "undefined" || process.env.NODE_ENV !== "production") {
|
|
1206
|
-
emitLegacyDevtoolsEvent({ kind: "clearPatch", type, id, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1207
|
-
}
|
|
1208
|
-
return tx;
|
|
1209
|
-
},
|
|
1210
|
-
insertIdInList(key, id, position) {
|
|
1211
|
-
storeApi.getState().insertIdInList(key, id, position);
|
|
1212
|
-
return tx;
|
|
1213
|
-
},
|
|
1214
|
-
removeIdFromAllLists(type, id) {
|
|
1215
|
-
storeApi.getState().removeIdFromAllLists(type, id);
|
|
1216
|
-
return tx;
|
|
1217
|
-
},
|
|
1218
|
-
setEntitySyncMetadata(type, id, metadata) {
|
|
1219
|
-
storeApi.getState().setEntitySyncMetadata(type, id, metadata);
|
|
1220
|
-
return tx;
|
|
1221
|
-
},
|
|
1222
|
-
markEntityPending(type, id, origin = "optimistic") {
|
|
1223
|
-
storeApi.getState().setEntitySyncMetadata(type, id, {
|
|
1224
|
-
synced: false,
|
|
1225
|
-
origin,
|
|
1226
|
-
updatedAt: Date.now()
|
|
1227
|
-
});
|
|
1228
|
-
return tx;
|
|
1229
|
-
},
|
|
1230
|
-
markEntitySynced(type, id, origin = "server") {
|
|
1231
|
-
storeApi.getState().setEntitySyncMetadata(type, id, {
|
|
1232
|
-
synced: true,
|
|
1233
|
-
origin,
|
|
1234
|
-
updatedAt: Date.now()
|
|
1235
|
-
});
|
|
1236
|
-
return tx;
|
|
1237
|
-
},
|
|
1238
|
-
commit() {
|
|
1239
|
-
closed = true;
|
|
1240
|
-
},
|
|
1241
|
-
rollback() {
|
|
1242
|
-
if (closed) return;
|
|
1243
|
-
storeApi.setState(cloneGraphData(baseline));
|
|
1244
|
-
closed = true;
|
|
1245
|
-
},
|
|
1246
|
-
snapshot() {
|
|
1247
|
-
return cloneGraphData(storeApi.getState());
|
|
1248
|
-
}
|
|
1249
|
-
};
|
|
1250
|
-
return tx;
|
|
1251
|
-
}
|
|
1252
|
-
function createGraphAction(opts) {
|
|
1253
|
-
if (opts.key) {
|
|
1254
|
-
graphActionReplayers.set(opts.key, async (record, storeApi) => {
|
|
1255
|
-
const tx = createGraphTransaction(storeApi);
|
|
1256
|
-
try {
|
|
1257
|
-
const result = await opts.run(tx, record.input);
|
|
1258
|
-
tx.commit();
|
|
1259
|
-
return result;
|
|
1260
|
-
} catch (error) {
|
|
1261
|
-
tx.rollback();
|
|
1262
|
-
throw error;
|
|
1263
|
-
}
|
|
1264
|
-
});
|
|
1265
|
-
}
|
|
1266
|
-
return async (input) => {
|
|
1267
|
-
const tx = createGraphTransaction();
|
|
1268
|
-
const record = opts.key ? {
|
|
1269
|
-
id: `${opts.key}:${Date.now()}`,
|
|
1270
|
-
key: opts.key,
|
|
1271
|
-
input: structuredClone(input),
|
|
1272
|
-
enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1273
|
-
} : null;
|
|
1274
|
-
try {
|
|
1275
|
-
if (record) emitGraphActionEvent({ type: "enqueued", record });
|
|
1276
|
-
opts.optimistic?.(tx, input);
|
|
1277
|
-
const result = await opts.run(tx, input);
|
|
1278
|
-
opts.onSuccess?.(result, input, tx);
|
|
1279
|
-
tx.commit();
|
|
1280
|
-
if (record) emitGraphActionEvent({ type: "settled", record });
|
|
1281
|
-
return result;
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
tx.rollback();
|
|
1284
|
-
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
1285
|
-
if (record) emitGraphActionEvent({ type: "settled", record });
|
|
1286
|
-
opts.onError?.(normalized, input);
|
|
1287
|
-
throw normalized;
|
|
1288
|
-
}
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
function subscribeGraphActionEvents(listener) {
|
|
1292
|
-
graphActionListeners.add(listener);
|
|
1293
|
-
return () => graphActionListeners.delete(listener);
|
|
1294
|
-
}
|
|
1295
|
-
async function replayRegisteredGraphAction(record, storeApi = graphStore) {
|
|
1296
|
-
const replayer = graphActionReplayers.get(record.key);
|
|
1297
|
-
if (!replayer) throw new Error(`No graph action registered for key "${record.key}"`);
|
|
1298
|
-
return replayer(record, storeApi);
|
|
1299
|
-
}
|
|
1300
|
-
function cloneGraphData(source = graphStore.getState()) {
|
|
1301
|
-
return {
|
|
1302
|
-
entities: structuredClone(source.entities),
|
|
1303
|
-
patches: structuredClone(source.patches),
|
|
1304
|
-
entityStates: structuredClone(source.entityStates),
|
|
1305
|
-
syncMetadata: structuredClone(source.syncMetadata),
|
|
1306
|
-
lists: structuredClone(source.lists)
|
|
1307
|
-
};
|
|
1308
|
-
}
|
|
1309
|
-
function emitGraphActionEvent(event) {
|
|
1310
|
-
for (const listener of graphActionListeners) listener(event);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// src/graph-effects.ts
|
|
1314
|
-
function createGraphEffect(opts) {
|
|
1315
|
-
const getKey = opts.getKey ?? defaultGetKey;
|
|
1316
|
-
const isEqual = opts.isEqual ?? defaultIsEqual;
|
|
1317
|
-
let initialized = false;
|
|
1318
|
-
let previous = /* @__PURE__ */ new Map();
|
|
1319
|
-
const evaluate = () => {
|
|
1320
|
-
const nextValues = normalizeQueryResult(opts.query());
|
|
1321
|
-
const next = /* @__PURE__ */ new Map();
|
|
1322
|
-
nextValues.forEach((value, index) => {
|
|
1323
|
-
next.set(getKey(value, index), value);
|
|
1324
|
-
});
|
|
1325
|
-
if (!initialized) {
|
|
1326
|
-
initialized = true;
|
|
1327
|
-
previous = next;
|
|
1328
|
-
if (opts.skipInitial) return;
|
|
1329
|
-
}
|
|
1330
|
-
for (const [key, value] of next.entries()) {
|
|
1331
|
-
const previousValue = previous.get(key);
|
|
1332
|
-
if (previousValue === void 0) {
|
|
1333
|
-
opts.onEnter?.({ key, value });
|
|
1334
|
-
continue;
|
|
1335
|
-
}
|
|
1336
|
-
if (!isEqual(previousValue, value)) {
|
|
1337
|
-
opts.onUpdate?.({ key, value, previousValue });
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
for (const [key, previousValue] of previous.entries()) {
|
|
1341
|
-
if (!next.has(key)) opts.onExit?.({ key, previousValue });
|
|
1342
|
-
}
|
|
1343
|
-
previous = next;
|
|
1344
|
-
};
|
|
1345
|
-
evaluate();
|
|
1346
|
-
const unsubscribe = useGraphStore.subscribe(() => {
|
|
1347
|
-
evaluate();
|
|
1348
|
-
});
|
|
1349
|
-
return {
|
|
1350
|
-
dispose: () => {
|
|
1351
|
-
unsubscribe();
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
function normalizeQueryResult(value) {
|
|
1356
|
-
if (value == null) return [];
|
|
1357
|
-
return Array.isArray(value) ? value : [value];
|
|
1358
|
-
}
|
|
1359
|
-
function defaultGetKey(value, index) {
|
|
1360
|
-
if (value && typeof value === "object") {
|
|
1361
|
-
const record = value;
|
|
1362
|
-
if (typeof record.id === "string") return record.id;
|
|
1363
|
-
if (typeof record.$key === "string") return record.$key;
|
|
1364
|
-
}
|
|
1365
|
-
return String(index);
|
|
1366
|
-
}
|
|
1367
|
-
function defaultIsEqual(previousValue, nextValue) {
|
|
1368
|
-
return JSON.stringify(previousValue) === JSON.stringify(nextValue);
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
// src/schema.ts
|
|
1372
|
-
var schemaRegistry = /* @__PURE__ */ new Map();
|
|
1373
|
-
function registerEntityJsonSchema(config) {
|
|
1374
|
-
schemaRegistry.set(registryKey(config.entityType, config.field, config.schemaId), config);
|
|
1375
|
-
}
|
|
1376
|
-
function registerRuntimeSchema(config) {
|
|
1377
|
-
registerEntityJsonSchema(config);
|
|
1378
|
-
}
|
|
1379
|
-
function getEntityJsonSchema(opts) {
|
|
1380
|
-
const exact = schemaRegistry.get(registryKey(opts.entityType, opts.field, opts.schemaId));
|
|
1381
|
-
if (exact) return exact;
|
|
1382
|
-
if (opts.field) {
|
|
1383
|
-
const byField = schemaRegistry.get(registryKey(opts.entityType, opts.field));
|
|
1384
|
-
if (byField) return byField;
|
|
1385
|
-
}
|
|
1386
|
-
if (opts.schemaId) {
|
|
1387
|
-
const byId = schemaRegistry.get(registryKey(opts.entityType, void 0, opts.schemaId));
|
|
1388
|
-
if (byId) return byId;
|
|
1389
|
-
}
|
|
1390
|
-
for (const schema of schemaRegistry.values()) {
|
|
1391
|
-
if (schema.entityType !== opts.entityType) continue;
|
|
1392
|
-
if (opts.field && schema.field !== opts.field) continue;
|
|
1393
|
-
return schema;
|
|
1394
|
-
}
|
|
1395
|
-
return null;
|
|
1396
|
-
}
|
|
1397
|
-
function exportGraphSnapshotWithSchemas(opts) {
|
|
1398
|
-
return JSON.stringify(
|
|
1399
|
-
{
|
|
1400
|
-
scope: opts.scope,
|
|
1401
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1402
|
-
data: opts.data,
|
|
1403
|
-
schemas: opts.schemas.filter(Boolean)
|
|
1404
|
-
},
|
|
1405
|
-
null,
|
|
1406
|
-
opts.pretty === false ? 0 : 2
|
|
1407
|
-
);
|
|
1408
|
-
}
|
|
1409
|
-
function escapeHtml(input) {
|
|
1410
|
-
return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1411
|
-
}
|
|
1412
|
-
function renderMarkdownToHtml(value) {
|
|
1413
|
-
const escaped = escapeHtml(value);
|
|
1414
|
-
const blocks = escaped.split(/\n{2,}/).map((b) => b.trim()).filter(Boolean);
|
|
1415
|
-
return blocks.map((block) => renderMarkdownBlock(block)).join("");
|
|
1416
|
-
}
|
|
1417
|
-
function inferSchemaFieldType(schema) {
|
|
1418
|
-
const forced = schema["x-field-type"];
|
|
1419
|
-
if (forced === "markdown") return "markdown";
|
|
1420
|
-
if (schema.format === "markdown") return "markdown";
|
|
1421
|
-
if (schema.enum) return "enum";
|
|
1422
|
-
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
1423
|
-
switch (type) {
|
|
1424
|
-
case "boolean":
|
|
1425
|
-
return "boolean";
|
|
1426
|
-
case "integer":
|
|
1427
|
-
case "number":
|
|
1428
|
-
return "number";
|
|
1429
|
-
case "string":
|
|
1430
|
-
if (schema.format === "email") return "email";
|
|
1431
|
-
if (schema.format === "uri" || schema.format === "url") return "url";
|
|
1432
|
-
if (schema.format === "date" || schema.format === "date-time") return "date";
|
|
1433
|
-
return "text";
|
|
1434
|
-
case "array":
|
|
1435
|
-
case "object":
|
|
1436
|
-
return "json";
|
|
1437
|
-
default:
|
|
1438
|
-
return "text";
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
function registryKey(entityType, field, schemaId) {
|
|
1442
|
-
return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
|
|
1443
|
-
}
|
|
1444
|
-
function humanizeFieldName(value) {
|
|
1445
|
-
return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1446
|
-
}
|
|
1447
|
-
function renderMarkdownBlock(block) {
|
|
1448
|
-
if (block.startsWith("# ")) return `<h1>${renderInlineMarkdown(block.slice(2))}</h1>`;
|
|
1449
|
-
if (block.startsWith("## ")) return `<h2>${renderInlineMarkdown(block.slice(3))}</h2>`;
|
|
1450
|
-
return `<p>${renderInlineMarkdown(block).replaceAll("\n", "<br/>")}</p>`;
|
|
1451
|
-
}
|
|
1452
|
-
function renderInlineMarkdown(block) {
|
|
1453
|
-
return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
// src/ai-interop.ts
|
|
1457
|
-
function exportGraphSnapshot(opts) {
|
|
1458
|
-
const payload = {
|
|
1459
|
-
scope: opts.scope,
|
|
1460
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1461
|
-
data: opts.data
|
|
1462
|
-
};
|
|
1463
|
-
return JSON.stringify(payload, null, opts.pretty === false ? 0 : 2);
|
|
1464
|
-
}
|
|
1465
|
-
function createGraphTool(handler) {
|
|
1466
|
-
return (input) => handler(input, {
|
|
1467
|
-
store: useGraphStore.getState(),
|
|
1468
|
-
queryOnce,
|
|
1469
|
-
exportGraphSnapshot
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
function createSchemaGraphTool(handler) {
|
|
1473
|
-
return (input) => handler(input, {
|
|
1474
|
-
store: useGraphStore.getState(),
|
|
1475
|
-
queryOnce,
|
|
1476
|
-
exportGraphSnapshot,
|
|
1477
|
-
getEntityJsonSchema,
|
|
1478
|
-
exportGraphSnapshotWithSchemas
|
|
1479
|
-
});
|
|
1480
|
-
}
|
|
1481
|
-
var DEFAULT_STORAGE_KEY = "prometheus:graph";
|
|
1482
|
-
var graphSyncStatusStore = vanilla.createStore()((set) => ({
|
|
1483
|
-
status: {
|
|
1484
|
-
phase: "idle",
|
|
1485
|
-
isOnline: true,
|
|
1486
|
-
isSynced: true,
|
|
1487
|
-
pendingActions: 0,
|
|
1488
|
-
lastHydratedAt: null,
|
|
1489
|
-
lastPersistedAt: null,
|
|
1490
|
-
storageKey: null,
|
|
1491
|
-
error: null
|
|
1492
|
-
},
|
|
1493
|
-
setStatus: (status) => set((state) => ({
|
|
1494
|
-
status: {
|
|
1495
|
-
...state.status,
|
|
1496
|
-
...status
|
|
1497
|
-
}
|
|
1498
|
-
}))
|
|
1499
|
-
}));
|
|
1500
|
-
var pendingActions = /* @__PURE__ */ new Map();
|
|
1501
|
-
function getGraphSyncStatus() {
|
|
1502
|
-
return graphSyncStatusStore.getState().status;
|
|
1503
|
-
}
|
|
1504
|
-
async function persistGraphToStorage(opts) {
|
|
1505
|
-
const storeApi = opts.store ?? graphStore;
|
|
1506
|
-
const payload = {
|
|
1507
|
-
version: 1,
|
|
1508
|
-
snapshot: cloneGraphSnapshot(storeApi),
|
|
1509
|
-
pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
|
|
1510
|
-
};
|
|
1511
|
-
const json = JSON.stringify(payload);
|
|
1512
|
-
await opts.storage.set(opts.key, json);
|
|
1513
|
-
const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1514
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1515
|
-
lastPersistedAt: persistedAt,
|
|
1516
|
-
storageKey: opts.key,
|
|
1517
|
-
pendingActions: payload.pendingActions.length
|
|
1518
|
-
});
|
|
1519
|
-
return {
|
|
1520
|
-
ok: true,
|
|
1521
|
-
key: opts.key,
|
|
1522
|
-
bytes: json.length,
|
|
1523
|
-
persistedAt
|
|
1524
|
-
};
|
|
1525
|
-
}
|
|
1526
|
-
async function hydrateGraphFromStorage(opts) {
|
|
1527
|
-
const storeApi = opts.store ?? graphStore;
|
|
1528
|
-
const raw = await opts.storage.get(opts.key);
|
|
1529
|
-
if (!raw) {
|
|
1530
|
-
return {
|
|
1531
|
-
ok: false,
|
|
1532
|
-
key: opts.key,
|
|
1533
|
-
hydratedAt: null,
|
|
1534
|
-
entityCounts: {},
|
|
1535
|
-
error: "No persisted graph snapshot found"
|
|
1536
|
-
};
|
|
1537
|
-
}
|
|
1538
|
-
try {
|
|
1539
|
-
const parsed = JSON.parse(raw);
|
|
1540
|
-
storeApi.setState(parsed.snapshot);
|
|
1541
|
-
pendingActions.clear();
|
|
1542
|
-
for (const action of parsed.pendingActions ?? []) pendingActions.set(action.id, action);
|
|
1543
|
-
const hydratedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1544
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1545
|
-
lastHydratedAt: hydratedAt,
|
|
1546
|
-
storageKey: opts.key,
|
|
1547
|
-
pendingActions: pendingActions.size,
|
|
1548
|
-
error: null
|
|
1549
|
-
});
|
|
1550
|
-
return {
|
|
1551
|
-
ok: true,
|
|
1552
|
-
key: opts.key,
|
|
1553
|
-
hydratedAt,
|
|
1554
|
-
entityCounts: Object.fromEntries(
|
|
1555
|
-
Object.entries(parsed.snapshot.entities).map(([type, entities]) => [type, Object.keys(entities).length])
|
|
1556
|
-
),
|
|
1557
|
-
pendingActions: Array.from(pendingActions.values())
|
|
1558
|
-
};
|
|
1559
|
-
} catch (error) {
|
|
1560
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1561
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1562
|
-
phase: "error",
|
|
1563
|
-
error: message,
|
|
1564
|
-
storageKey: opts.key
|
|
1565
|
-
});
|
|
1566
|
-
return {
|
|
1567
|
-
ok: false,
|
|
1568
|
-
key: opts.key,
|
|
1569
|
-
hydratedAt: null,
|
|
1570
|
-
entityCounts: {},
|
|
1571
|
-
error: message
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
function startLocalFirstGraph(opts) {
|
|
1576
|
-
const storeApi = opts.store ?? graphStore;
|
|
1577
|
-
const key = opts.key ?? DEFAULT_STORAGE_KEY;
|
|
1578
|
-
const persistDebounceMs = opts.persistDebounceMs ?? 50;
|
|
1579
|
-
const statusStore = graphSyncStatusStore.getState();
|
|
1580
|
-
statusStore.setStatus({
|
|
1581
|
-
phase: "hydrating",
|
|
1582
|
-
storageKey: key,
|
|
1583
|
-
isOnline: opts.onlineSource?.getIsOnline() ?? getDefaultOnlineSource().getIsOnline(),
|
|
1584
|
-
isSynced: pendingActions.size === 0,
|
|
1585
|
-
error: null
|
|
1586
|
-
});
|
|
1587
|
-
let persistTimer = null;
|
|
1588
|
-
const schedulePersist = () => {
|
|
1589
|
-
if (persistTimer) clearTimeout(persistTimer);
|
|
1590
|
-
persistTimer = setTimeout(() => {
|
|
1591
|
-
void persistGraphToStorage({ storage: opts.storage, key, store: storeApi });
|
|
1592
|
-
}, persistDebounceMs);
|
|
1593
|
-
};
|
|
1594
|
-
const graphUnsub = storeApi.subscribe(() => {
|
|
1595
|
-
schedulePersist();
|
|
1596
|
-
});
|
|
1597
|
-
const actionUnsub = subscribeGraphActionEvents((event) => {
|
|
1598
|
-
if (event.type === "enqueued") pendingActions.set(event.record.id, event.record);
|
|
1599
|
-
if (event.type === "settled") pendingActions.delete(event.record.id);
|
|
1600
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1601
|
-
pendingActions: pendingActions.size,
|
|
1602
|
-
isSynced: pendingActions.size === 0
|
|
1603
|
-
});
|
|
1604
|
-
schedulePersist();
|
|
1605
|
-
});
|
|
1606
|
-
const onlineSource = opts.onlineSource ?? getDefaultOnlineSource();
|
|
1607
|
-
const onlineUnsub = onlineSource.subscribe((online) => {
|
|
1608
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1609
|
-
isOnline: online,
|
|
1610
|
-
phase: online ? "ready" : "offline"
|
|
1611
|
-
});
|
|
1612
|
-
});
|
|
1613
|
-
const ready = (async () => {
|
|
1614
|
-
const hydrated = await hydrateGraphFromStorage({ storage: opts.storage, key, store: storeApi });
|
|
1615
|
-
if (opts.replayPendingActions && hydrated.ok && pendingActions.size > 0) {
|
|
1616
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1617
|
-
phase: "syncing",
|
|
1618
|
-
isSynced: false
|
|
1619
|
-
});
|
|
1620
|
-
const policy = resolveRetryPolicy(opts.retryPolicy);
|
|
1621
|
-
for (const action of Array.from(pendingActions.values())) {
|
|
1622
|
-
await replayActionWithRetry(action, policy, storeApi);
|
|
1623
|
-
pendingActions.delete(action.id);
|
|
1624
|
-
}
|
|
1625
|
-
await persistGraphToStorage({ storage: opts.storage, key, store: storeApi });
|
|
1626
|
-
}
|
|
1627
|
-
const online = onlineSource.getIsOnline();
|
|
1628
|
-
graphSyncStatusStore.getState().setStatus({
|
|
1629
|
-
phase: online ? "ready" : "offline",
|
|
1630
|
-
isOnline: online,
|
|
1631
|
-
isSynced: pendingActions.size === 0,
|
|
1632
|
-
pendingActions: pendingActions.size
|
|
1633
|
-
});
|
|
1634
|
-
})();
|
|
1635
|
-
return {
|
|
1636
|
-
ready,
|
|
1637
|
-
dispose() {
|
|
1638
|
-
graphUnsub();
|
|
1639
|
-
actionUnsub();
|
|
1640
|
-
onlineUnsub();
|
|
1641
|
-
if (persistTimer) clearTimeout(persistTimer);
|
|
1642
|
-
},
|
|
1643
|
-
async persistNow() {
|
|
1644
|
-
await persistGraphToStorage({ storage: opts.storage, key, store: storeApi });
|
|
1645
|
-
},
|
|
1646
|
-
hydrate() {
|
|
1647
|
-
return hydrateGraphFromStorage({ storage: opts.storage, key, store: storeApi });
|
|
1648
|
-
},
|
|
1649
|
-
getStatus() {
|
|
1650
|
-
return graphSyncStatusStore.getState().status;
|
|
1651
|
-
}
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
function cloneGraphSnapshot(storeApi) {
|
|
1655
|
-
const state = storeApi.getState();
|
|
1656
|
-
return {
|
|
1657
|
-
entities: structuredClone(state.entities),
|
|
1658
|
-
patches: structuredClone(state.patches),
|
|
1659
|
-
entityStates: structuredClone(state.entityStates),
|
|
1660
|
-
syncMetadata: structuredClone(state.syncMetadata),
|
|
1661
|
-
lists: structuredClone(state.lists)
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
function resolveRetryPolicy(policy) {
|
|
1665
|
-
return {
|
|
1666
|
-
maxAttempts: policy?.maxAttempts ?? 5,
|
|
1667
|
-
initialDelayMs: policy?.initialDelayMs ?? 500,
|
|
1668
|
-
maxDelayMs: policy?.maxDelayMs ?? 3e4,
|
|
1669
|
-
backoffFactor: policy?.backoffFactor ?? 2,
|
|
1670
|
-
jitter: policy?.jitter ?? "equal",
|
|
1671
|
-
poisonHandler: policy?.poisonHandler
|
|
1672
|
-
};
|
|
1673
|
-
}
|
|
1674
|
-
function computeDelay(policy, attempt) {
|
|
1675
|
-
const base = Math.min(
|
|
1676
|
-
policy.initialDelayMs * Math.pow(policy.backoffFactor, Math.max(0, attempt - 1)),
|
|
1677
|
-
policy.maxDelayMs
|
|
1678
|
-
);
|
|
1679
|
-
switch (policy.jitter) {
|
|
1680
|
-
case "none":
|
|
1681
|
-
return base;
|
|
1682
|
-
case "full":
|
|
1683
|
-
return Math.random() * base;
|
|
1684
|
-
case "equal":
|
|
1685
|
-
default:
|
|
1686
|
-
return base / 2 + Math.random() * (base / 2);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
function sleep(ms) {
|
|
1690
|
-
if (ms <= 0) return Promise.resolve();
|
|
1691
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
1692
|
-
}
|
|
1693
|
-
async function replayActionWithRetry(action, policy, storeApi = graphStore) {
|
|
1694
|
-
let lastError = null;
|
|
1695
|
-
for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
|
|
1696
|
-
try {
|
|
1697
|
-
await replayRegisteredGraphAction(action, storeApi);
|
|
1698
|
-
return { ok: true };
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
lastError = error;
|
|
1701
|
-
if (attempt >= policy.maxAttempts) break;
|
|
1702
|
-
await sleep(computeDelay(policy, attempt));
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
try {
|
|
1706
|
-
await policy.poisonHandler?.(action, lastError);
|
|
1707
|
-
} catch {
|
|
1708
|
-
}
|
|
1709
|
-
return { ok: false, poisoned: true, error: lastError };
|
|
1710
|
-
}
|
|
1711
|
-
function getDefaultOnlineSource() {
|
|
1712
|
-
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1713
|
-
return {
|
|
1714
|
-
getIsOnline: () => window.navigator.onLine,
|
|
1715
|
-
subscribe: (listener) => {
|
|
1716
|
-
const onlineHandler = () => listener(true);
|
|
1717
|
-
const offlineHandler = () => listener(false);
|
|
1718
|
-
window.addEventListener("online", onlineHandler);
|
|
1719
|
-
window.addEventListener("offline", offlineHandler);
|
|
1720
|
-
return () => {
|
|
1721
|
-
window.removeEventListener("online", onlineHandler);
|
|
1722
|
-
window.removeEventListener("offline", offlineHandler);
|
|
1723
|
-
};
|
|
1724
|
-
}
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
return {
|
|
1728
|
-
getIsOnline: () => true,
|
|
1729
|
-
subscribe: () => () => {
|
|
1730
|
-
}
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
// src/engine.ts
|
|
1735
|
-
function serializeKey(key) {
|
|
1736
|
-
return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
|
|
1737
|
-
}
|
|
1738
|
-
function sleep2(ms) {
|
|
1739
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
1740
|
-
}
|
|
1741
|
-
var inflightByStore = /* @__PURE__ */ new WeakMap();
|
|
1742
|
-
function inflightFor(store) {
|
|
1743
|
-
let inflight = inflightByStore.get(store);
|
|
1744
|
-
if (!inflight) {
|
|
1745
|
-
inflight = /* @__PURE__ */ new Map();
|
|
1746
|
-
inflightByStore.set(store, inflight);
|
|
1747
|
-
}
|
|
1748
|
-
return inflight;
|
|
1749
|
-
}
|
|
1750
|
-
function dedupe(key, fn, store = graphStore) {
|
|
1751
|
-
const inflight = inflightFor(store);
|
|
1752
|
-
if (inflight.has(key)) return inflight.get(key);
|
|
1753
|
-
const p = fn().finally(() => inflight.delete(key));
|
|
1754
|
-
inflight.set(key, p);
|
|
1755
|
-
return p;
|
|
1756
|
-
}
|
|
1757
|
-
var globalSubscriberStatsListeners = /* @__PURE__ */ new Set();
|
|
1758
|
-
var subscriberStatsListenersByStore = /* @__PURE__ */ new WeakMap();
|
|
1759
|
-
function subscriberStatsListenersFor(store) {
|
|
1760
|
-
let listeners2 = subscriberStatsListenersByStore.get(store);
|
|
1761
|
-
if (!listeners2) {
|
|
1762
|
-
listeners2 = /* @__PURE__ */ new Set();
|
|
1763
|
-
subscriberStatsListenersByStore.set(store, listeners2);
|
|
1764
|
-
}
|
|
1765
|
-
return listeners2;
|
|
1766
|
-
}
|
|
1767
|
-
function emitSubscriberStatsChange(store) {
|
|
1768
|
-
for (const cb of /* @__PURE__ */ new Set([
|
|
1769
|
-
...globalSubscriberStatsListeners,
|
|
1770
|
-
...subscriberStatsListenersFor(store)
|
|
1771
|
-
])) cb();
|
|
1772
|
-
}
|
|
1773
|
-
var subscribersByStore = /* @__PURE__ */ new WeakMap();
|
|
1774
|
-
function subscribersFor(store) {
|
|
1775
|
-
let subscribers = subscribersByStore.get(store);
|
|
1776
|
-
if (!subscribers) {
|
|
1777
|
-
subscribers = /* @__PURE__ */ new Map();
|
|
1778
|
-
subscribersByStore.set(store, subscribers);
|
|
1779
|
-
}
|
|
1780
|
-
return subscribers;
|
|
1781
|
-
}
|
|
1782
|
-
function registerSubscriber(key, store = graphStore) {
|
|
1783
|
-
const subscribers = subscribersFor(store);
|
|
1784
|
-
const token = Symbol(key);
|
|
1785
|
-
if (!subscribers.has(key)) subscribers.set(key, /* @__PURE__ */ new Set());
|
|
1786
|
-
subscribers.get(key).add(token);
|
|
1787
|
-
emitSubscriberStatsChange(store);
|
|
1788
|
-
return token;
|
|
1789
|
-
}
|
|
1790
|
-
function unregisterSubscriber(key, token, store = graphStore) {
|
|
1791
|
-
const subscribers = subscribersFor(store);
|
|
1792
|
-
const set = subscribers.get(key);
|
|
1793
|
-
if (!set) return;
|
|
1794
|
-
set.delete(token);
|
|
1795
|
-
if (set.size === 0) subscribers.delete(key);
|
|
1796
|
-
emitSubscriberStatsChange(store);
|
|
1797
|
-
}
|
|
1798
|
-
function hasSubscribers(key, store = graphStore) {
|
|
1799
|
-
return (subscribersFor(store).get(key)?.size ?? 0) > 0;
|
|
1800
|
-
}
|
|
1801
|
-
var DEFAULT_OPTIONS = {
|
|
1802
|
-
defaultStaleTime: 3e4,
|
|
1803
|
-
defaultGcTime: 3e5,
|
|
1804
|
-
gcInterval: 6e4,
|
|
1805
|
-
maxRetries: 3,
|
|
1806
|
-
retryBaseDelay: 1e3,
|
|
1807
|
-
revalidateOnFocus: true,
|
|
1808
|
-
revalidateOnReconnect: true
|
|
1809
|
-
};
|
|
1810
|
-
var engineOptions = { ...DEFAULT_OPTIONS };
|
|
1811
|
-
function subscribeSubscriberStats(onChange, store) {
|
|
1812
|
-
const listeners2 = store ? subscriberStatsListenersFor(store) : globalSubscriberStatsListeners;
|
|
1813
|
-
listeners2.add(onChange);
|
|
1814
|
-
return () => listeners2.delete(onChange);
|
|
1815
|
-
}
|
|
1816
|
-
function getActiveSubscriberCount(store = graphStore) {
|
|
1817
|
-
let n = 0;
|
|
1818
|
-
for (const set of subscribersFor(store).values()) n += set.size;
|
|
1819
|
-
return n;
|
|
1820
|
-
}
|
|
1821
|
-
function subscribeDevtoolsEvent(cb) {
|
|
1822
|
-
return subscribeLegacyDevtoolsEvent(cb);
|
|
1823
|
-
}
|
|
1824
|
-
var gcIntervalsByStore = /* @__PURE__ */ new Map();
|
|
1825
|
-
function runGarbageCollection(storeApi = graphStore) {
|
|
1826
|
-
const store = storeApi.getState();
|
|
1827
|
-
const { defaultGcTime: gcTime } = getEngineOptions();
|
|
1828
|
-
const now = Date.now();
|
|
1829
|
-
const toRemove = [];
|
|
1830
|
-
for (const type of Object.keys(store.entities)) {
|
|
1831
|
-
const bucket = store.entities[type];
|
|
1832
|
-
if (!bucket) continue;
|
|
1833
|
-
for (const id of Object.keys(bucket)) {
|
|
1834
|
-
const key = `${type}:${id}`;
|
|
1835
|
-
if (hasSubscribers(key, storeApi)) continue;
|
|
1836
|
-
const patch = store.patches[type]?.[id];
|
|
1837
|
-
if (patch !== void 0 && Object.keys(patch).length > 0) continue;
|
|
1838
|
-
const entityState = store.entityStates[key];
|
|
1839
|
-
if (entityState?.isFetching) continue;
|
|
1840
|
-
const lastFetched = entityState?.lastFetched;
|
|
1841
|
-
if (lastFetched == null) continue;
|
|
1842
|
-
if (now - lastFetched <= gcTime) continue;
|
|
1843
|
-
toRemove.push({ type, id });
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
for (const { type, id } of toRemove) {
|
|
1847
|
-
store.removeEntity(type, id);
|
|
1848
|
-
store.removeIdFromAllLists(type, id);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
function stopGarbageCollector(storeApi = graphStore) {
|
|
1852
|
-
const intervalId = gcIntervalsByStore.get(storeApi);
|
|
1853
|
-
if (intervalId != null && typeof clearInterval !== "undefined") {
|
|
1854
|
-
clearInterval(intervalId);
|
|
1855
|
-
gcIntervalsByStore.delete(storeApi);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
function startGarbageCollector(storeApi = graphStore) {
|
|
1859
|
-
stopGarbageCollector(storeApi);
|
|
1860
|
-
if (typeof window === "undefined" || typeof setInterval === "undefined") return () => {
|
|
1861
|
-
};
|
|
1862
|
-
const intervalId = setInterval(
|
|
1863
|
-
() => runGarbageCollection(storeApi),
|
|
1864
|
-
getEngineOptions().gcInterval
|
|
1865
|
-
);
|
|
1866
|
-
gcIntervalsByStore.set(storeApi, intervalId);
|
|
1867
|
-
return () => stopGarbageCollector(storeApi);
|
|
1868
|
-
}
|
|
1869
|
-
function restartGarbageCollector(storeApi = graphStore) {
|
|
1870
|
-
startGarbageCollector(storeApi);
|
|
1871
|
-
}
|
|
1872
|
-
function configureEngine(opts) {
|
|
1873
|
-
engineOptions = { ...DEFAULT_OPTIONS, ...opts };
|
|
1874
|
-
const activeStores = [...gcIntervalsByStore.keys()];
|
|
1875
|
-
if (activeStores.length === 0) restartGarbageCollector();
|
|
1876
|
-
else for (const storeApi of activeStores) restartGarbageCollector(storeApi);
|
|
1877
|
-
}
|
|
1878
|
-
function getEngineOptions() {
|
|
1879
|
-
return engineOptions;
|
|
1880
|
-
}
|
|
1881
|
-
async function fetchEntity(opts, engineOpts, storeApi = graphStore) {
|
|
1882
|
-
const { type, id, fetch, normalize, sideEffects, idField = "id" } = opts;
|
|
1883
|
-
if (!id) return;
|
|
1884
|
-
storeApi.getState().setEntityFetching(type, id, true);
|
|
1885
|
-
const attempt = async (retries) => {
|
|
1886
|
-
try {
|
|
1887
|
-
const raw = await fetch(id);
|
|
1888
|
-
const normalized = normalize(raw);
|
|
1889
|
-
const resolvedId = normalized[idField] ?? id;
|
|
1890
|
-
storeApi.getState().upsertEntity(type, resolvedId, normalized);
|
|
1891
|
-
storeApi.getState().setEntityFetched(type, resolvedId);
|
|
1892
|
-
if (sideEffects) sideEffects(raw, storeApi);
|
|
1893
|
-
opts.onSuccess?.(normalized);
|
|
1894
|
-
} catch (err) {
|
|
1895
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1896
|
-
if (retries < engineOpts.maxRetries) {
|
|
1897
|
-
await sleep2(engineOpts.retryBaseDelay * Math.pow(2, retries));
|
|
1898
|
-
return attempt(retries + 1);
|
|
1899
|
-
}
|
|
1900
|
-
storeApi.getState().setEntityError(type, id, error.message);
|
|
1901
|
-
opts.onError?.(error);
|
|
1902
|
-
}
|
|
1903
|
-
};
|
|
1904
|
-
await dedupe(`${type}:${id}`, () => attempt(0), storeApi);
|
|
1905
|
-
}
|
|
1906
|
-
async function fetchList(opts, params, engineOpts, isLoadMore = false, storeApi = graphStore) {
|
|
1907
|
-
const { type, queryKey, fetch, normalize, sideEffects, mode = "replace" } = opts;
|
|
1908
|
-
const key = serializeKey(queryKey);
|
|
1909
|
-
const store = storeApi.getState();
|
|
1910
|
-
if (!fetch || !normalize) return;
|
|
1911
|
-
if (isLoadMore) store.setListFetchingMore(key, true);
|
|
1912
|
-
else store.setListFetching(key, true);
|
|
1913
|
-
const attempt = async (retries) => {
|
|
1914
|
-
try {
|
|
1915
|
-
const response = await fetch(params);
|
|
1916
|
-
const normalized = response.items.map(normalize);
|
|
1917
|
-
const meta = { total: response.total ?? null, nextCursor: response.nextCursor ?? null, prevCursor: response.prevCursor ?? null, hasNextPage: response.hasNextPage ?? !!response.nextCursor, hasPrevPage: response.hasPrevPage ?? !!response.prevCursor, currentPage: response.page ?? null, pageSize: response.pageSize ?? null };
|
|
1918
|
-
storeApi.getState().ingestFetchedList(
|
|
1919
|
-
type,
|
|
1920
|
-
normalized.map(({ id, data }) => ({ id, data })),
|
|
1921
|
-
{ lists: [{ key, mode: mode === "append" && isLoadMore ? "append" : "replace", meta }] }
|
|
1922
|
-
);
|
|
1923
|
-
if (sideEffects) sideEffects(response.items, storeApi);
|
|
1924
|
-
opts.onSuccess?.(response);
|
|
1925
|
-
} catch (err) {
|
|
1926
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
1927
|
-
if (retries < engineOpts.maxRetries) {
|
|
1928
|
-
await sleep2(engineOpts.retryBaseDelay * Math.pow(2, retries));
|
|
1929
|
-
return attempt(retries + 1);
|
|
1930
|
-
}
|
|
1931
|
-
storeApi.getState().setListError(key, error.message);
|
|
1932
|
-
opts.onError?.(error);
|
|
1933
|
-
}
|
|
1934
|
-
};
|
|
1935
|
-
await dedupe(isLoadMore ? `${key}:more` : key, () => attempt(0), storeApi);
|
|
1936
|
-
}
|
|
1937
|
-
var globalListenerAttachments = /* @__PURE__ */ new WeakMap();
|
|
1938
|
-
function releaseGlobalListenerAttachment(storeApi, attachment) {
|
|
1939
|
-
let released = false;
|
|
1940
|
-
return () => {
|
|
1941
|
-
if (released) return;
|
|
1942
|
-
released = true;
|
|
1943
|
-
const current = globalListenerAttachments.get(storeApi);
|
|
1944
|
-
if (current !== attachment) return;
|
|
1945
|
-
current.references -= 1;
|
|
1946
|
-
if (current.references > 0) return;
|
|
1947
|
-
current.dispose();
|
|
1948
|
-
globalListenerAttachments.delete(storeApi);
|
|
1949
|
-
};
|
|
1950
|
-
}
|
|
1951
|
-
function attachGlobalListeners(storeApi = graphStore) {
|
|
1952
|
-
if (typeof window === "undefined") return () => {
|
|
1953
|
-
};
|
|
1954
|
-
const existing = globalListenerAttachments.get(storeApi);
|
|
1955
|
-
if (existing) {
|
|
1956
|
-
existing.references += 1;
|
|
1957
|
-
return releaseGlobalListenerAttachment(storeApi, existing);
|
|
1958
|
-
}
|
|
1959
|
-
const stopGc = startGarbageCollector(storeApi);
|
|
1960
|
-
const revalidateAll = () => {
|
|
1961
|
-
const state = storeApi.getState();
|
|
1962
|
-
for (const key of subscribersFor(storeApi).keys()) {
|
|
1963
|
-
if (!hasSubscribers(key, storeApi)) continue;
|
|
1964
|
-
const colonIdx = key.indexOf(":");
|
|
1965
|
-
if (colonIdx === -1) continue;
|
|
1966
|
-
const type = key.slice(0, colonIdx);
|
|
1967
|
-
const id = key.slice(colonIdx + 1);
|
|
1968
|
-
state.setEntityStale(type, id, true);
|
|
1969
|
-
}
|
|
1970
|
-
};
|
|
1971
|
-
const revalidateWhenVisible = () => {
|
|
1972
|
-
if (document.visibilityState === "visible") revalidateAll();
|
|
1973
|
-
};
|
|
1974
|
-
const revalidateOnFocus = engineOptions.revalidateOnFocus;
|
|
1975
|
-
const revalidateOnReconnect = engineOptions.revalidateOnReconnect;
|
|
1976
|
-
if (revalidateOnFocus) {
|
|
1977
|
-
window.addEventListener("visibilitychange", revalidateWhenVisible);
|
|
1978
|
-
window.addEventListener("focus", revalidateAll);
|
|
1979
|
-
}
|
|
1980
|
-
if (revalidateOnReconnect) window.addEventListener("online", revalidateAll);
|
|
1981
|
-
const attachment = {
|
|
1982
|
-
references: 1,
|
|
1983
|
-
dispose: () => {
|
|
1984
|
-
if (revalidateOnFocus) {
|
|
1985
|
-
window.removeEventListener("visibilitychange", revalidateWhenVisible);
|
|
1986
|
-
window.removeEventListener("focus", revalidateAll);
|
|
1987
|
-
}
|
|
1988
|
-
if (revalidateOnReconnect) {
|
|
1989
|
-
window.removeEventListener("online", revalidateAll);
|
|
1990
|
-
}
|
|
1991
|
-
stopGc();
|
|
1992
|
-
}
|
|
1993
|
-
};
|
|
1994
|
-
globalListenerAttachments.set(storeApi, attachment);
|
|
1995
|
-
return releaseGlobalListenerAttachment(storeApi, attachment);
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
// src/devtools-event-bus.ts
|
|
1999
|
-
var _busInjectMap = /* @__PURE__ */ new WeakMap();
|
|
2000
|
-
var registries = /* @__PURE__ */ new Map();
|
|
2001
|
-
function __resetStoreRegistry() {
|
|
2002
|
-
registries.clear();
|
|
2003
|
-
}
|
|
2004
|
-
function createDevtoolsEventBus(opts) {
|
|
2005
|
-
const bufferSize = opts?.bufferSize ?? 500;
|
|
2006
|
-
const threshold = opts?.coalesceBurstThreshold ?? 10;
|
|
2007
|
-
const buffer = bufferSize > 0 ? new Array(bufferSize) : [];
|
|
2008
|
-
let head = 0;
|
|
2009
|
-
let count = 0;
|
|
2010
|
-
function writeToBuffer(event) {
|
|
2011
|
-
if (bufferSize === 0) return;
|
|
2012
|
-
buffer[head] = event;
|
|
2013
|
-
head = (head + 1) % bufferSize;
|
|
2014
|
-
if (count < bufferSize) count++;
|
|
2015
|
-
}
|
|
2016
|
-
function getBuffer() {
|
|
2017
|
-
if (count === 0) return [];
|
|
2018
|
-
if (count < bufferSize) return buffer.slice(0, count);
|
|
2019
|
-
return [...buffer.slice(head), ...buffer.slice(0, head)];
|
|
2020
|
-
}
|
|
2021
|
-
const subscribers = /* @__PURE__ */ new Set();
|
|
2022
|
-
function dispatchToSubscribers(event) {
|
|
2023
|
-
const snap = [...subscribers];
|
|
2024
|
-
for (const cb of snap) {
|
|
2025
|
-
try {
|
|
2026
|
-
cb(event);
|
|
2027
|
-
} catch (err) {
|
|
2028
|
-
console.warn("[devtools-event-bus] subscriber threw:", err);
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
function dispatchOne(event) {
|
|
2033
|
-
writeToBuffer(event);
|
|
2034
|
-
dispatchToSubscribers(event);
|
|
2035
|
-
}
|
|
2036
|
-
let pendingBurst = [];
|
|
2037
|
-
let dispatchedCount = 0;
|
|
2038
|
-
let flushScheduled = false;
|
|
2039
|
-
let destroyed = false;
|
|
2040
|
-
function flush() {
|
|
2041
|
-
if (pendingBurst.length === 0) {
|
|
2042
|
-
flushScheduled = false;
|
|
2043
|
-
return;
|
|
2044
|
-
}
|
|
2045
|
-
const burst = pendingBurst;
|
|
2046
|
-
const alreadySent = dispatchedCount;
|
|
2047
|
-
pendingBurst = [];
|
|
2048
|
-
dispatchedCount = 0;
|
|
2049
|
-
flushScheduled = false;
|
|
2050
|
-
const remaining = burst.slice(alreadySent);
|
|
2051
|
-
if (remaining.length === 0) return;
|
|
2052
|
-
if (threshold === 0 || burst.length <= threshold) {
|
|
2053
|
-
for (const ev of remaining) dispatchOne(ev);
|
|
2054
|
-
} else {
|
|
2055
|
-
const individualSlots = Math.max(0, threshold - alreadySent);
|
|
2056
|
-
const toDispatchIndividually = remaining.slice(0, individualSlots);
|
|
2057
|
-
const toCoalesce = remaining.slice(individualSlots);
|
|
2058
|
-
for (const ev of toDispatchIndividually) dispatchOne(ev);
|
|
2059
|
-
if (toCoalesce.length > 0) {
|
|
2060
|
-
dispatchOne({
|
|
2061
|
-
kind: "list",
|
|
2062
|
-
key: "burst-coalesce",
|
|
2063
|
-
idCount: toCoalesce.length,
|
|
2064
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2065
|
-
});
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
function handleEngineEvent(event) {
|
|
2070
|
-
if (destroyed) return;
|
|
2071
|
-
if (threshold === 0) {
|
|
2072
|
-
dispatchOne(event);
|
|
2073
|
-
return;
|
|
2074
|
-
}
|
|
2075
|
-
pendingBurst.push(event);
|
|
2076
|
-
if (pendingBurst.length <= threshold) {
|
|
2077
|
-
dispatchOne(event);
|
|
2078
|
-
dispatchedCount++;
|
|
2079
|
-
} else {
|
|
2080
|
-
if (!flushScheduled) {
|
|
2081
|
-
flushScheduled = true;
|
|
2082
|
-
Promise.resolve().then(() => flush());
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
const unsubscribeEngine = subscribeDevtoolsEvent(handleEngineEvent);
|
|
2087
|
-
const bus = {
|
|
2088
|
-
subscribe(cb) {
|
|
2089
|
-
if (destroyed) return () => {
|
|
2090
|
-
};
|
|
2091
|
-
const snapshot = getBuffer();
|
|
2092
|
-
for (const ev of snapshot) {
|
|
2093
|
-
try {
|
|
2094
|
-
cb(ev);
|
|
2095
|
-
} catch (err) {
|
|
2096
|
-
console.warn("[devtools-event-bus] subscriber threw during replay:", err);
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
subscribers.add(cb);
|
|
2100
|
-
return () => {
|
|
2101
|
-
subscribers.delete(cb);
|
|
2102
|
-
};
|
|
2103
|
-
},
|
|
2104
|
-
getBuffer,
|
|
2105
|
-
flush,
|
|
2106
|
-
inject(event) {
|
|
2107
|
-
handleEngineEvent(event);
|
|
2108
|
-
},
|
|
2109
|
-
destroy() {
|
|
2110
|
-
if (destroyed) return;
|
|
2111
|
-
destroyed = true;
|
|
2112
|
-
unsubscribeEngine();
|
|
2113
|
-
subscribers.clear();
|
|
2114
|
-
pendingBurst = [];
|
|
2115
|
-
dispatchedCount = 0;
|
|
2116
|
-
flushScheduled = false;
|
|
2117
|
-
const registry = registries.get(bus);
|
|
2118
|
-
if (registry) {
|
|
2119
|
-
for (const entry of registry.values()) {
|
|
2120
|
-
if (!entry.active) continue;
|
|
2121
|
-
entry.active = false;
|
|
2122
|
-
try {
|
|
2123
|
-
entry.unsubscribeSrc();
|
|
2124
|
-
} catch {
|
|
2125
|
-
}
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
registries.delete(bus);
|
|
2129
|
-
_busInjectMap.delete(bus);
|
|
2130
|
-
}
|
|
2131
|
-
};
|
|
2132
|
-
_busInjectMap.set(bus, handleEngineEvent);
|
|
2133
|
-
return bus;
|
|
2134
|
-
}
|
|
2135
|
-
function registerStore(bus, source, name) {
|
|
2136
|
-
let registry = registries.get(bus);
|
|
2137
|
-
if (!registry) {
|
|
2138
|
-
registry = /* @__PURE__ */ new Map();
|
|
2139
|
-
registries.set(bus, registry);
|
|
2140
|
-
}
|
|
2141
|
-
if (registry.get(name)?.active === true) {
|
|
2142
|
-
throw new Error(
|
|
2143
|
-
`[devtools-store-registry] Store name "${name}" is already registered and active`
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
const inject = _busInjectMap.get(bus);
|
|
2147
|
-
if (!inject) {
|
|
2148
|
-
throw new Error(
|
|
2149
|
-
`[devtools-store-registry] Bus is destroyed or was not created by createDevtoolsEventBus`
|
|
2150
|
-
);
|
|
2151
|
-
}
|
|
2152
|
-
const unsubscribeSrc = source(inject);
|
|
2153
|
-
const entry = { name, active: true, bus, unsubscribeSrc };
|
|
2154
|
-
registry.set(name, entry);
|
|
2155
|
-
return () => {
|
|
2156
|
-
if (!entry.active) return;
|
|
2157
|
-
entry.active = false;
|
|
2158
|
-
unsubscribeSrc();
|
|
2159
|
-
};
|
|
2160
|
-
}
|
|
2161
|
-
function getRegisteredStores(bus) {
|
|
2162
|
-
const entries = bus ? [...registries.get(bus)?.values() ?? []] : [...registries.values()].flatMap((registry) => [...registry.values()]);
|
|
2163
|
-
return entries.map((entry) => ({ name: entry.name, active: entry.active }));
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
|
-
// src/devtools-compatibility-bridge.ts
|
|
2167
|
-
var BRIDGE_KEY = /* @__PURE__ */ Symbol.for("prometheus.entity-graph.devtools.compatibility.v1");
|
|
2168
|
-
var bridgeHost = globalThis;
|
|
2169
|
-
var existingBridge = bridgeHost[BRIDGE_KEY];
|
|
2170
|
-
var bridgeState = existingBridge?.version === 1 ? existingBridge : { version: 1, entries: /* @__PURE__ */ new WeakMap(), factory: null };
|
|
2171
|
-
bridgeHost[BRIDGE_KEY] = bridgeState;
|
|
2172
|
-
function getOrCreateEntry(store) {
|
|
2173
|
-
let entry = bridgeState.entries.get(store);
|
|
2174
|
-
if (!entry) {
|
|
2175
|
-
entry = {
|
|
2176
|
-
delegate: null,
|
|
2177
|
-
listeners: /* @__PURE__ */ new Set(),
|
|
2178
|
-
unsubscribeDelegate: null,
|
|
2179
|
-
compatibilityDetach: null
|
|
2180
|
-
};
|
|
2181
|
-
bridgeState.entries.set(store, entry);
|
|
2182
|
-
}
|
|
2183
|
-
return entry;
|
|
2184
|
-
}
|
|
2185
|
-
function ensureDelegate(store, entry) {
|
|
2186
|
-
const factory = bridgeState.factory;
|
|
2187
|
-
if (entry.delegate || entry.compatibilityDetach || !factory) return;
|
|
2188
|
-
entry.compatibilityDetach = factory(store);
|
|
2189
|
-
}
|
|
2190
|
-
function getGraphDevtoolsCompatibilityDelegate(store) {
|
|
2191
|
-
const entry = getOrCreateEntry(store);
|
|
2192
|
-
ensureDelegate(store, entry);
|
|
2193
|
-
return entry.delegate;
|
|
2194
|
-
}
|
|
2195
|
-
function subscribeGraphDevtoolsCompatibility(store, listener) {
|
|
2196
|
-
const entry = getOrCreateEntry(store);
|
|
2197
|
-
entry.listeners.add(listener);
|
|
2198
|
-
ensureDelegate(store, entry);
|
|
2199
|
-
return () => {
|
|
2200
|
-
entry.listeners.delete(listener);
|
|
2201
|
-
if (entry.listeners.size === 0 && entry.delegate === null) bridgeState.entries.delete(store);
|
|
2202
|
-
};
|
|
2203
|
-
}
|
|
2204
|
-
|
|
2205
|
-
// src/devtools-time-travel.ts
|
|
2206
|
-
function configureTimeTravel(opts, storeApi = graphStore) {
|
|
2207
|
-
if (opts.capacity && opts.capacity > 0) {
|
|
2208
|
-
getGraphDevtoolsCompatibilityDelegate(storeApi)?.configure(opts.capacity);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
function recordGraphSnapshot(label, storeApi = graphStore) {
|
|
2212
|
-
return getGraphDevtoolsCompatibilityDelegate(storeApi)?.capture(label) ?? -1;
|
|
2213
|
-
}
|
|
2214
|
-
function restoreGraphSnapshot(index, storeApi = graphStore) {
|
|
2215
|
-
return getGraphDevtoolsCompatibilityDelegate(storeApi)?.restoreByIndex(index) ?? false;
|
|
2216
|
-
}
|
|
2217
|
-
function restoreGraphSnapshotBySeq(seq, storeApi = graphStore) {
|
|
2218
|
-
return getGraphDevtoolsCompatibilityDelegate(storeApi)?.restoreByCursor(seq) ?? false;
|
|
2219
|
-
}
|
|
2220
|
-
function stepTimeTravel(delta, storeApi = graphStore) {
|
|
2221
|
-
return getGraphDevtoolsCompatibilityDelegate(storeApi)?.step(delta) ?? false;
|
|
2222
|
-
}
|
|
2223
|
-
function getTimeTravelState(storeApi = graphStore) {
|
|
2224
|
-
return getGraphDevtoolsCompatibilityDelegate(storeApi)?.getState() ?? {
|
|
2225
|
-
snapshots: [],
|
|
2226
|
-
cursor: null,
|
|
2227
|
-
capacity: 50
|
|
2228
|
-
};
|
|
2229
|
-
}
|
|
2230
|
-
function subscribeTimeTravel(cb, storeApi = graphStore) {
|
|
2231
|
-
return subscribeGraphDevtoolsCompatibility(storeApi, cb);
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
// src/view/incremental.ts
|
|
2235
|
-
var IncrementalView = class {
|
|
2236
|
-
opts;
|
|
2237
|
-
ids = [];
|
|
2238
|
-
members = /* @__PURE__ */ new Set();
|
|
2239
|
-
constructor(opts) {
|
|
2240
|
-
this.opts = opts;
|
|
2241
|
-
}
|
|
2242
|
-
/** Whether an entity qualifies for this view (filter + search). */
|
|
2243
|
-
qualifies(entity) {
|
|
2244
|
-
if (!entity) return false;
|
|
2245
|
-
if (this.opts.filter && !matchesFilter(entity, this.opts.filter)) return false;
|
|
2246
|
-
if (this.opts.search?.query && !matchesSearch(entity, this.opts.search.query, this.opts.search.fields)) return false;
|
|
2247
|
-
return true;
|
|
2248
|
-
}
|
|
2249
|
-
/** Comparator over ids using the configured sort (stable, id tiebreak). */
|
|
2250
|
-
cmp(aId, bId) {
|
|
2251
|
-
const a = this.opts.getEntity(aId);
|
|
2252
|
-
const b = this.opts.getEntity(bId);
|
|
2253
|
-
if (!a || !b) return 0;
|
|
2254
|
-
if (this.opts.sort && this.opts.sort.length > 0) {
|
|
2255
|
-
const r = compareEntities(a, b, this.opts.sort);
|
|
2256
|
-
if (r !== 0) return r;
|
|
2257
|
-
}
|
|
2258
|
-
return aId < bId ? -1 : aId > bId ? 1 : 0;
|
|
2259
|
-
}
|
|
2260
|
-
/** Binary-search the insertion index for `id` among current `ids`. */
|
|
2261
|
-
insertionIndex(id) {
|
|
2262
|
-
let lo = 0;
|
|
2263
|
-
let hi = this.ids.length;
|
|
2264
|
-
while (lo < hi) {
|
|
2265
|
-
const mid = lo + hi >>> 1;
|
|
2266
|
-
if (this.cmp(this.ids[mid], id) < 0) lo = mid + 1;
|
|
2267
|
-
else hi = mid;
|
|
2268
|
-
}
|
|
2269
|
-
return lo;
|
|
2270
|
-
}
|
|
2271
|
-
/** Build the initial sorted result from a full id set. */
|
|
2272
|
-
seed(ids) {
|
|
2273
|
-
const kept = [];
|
|
2274
|
-
this.members.clear();
|
|
2275
|
-
for (const id of ids) {
|
|
2276
|
-
if (this.qualifies(this.opts.getEntity(id))) {
|
|
2277
|
-
kept.push(id);
|
|
2278
|
-
this.members.add(id);
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
if (this.opts.sort && this.opts.sort.length > 0) {
|
|
2282
|
-
kept.sort((a, b) => this.cmp(a, b));
|
|
2283
|
-
}
|
|
2284
|
-
this.ids = kept;
|
|
2285
|
-
}
|
|
2286
|
-
/**
|
|
2287
|
-
* Update the view for ONE entity that changed/was added/removed.
|
|
2288
|
-
* Returns true if the result order changed.
|
|
2289
|
-
*
|
|
2290
|
-
* O(log n) to locate; the array splice is O(n) memory-move but performs no
|
|
2291
|
-
* filter/sort over the whole set — the differentiator from full re-derivation.
|
|
2292
|
-
*/
|
|
2293
|
-
applyChange(id) {
|
|
2294
|
-
const entity = this.opts.getEntity(id);
|
|
2295
|
-
const wasMember = this.members.has(id);
|
|
2296
|
-
const isMember = this.qualifies(entity);
|
|
2297
|
-
if (!wasMember && !isMember) return false;
|
|
2298
|
-
if (wasMember && !isMember) {
|
|
2299
|
-
const idx2 = this.ids.indexOf(id);
|
|
2300
|
-
if (idx2 !== -1) this.ids.splice(idx2, 1);
|
|
2301
|
-
this.members.delete(id);
|
|
2302
|
-
return true;
|
|
2303
|
-
}
|
|
2304
|
-
if (!wasMember && isMember) {
|
|
2305
|
-
const idx2 = this.insertionIndex(id);
|
|
2306
|
-
this.ids.splice(idx2, 0, id);
|
|
2307
|
-
this.members.add(id);
|
|
2308
|
-
return true;
|
|
2309
|
-
}
|
|
2310
|
-
const cur = this.ids.indexOf(id);
|
|
2311
|
-
if (cur !== -1) this.ids.splice(cur, 1);
|
|
2312
|
-
const idx = this.insertionIndex(id);
|
|
2313
|
-
this.ids.splice(idx, 0, id);
|
|
2314
|
-
return cur !== idx;
|
|
2315
|
-
}
|
|
2316
|
-
/** Current ordered id list. */
|
|
2317
|
-
result() {
|
|
2318
|
-
return this.ids;
|
|
2319
|
-
}
|
|
2320
|
-
/** Current member count. */
|
|
2321
|
-
size() {
|
|
2322
|
-
return this.ids.length;
|
|
2323
|
-
}
|
|
2324
|
-
};
|
|
2325
|
-
|
|
2326
|
-
// src/crud/relations.ts
|
|
2327
|
-
var schemaRegistry2 = /* @__PURE__ */ new Map();
|
|
2328
|
-
function registerSchema(schema) {
|
|
2329
|
-
schemaRegistry2.set(schema.type, schema);
|
|
2330
|
-
}
|
|
2331
|
-
function getSchema(type) {
|
|
2332
|
-
return schemaRegistry2.get(type) ?? null;
|
|
2333
|
-
}
|
|
2334
|
-
function cascadeInvalidation(ctx, storeApi = graphStore) {
|
|
2335
|
-
const schema = schemaRegistry2.get(ctx.type);
|
|
2336
|
-
if (!schema) return;
|
|
2337
|
-
const store = storeApi.getState();
|
|
2338
|
-
if (schema.globalListKeys) for (const key of schema.globalListKeys) store.invalidateLists(key);
|
|
2339
|
-
if (!schema.relations) return;
|
|
2340
|
-
for (const [, relation] of Object.entries(schema.relations)) {
|
|
2341
|
-
switch (relation.cardinality) {
|
|
2342
|
-
case "belongsTo": {
|
|
2343
|
-
const prevFk = ctx.previous?.[relation.foreignKey];
|
|
2344
|
-
const nextFk = ctx.next?.[relation.foreignKey];
|
|
2345
|
-
if (prevFk && prevFk !== nextFk && relation.invalidateTargetLists) for (const kp of relation.invalidateTargetLists) store.invalidateLists((k) => k.startsWith(kp) && k.includes(prevFk));
|
|
2346
|
-
if (nextFk && relation.invalidateTargetLists) for (const kp of relation.invalidateTargetLists) store.invalidateLists((k) => k.startsWith(kp) && k.includes(nextFk));
|
|
2347
|
-
if (prevFk) store.invalidateEntity(relation.targetType, prevFk);
|
|
2348
|
-
if (nextFk) store.invalidateEntity(relation.targetType, nextFk);
|
|
2349
|
-
break;
|
|
2350
|
-
}
|
|
2351
|
-
case "hasMany":
|
|
2352
|
-
store.invalidateLists(serializeKey(relation.listKeyPrefix(ctx.id)));
|
|
2353
|
-
break;
|
|
2354
|
-
case "manyToMany": {
|
|
2355
|
-
const prevIds = ctx.previous?.[relation.localArrayField ?? ""];
|
|
2356
|
-
const nextIds = ctx.next?.[relation.localArrayField ?? ""];
|
|
2357
|
-
for (const relatedId of /* @__PURE__ */ new Set([...prevIds ?? [], ...nextIds ?? []])) store.invalidateLists(serializeKey(relation.listKeyPrefix(relatedId)));
|
|
2358
|
-
break;
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
for (const [, otherSchema] of schemaRegistry2) {
|
|
2363
|
-
if (!otherSchema.relations) continue;
|
|
2364
|
-
for (const [, rel] of Object.entries(otherSchema.relations)) {
|
|
2365
|
-
if (rel.targetType !== ctx.type) continue;
|
|
2366
|
-
if (rel.cardinality === "hasMany") store.invalidateLists(serializeKey(rel.listKeyPrefix(ctx.id)));
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
function readRelations(type, entity, storeApi = graphStore) {
|
|
2371
|
-
const schema = schemaRegistry2.get(type);
|
|
2372
|
-
if (!schema?.relations) return {};
|
|
2373
|
-
const store = storeApi.getState();
|
|
2374
|
-
const result = {};
|
|
2375
|
-
for (const [name, relation] of Object.entries(schema.relations)) {
|
|
2376
|
-
switch (relation.cardinality) {
|
|
2377
|
-
case "belongsTo": {
|
|
2378
|
-
const fkValue = entity[relation.foreignKey];
|
|
2379
|
-
result[name] = fkValue ? store.readEntity(relation.targetType, fkValue) : null;
|
|
2380
|
-
break;
|
|
2381
|
-
}
|
|
2382
|
-
case "hasMany": {
|
|
2383
|
-
const listKey = serializeKey(relation.listKeyPrefix(entity.id));
|
|
2384
|
-
const listState = store.lists[listKey];
|
|
2385
|
-
result[name] = listState ? listState.ids.map((id) => store.readEntity(relation.targetType, id)).filter(Boolean) : [];
|
|
2386
|
-
break;
|
|
2387
|
-
}
|
|
2388
|
-
case "manyToMany": {
|
|
2389
|
-
const ids = entity[relation.localArrayField ?? ""];
|
|
2390
|
-
result[name] = ids ? ids.map((id) => store.readEntity(relation.targetType, id)).filter(Boolean) : [];
|
|
2391
|
-
break;
|
|
2392
|
-
}
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
return result;
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
// src/agent/json-patch.ts
|
|
2399
|
-
function unescape(token) {
|
|
2400
|
-
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
2401
|
-
}
|
|
2402
|
-
function parsePointer(pointer) {
|
|
2403
|
-
if (pointer === "") return [];
|
|
2404
|
-
if (pointer[0] !== "/") throw new Error(`Invalid JSON Pointer: "${pointer}"`);
|
|
2405
|
-
return pointer.slice(1).split("/").map(unescape);
|
|
2406
|
-
}
|
|
2407
|
-
function isObject2(v) {
|
|
2408
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2409
|
-
}
|
|
2410
|
-
function clone(v) {
|
|
2411
|
-
return structuredClone(v);
|
|
2412
|
-
}
|
|
2413
|
-
function getAt(doc, segs) {
|
|
2414
|
-
let cur = doc;
|
|
2415
|
-
for (const s of segs) {
|
|
2416
|
-
if (Array.isArray(cur)) cur = cur[Number(s)];
|
|
2417
|
-
else if (isObject2(cur)) cur = cur[s];
|
|
2418
|
-
else return void 0;
|
|
2419
|
-
}
|
|
2420
|
-
return cur;
|
|
2421
|
-
}
|
|
2422
|
-
function setAt(doc, segs, value, mode) {
|
|
2423
|
-
if (segs.length === 0) return value;
|
|
2424
|
-
const root = clone(doc);
|
|
2425
|
-
let cur = root;
|
|
2426
|
-
for (let i = 0; i < segs.length - 1; i++) {
|
|
2427
|
-
const s = segs[i];
|
|
2428
|
-
const next = Array.isArray(cur) ? cur[Number(s)] : isObject2(cur) ? cur[s] : void 0;
|
|
2429
|
-
if (next === void 0 || next === null) {
|
|
2430
|
-
const created = {};
|
|
2431
|
-
if (Array.isArray(cur)) cur[Number(s)] = created;
|
|
2432
|
-
else if (isObject2(cur)) cur[s] = created;
|
|
2433
|
-
cur = created;
|
|
2434
|
-
} else {
|
|
2435
|
-
cur = next;
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
const last = segs[segs.length - 1];
|
|
2439
|
-
if (Array.isArray(cur)) {
|
|
2440
|
-
const idx = last === "-" ? cur.length : Number(last);
|
|
2441
|
-
if (mode === "add") cur.splice(idx, 0, value);
|
|
2442
|
-
else cur[idx] = value;
|
|
2443
|
-
} else if (isObject2(cur)) {
|
|
2444
|
-
cur[last] = value;
|
|
2445
|
-
}
|
|
2446
|
-
return root;
|
|
2447
|
-
}
|
|
2448
|
-
function removeAt(doc, segs) {
|
|
2449
|
-
if (segs.length === 0) return void 0;
|
|
2450
|
-
const root = clone(doc);
|
|
2451
|
-
let cur = root;
|
|
2452
|
-
for (let i = 0; i < segs.length - 1; i++) {
|
|
2453
|
-
const s = segs[i];
|
|
2454
|
-
cur = Array.isArray(cur) ? cur[Number(s)] : isObject2(cur) ? cur[s] : void 0;
|
|
2455
|
-
if (cur === void 0) return root;
|
|
2456
|
-
}
|
|
2457
|
-
const last = segs[segs.length - 1];
|
|
2458
|
-
if (Array.isArray(cur)) cur.splice(Number(last), 1);
|
|
2459
|
-
else if (isObject2(cur)) delete cur[last];
|
|
2460
|
-
return root;
|
|
2461
|
-
}
|
|
2462
|
-
function applyJsonPatch(doc, ops) {
|
|
2463
|
-
let current = doc;
|
|
2464
|
-
for (const op of ops) {
|
|
2465
|
-
const segs = parsePointer(op.path);
|
|
2466
|
-
switch (op.op) {
|
|
2467
|
-
case "add":
|
|
2468
|
-
current = setAt(current, segs, op.value, "add");
|
|
2469
|
-
break;
|
|
2470
|
-
case "replace":
|
|
2471
|
-
current = setAt(current, segs, op.value, "replace");
|
|
2472
|
-
break;
|
|
2473
|
-
case "remove":
|
|
2474
|
-
current = removeAt(current, segs);
|
|
2475
|
-
break;
|
|
2476
|
-
case "move": {
|
|
2477
|
-
const fromSegs = parsePointer(op.from ?? "");
|
|
2478
|
-
const moved = getAt(current, fromSegs);
|
|
2479
|
-
current = removeAt(current, fromSegs);
|
|
2480
|
-
current = setAt(current, segs, moved, "add");
|
|
2481
|
-
break;
|
|
2482
|
-
}
|
|
2483
|
-
case "copy": {
|
|
2484
|
-
const fromSegs = parsePointer(op.from ?? "");
|
|
2485
|
-
const copied = clone(getAt(current, fromSegs));
|
|
2486
|
-
current = setAt(current, segs, copied, "add");
|
|
2487
|
-
break;
|
|
2488
|
-
}
|
|
2489
|
-
case "test": {
|
|
2490
|
-
const actual = getAt(current, segs);
|
|
2491
|
-
if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
|
|
2492
|
-
throw new Error(`JSON Patch test failed at "${op.path}"`);
|
|
2493
|
-
}
|
|
2494
|
-
break;
|
|
2495
|
-
}
|
|
2496
|
-
default:
|
|
2497
|
-
throw new Error(`Unsupported JSON Patch op: ${op.op}`);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
return current;
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
// src/agent/ag-ui-bridge.ts
|
|
2504
|
-
function isObject3(v) {
|
|
2505
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2506
|
-
}
|
|
2507
|
-
function getAtPointer(state, pointer) {
|
|
2508
|
-
if (pointer === "" || pointer === "/") return state;
|
|
2509
|
-
const segs = pointer.slice(1).split("/").map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
2510
|
-
let cur = state;
|
|
2511
|
-
for (const s of segs) {
|
|
2512
|
-
if (Array.isArray(cur)) cur = cur[Number(s)];
|
|
2513
|
-
else if (isObject3(cur)) cur = cur[s];
|
|
2514
|
-
else return void 0;
|
|
2515
|
-
}
|
|
2516
|
-
return cur;
|
|
2517
|
-
}
|
|
2518
|
-
function writeEntity(mapping, id, obj) {
|
|
2519
|
-
const store = useGraphStore.getState();
|
|
2520
|
-
if ((mapping.write ?? "upsert") === "replace") {
|
|
2521
|
-
store.replaceEntity(mapping.entityType, id, obj);
|
|
2522
|
-
} else {
|
|
2523
|
-
store.upsertEntity(mapping.entityType, id, obj);
|
|
2524
|
-
}
|
|
2525
|
-
}
|
|
2526
|
-
function projectState(state, mappings) {
|
|
2527
|
-
for (const mapping of mappings) {
|
|
2528
|
-
const slice = getAtPointer(state, mapping.pointer);
|
|
2529
|
-
if (slice === void 0) continue;
|
|
2530
|
-
if (mapping.kind === "single") {
|
|
2531
|
-
if (!isObject3(slice)) continue;
|
|
2532
|
-
const id = mapping.constantId ?? String(slice[mapping.idField ?? "id"] ?? "");
|
|
2533
|
-
if (!id) continue;
|
|
2534
|
-
writeEntity(mapping, id, slice);
|
|
2535
|
-
} else {
|
|
2536
|
-
const items = [];
|
|
2537
|
-
if (Array.isArray(slice)) {
|
|
2538
|
-
for (const item of slice) {
|
|
2539
|
-
if (isObject3(item)) {
|
|
2540
|
-
const id = String(item[mapping.idField ?? "id"] ?? "");
|
|
2541
|
-
if (id) items.push([id, item]);
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
} else if (isObject3(slice)) {
|
|
2545
|
-
for (const [key, item] of Object.entries(slice)) {
|
|
2546
|
-
if (isObject3(item)) {
|
|
2547
|
-
const id = String(item[mapping.idField ?? "id"] ?? key);
|
|
2548
|
-
items.push([id, item]);
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
for (const [id, obj] of items) writeEntity(mapping, id, obj);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
}
|
|
2556
|
-
function applyAgUiSnapshot(event, options) {
|
|
2557
|
-
if (!isObject3(event.snapshot)) return;
|
|
2558
|
-
projectState(event.snapshot, options.mappings);
|
|
2559
|
-
}
|
|
2560
|
-
function applyAgUiDelta(event, options, currentState) {
|
|
2561
|
-
const base = currentState ?? reconstructState(options.mappings);
|
|
2562
|
-
const next = applyJsonPatch(base, event.delta);
|
|
2563
|
-
if (isObject3(next)) projectState(next, options.mappings);
|
|
2564
|
-
}
|
|
2565
|
-
function reconstructState(mappings) {
|
|
2566
|
-
const store = useGraphStore.getState();
|
|
2567
|
-
const state = {};
|
|
2568
|
-
for (const mapping of mappings) {
|
|
2569
|
-
const key = mapping.pointer.replace(/^\//, "").split("/")[0];
|
|
2570
|
-
if (!key) continue;
|
|
2571
|
-
const bucket = store.entities[mapping.entityType] ?? {};
|
|
2572
|
-
if (mapping.kind === "single") {
|
|
2573
|
-
const id = mapping.constantId;
|
|
2574
|
-
if (id && bucket[id]) state[key] = bucket[id];
|
|
2575
|
-
} else {
|
|
2576
|
-
state[key] = Object.values(bucket);
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
return state;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
// src/adapters/realtime-manager.ts
|
|
2583
|
-
var RealtimeManager = class {
|
|
2584
|
-
adapters = /* @__PURE__ */ new Map();
|
|
2585
|
-
pendingChanges = [];
|
|
2586
|
-
pendingListKeys = /* @__PURE__ */ new Set();
|
|
2587
|
-
flushTimer = null;
|
|
2588
|
-
opts;
|
|
2589
|
-
store;
|
|
2590
|
-
constructor(opts = {}) {
|
|
2591
|
-
this.store = opts.store ?? graphStore;
|
|
2592
|
-
this.opts = {
|
|
2593
|
-
flushInterval: opts.flushInterval ?? 16,
|
|
2594
|
-
onStatusChange: opts.onStatusChange ?? (() => {
|
|
2595
|
-
}),
|
|
2596
|
-
onChangeReceived: opts.onChangeReceived ?? (() => {
|
|
2597
|
-
})
|
|
2598
|
-
};
|
|
2599
|
-
}
|
|
2600
|
-
register(adapter, channels, normalize) {
|
|
2601
|
-
const existing = this.adapters.get(adapter.name);
|
|
2602
|
-
const registered = existing ?? { adapter, unsubscribes: [] };
|
|
2603
|
-
if (!existing) this.adapters.set(adapter.name, registered);
|
|
2604
|
-
if (adapter.onStatusChange) {
|
|
2605
|
-
registered.unsubscribes.push(adapter.onStatusChange((s) => this.opts.onStatusChange(adapter.name, s)));
|
|
2606
|
-
}
|
|
2607
|
-
for (const channel of channels) {
|
|
2608
|
-
const unsub = adapter.subscribe(
|
|
2609
|
-
{ label: `${adapter.name}/${channel.type}`, replayOnConnect: true },
|
|
2610
|
-
(cs) => this.handleChangeset(adapter.name, cs, normalize)
|
|
2611
|
-
);
|
|
2612
|
-
registered.unsubscribes.push(unsub);
|
|
2613
|
-
}
|
|
2614
|
-
return () => this.unregister(adapter.name);
|
|
2615
|
-
}
|
|
2616
|
-
unregister(name) {
|
|
2617
|
-
const r = this.adapters.get(name);
|
|
2618
|
-
if (!r) return;
|
|
2619
|
-
for (const u of r.unsubscribes) u();
|
|
2620
|
-
this.adapters.delete(name);
|
|
2621
|
-
}
|
|
2622
|
-
unregisterAll() {
|
|
2623
|
-
for (const n of this.adapters.keys()) this.unregister(n);
|
|
2624
|
-
}
|
|
2625
|
-
handleChangeset(name, cs, normalize) {
|
|
2626
|
-
for (const rawChange of cs.changes) {
|
|
2627
|
-
const change = normalize ? normalize(rawChange) : rawChange;
|
|
2628
|
-
if (!change) continue;
|
|
2629
|
-
this.opts.onChangeReceived(name, change);
|
|
2630
|
-
this.pendingChanges.push(change);
|
|
2631
|
-
}
|
|
2632
|
-
if (cs.affectedListKeys) for (const k of cs.affectedListKeys) this.pendingListKeys.add(k);
|
|
2633
|
-
this.scheduleFlush();
|
|
2634
|
-
}
|
|
2635
|
-
scheduleFlush() {
|
|
2636
|
-
if (this.opts.flushInterval === 0) {
|
|
2637
|
-
this.flush();
|
|
2638
|
-
return;
|
|
2639
|
-
}
|
|
2640
|
-
if (this.flushTimer !== null) return;
|
|
2641
|
-
this.flushTimer = setTimeout(() => {
|
|
2642
|
-
this.flushTimer = null;
|
|
2643
|
-
this.flush();
|
|
2644
|
-
}, this.opts.flushInterval);
|
|
2645
|
-
}
|
|
2646
|
-
flush() {
|
|
2647
|
-
if (this.pendingChanges.length === 0 && this.pendingListKeys.size === 0) return;
|
|
2648
|
-
const changes = this.pendingChanges.splice(0);
|
|
2649
|
-
const listKeys = new Set(this.pendingListKeys);
|
|
2650
|
-
this.pendingListKeys.clear();
|
|
2651
|
-
const store = this.store.getState();
|
|
2652
|
-
for (const change of coalesceChanges(changes)) {
|
|
2653
|
-
switch (change.op) {
|
|
2654
|
-
case "insert":
|
|
2655
|
-
case "upsert":
|
|
2656
|
-
if (change.data) {
|
|
2657
|
-
store.upsertEntity(change.type, change.id, change.data);
|
|
2658
|
-
store.setEntityFetched(change.type, change.id);
|
|
2659
|
-
}
|
|
2660
|
-
break;
|
|
2661
|
-
case "update":
|
|
2662
|
-
if (change.patch) {
|
|
2663
|
-
if (store.entities[change.type]?.[change.id]) store.upsertEntity(change.type, change.id, change.patch);
|
|
2664
|
-
else store.invalidateEntity(change.type, change.id);
|
|
2665
|
-
} else if (change.data) {
|
|
2666
|
-
store.upsertEntity(change.type, change.id, change.data);
|
|
2667
|
-
store.setEntityFetched(change.type, change.id);
|
|
2668
|
-
}
|
|
2669
|
-
break;
|
|
2670
|
-
case "delete":
|
|
2671
|
-
store.removeEntity(change.type, change.id);
|
|
2672
|
-
store.removeIdFromAllLists(change.type, change.id);
|
|
2673
|
-
break;
|
|
2674
|
-
}
|
|
2675
|
-
}
|
|
2676
|
-
for (const key of listKeys) store.invalidateLists(key);
|
|
2677
|
-
}
|
|
2678
|
-
forceFlush() {
|
|
2679
|
-
if (this.flushTimer !== null) {
|
|
2680
|
-
clearTimeout(this.flushTimer);
|
|
2681
|
-
this.flushTimer = null;
|
|
2682
|
-
}
|
|
2683
|
-
this.flush();
|
|
2684
|
-
}
|
|
2685
|
-
};
|
|
2686
|
-
function coalesceChanges(changes) {
|
|
2687
|
-
const byKey = /* @__PURE__ */ new Map();
|
|
2688
|
-
for (const c of changes) {
|
|
2689
|
-
const key = `${c.type}:${c.id}`;
|
|
2690
|
-
const existing = byKey.get(key);
|
|
2691
|
-
if (!existing) {
|
|
2692
|
-
byKey.set(key, { ...c });
|
|
2693
|
-
continue;
|
|
2694
|
-
}
|
|
2695
|
-
if (c.op === "delete") {
|
|
2696
|
-
byKey.set(key, c);
|
|
2697
|
-
continue;
|
|
2698
|
-
}
|
|
2699
|
-
if (existing.op === "delete") continue;
|
|
2700
|
-
if (existing.op === "update" && c.op === "update") {
|
|
2701
|
-
byKey.set(key, {
|
|
2702
|
-
...existing,
|
|
2703
|
-
patch: { ...existing.patch ?? {}, ...c.patch ?? {} }
|
|
2704
|
-
});
|
|
2705
|
-
continue;
|
|
2706
|
-
}
|
|
2707
|
-
byKey.set(key, { ...existing, op: "upsert", data: c.data ?? existing.data, patch: c.patch ? { ...existing.patch ?? {}, ...c.patch } : existing.patch });
|
|
2708
|
-
}
|
|
2709
|
-
return Array.from(byKey.values());
|
|
2710
|
-
}
|
|
2711
|
-
var _manager = null;
|
|
2712
|
-
function getRealtimeManager(opts) {
|
|
2713
|
-
if (!_manager) _manager = new RealtimeManager(opts);
|
|
2714
|
-
return _manager;
|
|
2715
|
-
}
|
|
2716
|
-
function resetRealtimeManager() {
|
|
2717
|
-
_manager?.unregisterAll();
|
|
2718
|
-
_manager = null;
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
// src/adapters/realtime-adapters.ts
|
|
2722
|
-
function createWebSocketAdapter(opts) {
|
|
2723
|
-
const {
|
|
2724
|
-
url,
|
|
2725
|
-
parseMessage,
|
|
2726
|
-
protocols,
|
|
2727
|
-
reconnectBaseDelay = 1e3,
|
|
2728
|
-
maxReconnectAttempts = Infinity,
|
|
2729
|
-
pingInterval = 3e4,
|
|
2730
|
-
pingMessage = '{"type":"ping"}'
|
|
2731
|
-
} = opts;
|
|
2732
|
-
const statusCbs = /* @__PURE__ */ new Set();
|
|
2733
|
-
const handlers = /* @__PURE__ */ new Set();
|
|
2734
|
-
let ws = null;
|
|
2735
|
-
let attempts = 0;
|
|
2736
|
-
let reconnTimer = null;
|
|
2737
|
-
let pingTimer = null;
|
|
2738
|
-
let stopped = false;
|
|
2739
|
-
const emit = (s) => {
|
|
2740
|
-
for (const cb of statusCbs) cb(s);
|
|
2741
|
-
};
|
|
2742
|
-
function connect() {
|
|
2743
|
-
if (stopped) return;
|
|
2744
|
-
const u = typeof url === "function" ? url() : url;
|
|
2745
|
-
emit("connecting");
|
|
2746
|
-
ws = new WebSocket(u, protocols);
|
|
2747
|
-
ws.onopen = () => {
|
|
2748
|
-
attempts = 0;
|
|
2749
|
-
emit("connected");
|
|
2750
|
-
if (pingInterval > 0)
|
|
2751
|
-
pingTimer = setInterval(() => {
|
|
2752
|
-
if (ws?.readyState === 1) ws.send(pingMessage);
|
|
2753
|
-
}, pingInterval);
|
|
2754
|
-
};
|
|
2755
|
-
ws.onmessage = (ev) => {
|
|
2756
|
-
let parsed;
|
|
2757
|
-
try {
|
|
2758
|
-
parsed = JSON.parse(ev.data);
|
|
2759
|
-
} catch {
|
|
2760
|
-
return;
|
|
2761
|
-
}
|
|
2762
|
-
const changes = parseMessage ? parseMessage(parsed) : defaultParse(parsed);
|
|
2763
|
-
if (!changes?.length) return;
|
|
2764
|
-
for (const h of handlers) h({ changes, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2765
|
-
};
|
|
2766
|
-
ws.onclose = () => {
|
|
2767
|
-
emit("disconnected");
|
|
2768
|
-
clearInterval(pingTimer);
|
|
2769
|
-
if (!stopped && attempts < maxReconnectAttempts)
|
|
2770
|
-
reconnTimer = setTimeout(connect, reconnectBaseDelay * Math.pow(2, Math.min(attempts++, 6)));
|
|
2771
|
-
};
|
|
2772
|
-
ws.onerror = () => {
|
|
2773
|
-
emit("error");
|
|
2774
|
-
ws?.close();
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
return {
|
|
2778
|
-
name: "websocket",
|
|
2779
|
-
subscribe(_cfg, handler) {
|
|
2780
|
-
handlers.add(handler);
|
|
2781
|
-
if (!ws || ws.readyState === WebSocket.CLOSED) connect();
|
|
2782
|
-
return () => {
|
|
2783
|
-
handlers.delete(handler);
|
|
2784
|
-
if (handlers.size === 0) {
|
|
2785
|
-
stopped = true;
|
|
2786
|
-
clearTimeout(reconnTimer);
|
|
2787
|
-
clearInterval(pingTimer);
|
|
2788
|
-
ws?.close();
|
|
2789
|
-
ws = null;
|
|
2790
|
-
}
|
|
2791
|
-
};
|
|
2792
|
-
},
|
|
2793
|
-
onStatusChange: (cb) => {
|
|
2794
|
-
statusCbs.add(cb);
|
|
2795
|
-
return () => statusCbs.delete(cb);
|
|
2796
|
-
}
|
|
2797
|
-
};
|
|
2798
|
-
}
|
|
2799
|
-
function defaultParse(data) {
|
|
2800
|
-
if (!data || typeof data !== "object") return null;
|
|
2801
|
-
const d = data;
|
|
2802
|
-
if (d.op && d.type && d.id) return [d];
|
|
2803
|
-
if (Array.isArray(d.changes)) return d.changes;
|
|
2804
|
-
return null;
|
|
2805
|
-
}
|
|
2806
|
-
function createSupabaseRealtimeAdapter(client, opts = {}) {
|
|
2807
|
-
const {
|
|
2808
|
-
tableTypeMap = {},
|
|
2809
|
-
extractId = (r) => String(r.id),
|
|
2810
|
-
schema = "public"
|
|
2811
|
-
} = opts;
|
|
2812
|
-
const statusCbs = /* @__PURE__ */ new Set();
|
|
2813
|
-
function resolveType(table) {
|
|
2814
|
-
return tableTypeMap[table] ?? table.charAt(0).toUpperCase() + table.slice(1);
|
|
2815
|
-
}
|
|
2816
|
-
function payloadToChange(p) {
|
|
2817
|
-
const type = resolveType(p.table);
|
|
2818
|
-
if (p.eventType === "DELETE") {
|
|
2819
|
-
const id2 = extractId(p.old);
|
|
2820
|
-
return id2 ? { op: "delete", type, id: id2 } : null;
|
|
2821
|
-
}
|
|
2822
|
-
const id = extractId(p.new);
|
|
2823
|
-
if (!id) return null;
|
|
2824
|
-
return { op: p.eventType === "INSERT" ? "insert" : "upsert", type, id, data: p.new };
|
|
2825
|
-
}
|
|
2826
|
-
return {
|
|
2827
|
-
name: "supabase-realtime",
|
|
2828
|
-
/**
|
|
2829
|
-
* Generic subscribe is a no-op for Supabase. Use `subscribeChannel()` instead,
|
|
2830
|
-
* which accepts a `ChannelConfig` with Supabase-specific table/filter parameters.
|
|
2831
|
-
*/
|
|
2832
|
-
subscribe(_cfg, _handler) {
|
|
2833
|
-
return () => {
|
|
2834
|
-
};
|
|
2835
|
-
},
|
|
2836
|
-
onStatusChange: (cb) => {
|
|
2837
|
-
statusCbs.add(cb);
|
|
2838
|
-
return () => statusCbs.delete(cb);
|
|
2839
|
-
},
|
|
2840
|
-
subscribeChannel(config) {
|
|
2841
|
-
const tableName = Object.entries(tableTypeMap).find(([, t]) => t === config.type)?.[0] ?? config.type.toLowerCase();
|
|
2842
|
-
const filterStr = config.id ? `id=eq.${config.id}` : config.filter ? Object.entries(config.filter).map(([k, v]) => `${k}=eq.${v}`).join(",") : void 0;
|
|
2843
|
-
const channel = client.channel(`entity-store:${config.type}:${config.id ?? "all"}`).on("postgres_changes", { event: "*", schema, table: tableName, ...filterStr ? { filter: filterStr } : {} }, (payload) => {
|
|
2844
|
-
const change = payloadToChange(payload);
|
|
2845
|
-
if (!change) return;
|
|
2846
|
-
config._handler?.({ changes: [change], timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2847
|
-
}).subscribe((status) => {
|
|
2848
|
-
const s = status === "SUBSCRIBED" ? "connected" : status === "CHANNEL_ERROR" ? "error" : status === "CLOSED" ? "disconnected" : "connecting";
|
|
2849
|
-
for (const cb of statusCbs) cb(s);
|
|
2850
|
-
});
|
|
2851
|
-
return () => {
|
|
2852
|
-
channel.unsubscribe();
|
|
2853
|
-
};
|
|
2854
|
-
}
|
|
2855
|
-
};
|
|
2856
|
-
}
|
|
2857
|
-
function createConvexAdapter(opts) {
|
|
2858
|
-
const { client, channels } = opts;
|
|
2859
|
-
const statusCbs = /* @__PURE__ */ new Set();
|
|
2860
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
2861
|
-
function diffSnapshot(type, prev, next, extractId, normalize) {
|
|
2862
|
-
const changes = [];
|
|
2863
|
-
const nextIds = /* @__PURE__ */ new Set();
|
|
2864
|
-
for (const record of next) {
|
|
2865
|
-
const id = extractId(record);
|
|
2866
|
-
nextIds.add(id);
|
|
2867
|
-
const data = normalize ? normalize(record) : record;
|
|
2868
|
-
changes.push({ op: prev.has(id) ? "upsert" : "insert", type, id, data });
|
|
2869
|
-
prev.set(id, record);
|
|
2870
|
-
}
|
|
2871
|
-
for (const [id] of prev) {
|
|
2872
|
-
if (!nextIds.has(id)) {
|
|
2873
|
-
changes.push({ op: "delete", type, id });
|
|
2874
|
-
prev.delete(id);
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
return changes;
|
|
2878
|
-
}
|
|
2879
|
-
return {
|
|
2880
|
-
name: "convex",
|
|
2881
|
-
subscribe(_cfg, handler) {
|
|
2882
|
-
const unsubs = [];
|
|
2883
|
-
for (const ch of channels) {
|
|
2884
|
-
const { type, query, args = {}, normalize } = ch;
|
|
2885
|
-
const extractId = ch.extractId ?? ((r) => String(
|
|
2886
|
-
r._id ?? r.id
|
|
2887
|
-
));
|
|
2888
|
-
if (!snapshots.has(type)) snapshots.set(type, /* @__PURE__ */ new Map());
|
|
2889
|
-
const snap = snapshots.get(type);
|
|
2890
|
-
unsubs.push(
|
|
2891
|
-
client.onUpdate(query, args, (records) => {
|
|
2892
|
-
const changes = diffSnapshot(type, snap, records, extractId, normalize);
|
|
2893
|
-
if (changes.length > 0)
|
|
2894
|
-
handler({ changes, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2895
|
-
})
|
|
2896
|
-
);
|
|
2897
|
-
}
|
|
2898
|
-
for (const cb of statusCbs) cb("connected");
|
|
2899
|
-
return () => {
|
|
2900
|
-
for (const u of unsubs) u();
|
|
2901
|
-
for (const cb of statusCbs) cb("disconnected");
|
|
2902
|
-
};
|
|
2903
|
-
},
|
|
2904
|
-
onStatusChange: (cb) => {
|
|
2905
|
-
statusCbs.add(cb);
|
|
2906
|
-
return () => statusCbs.delete(cb);
|
|
2907
|
-
}
|
|
2908
|
-
};
|
|
2909
|
-
}
|
|
2910
|
-
function createGraphQLSubscriptionAdapter(opts) {
|
|
2911
|
-
const {
|
|
2912
|
-
client,
|
|
2913
|
-
subscriptions,
|
|
2914
|
-
extractId = (n) => String(n.id),
|
|
2915
|
-
normalize
|
|
2916
|
-
} = opts;
|
|
2917
|
-
const statusCbs = /* @__PURE__ */ new Set();
|
|
2918
|
-
function payloadToChange(type, p) {
|
|
2919
|
-
const gqlType = p.type ?? "updated";
|
|
2920
|
-
if (gqlType === "deleted") {
|
|
2921
|
-
const id2 = p.id ?? (p.node ? extractId(p.node, type) : null);
|
|
2922
|
-
return id2 ? { op: "delete", type, id: id2 } : null;
|
|
2923
|
-
}
|
|
2924
|
-
if (!p.node) return null;
|
|
2925
|
-
const id = extractId(p.node, type);
|
|
2926
|
-
const data = normalize ? normalize(p.node, type) : stripTypename(p.node);
|
|
2927
|
-
const op = gqlType === "created" ? "insert" : "upsert";
|
|
2928
|
-
return { op, type, id, data };
|
|
2929
|
-
}
|
|
2930
|
-
return {
|
|
2931
|
-
name: "graphql-subscription",
|
|
2932
|
-
subscribe(_cfg, handler) {
|
|
2933
|
-
const unsubs = [];
|
|
2934
|
-
for (const sub of subscriptions) {
|
|
2935
|
-
const { type, document: document2, variables, getPayload } = sub;
|
|
2936
|
-
unsubs.push(
|
|
2937
|
-
client.subscribe(
|
|
2938
|
-
{ query: document2, variables },
|
|
2939
|
-
{
|
|
2940
|
-
next: ({ data }) => {
|
|
2941
|
-
const raw = getPayload(data);
|
|
2942
|
-
if (!raw) return;
|
|
2943
|
-
const payloads = Array.isArray(raw) ? raw : [raw];
|
|
2944
|
-
const changes = payloads.map((p) => payloadToChange(type, p)).filter((c) => c !== null);
|
|
2945
|
-
if (changes.length > 0)
|
|
2946
|
-
handler({ changes, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2947
|
-
},
|
|
2948
|
-
error: (e) => {
|
|
2949
|
-
console.error(`[GQLSub] ${type}:`, e);
|
|
2950
|
-
for (const cb of statusCbs) cb("error");
|
|
2951
|
-
},
|
|
2952
|
-
complete: () => {
|
|
2953
|
-
for (const cb of statusCbs) cb("disconnected");
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
)
|
|
2957
|
-
);
|
|
2958
|
-
}
|
|
2959
|
-
for (const cb of statusCbs) cb("connected");
|
|
2960
|
-
return () => {
|
|
2961
|
-
for (const u of unsubs) u();
|
|
2962
|
-
};
|
|
2963
|
-
},
|
|
2964
|
-
onStatusChange: (cb) => {
|
|
2965
|
-
statusCbs.add(cb);
|
|
2966
|
-
return () => statusCbs.delete(cb);
|
|
2967
|
-
}
|
|
2968
|
-
};
|
|
2969
|
-
}
|
|
2970
|
-
function stripTypename(obj) {
|
|
2971
|
-
const result = {};
|
|
2972
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
2973
|
-
if (k === "__typename") continue;
|
|
2974
|
-
result[k] = v && typeof v === "object" && !Array.isArray(v) ? stripTypename(v) : v;
|
|
2975
|
-
}
|
|
2976
|
-
return result;
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
|
-
// src/adapters/surreal-live.ts
|
|
2980
|
-
function createSurrealLiveAdapter(opts) {
|
|
2981
|
-
return new SurrealLiveAdapter(opts);
|
|
2982
|
-
}
|
|
2983
|
-
var DEFAULT_INITIAL_DELAY = 500;
|
|
2984
|
-
var DEFAULT_MAX_DELAY = 3e4;
|
|
2985
|
-
var DEFAULT_CONNECTED_SETTLE = 3e4;
|
|
2986
|
-
var DEFAULT_CHECKPOINT_FIELD = "updated_at";
|
|
2987
|
-
var DEFAULT_PERMANENT_PATTERNS = [
|
|
2988
|
-
/authentication/i,
|
|
2989
|
-
/unauthor/i,
|
|
2990
|
-
/namespace/i,
|
|
2991
|
-
/unknown\s+table/i,
|
|
2992
|
-
/permission/i
|
|
2993
|
-
];
|
|
2994
|
-
var STATUS_PRIORITY = ["error", "disconnected", "connecting", "connected"];
|
|
2995
|
-
var SurrealLiveAdapter = class {
|
|
2996
|
-
name;
|
|
2997
|
-
opts;
|
|
2998
|
-
channels = /* @__PURE__ */ new Map();
|
|
2999
|
-
channelStates = /* @__PURE__ */ new Map();
|
|
3000
|
-
statusCallbacks = /* @__PURE__ */ new Set();
|
|
3001
|
-
_status = "connecting";
|
|
3002
|
-
constructor(rawOpts) {
|
|
3003
|
-
this.opts = {
|
|
3004
|
-
name: rawOpts.name ?? "surreal-live",
|
|
3005
|
-
surreal: rawOpts.surreal,
|
|
3006
|
-
tables: rawOpts.tables,
|
|
3007
|
-
initialQueryStrategy: rawOpts.initialQueryStrategy ?? "select-then-live",
|
|
3008
|
-
initialDelayMs: rawOpts.initialDelayMs ?? DEFAULT_INITIAL_DELAY,
|
|
3009
|
-
maxDelayMs: rawOpts.maxDelayMs ?? DEFAULT_MAX_DELAY,
|
|
3010
|
-
connectedSettleMs: rawOpts.connectedSettleMs ?? DEFAULT_CONNECTED_SETTLE,
|
|
3011
|
-
checkpointField: rawOpts.checkpointField ?? DEFAULT_CHECKPOINT_FIELD,
|
|
3012
|
-
permanentErrorPatterns: rawOpts.permanentErrorPatterns ?? DEFAULT_PERMANENT_PATTERNS,
|
|
3013
|
-
checkpointStore: rawOpts.checkpointStore,
|
|
3014
|
-
listKeyResolver: rawOpts.listKeyResolver
|
|
3015
|
-
};
|
|
3016
|
-
this.name = this.opts.name;
|
|
3017
|
-
}
|
|
3018
|
-
subscribe(config, handler) {
|
|
3019
|
-
const channelConfig = this._resolveChannelConfig(config);
|
|
3020
|
-
const key = this._channelKey(config, channelConfig);
|
|
3021
|
-
const channel = new SurrealChannel(this, config, channelConfig, key, handler);
|
|
3022
|
-
this.channels.set(key, channel);
|
|
3023
|
-
this.channelStates.set(key, "connecting");
|
|
3024
|
-
this._recomputeStatus();
|
|
3025
|
-
channel.start().catch(() => {
|
|
3026
|
-
});
|
|
3027
|
-
return () => {
|
|
3028
|
-
const ch = this.channels.get(key);
|
|
3029
|
-
if (!ch) return;
|
|
3030
|
-
this.channels.delete(key);
|
|
3031
|
-
this.channelStates.delete(key);
|
|
3032
|
-
void ch.stop();
|
|
3033
|
-
this._recomputeStatus();
|
|
3034
|
-
};
|
|
3035
|
-
}
|
|
3036
|
-
onStatusChange = (cb) => {
|
|
3037
|
-
this.statusCallbacks.add(cb);
|
|
3038
|
-
return () => {
|
|
3039
|
-
this.statusCallbacks.delete(cb);
|
|
3040
|
-
};
|
|
3041
|
-
};
|
|
3042
|
-
// ─── package-private helpers used by SurrealChannel ───────────────────────
|
|
3043
|
-
_setChannelStatus(key, status) {
|
|
3044
|
-
if (this.channelStates.get(key) === status) return;
|
|
3045
|
-
this.channelStates.set(key, status);
|
|
3046
|
-
this._recomputeStatus();
|
|
3047
|
-
}
|
|
3048
|
-
_getOpts() {
|
|
3049
|
-
return this.opts;
|
|
3050
|
-
}
|
|
3051
|
-
_recomputeStatus() {
|
|
3052
|
-
const states = [...this.channelStates.values()];
|
|
3053
|
-
const aggregate = states.length === 0 ? "connecting" : STATUS_PRIORITY.find((s) => states.includes(s)) ?? "connecting";
|
|
3054
|
-
if (aggregate === this._status) return;
|
|
3055
|
-
this._status = aggregate;
|
|
3056
|
-
for (const cb of this.statusCallbacks) cb(aggregate);
|
|
3057
|
-
}
|
|
3058
|
-
_resolveChannelConfig(config) {
|
|
3059
|
-
const labelType = config.label?.split("/").pop();
|
|
3060
|
-
const match = this.opts.tables.find((t) => t.type === labelType) ?? this.opts.tables[0];
|
|
3061
|
-
if (!match) {
|
|
3062
|
-
throw new Error(
|
|
3063
|
-
`[surreal-live] no tables configured; pass at least one SurrealTableConfig to createSurrealLiveAdapter`
|
|
3064
|
-
);
|
|
3065
|
-
}
|
|
3066
|
-
return {
|
|
3067
|
-
type: match.type
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
_channelKey(config, channelConfig) {
|
|
3071
|
-
const label = config.label ?? `${this.name}/${channelConfig.type}`;
|
|
3072
|
-
return label;
|
|
3073
|
-
}
|
|
3074
|
-
};
|
|
3075
|
-
var SurrealChannel = class {
|
|
3076
|
-
constructor(adapter, config, channelConfig, key, handler) {
|
|
3077
|
-
this.adapter = adapter;
|
|
3078
|
-
this.config = config;
|
|
3079
|
-
this.channelConfig = channelConfig;
|
|
3080
|
-
this.key = key;
|
|
3081
|
-
this.handler = handler;
|
|
3082
|
-
}
|
|
3083
|
-
liveUuid = null;
|
|
3084
|
-
buffer = [];
|
|
3085
|
-
seedComplete = false;
|
|
3086
|
-
attempt = 0;
|
|
3087
|
-
connectedSettleTimer = null;
|
|
3088
|
-
reconnectTimer = null;
|
|
3089
|
-
aborted = false;
|
|
3090
|
-
status = "connecting";
|
|
3091
|
-
async start() {
|
|
3092
|
-
if (this.aborted) return;
|
|
3093
|
-
this._setStatus("connecting");
|
|
3094
|
-
try {
|
|
3095
|
-
await this._openLive();
|
|
3096
|
-
const opts = this.adapter._getOpts();
|
|
3097
|
-
if (opts.initialQueryStrategy !== "live-only") {
|
|
3098
|
-
await this._runSeed();
|
|
3099
|
-
}
|
|
3100
|
-
this.seedComplete = true;
|
|
3101
|
-
const buffered = this.buffer.splice(0);
|
|
3102
|
-
for (const action of buffered) this._handleAction(action);
|
|
3103
|
-
this._setStatus("connected");
|
|
3104
|
-
this._armConnectedSettle();
|
|
3105
|
-
} catch (err) {
|
|
3106
|
-
this._scheduleReconnect(err);
|
|
3107
|
-
}
|
|
3108
|
-
}
|
|
3109
|
-
async stop() {
|
|
3110
|
-
this.aborted = true;
|
|
3111
|
-
if (this.reconnectTimer) {
|
|
3112
|
-
clearTimeout(this.reconnectTimer);
|
|
3113
|
-
this.reconnectTimer = null;
|
|
3114
|
-
}
|
|
3115
|
-
if (this.connectedSettleTimer) {
|
|
3116
|
-
clearTimeout(this.connectedSettleTimer);
|
|
3117
|
-
this.connectedSettleTimer = null;
|
|
3118
|
-
}
|
|
3119
|
-
if (this.liveUuid) {
|
|
3120
|
-
const uuid = this.liveUuid;
|
|
3121
|
-
this.liveUuid = null;
|
|
3122
|
-
try {
|
|
3123
|
-
await this.adapter._getOpts().surreal.kill(uuid);
|
|
3124
|
-
} catch {
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
// ─── private ──────────────────────────────────────────────────────────────
|
|
3129
|
-
_table() {
|
|
3130
|
-
const t = this.adapter._getOpts().tables.find((x) => x.type === this.channelConfig.type);
|
|
3131
|
-
return t?.table ?? this.channelConfig.type;
|
|
3132
|
-
}
|
|
3133
|
-
_idField() {
|
|
3134
|
-
const t = this.adapter._getOpts().tables.find((x) => x.type === this.channelConfig.type);
|
|
3135
|
-
return t?.idField ?? "id";
|
|
3136
|
-
}
|
|
3137
|
-
async _openLive() {
|
|
3138
|
-
if (this.aborted) return;
|
|
3139
|
-
const surreal = this.adapter._getOpts().surreal;
|
|
3140
|
-
this.liveUuid = await surreal.live(this._table(), (action) => {
|
|
3141
|
-
this._onLiveAction(action);
|
|
3142
|
-
});
|
|
3143
|
-
}
|
|
3144
|
-
_onLiveAction(action) {
|
|
3145
|
-
if (this.aborted) return;
|
|
3146
|
-
if (!this.seedComplete) {
|
|
3147
|
-
this.buffer.push(action);
|
|
3148
|
-
return;
|
|
3149
|
-
}
|
|
3150
|
-
this._handleAction(action);
|
|
3151
|
-
}
|
|
3152
|
-
_handleAction(action) {
|
|
3153
|
-
const op = mapAction(action.action);
|
|
3154
|
-
if (op === "close") {
|
|
3155
|
-
this._scheduleReconnect(new Error("SurrealDB live query closed"));
|
|
3156
|
-
return;
|
|
3157
|
-
}
|
|
3158
|
-
if (op === null) {
|
|
3159
|
-
console.warn(`[surreal-live] unknown action "${action.action}"; skipping`);
|
|
3160
|
-
return;
|
|
3161
|
-
}
|
|
3162
|
-
const allowed = this.channelConfig.operations;
|
|
3163
|
-
if (allowed && !allowed.includes(op)) return;
|
|
3164
|
-
const change = this._rowToChange(op, action.result);
|
|
3165
|
-
if (!change) return;
|
|
3166
|
-
const changeset = {
|
|
3167
|
-
changes: [change],
|
|
3168
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3169
|
-
affectedListKeys: this._resolveListKeys([change])
|
|
3170
|
-
};
|
|
3171
|
-
this.handler(changeset);
|
|
3172
|
-
void this._persistCheckpoint(change);
|
|
3173
|
-
}
|
|
3174
|
-
async _runSeed() {
|
|
3175
|
-
const surreal = this.adapter._getOpts().surreal;
|
|
3176
|
-
const table = this._table();
|
|
3177
|
-
const sql = `SELECT * FROM ${table}`;
|
|
3178
|
-
const rows = await surreal.query(sql) ?? [];
|
|
3179
|
-
const changes = [];
|
|
3180
|
-
for (const row of rows) {
|
|
3181
|
-
const change = this._rowToChange("insert", row);
|
|
3182
|
-
if (change) changes.push(change);
|
|
3183
|
-
}
|
|
3184
|
-
const changeset = {
|
|
3185
|
-
changes,
|
|
3186
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3187
|
-
affectedListKeys: this._resolveListKeys(changes)
|
|
3188
|
-
};
|
|
3189
|
-
this.handler(changeset);
|
|
3190
|
-
for (const change of changes) void this._persistCheckpoint(change);
|
|
3191
|
-
}
|
|
3192
|
-
_rowToChange(op, row) {
|
|
3193
|
-
if (!row && op !== "delete") return null;
|
|
3194
|
-
const idValue = row?.[this._idField()];
|
|
3195
|
-
if (idValue == null) {
|
|
3196
|
-
console.warn(`[surreal-live] action ${op} missing id field "${this._idField()}"; skipping`);
|
|
3197
|
-
return null;
|
|
3198
|
-
}
|
|
3199
|
-
const id = String(idValue);
|
|
3200
|
-
const base = {
|
|
3201
|
-
op,
|
|
3202
|
-
type: this.channelConfig.type,
|
|
3203
|
-
id
|
|
3204
|
-
};
|
|
3205
|
-
if (op === "delete") return base;
|
|
3206
|
-
return { ...base, data: row };
|
|
3207
|
-
}
|
|
3208
|
-
_resolveListKeys(changes) {
|
|
3209
|
-
const resolver = this.adapter._getOpts().listKeyResolver;
|
|
3210
|
-
if (!resolver) return void 0;
|
|
3211
|
-
const acc = /* @__PURE__ */ new Set();
|
|
3212
|
-
for (const change of changes) {
|
|
3213
|
-
const keys = resolver(change);
|
|
3214
|
-
if (!keys) continue;
|
|
3215
|
-
for (const k of keys) acc.add(k);
|
|
3216
|
-
}
|
|
3217
|
-
return acc.size === 0 ? void 0 : [...acc];
|
|
3218
|
-
}
|
|
3219
|
-
async _persistCheckpoint(change) {
|
|
3220
|
-
const opts = this.adapter._getOpts();
|
|
3221
|
-
if (!opts.checkpointStore) return;
|
|
3222
|
-
const value = change.data?.[opts.checkpointField];
|
|
3223
|
-
if (value == null) return;
|
|
3224
|
-
try {
|
|
3225
|
-
await opts.checkpointStore.set(this.key, String(value));
|
|
3226
|
-
} catch {
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
async _replayFromCheckpoint() {
|
|
3230
|
-
const opts = this.adapter._getOpts();
|
|
3231
|
-
if (!opts.checkpointStore) return;
|
|
3232
|
-
const stored = await opts.checkpointStore.get(this.key);
|
|
3233
|
-
if (!stored) return;
|
|
3234
|
-
const sql = `SELECT * FROM ${this._table()} WHERE ${opts.checkpointField} > $stored ORDER BY ${opts.checkpointField} ASC`;
|
|
3235
|
-
const rows = await opts.surreal.query(sql, { stored }) ?? [];
|
|
3236
|
-
if (rows.length === 0) return;
|
|
3237
|
-
const changes = [];
|
|
3238
|
-
for (const row of rows) {
|
|
3239
|
-
const change = this._rowToChange("update", row);
|
|
3240
|
-
if (change) changes.push(change);
|
|
3241
|
-
}
|
|
3242
|
-
if (changes.length === 0) return;
|
|
3243
|
-
const changeset = {
|
|
3244
|
-
changes,
|
|
3245
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3246
|
-
affectedListKeys: this._resolveListKeys(changes)
|
|
3247
|
-
};
|
|
3248
|
-
this.handler(changeset);
|
|
3249
|
-
for (const change of changes) void this._persistCheckpoint(change);
|
|
3250
|
-
}
|
|
3251
|
-
_scheduleReconnect(reason) {
|
|
3252
|
-
if (this.aborted) return;
|
|
3253
|
-
if (this._isPermanent(reason)) {
|
|
3254
|
-
this._setStatus("error");
|
|
3255
|
-
return;
|
|
3256
|
-
}
|
|
3257
|
-
this._setStatus("disconnected");
|
|
3258
|
-
this.attempt += 1;
|
|
3259
|
-
const opts = this.adapter._getOpts();
|
|
3260
|
-
const exp = Math.min(this.attempt - 1, 6);
|
|
3261
|
-
const base = Math.min(opts.initialDelayMs * 2 ** exp, opts.maxDelayMs);
|
|
3262
|
-
const jitter = base * 0.25 * (Math.random() * 2 - 1);
|
|
3263
|
-
const delay = Math.max(0, Math.round(base + jitter));
|
|
3264
|
-
this._setStatus("connecting");
|
|
3265
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
3266
|
-
if (this.aborted) return;
|
|
3267
|
-
try {
|
|
3268
|
-
await this._openLive();
|
|
3269
|
-
if (opts.checkpointStore) await this._replayFromCheckpoint();
|
|
3270
|
-
this._setStatus("connected");
|
|
3271
|
-
this._armConnectedSettle();
|
|
3272
|
-
} catch (err) {
|
|
3273
|
-
this._scheduleReconnect(err);
|
|
3274
|
-
}
|
|
3275
|
-
}, delay);
|
|
3276
|
-
}
|
|
3277
|
-
_isPermanent(reason) {
|
|
3278
|
-
const msg = reason instanceof Error ? reason.message : String(reason ?? "");
|
|
3279
|
-
const patterns = this.adapter._getOpts().permanentErrorPatterns;
|
|
3280
|
-
return patterns.some((re) => re.test(msg));
|
|
3281
|
-
}
|
|
3282
|
-
_armConnectedSettle() {
|
|
3283
|
-
if (this.connectedSettleTimer) clearTimeout(this.connectedSettleTimer);
|
|
3284
|
-
this.connectedSettleTimer = setTimeout(() => {
|
|
3285
|
-
this.attempt = 0;
|
|
3286
|
-
}, this.adapter._getOpts().connectedSettleMs);
|
|
3287
|
-
}
|
|
3288
|
-
_setStatus(status) {
|
|
3289
|
-
if (this.status === status) return;
|
|
3290
|
-
this.status = status;
|
|
3291
|
-
this.adapter._setChannelStatus(this.key, status);
|
|
3292
|
-
}
|
|
3293
|
-
};
|
|
3294
|
-
function mapAction(action) {
|
|
3295
|
-
switch ((action ?? "").toUpperCase()) {
|
|
3296
|
-
case "CREATE":
|
|
3297
|
-
return "insert";
|
|
3298
|
-
case "UPDATE":
|
|
3299
|
-
return "update";
|
|
3300
|
-
case "DELETE":
|
|
3301
|
-
return "delete";
|
|
3302
|
-
case "CLOSE":
|
|
3303
|
-
return "close";
|
|
3304
|
-
default:
|
|
3305
|
-
return null;
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
// src/adapters/flint.ts
|
|
3310
|
-
function checkpointKey(channelId, consumerId) {
|
|
3311
|
-
return `flint:${channelId}:${consumerId}`;
|
|
3312
|
-
}
|
|
3313
|
-
function createFlintAdapter(options) {
|
|
3314
|
-
const {
|
|
3315
|
-
client,
|
|
3316
|
-
channelId,
|
|
3317
|
-
consumerId,
|
|
3318
|
-
entityType,
|
|
3319
|
-
checkpoints,
|
|
3320
|
-
resolveOp = () => "upsert"
|
|
3321
|
-
} = options;
|
|
3322
|
-
const name = options.name ?? `flint:${channelId}`;
|
|
3323
|
-
const statusListeners = /* @__PURE__ */ new Set();
|
|
3324
|
-
function emitStatus(s) {
|
|
3325
|
-
for (const cb of statusListeners) cb(s);
|
|
3326
|
-
}
|
|
3327
|
-
return {
|
|
3328
|
-
name,
|
|
3329
|
-
subscribe(_config, handler) {
|
|
3330
|
-
let cancelled = false;
|
|
3331
|
-
emitStatus("connecting");
|
|
3332
|
-
(async () => {
|
|
3333
|
-
const key = checkpointKey(channelId, consumerId);
|
|
3334
|
-
const lastSeen = checkpoints ? await checkpoints.get(key) : void 0;
|
|
3335
|
-
const query = {
|
|
3336
|
-
channelId,
|
|
3337
|
-
consumerId,
|
|
3338
|
-
...entityType !== void 0 ? { entityType } : {},
|
|
3339
|
-
...lastSeen !== void 0 ? { fromOffset: lastSeen + 1n } : {}
|
|
3340
|
-
};
|
|
3341
|
-
try {
|
|
3342
|
-
emitStatus("connected");
|
|
3343
|
-
for await (const event of client.watchEntities(query)) {
|
|
3344
|
-
if (cancelled) break;
|
|
3345
|
-
if (entityType !== void 0 && event.entityType !== entityType) continue;
|
|
3346
|
-
const change = {
|
|
3347
|
-
op: resolveOp(event),
|
|
3348
|
-
type: event.entityType,
|
|
3349
|
-
id: event.entityId,
|
|
3350
|
-
data: event.data
|
|
3351
|
-
};
|
|
3352
|
-
const changeset = {
|
|
3353
|
-
changes: [change],
|
|
3354
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3355
|
-
};
|
|
3356
|
-
handler(changeset);
|
|
3357
|
-
if (checkpoints) await checkpoints.set(key, event.offset);
|
|
3358
|
-
}
|
|
3359
|
-
if (!cancelled) emitStatus("disconnected");
|
|
3360
|
-
} catch {
|
|
3361
|
-
if (!cancelled) emitStatus("error");
|
|
3362
|
-
}
|
|
3363
|
-
})();
|
|
3364
|
-
return () => {
|
|
3365
|
-
cancelled = true;
|
|
3366
|
-
emitStatus("disconnected");
|
|
3367
|
-
};
|
|
3368
|
-
},
|
|
3369
|
-
onStatusChange(cb) {
|
|
3370
|
-
statusListeners.add(cb);
|
|
3371
|
-
return () => statusListeners.delete(cb);
|
|
3372
|
-
}
|
|
3373
|
-
};
|
|
3374
|
-
}
|
|
3375
|
-
async function publishFlintMutation(client, record) {
|
|
3376
|
-
await client.mutateEntity(record);
|
|
3377
|
-
}
|
|
3378
|
-
|
|
3379
|
-
// src/adapters/electricsql.ts
|
|
3380
|
-
function createElectricAdapter(opts) {
|
|
3381
|
-
const { pglite, tables, onSynced } = opts;
|
|
3382
|
-
const statusCbs = /* @__PURE__ */ new Set();
|
|
3383
|
-
const syncedCbs = /* @__PURE__ */ new Set();
|
|
3384
|
-
const syncedTables = /* @__PURE__ */ new Set();
|
|
3385
|
-
let globalHandler = null;
|
|
3386
|
-
function checkAllSynced() {
|
|
3387
|
-
if (tables.every((t) => syncedTables.has(t.table))) {
|
|
3388
|
-
onSynced?.();
|
|
3389
|
-
for (const cb of syncedCbs) cb();
|
|
3390
|
-
}
|
|
3391
|
-
}
|
|
3392
|
-
function toChange(tc, msg) {
|
|
3393
|
-
const { type, idColumn = "id", normalize } = tc;
|
|
3394
|
-
const op = msg.headers.operation;
|
|
3395
|
-
const raw = msg.value;
|
|
3396
|
-
const id = String(raw[idColumn]);
|
|
3397
|
-
if (!id) return null;
|
|
3398
|
-
const data = normalize ? normalize(raw) : raw;
|
|
3399
|
-
if (op === "delete") return { op: "delete", type, id };
|
|
3400
|
-
return { op: op === "insert" ? "insert" : "upsert", type, id, data };
|
|
3401
|
-
}
|
|
3402
|
-
const shapeUnsubs = [];
|
|
3403
|
-
for (const tc of tables) {
|
|
3404
|
-
shapeUnsubs.push(tc.shapeStream.subscribe((msgs) => {
|
|
3405
|
-
if (!globalHandler) return;
|
|
3406
|
-
const changes = msgs.map((m) => toChange(tc, m)).filter((c) => c !== null);
|
|
3407
|
-
if (changes.length > 0) globalHandler({ changes, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
3408
|
-
if (tc.shapeStream.isUpToDate) {
|
|
3409
|
-
syncedTables.add(tc.table);
|
|
3410
|
-
checkAllSynced();
|
|
3411
|
-
}
|
|
3412
|
-
}, (e) => {
|
|
3413
|
-
console.error(`[Electric] ${tc.table}:`, e);
|
|
3414
|
-
for (const cb of statusCbs) cb("error");
|
|
3415
|
-
}));
|
|
3416
|
-
}
|
|
3417
|
-
void (async () => {
|
|
3418
|
-
for (const tc of tables) {
|
|
3419
|
-
await pglite.listen(`entity_store_${tc.table}`, (payload) => {
|
|
3420
|
-
if (!globalHandler) return;
|
|
3421
|
-
try {
|
|
3422
|
-
const parsed = JSON.parse(payload);
|
|
3423
|
-
const change = toChange(tc, { headers: { operation: parsed.op }, offset: "", key: String(parsed.row[tc.idColumn ?? "id"]), value: parsed.row });
|
|
3424
|
-
if (change) globalHandler({ changes: [change] });
|
|
3425
|
-
} catch {
|
|
3426
|
-
}
|
|
3427
|
-
});
|
|
3428
|
-
}
|
|
3429
|
-
})();
|
|
3430
|
-
return {
|
|
3431
|
-
name: "electricsql",
|
|
3432
|
-
subscribe(_cfg, handler) {
|
|
3433
|
-
globalHandler = handler;
|
|
3434
|
-
for (const cb of statusCbs) cb("connected");
|
|
3435
|
-
return () => {
|
|
3436
|
-
globalHandler = null;
|
|
3437
|
-
for (const u of shapeUnsubs) u();
|
|
3438
|
-
};
|
|
3439
|
-
},
|
|
3440
|
-
onStatusChange(cb) {
|
|
3441
|
-
statusCbs.add(cb);
|
|
3442
|
-
return () => statusCbs.delete(cb);
|
|
3443
|
-
},
|
|
3444
|
-
async query(sql, params) {
|
|
3445
|
-
const r = await pglite.query(sql, params);
|
|
3446
|
-
return { rows: r.rows };
|
|
3447
|
-
},
|
|
3448
|
-
async execute(sql, _params) {
|
|
3449
|
-
await pglite.exec(sql);
|
|
3450
|
-
},
|
|
3451
|
-
isSynced() {
|
|
3452
|
-
return tables.every((t) => syncedTables.has(t.table));
|
|
3453
|
-
},
|
|
3454
|
-
onSyncComplete(cb) {
|
|
3455
|
-
if (this.isSynced()) {
|
|
3456
|
-
cb();
|
|
3457
|
-
return () => {
|
|
3458
|
-
};
|
|
3459
|
-
}
|
|
3460
|
-
syncedCbs.add(cb);
|
|
3461
|
-
return () => syncedCbs.delete(cb);
|
|
3462
|
-
}
|
|
3463
|
-
};
|
|
3464
|
-
}
|
|
3465
|
-
|
|
3466
|
-
// src/adapters/electricsql-tenant.ts
|
|
3467
|
-
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3468
|
-
function assertUuid(companyId) {
|
|
3469
|
-
if (typeof companyId !== "string" || !UUID_RE.test(companyId)) {
|
|
3470
|
-
throw new Error(
|
|
3471
|
-
`tenant-scoped adapter: companyId must be a UUID, received "${companyId}".`
|
|
3472
|
-
);
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
3475
|
-
function assertSafeColumn(column) {
|
|
3476
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
|
|
3477
|
-
throw new Error(`tenant-scoped adapter: unsafe tenantColumn "${column}".`);
|
|
3478
|
-
}
|
|
3479
|
-
}
|
|
3480
|
-
function buildTenantWhere(tenantColumn, companyId, tableLabel) {
|
|
3481
|
-
if (typeof tenantColumn === "undefined") {
|
|
3482
|
-
throw new Error(
|
|
3483
|
-
`shape "${tableLabel}" lacks tenantColumn \u2014 tenant-scoped adapter refuses to attach unscoped shapes.`
|
|
3484
|
-
);
|
|
3485
|
-
}
|
|
3486
|
-
assertUuid(companyId);
|
|
3487
|
-
if (tenantColumn === null) {
|
|
3488
|
-
return `id = '${companyId}'`;
|
|
3489
|
-
}
|
|
3490
|
-
assertSafeColumn(tenantColumn);
|
|
3491
|
-
return `${tenantColumn} = '${companyId}'`;
|
|
3492
|
-
}
|
|
3493
|
-
function createTenantScopedElectricAdapter(opts) {
|
|
3494
|
-
const { pglite, tenantClaim, tables, onSynced } = opts;
|
|
3495
|
-
assertUuid(tenantClaim.companyId);
|
|
3496
|
-
const wired = tables.map((tc) => {
|
|
3497
|
-
const where = buildTenantWhere(tc.tenantColumn, tenantClaim.companyId, tc.table);
|
|
3498
|
-
const shapeStream = tc.shapeStreamFactory({ table: tc.table, where, tenantClaim });
|
|
3499
|
-
return {
|
|
3500
|
-
type: tc.type,
|
|
3501
|
-
table: tc.table,
|
|
3502
|
-
where,
|
|
3503
|
-
idColumn: tc.primaryKey?.[0] ?? "id",
|
|
3504
|
-
normalize: tc.normalize,
|
|
3505
|
-
shapeStream
|
|
3506
|
-
};
|
|
3507
|
-
});
|
|
3508
|
-
return createElectricAdapter({ pglite, tables: wired, onSynced });
|
|
3509
|
-
}
|
|
3510
|
-
|
|
3511
|
-
// src/adapters/pglite-persistence.ts
|
|
3512
|
-
var DEFAULT_TABLE = "_graph_snapshot";
|
|
3513
|
-
async function createPGlitePersistenceAdapter(pglite, options = {}) {
|
|
3514
|
-
const tableName = options.tableName ?? DEFAULT_TABLE;
|
|
3515
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) {
|
|
3516
|
-
throw new Error(`createPGlitePersistenceAdapter: invalid tableName "${tableName}"`);
|
|
3517
|
-
}
|
|
3518
|
-
await pglite.exec(
|
|
3519
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
3520
|
-
key TEXT PRIMARY KEY,
|
|
3521
|
-
value TEXT NOT NULL,
|
|
3522
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
3523
|
-
)`
|
|
3524
|
-
);
|
|
3525
|
-
return {
|
|
3526
|
-
async get(key) {
|
|
3527
|
-
const result = await pglite.query(
|
|
3528
|
-
`SELECT value FROM ${tableName} WHERE key = $1`,
|
|
3529
|
-
[key]
|
|
3530
|
-
);
|
|
3531
|
-
const row = result.rows[0];
|
|
3532
|
-
return row?.value ?? null;
|
|
3533
|
-
},
|
|
3534
|
-
async set(key, value) {
|
|
3535
|
-
await pglite.query(
|
|
3536
|
-
`INSERT INTO ${tableName} (key, value, updated_at)
|
|
3537
|
-
VALUES ($1, $2, now())
|
|
3538
|
-
ON CONFLICT (key) DO UPDATE
|
|
3539
|
-
SET value = EXCLUDED.value,
|
|
3540
|
-
updated_at = now()`,
|
|
3541
|
-
[key, value]
|
|
3542
|
-
);
|
|
3543
|
-
},
|
|
3544
|
-
async remove(key) {
|
|
3545
|
-
await pglite.query(`DELETE FROM ${tableName} WHERE key = $1`, [key]);
|
|
3546
|
-
}
|
|
3547
|
-
};
|
|
3548
|
-
}
|
|
3549
|
-
|
|
3550
|
-
// src/adapters/tauri-sql-persistence.ts
|
|
3551
|
-
var DEFAULT_TABLE2 = "_graph_snapshot";
|
|
3552
|
-
async function createTauriSqlPersistenceAdapter(db, options = {}) {
|
|
3553
|
-
const tableName = options.tableName ?? DEFAULT_TABLE2;
|
|
3554
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) {
|
|
3555
|
-
throw new Error(`createTauriSqlPersistenceAdapter: invalid tableName "${tableName}"`);
|
|
3556
|
-
}
|
|
3557
|
-
await db.execute(
|
|
3558
|
-
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
3559
|
-
key TEXT PRIMARY KEY,
|
|
3560
|
-
value TEXT NOT NULL,
|
|
3561
|
-
updated_at INTEGER NOT NULL
|
|
3562
|
-
)`
|
|
3563
|
-
);
|
|
3564
|
-
return {
|
|
3565
|
-
async get(key) {
|
|
3566
|
-
const rows = await db.select(
|
|
3567
|
-
`SELECT value FROM ${tableName} WHERE key = ?`,
|
|
3568
|
-
[key]
|
|
3569
|
-
);
|
|
3570
|
-
return rows[0]?.value ?? null;
|
|
3571
|
-
},
|
|
3572
|
-
async set(key, value) {
|
|
3573
|
-
await db.execute(
|
|
3574
|
-
`INSERT INTO ${tableName} (key, value, updated_at)
|
|
3575
|
-
VALUES (?, ?, ?)
|
|
3576
|
-
ON CONFLICT (key) DO UPDATE
|
|
3577
|
-
SET value = excluded.value,
|
|
3578
|
-
updated_at = excluded.updated_at`,
|
|
3579
|
-
[key, value, Date.now()]
|
|
3580
|
-
);
|
|
3581
|
-
},
|
|
3582
|
-
async remove(key) {
|
|
3583
|
-
await db.execute(`DELETE FROM ${tableName} WHERE key = ?`, [key]);
|
|
3584
|
-
}
|
|
3585
|
-
};
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
|
-
// src/schema-from-sql.ts
|
|
3589
|
-
function parseCreateTable(sql) {
|
|
3590
|
-
const headerMatch = sql.match(
|
|
3591
|
-
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["]?([A-Za-z_][A-Za-z0-9_]*)["]?\s*\(([\s\S]*)\)\s*;?\s*$/im
|
|
3592
|
-
);
|
|
3593
|
-
if (!headerMatch) {
|
|
3594
|
-
throw new Error("parseCreateTable: could not locate a CREATE TABLE block");
|
|
3595
|
-
}
|
|
3596
|
-
const tableName = headerMatch[1];
|
|
3597
|
-
const body = headerMatch[2];
|
|
3598
|
-
const columns = [];
|
|
3599
|
-
for (const rawLine of splitTopLevelCommas(body)) {
|
|
3600
|
-
const line = rawLine.trim();
|
|
3601
|
-
if (!line) continue;
|
|
3602
|
-
if (/^(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|LIKE|EXCLUDE)\b/i.test(line)) {
|
|
3603
|
-
continue;
|
|
3604
|
-
}
|
|
3605
|
-
const colMatch = line.match(/^["]?([A-Za-z_][A-Za-z0-9_]*)["]?\s+([A-Za-z][A-Za-z0-9_]*(?:\s*\([^)]*\))?(?:\s*\[\s*\])?)([\s\S]*)$/);
|
|
3606
|
-
if (!colMatch) continue;
|
|
3607
|
-
const name = colMatch[1];
|
|
3608
|
-
const sqlType = colMatch[2].replace(/\s+/g, "").toUpperCase();
|
|
3609
|
-
const rest = colMatch[3].toUpperCase();
|
|
3610
|
-
const notNull = /\bNOT\s+NULL\b/.test(rest);
|
|
3611
|
-
const hasDefault = /\bDEFAULT\b/.test(rest);
|
|
3612
|
-
columns.push({ name, sqlType, notNull, hasDefault });
|
|
3613
|
-
}
|
|
3614
|
-
return { tableName, columns };
|
|
3615
|
-
}
|
|
3616
|
-
function splitTopLevelCommas(body) {
|
|
3617
|
-
const parts = [];
|
|
3618
|
-
let depth = 0;
|
|
3619
|
-
let current = "";
|
|
3620
|
-
for (const ch of body) {
|
|
3621
|
-
if (ch === "(") depth++;
|
|
3622
|
-
else if (ch === ")") depth--;
|
|
3623
|
-
if (ch === "," && depth === 0) {
|
|
3624
|
-
parts.push(current);
|
|
3625
|
-
current = "";
|
|
3626
|
-
} else {
|
|
3627
|
-
current += ch;
|
|
3628
|
-
}
|
|
3629
|
-
}
|
|
3630
|
-
if (current.trim()) parts.push(current);
|
|
3631
|
-
return parts;
|
|
3632
|
-
}
|
|
3633
|
-
function sqlTypeToJsonSchema(sqlType) {
|
|
3634
|
-
const t = sqlType.replace(/\s+/g, "").toUpperCase();
|
|
3635
|
-
if (/^TEXT\[\]$/.test(t)) {
|
|
3636
|
-
return { schema: { type: "array", items: { type: "string" } } };
|
|
3637
|
-
}
|
|
3638
|
-
if (t === "UUID" || t === "TEXT" || /^VARCHAR(\(.*\))?$/.test(t) || /^CHAR(\(.*\))?$/.test(t)) {
|
|
3639
|
-
return { schema: { type: "string" } };
|
|
3640
|
-
}
|
|
3641
|
-
if (t === "INTEGER" || t === "INT" || t === "BIGINT" || t === "SMALLINT" || t === "INT4" || t === "INT8" || t === "INT2") {
|
|
3642
|
-
return { schema: { type: "integer" } };
|
|
3643
|
-
}
|
|
3644
|
-
if (/^NUMERIC(\(.*\))?$/.test(t) || /^DECIMAL(\(.*\))?$/.test(t) || t === "REAL" || t === "DOUBLEPRECISION" || t === "FLOAT") {
|
|
3645
|
-
return { schema: { type: "number" } };
|
|
3646
|
-
}
|
|
3647
|
-
if (t === "BOOLEAN" || t === "BOOL") {
|
|
3648
|
-
return { schema: { type: "boolean" } };
|
|
3649
|
-
}
|
|
3650
|
-
if (t === "TIMESTAMPTZ" || t === "TIMESTAMP" || t === "TIMESTAMPWITHTIMEZONE" || t === "TIMESTAMPWITHOUTTIMEZONE") {
|
|
3651
|
-
return { schema: { type: "string", format: "date-time" } };
|
|
3652
|
-
}
|
|
3653
|
-
if (t === "DATE") {
|
|
3654
|
-
return { schema: { type: "string", format: "date" } };
|
|
3655
|
-
}
|
|
3656
|
-
if (t === "JSONB" || t === "JSON") {
|
|
3657
|
-
return { schema: { type: "object" } };
|
|
3658
|
-
}
|
|
3659
|
-
return {
|
|
3660
|
-
schema: { type: "string" },
|
|
3661
|
-
warning: `sql type "${sqlType}" not explicitly mapped; defaulting to string`
|
|
3662
|
-
};
|
|
3663
|
-
}
|
|
3664
|
-
function deepMergeUnknown(base, override) {
|
|
3665
|
-
const out = { ...base };
|
|
3666
|
-
for (const [key, value] of Object.entries(override)) {
|
|
3667
|
-
const baseValue = base[key];
|
|
3668
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value) && baseValue !== null && typeof baseValue === "object" && !Array.isArray(baseValue)) {
|
|
3669
|
-
out[key] = deepMergeUnknown(
|
|
3670
|
-
baseValue,
|
|
3671
|
-
value
|
|
3672
|
-
);
|
|
3673
|
-
} else {
|
|
3674
|
-
out[key] = value;
|
|
3675
|
-
}
|
|
3676
|
-
}
|
|
3677
|
-
return out;
|
|
3678
|
-
}
|
|
3679
|
-
function mergeEntityConfig(generated, overrides) {
|
|
3680
|
-
if (!overrides) return generated;
|
|
3681
|
-
const merged = deepMergeUnknown(
|
|
3682
|
-
generated,
|
|
3683
|
-
overrides
|
|
3684
|
-
);
|
|
3685
|
-
const refined = merged;
|
|
3686
|
-
return {
|
|
3687
|
-
...refined,
|
|
3688
|
-
entityType: generated.entityType,
|
|
3689
|
-
schema: refined.schema ?? generated.schema
|
|
3690
|
-
};
|
|
3691
|
-
}
|
|
3692
|
-
function registerEntityFromSql(opts) {
|
|
3693
|
-
const parsed = parseCreateTable(opts.createTableSql);
|
|
3694
|
-
const properties = {};
|
|
3695
|
-
const required = [];
|
|
3696
|
-
for (const col of parsed.columns) {
|
|
3697
|
-
const { schema, warning } = sqlTypeToJsonSchema(col.sqlType);
|
|
3698
|
-
const propSchema = { ...schema };
|
|
3699
|
-
if (warning) {
|
|
3700
|
-
propSchema["x-warning"] = warning;
|
|
3701
|
-
}
|
|
3702
|
-
properties[col.name] = propSchema;
|
|
3703
|
-
if (col.notNull && !col.hasDefault) {
|
|
3704
|
-
required.push(col.name);
|
|
3705
|
-
}
|
|
3706
|
-
}
|
|
3707
|
-
const generated = {
|
|
3708
|
-
entityType: opts.entityType,
|
|
3709
|
-
source: "runtime",
|
|
3710
|
-
schema: {
|
|
3711
|
-
type: "object",
|
|
3712
|
-
title: parsed.tableName,
|
|
3713
|
-
properties,
|
|
3714
|
-
required
|
|
3715
|
-
}
|
|
3716
|
-
};
|
|
3717
|
-
const config = mergeEntityConfig(generated, opts.overrides);
|
|
3718
|
-
registerEntityJsonSchema(config);
|
|
3719
|
-
return config;
|
|
3720
|
-
}
|
|
3721
|
-
|
|
3722
|
-
// src/table/row-models.ts
|
|
3723
|
-
function resolveAccessor(columnDef, row, index) {
|
|
3724
|
-
if (columnDef.accessorFn) return columnDef.accessorFn(row, index);
|
|
3725
|
-
if (columnDef.accessorKey) return row[columnDef.accessorKey];
|
|
3726
|
-
return void 0;
|
|
3727
|
-
}
|
|
3728
|
-
var builtInFilterFns = {
|
|
3729
|
-
auto: (row, columnId, filterValue) => {
|
|
3730
|
-
const val = row.getValue(columnId);
|
|
3731
|
-
if (filterValue == null || filterValue === "") return true;
|
|
3732
|
-
if (typeof val === "string") return val.toLowerCase().includes(String(filterValue).toLowerCase());
|
|
3733
|
-
if (typeof val === "number") return val === Number(filterValue);
|
|
3734
|
-
if (typeof val === "boolean") return val === filterValue;
|
|
3735
|
-
return String(val).toLowerCase().includes(String(filterValue).toLowerCase());
|
|
3736
|
-
}
|
|
3737
|
-
};
|
|
3738
|
-
function defaultSortingFn(a, b) {
|
|
3739
|
-
if (a == null && b == null) return 0;
|
|
3740
|
-
if (a == null) return 1;
|
|
3741
|
-
if (b == null) return -1;
|
|
3742
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
3743
|
-
if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
|
|
3744
|
-
return String(a).localeCompare(String(b));
|
|
3745
|
-
}
|
|
3746
|
-
function createRow(original, index, columns, table, depth = 0, parentId, subRows = []) {
|
|
3747
|
-
const id = table.options.getRowId ? table.options.getRowId(original, index) : String(index);
|
|
3748
|
-
const row = {
|
|
3749
|
-
id,
|
|
3750
|
-
index,
|
|
3751
|
-
original,
|
|
3752
|
-
depth,
|
|
3753
|
-
parentId,
|
|
3754
|
-
subRows,
|
|
3755
|
-
getValue: (columnId) => {
|
|
3756
|
-
const col = columns.find((c) => c.id === columnId);
|
|
3757
|
-
if (!col) return void 0;
|
|
3758
|
-
return resolveAccessor(col.columnDef, original, index);
|
|
3759
|
-
},
|
|
3760
|
-
renderValue: (columnId) => {
|
|
3761
|
-
const val = row.getValue(columnId);
|
|
3762
|
-
return val ?? null;
|
|
3763
|
-
},
|
|
3764
|
-
getIsSelected: () => !!table.getState().rowSelection[id],
|
|
3765
|
-
getCanSelect: () => {
|
|
3766
|
-
const opt = table.options.enableRowSelection;
|
|
3767
|
-
if (opt === false) return false;
|
|
3768
|
-
if (typeof opt === "function") return opt(row);
|
|
3769
|
-
return true;
|
|
3770
|
-
},
|
|
3771
|
-
getIsAllSubRowsSelected: () => subRows.length > 0 && subRows.every((sr) => sr.getIsSelected()),
|
|
3772
|
-
getIsSomeSelected: () => subRows.some((sr) => sr.getIsSelected() || sr.getIsSomeSelected()),
|
|
3773
|
-
toggleSelected: (value) => {
|
|
3774
|
-
const next = value ?? !row.getIsSelected();
|
|
3775
|
-
table.setRowSelection((prev) => ({ ...prev, [id]: next }));
|
|
3776
|
-
},
|
|
3777
|
-
getToggleSelectedHandler: () => (e) => {
|
|
3778
|
-
row.toggleSelected();
|
|
3779
|
-
},
|
|
3780
|
-
getIsExpanded: () => {
|
|
3781
|
-
const expanded = table.getState().expanded;
|
|
3782
|
-
if (expanded === true) return true;
|
|
3783
|
-
return !!expanded[id];
|
|
3784
|
-
},
|
|
3785
|
-
getCanExpand: () => subRows.length > 0,
|
|
3786
|
-
toggleExpanded: (value) => {
|
|
3787
|
-
const next = value ?? !row.getIsExpanded();
|
|
3788
|
-
table.setExpanded((prev) => {
|
|
3789
|
-
if (prev === true) {
|
|
3790
|
-
const allRows = {};
|
|
3791
|
-
table.getCoreRowModel().flatRows.forEach((r) => {
|
|
3792
|
-
allRows[r.id] = true;
|
|
3793
|
-
});
|
|
3794
|
-
allRows[id] = next;
|
|
3795
|
-
return allRows;
|
|
3796
|
-
}
|
|
3797
|
-
return { ...prev, [id]: next };
|
|
3798
|
-
});
|
|
3799
|
-
},
|
|
3800
|
-
getToggleExpandedHandler: () => () => row.toggleExpanded(),
|
|
3801
|
-
getIsGrouped: () => !!row.groupingColumnId,
|
|
3802
|
-
groupingColumnId: void 0,
|
|
3803
|
-
groupingValue: void 0,
|
|
3804
|
-
getVisibleCells: () => row.getAllCells().filter((cell) => cell.column.getIsVisible()),
|
|
3805
|
-
getAllCells: () => columns.map((col) => createCell(row, col, table)),
|
|
3806
|
-
getIsPinned: () => false,
|
|
3807
|
-
pin: () => {
|
|
3808
|
-
}
|
|
3809
|
-
};
|
|
3810
|
-
return row;
|
|
3811
|
-
}
|
|
3812
|
-
function createCell(row, column, table) {
|
|
3813
|
-
const cellId = `${row.id}_${column.id}`;
|
|
3814
|
-
const cell = {
|
|
3815
|
-
id: cellId,
|
|
3816
|
-
row,
|
|
3817
|
-
column,
|
|
3818
|
-
getValue: () => row.getValue(column.id),
|
|
3819
|
-
renderValue: () => row.renderValue(column.id),
|
|
3820
|
-
getIsGrouped: () => row.getIsGrouped() && row.groupingColumnId === column.id,
|
|
3821
|
-
getIsPlaceholder: () => false,
|
|
3822
|
-
getIsAggregated: () => row.subRows.length > 0 && row.getIsGrouped() && row.groupingColumnId !== column.id,
|
|
3823
|
-
getContext: () => ({
|
|
3824
|
-
table,
|
|
3825
|
-
row,
|
|
3826
|
-
cell,
|
|
3827
|
-
column,
|
|
3828
|
-
getValue: cell.getValue,
|
|
3829
|
-
renderValue: cell.renderValue
|
|
3830
|
-
})
|
|
3831
|
-
};
|
|
3832
|
-
return cell;
|
|
3833
|
-
}
|
|
3834
|
-
function getCoreRowModel(data, columns, table) {
|
|
3835
|
-
const getSubRows = table.options.getSubRows;
|
|
3836
|
-
const flatRows = [];
|
|
3837
|
-
function processRows(items, depth, parentId) {
|
|
3838
|
-
return items.map((item, index) => {
|
|
3839
|
-
const subData = getSubRows?.(item, index);
|
|
3840
|
-
const row = createRow(item, flatRows.length, columns, table, depth, parentId);
|
|
3841
|
-
flatRows.push(row);
|
|
3842
|
-
if (subData && subData.length > 0) {
|
|
3843
|
-
row.subRows = processRows(subData, depth + 1, row.id);
|
|
3844
|
-
}
|
|
3845
|
-
return row;
|
|
3846
|
-
});
|
|
3847
|
-
}
|
|
3848
|
-
const rows = processRows(data, 0);
|
|
3849
|
-
const rowsById = {};
|
|
3850
|
-
flatRows.forEach((r) => {
|
|
3851
|
-
rowsById[r.id] = r;
|
|
3852
|
-
});
|
|
3853
|
-
return { rows, flatRows, rowsById };
|
|
3854
|
-
}
|
|
3855
|
-
function getFilteredRowModel(rowModel, columnFilters, globalFilter, columns, globalFilterFn) {
|
|
3856
|
-
const hasColumnFilters = columnFilters.length > 0;
|
|
3857
|
-
const hasGlobalFilter = globalFilter != null && globalFilter !== "";
|
|
3858
|
-
if (!hasColumnFilters && !hasGlobalFilter) return rowModel;
|
|
3859
|
-
const filterFns = /* @__PURE__ */ new Map();
|
|
3860
|
-
for (const cf of columnFilters) {
|
|
3861
|
-
const col = columns.find((c) => c.id === cf.id);
|
|
3862
|
-
if (col) {
|
|
3863
|
-
const fn = col.columnDef.filterFn;
|
|
3864
|
-
filterFns.set(cf.id, typeof fn === "function" ? fn : builtInFilterFns.auto);
|
|
3865
|
-
}
|
|
3866
|
-
}
|
|
3867
|
-
const resolvedGlobalFilterFn = globalFilterFn ?? builtInFilterFns.auto;
|
|
3868
|
-
const flatRows = [];
|
|
3869
|
-
function filterRows(rows2) {
|
|
3870
|
-
return rows2.filter((row) => {
|
|
3871
|
-
for (const cf of columnFilters) {
|
|
3872
|
-
const fn = filterFns.get(cf.id);
|
|
3873
|
-
if (fn && !fn(row, cf.id, cf.value)) return false;
|
|
3874
|
-
}
|
|
3875
|
-
if (hasGlobalFilter) {
|
|
3876
|
-
const matches = columns.some(
|
|
3877
|
-
(col) => resolvedGlobalFilterFn(row, col.id, globalFilter)
|
|
3878
|
-
);
|
|
3879
|
-
if (!matches) return false;
|
|
3880
|
-
}
|
|
3881
|
-
flatRows.push(row);
|
|
3882
|
-
if (row.subRows.length > 0) {
|
|
3883
|
-
const filteredSubRows = filterRows(row.subRows);
|
|
3884
|
-
if (filteredSubRows.length > 0) {
|
|
3885
|
-
row = { ...row, subRows: filteredSubRows };
|
|
3886
|
-
return true;
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
return true;
|
|
3890
|
-
});
|
|
3891
|
-
}
|
|
3892
|
-
const rows = filterRows(rowModel.rows);
|
|
3893
|
-
const rowsById = {};
|
|
3894
|
-
flatRows.forEach((r) => {
|
|
3895
|
-
rowsById[r.id] = r;
|
|
3896
|
-
});
|
|
3897
|
-
return { rows, flatRows, rowsById };
|
|
3898
|
-
}
|
|
3899
|
-
function getSortedRowModel(rowModel, sorting, columns) {
|
|
3900
|
-
if (sorting.length === 0) return rowModel;
|
|
3901
|
-
const sortFns = sorting.map((s) => {
|
|
3902
|
-
const col = columns.find((c) => c.id === s.id);
|
|
3903
|
-
const colDef = col?.columnDef ?? null;
|
|
3904
|
-
let fn = null;
|
|
3905
|
-
if (colDef?.sortingFn && typeof colDef.sortingFn === "function") {
|
|
3906
|
-
fn = colDef.sortingFn;
|
|
3907
|
-
}
|
|
3908
|
-
return { columnId: s.id, desc: s.desc, fn, colDef };
|
|
3909
|
-
});
|
|
3910
|
-
function sortRows(rows2) {
|
|
3911
|
-
const sorted = [...rows2].sort((rowA, rowB) => {
|
|
3912
|
-
for (const { columnId, desc, fn, colDef } of sortFns) {
|
|
3913
|
-
let result;
|
|
3914
|
-
if (fn) {
|
|
3915
|
-
result = fn(rowA, rowB, columnId);
|
|
3916
|
-
} else {
|
|
3917
|
-
const a = rowA.getValue(columnId);
|
|
3918
|
-
const b = rowB.getValue(columnId);
|
|
3919
|
-
result = defaultSortingFn(a, b);
|
|
3920
|
-
}
|
|
3921
|
-
if (colDef?.invertSorting) result = -result;
|
|
3922
|
-
if (result !== 0) return desc ? -result : result;
|
|
3923
|
-
}
|
|
3924
|
-
return 0;
|
|
3925
|
-
});
|
|
3926
|
-
return sorted.map((row) => {
|
|
3927
|
-
if (row.subRows.length > 0) {
|
|
3928
|
-
return { ...row, subRows: sortRows(row.subRows) };
|
|
3929
|
-
}
|
|
3930
|
-
return row;
|
|
3931
|
-
});
|
|
3932
|
-
}
|
|
3933
|
-
const rows = sortRows(rowModel.rows);
|
|
3934
|
-
const flatRows = [];
|
|
3935
|
-
function flatten(rows2) {
|
|
3936
|
-
rows2.forEach((r) => {
|
|
3937
|
-
flatRows.push(r);
|
|
3938
|
-
if (r.subRows.length) flatten(r.subRows);
|
|
3939
|
-
});
|
|
3940
|
-
}
|
|
3941
|
-
flatten(rows);
|
|
3942
|
-
const rowsById = {};
|
|
3943
|
-
flatRows.forEach((r) => {
|
|
3944
|
-
rowsById[r.id] = r;
|
|
3945
|
-
});
|
|
3946
|
-
return { rows, flatRows, rowsById };
|
|
3947
|
-
}
|
|
3948
|
-
function getGroupedRowModel(rowModel, grouping, columns, table) {
|
|
3949
|
-
if (grouping.length === 0) return rowModel;
|
|
3950
|
-
function groupRows(rows2, depth) {
|
|
3951
|
-
if (depth >= grouping.length) return rows2;
|
|
3952
|
-
const columnId = grouping[depth];
|
|
3953
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3954
|
-
for (const row of rows2) {
|
|
3955
|
-
const value = row.getValue(columnId);
|
|
3956
|
-
const key = value ?? "__null__";
|
|
3957
|
-
if (!groups.has(key)) groups.set(key, []);
|
|
3958
|
-
groups.get(key).push(row);
|
|
3959
|
-
}
|
|
3960
|
-
const groupedRows = [];
|
|
3961
|
-
let groupIndex = 0;
|
|
3962
|
-
for (const [key, groupItems] of groups) {
|
|
3963
|
-
const firstRow = groupItems[0];
|
|
3964
|
-
const groupRow = createRow(
|
|
3965
|
-
firstRow.original,
|
|
3966
|
-
groupIndex++,
|
|
3967
|
-
columns,
|
|
3968
|
-
table,
|
|
3969
|
-
depth
|
|
3970
|
-
);
|
|
3971
|
-
groupRow.groupingColumnId = columnId;
|
|
3972
|
-
groupRow.groupingValue = key === "__null__" ? null : key;
|
|
3973
|
-
groupRow.subRows = groupRows(groupItems, depth + 1);
|
|
3974
|
-
groupedRows.push(groupRow);
|
|
3975
|
-
}
|
|
3976
|
-
return groupedRows;
|
|
3977
|
-
}
|
|
3978
|
-
const rows = groupRows(rowModel.rows, 0);
|
|
3979
|
-
const flatRows = [];
|
|
3980
|
-
function flatten(items) {
|
|
3981
|
-
items.forEach((r) => {
|
|
3982
|
-
flatRows.push(r);
|
|
3983
|
-
if (r.subRows.length) flatten(r.subRows);
|
|
3984
|
-
});
|
|
3985
|
-
}
|
|
3986
|
-
flatten(rows);
|
|
3987
|
-
const rowsById = {};
|
|
3988
|
-
flatRows.forEach((r) => {
|
|
3989
|
-
rowsById[r.id] = r;
|
|
3990
|
-
});
|
|
3991
|
-
return { rows, flatRows, rowsById };
|
|
3992
|
-
}
|
|
3993
|
-
function getExpandedRowModel(rowModel, expanded) {
|
|
3994
|
-
const flatRows = [];
|
|
3995
|
-
function expandRows(rows2) {
|
|
3996
|
-
const result = [];
|
|
3997
|
-
for (const row of rows2) {
|
|
3998
|
-
result.push(row);
|
|
3999
|
-
flatRows.push(row);
|
|
4000
|
-
const isExpanded = expanded === true || typeof expanded === "object" && expanded[row.id];
|
|
4001
|
-
if (isExpanded && row.subRows.length > 0) {
|
|
4002
|
-
const expandedSubRows = expandRows(row.subRows);
|
|
4003
|
-
result.push(...expandedSubRows);
|
|
4004
|
-
}
|
|
4005
|
-
}
|
|
4006
|
-
return result;
|
|
4007
|
-
}
|
|
4008
|
-
const rows = expandRows(rowModel.rows);
|
|
4009
|
-
const rowsById = {};
|
|
4010
|
-
flatRows.forEach((r) => {
|
|
4011
|
-
rowsById[r.id] = r;
|
|
4012
|
-
});
|
|
4013
|
-
return { rows, flatRows, rowsById };
|
|
4014
|
-
}
|
|
4015
|
-
function getPaginatedRowModel(rowModel, pagination) {
|
|
4016
|
-
const { pageIndex, pageSize } = pagination;
|
|
4017
|
-
const start = pageIndex * pageSize;
|
|
4018
|
-
const end = start + pageSize;
|
|
4019
|
-
const rows = rowModel.rows.slice(start, end);
|
|
4020
|
-
const flatRows = rows;
|
|
4021
|
-
const rowsById = {};
|
|
4022
|
-
flatRows.forEach((r) => {
|
|
4023
|
-
rowsById[r.id] = r;
|
|
4024
|
-
});
|
|
4025
|
-
return { rows, flatRows, rowsById };
|
|
4026
|
-
}
|
|
4027
|
-
function getSelectedRowModel(rowModel, selection) {
|
|
4028
|
-
const rows = rowModel.flatRows.filter((r) => selection[r.id]);
|
|
4029
|
-
const rowsById = {};
|
|
4030
|
-
rows.forEach((r) => {
|
|
4031
|
-
rowsById[r.id] = r;
|
|
4032
|
-
});
|
|
4033
|
-
return { rows, flatRows: rows, rowsById };
|
|
4034
|
-
}
|
|
4035
|
-
|
|
4036
|
-
// src/table/faceting.ts
|
|
4037
|
-
function getFacetedUniqueValues(rowModel, columnId) {
|
|
4038
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4039
|
-
for (const row of rowModel.flatRows) {
|
|
4040
|
-
const value = row.getValue(columnId);
|
|
4041
|
-
if (Array.isArray(value)) {
|
|
4042
|
-
for (const v of value) {
|
|
4043
|
-
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
4044
|
-
}
|
|
4045
|
-
} else {
|
|
4046
|
-
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
4047
|
-
}
|
|
4048
|
-
}
|
|
4049
|
-
return counts;
|
|
4050
|
-
}
|
|
4051
|
-
function getFacetedMinMaxValues(rowModel, columnId) {
|
|
4052
|
-
let min = Infinity;
|
|
4053
|
-
let max = -Infinity;
|
|
4054
|
-
let hasValue = false;
|
|
4055
|
-
for (const row of rowModel.flatRows) {
|
|
4056
|
-
const raw = row.getValue(columnId);
|
|
4057
|
-
const value = typeof raw === "number" ? raw : Number(raw);
|
|
4058
|
-
if (!Number.isNaN(value)) {
|
|
4059
|
-
hasValue = true;
|
|
4060
|
-
if (value < min) min = value;
|
|
4061
|
-
if (value > max) max = value;
|
|
4062
|
-
}
|
|
4063
|
-
}
|
|
4064
|
-
return hasValue ? [min, max] : void 0;
|
|
4065
|
-
}
|
|
4066
|
-
function getFacetedRowModel(preFilteredRowModel, columnId, allFilteredRowModel) {
|
|
4067
|
-
return preFilteredRowModel;
|
|
4068
|
-
}
|
|
4069
|
-
|
|
4070
|
-
exports.EMPTY_ENTITY_STATE = EMPTY_ENTITY_STATE;
|
|
4071
|
-
exports.EMPTY_IDS = EMPTY_IDS;
|
|
4072
|
-
exports.EMPTY_LIST_STATE = EMPTY_LIST_STATE;
|
|
4073
|
-
exports.EMPTY_SYNC_METADATA = EMPTY_SYNC_METADATA;
|
|
4074
|
-
exports.IncrementalView = IncrementalView;
|
|
4075
|
-
exports.RealtimeManager = RealtimeManager;
|
|
4076
|
-
exports.TerminalError = TerminalError;
|
|
4077
|
-
exports.TransientError = TransientError;
|
|
4078
|
-
exports.__resetActiveGraphStore = __resetActiveGraphStore;
|
|
4079
|
-
exports.__resetEntityTransports = __resetEntityTransports;
|
|
4080
|
-
exports.__resetStoreRegistry = __resetStoreRegistry;
|
|
4081
|
-
exports.applyAgUiDelta = applyAgUiDelta;
|
|
4082
|
-
exports.applyAgUiSnapshot = applyAgUiSnapshot;
|
|
4083
|
-
exports.applyJsonPatch = applyJsonPatch;
|
|
4084
|
-
exports.applyView = applyView;
|
|
4085
|
-
exports.attachGlobalListeners = attachGlobalListeners;
|
|
4086
|
-
exports.buildTenantWhere = buildTenantWhere;
|
|
4087
|
-
exports.cascadeInvalidation = cascadeInvalidation;
|
|
4088
|
-
exports.checkCompleteness = checkCompleteness;
|
|
4089
|
-
exports.collectDirtyPaths = collectDirtyPaths;
|
|
4090
|
-
exports.compareEntities = compareEntities;
|
|
4091
|
-
exports.configureEngine = configureEngine;
|
|
4092
|
-
exports.configureTimeTravel = configureTimeTravel;
|
|
4093
|
-
exports.createConvexAdapter = createConvexAdapter;
|
|
4094
|
-
exports.createDevtoolsEventBus = createDevtoolsEventBus;
|
|
4095
|
-
exports.createElectricAdapter = createElectricAdapter;
|
|
4096
|
-
exports.createFlintAdapter = createFlintAdapter;
|
|
4097
|
-
exports.createGraphAction = createGraphAction;
|
|
4098
|
-
exports.createGraphEffect = createGraphEffect;
|
|
4099
|
-
exports.createGraphQLSubscriptionAdapter = createGraphQLSubscriptionAdapter;
|
|
4100
|
-
exports.createGraphStore = createGraphStore;
|
|
4101
|
-
exports.createGraphTool = createGraphTool;
|
|
4102
|
-
exports.createGraphTransaction = createGraphTransaction;
|
|
4103
|
-
exports.createLoroMergeStrategy = createLoroMergeStrategy;
|
|
4104
|
-
exports.createPGlitePersistenceAdapter = createPGlitePersistenceAdapter;
|
|
4105
|
-
exports.createRow = createRow;
|
|
4106
|
-
exports.createSchemaGraphTool = createSchemaGraphTool;
|
|
4107
|
-
exports.createSupabaseRealtimeAdapter = createSupabaseRealtimeAdapter;
|
|
4108
|
-
exports.createSurrealLiveAdapter = createSurrealLiveAdapter;
|
|
4109
|
-
exports.createTauriSqlPersistenceAdapter = createTauriSqlPersistenceAdapter;
|
|
4110
|
-
exports.createTenantScopedElectricAdapter = createTenantScopedElectricAdapter;
|
|
4111
|
-
exports.createWebSocketAdapter = createWebSocketAdapter;
|
|
4112
|
-
exports.dedupe = dedupe;
|
|
4113
|
-
exports.escapeHtml = escapeHtml;
|
|
4114
|
-
exports.exportGraphSnapshot = exportGraphSnapshot;
|
|
4115
|
-
exports.exportGraphSnapshotWithSchemas = exportGraphSnapshotWithSchemas;
|
|
4116
|
-
exports.fetchEntity = fetchEntity;
|
|
4117
|
-
exports.fetchList = fetchList;
|
|
4118
|
-
exports.findInsertionIndex = findInsertionIndex;
|
|
4119
|
-
exports.flattenClauses = flattenClauses;
|
|
4120
|
-
exports.getActiveSubscriberCount = getActiveSubscriberCount;
|
|
4121
|
-
exports.getCoreRowModel = getCoreRowModel;
|
|
4122
|
-
exports.getElectricRealtimeManager = getRealtimeManager;
|
|
4123
|
-
exports.getEngineOptions = getEngineOptions;
|
|
4124
|
-
exports.getEntityJsonSchema = getEntityJsonSchema;
|
|
4125
|
-
exports.getEntityTransport = getEntityTransport;
|
|
4126
|
-
exports.getExpandedRowModel = getExpandedRowModel;
|
|
4127
|
-
exports.getFacetedMinMaxValues = getFacetedMinMaxValues;
|
|
4128
|
-
exports.getFacetedRowModel = getFacetedRowModel;
|
|
4129
|
-
exports.getFacetedUniqueValues = getFacetedUniqueValues;
|
|
4130
|
-
exports.getFilteredRowModel = getFilteredRowModel;
|
|
4131
|
-
exports.getGraphSyncStatus = getGraphSyncStatus;
|
|
4132
|
-
exports.getGroupedRowModel = getGroupedRowModel;
|
|
4133
|
-
exports.getMergeStrategy = getMergeStrategy;
|
|
4134
|
-
exports.getPaginatedRowModel = getPaginatedRowModel;
|
|
4135
|
-
exports.getRealtimeManager = getRealtimeManager;
|
|
4136
|
-
exports.getRegisteredEntityTypes = getRegisteredEntityTypes;
|
|
4137
|
-
exports.getRegisteredStores = getRegisteredStores;
|
|
4138
|
-
exports.getSchema = getSchema;
|
|
4139
|
-
exports.getSelectedRowModel = getSelectedRowModel;
|
|
4140
|
-
exports.getSortedRowModel = getSortedRowModel;
|
|
4141
|
-
exports.getTimeTravelState = getTimeTravelState;
|
|
4142
|
-
exports.getValueAtPath = getValueAtPath;
|
|
4143
|
-
exports.graphStore = graphStore;
|
|
4144
|
-
exports.graphSyncStatusStore = graphSyncStatusStore;
|
|
4145
|
-
exports.hasCustomPredicates = hasCustomPredicates;
|
|
4146
|
-
exports.hasMergeStrategy = hasMergeStrategy;
|
|
4147
|
-
exports.humanizeFieldName = humanizeFieldName;
|
|
4148
|
-
exports.hydrateGraphFromStorage = hydrateGraphFromStorage;
|
|
4149
|
-
exports.inferSchemaFieldType = inferSchemaFieldType;
|
|
4150
|
-
exports.lwwStrategy = lwwStrategy;
|
|
4151
|
-
exports.makeRestTransport = makeRestTransport;
|
|
4152
|
-
exports.matchesFilter = matchesFilter;
|
|
4153
|
-
exports.matchesSearch = matchesSearch;
|
|
4154
|
-
exports.parseCreateTable = parseCreateTable;
|
|
4155
|
-
exports.persistGraphToStorage = persistGraphToStorage;
|
|
4156
|
-
exports.prepareGraphStoreScope = prepareGraphStoreScope;
|
|
4157
|
-
exports.publishFlintMutation = publishFlintMutation;
|
|
4158
|
-
exports.queryOnce = queryOnce;
|
|
4159
|
-
exports.readRelations = readRelations;
|
|
4160
|
-
exports.recordGraphSnapshot = recordGraphSnapshot;
|
|
4161
|
-
exports.registerEntityFromSql = registerEntityFromSql;
|
|
4162
|
-
exports.registerEntityJsonSchema = registerEntityJsonSchema;
|
|
4163
|
-
exports.registerEntityTransport = registerEntityTransport;
|
|
4164
|
-
exports.registerMergeStrategy = registerMergeStrategy;
|
|
4165
|
-
exports.registerRuntimeSchema = registerRuntimeSchema;
|
|
4166
|
-
exports.registerSchema = registerSchema;
|
|
4167
|
-
exports.registerStore = registerStore;
|
|
4168
|
-
exports.registerSubscriber = registerSubscriber;
|
|
4169
|
-
exports.registryKey = registryKey;
|
|
4170
|
-
exports.renderMarkdownToHtml = renderMarkdownToHtml;
|
|
4171
|
-
exports.replayActionWithRetry = replayActionWithRetry;
|
|
4172
|
-
exports.resetRealtimeManager = resetRealtimeManager;
|
|
4173
|
-
exports.resolveActiveGraphStore = resolveActiveGraphStore;
|
|
4174
|
-
exports.restoreGraphSnapshot = restoreGraphSnapshot;
|
|
4175
|
-
exports.restoreGraphSnapshotBySeq = restoreGraphSnapshotBySeq;
|
|
4176
|
-
exports.runWithGraphStore = runWithGraphStore;
|
|
4177
|
-
exports.selectGraph = selectGraph;
|
|
4178
|
-
exports.serializeKey = serializeKey;
|
|
4179
|
-
exports.setActiveGraphStore = setActiveGraphStore;
|
|
4180
|
-
exports.setDefaultMergeStrategy = setDefaultMergeStrategy;
|
|
4181
|
-
exports.setValueAtPath = setValueAtPath;
|
|
4182
|
-
exports.sqlTypeToJsonSchema = sqlTypeToJsonSchema;
|
|
4183
|
-
exports.startGarbageCollector = startGarbageCollector;
|
|
4184
|
-
exports.startLocalFirstGraph = startLocalFirstGraph;
|
|
4185
|
-
exports.stepTimeTravel = stepTimeTravel;
|
|
4186
|
-
exports.stopGarbageCollector = stopGarbageCollector;
|
|
4187
|
-
exports.subscribeDevtoolsEvent = subscribeDevtoolsEvent;
|
|
4188
|
-
exports.subscribeSubscriberStats = subscribeSubscriberStats;
|
|
4189
|
-
exports.subscribeTimeTravel = subscribeTimeTravel;
|
|
4190
|
-
exports.toEntityError = toEntityError;
|
|
4191
|
-
exports.toGraphQLVariables = toGraphQLVariables;
|
|
4192
|
-
exports.toPrismaOrderBy = toPrismaOrderBy;
|
|
4193
|
-
exports.toPrismaWhere = toPrismaWhere;
|
|
4194
|
-
exports.toRestParams = toRestParams;
|
|
4195
|
-
exports.toSQLClauses = toSQLClauses;
|
|
4196
|
-
exports.unregisterSubscriber = unregisterSubscriber;
|
|
4197
|
-
exports.useGraphStore = useGraphStore;
|
|
4198
|
-
//# sourceMappingURL=index.cjs.map
|
|
4199
|
-
//# sourceMappingURL=index.cjs.map
|