@prometheus-ags/prometheus-entity-management 1.2.3 → 2.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/dist/index.mjs CHANGED
@@ -1,1738 +1,2470 @@
1
+ import React6, { createContext, useMemo, useRef, useState, useCallback, useEffect, useSyncExternalStore, useContext, useId } from 'react';
1
2
  import { create, createStore, useStore } from 'zustand';
2
3
  import { subscribeWithSelector, persist } from 'zustand/middleware';
3
4
  import { immer } from 'zustand/middleware/immer';
4
- import React6, { createContext, useMemo, useSyncExternalStore, useRef, useCallback, useEffect, useState, useContext, useId } from 'react';
5
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
5
  import { useShallow } from 'zustand/react/shallow';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import { useReactTable, getSortedRowModel, getCoreRowModel, flexRender } from '@tanstack/react-table';
8
8
  import { Search, X, Loader2, RefreshCw, ChevronLeft, ChevronRight, Pencil, Trash2 } from 'lucide-react';
9
9
  import { clsx } from 'clsx';
10
10
  import { twMerge } from 'tailwind-merge';
11
11
 
12
- // src/graph.ts
13
- var EMPTY_IDS = [];
14
- var EMPTY_ENTITY_STATE = {
15
- isFetching: false,
16
- lastFetched: null,
17
- error: null,
18
- stale: false
19
- };
20
- var EMPTY_SYNC_METADATA = {
21
- synced: true,
22
- origin: "server",
23
- updatedAt: null
12
+ // src/errors.ts
13
+ var TerminalError = class _TerminalError extends Error {
14
+ kind = "terminal";
15
+ status;
16
+ constructor(message, opts = {}) {
17
+ super(message, { cause: opts.cause });
18
+ this.name = "TerminalError";
19
+ this.status = opts.status;
20
+ if (typeof Error.captureStackTrace === "function") {
21
+ Error.captureStackTrace(this, _TerminalError);
22
+ }
23
+ }
24
24
  };
25
- var EMPTY_LIST_STATE = {
26
- ids: EMPTY_IDS,
27
- total: null,
28
- nextCursor: null,
29
- prevCursor: null,
30
- hasNextPage: false,
31
- hasPrevPage: false,
32
- isFetching: false,
33
- isFetchingMore: false,
34
- error: null,
35
- lastFetched: null,
36
- stale: false,
37
- currentPage: null,
38
- pageSize: null
25
+ var TransientError = class _TransientError extends Error {
26
+ kind = "transient";
27
+ status;
28
+ constructor(message, opts = {}) {
29
+ super(message, { cause: opts.cause });
30
+ this.name = "TransientError";
31
+ this.status = opts.status;
32
+ if (typeof Error.captureStackTrace === "function") {
33
+ Error.captureStackTrace(this, _TransientError);
34
+ }
35
+ }
39
36
  };
40
- function defaultEntityState() {
41
- return { ...EMPTY_ENTITY_STATE };
37
+ function toEntityError(err) {
38
+ if (err instanceof TerminalError) return err;
39
+ if (err instanceof TransientError) return err;
40
+ if (err && typeof err === "object" && "name" in err && err.name === "AbortError") {
41
+ return new TerminalError("Aborted", { cause: err });
42
+ }
43
+ if (err && typeof err === "object" && "status" in err) {
44
+ const status = Number(err.status);
45
+ const message2 = err && typeof err === "object" && "message" in err ? String(err.message ?? `HTTP ${status}`) : `HTTP ${status}`;
46
+ if (Number.isFinite(status) && status >= 400 && status < 500) {
47
+ return new TerminalError(message2, { status, cause: err });
48
+ }
49
+ if (Number.isFinite(status) && status >= 500) {
50
+ return new TransientError(message2, { status, cause: err });
51
+ }
52
+ }
53
+ const message = err instanceof Error ? err.message : String(err);
54
+ return new TransientError(message, { cause: err });
42
55
  }
43
- function defaultSyncMetadata() {
44
- return { ...EMPTY_SYNC_METADATA };
56
+
57
+ // src/transport/registry.ts
58
+ var transports = /* @__PURE__ */ new Map();
59
+ function registerEntityTransport(type, transport) {
60
+ transports.set(type, transport);
61
+ }
62
+ function getEntityTransport(type) {
63
+ const t = transports.get(type);
64
+ if (!t) {
65
+ throw new Error(
66
+ `[entity-management] No transport registered for entity type "${type}". Call registerEntityTransport("${type}", ...) at app boot.`
67
+ );
68
+ }
69
+ return t;
45
70
  }
46
- function defaultListState() {
47
- return { ...EMPTY_LIST_STATE, ids: [] };
71
+ function __resetEntityTransports() {
72
+ transports.clear();
48
73
  }
49
- function ek(type, id) {
50
- return `${type}:${id}`;
74
+ function getRegisteredEntityTypes() {
75
+ return Array.from(transports.keys());
51
76
  }
52
- var useGraphStore = create()(
53
- subscribeWithSelector(
54
- immer((set, get) => ({
55
- entities: {},
56
- patches: {},
57
- entityStates: {},
58
- syncMetadata: {},
59
- lists: {},
60
- upsertEntity: (type, id, data) => set((s) => {
61
- if (!s.entities[type]) s.entities[type] = {};
62
- s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
63
- const key = ek(type, id);
64
- if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
65
- }),
66
- upsertEntities: (type, entries) => set((s) => {
67
- if (!s.entities[type]) s.entities[type] = {};
68
- for (const { id, data } of entries) {
69
- s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
70
- const key = ek(type, id);
71
- if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
72
- }
73
- }),
74
- replaceEntity: (type, id, data) => set((s) => {
75
- if (!s.entities[type]) s.entities[type] = {};
76
- s.entities[type][id] = data;
77
- const key = ek(type, id);
78
- if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
79
- }),
80
- removeEntity: (type, id) => set((s) => {
81
- delete s.entities[type]?.[id];
82
- delete s.patches[type]?.[id];
83
- delete s.entityStates[ek(type, id)];
84
- delete s.syncMetadata[ek(type, id)];
85
- }),
86
- patchEntity: (type, id, patch) => set((s) => {
87
- if (!s.patches[type]) s.patches[type] = {};
88
- s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
89
- }),
90
- unpatchEntity: (type, id, keys) => set((s) => {
91
- const p = s.patches[type]?.[id];
92
- if (!p) return;
93
- for (const k of keys) delete p[k];
94
- }),
95
- clearPatch: (type, id) => set((s) => {
96
- delete s.patches[type]?.[id];
97
- }),
98
- setEntityFetching: (type, id, fetching) => set((s) => {
99
- const k = ek(type, id);
100
- if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
101
- s.entityStates[k].isFetching = fetching;
102
- }),
103
- setEntityError: (type, id, error) => set((s) => {
104
- const k = ek(type, id);
105
- if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
106
- s.entityStates[k].error = error;
107
- s.entityStates[k].isFetching = false;
108
- }),
109
- setEntityFetched: (type, id) => set((s) => {
110
- const k = ek(type, id);
111
- if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
112
- s.entityStates[k].lastFetched = Date.now();
113
- s.entityStates[k].isFetching = false;
114
- s.entityStates[k].error = null;
115
- s.entityStates[k].stale = false;
116
- s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
117
- }),
118
- setEntityStale: (type, id, stale) => set((s) => {
119
- const k = ek(type, id);
120
- if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
121
- s.entityStates[k].stale = stale;
122
- }),
123
- setEntitySyncMetadata: (type, id, metadata) => set((s) => {
124
- const k = ek(type, id);
125
- s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
126
- }),
127
- clearEntitySyncMetadata: (type, id) => set((s) => {
128
- delete s.syncMetadata[ek(type, id)];
129
- }),
130
- setListResult: (key, ids, meta) => set((s) => {
131
- const ex = s.lists[key] ?? defaultListState();
132
- s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
133
- }),
134
- appendListResult: (key, ids, meta) => set((s) => {
135
- const ex = s.lists[key] ?? defaultListState();
136
- s.lists[key] = { ...ex, ...meta, ids: Array.from(/* @__PURE__ */ new Set([...ex.ids, ...ids])), isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
137
- }),
138
- prependListResult: (key, ids, meta) => set((s) => {
139
- const ex = s.lists[key] ?? defaultListState();
140
- s.lists[key] = { ...ex, ...meta ?? {}, ids: Array.from(/* @__PURE__ */ new Set([...ids, ...ex.ids])), isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
141
- }),
142
- removeIdFromAllLists: (_type, id) => set((s) => {
143
- for (const key of Object.keys(s.lists)) {
144
- const list = s.lists[key];
145
- const idx = list.ids.indexOf(id);
146
- if (idx !== -1) {
147
- list.ids.splice(idx, 1);
148
- if (list.total !== null) list.total -= 1;
149
- }
150
- }
151
- }),
152
- insertIdInList: (key, id, position) => set((s) => {
153
- if (!s.lists[key]) s.lists[key] = defaultListState();
154
- const ids = s.lists[key].ids;
155
- const ex = ids.indexOf(id);
156
- if (ex !== -1) ids.splice(ex, 1);
157
- if (position === "start") ids.unshift(id);
158
- else if (position === "end") ids.push(id);
159
- else ids.splice(position, 0, id);
160
- }),
161
- setListFetching: (key, fetching) => set((s) => {
162
- if (!s.lists[key]) s.lists[key] = defaultListState();
163
- s.lists[key].isFetching = fetching;
164
- }),
165
- setListFetchingMore: (key, fetchingMore) => set((s) => {
166
- if (!s.lists[key]) s.lists[key] = defaultListState();
167
- s.lists[key].isFetchingMore = fetchingMore;
168
- }),
169
- setListError: (key, error) => set((s) => {
170
- if (!s.lists[key]) s.lists[key] = defaultListState();
171
- s.lists[key].error = error;
172
- s.lists[key].isFetching = false;
173
- s.lists[key].isFetchingMore = false;
174
- }),
175
- setListStale: (key, stale) => set((s) => {
176
- if (!s.lists[key]) s.lists[key] = defaultListState();
177
- s.lists[key].stale = stale;
178
- }),
179
- invalidateEntity: (type, id) => set((s) => {
180
- if (id) {
181
- const k = ek(type, id);
182
- if (s.entityStates[k]) s.entityStates[k].stale = true;
183
- } else {
184
- for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
185
- }
186
- }),
187
- invalidateLists: (matcher) => set((s) => {
188
- const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
189
- for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
190
- }),
191
- invalidateType: (type) => {
192
- get().invalidateEntity(type);
193
- get().invalidateLists(type);
194
- },
195
- readEntity: (type, id) => {
196
- const s = get();
197
- const base = s.entities[type]?.[id];
198
- if (!base) return null;
199
- const patch = s.patches[type]?.[id];
200
- return patch ? { ...base, ...patch } : base;
201
- },
202
- readEntitySnapshot: (type, id) => {
203
- const s = get();
204
- const base = s.entities[type]?.[id];
205
- if (!base) return null;
206
- const patch = s.patches[type]?.[id];
207
- const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
208
- return {
209
- ...patch ? { ...base, ...patch } : base,
210
- $synced: metadata.synced,
211
- $origin: metadata.origin,
212
- $updatedAt: metadata.updatedAt
213
- };
214
- }
215
- }))
216
- )
217
- );
218
77
 
219
- // src/graph-query.ts
220
- function queryOnce(opts) {
221
- const store = useGraphStore.getState();
222
- const ids = resolveCandidateIds(store, opts);
223
- let rows = ids.map((id) => store.readEntitySnapshot(opts.type, id)).filter((row) => row !== null);
224
- if (opts.where) rows = rows.filter(opts.where);
225
- if (opts.sort) rows = [...rows].sort(opts.sort);
226
- const projected = rows.map((row) => applySelection(projectRow(row, opts.include, store), opts.select));
227
- if (opts.id) return projected[0] ?? null;
228
- return projected;
229
- }
230
- var selectGraph = queryOnce;
231
- function resolveCandidateIds(store, opts) {
232
- if (opts.id) return [opts.id];
233
- if (opts.ids) return opts.ids;
234
- if (opts.listKey) return store.lists[opts.listKey]?.ids ?? [];
235
- return Object.keys(store.entities[opts.type] ?? {});
236
- }
237
- function projectRow(row, include, store) {
238
- if (!include) return row;
239
- const projected = { ...row };
240
- for (const [key, relation] of Object.entries(include)) {
241
- const related = resolveRelation(row, relation, store);
242
- projected[key] = related;
243
- }
244
- return projected;
78
+ // src/view/prisma-compile.ts
79
+ function nestWhereField(parts, leaf) {
80
+ if (parts.length === 0) return {};
81
+ if (parts.length === 1) return { [parts[0]]: leaf };
82
+ return { [parts[0]]: nestWhereField(parts.slice(1), leaf) };
245
83
  }
246
- function resolveRelation(entity, relation, store) {
247
- const include = relation.include;
248
- switch (relation.via.kind) {
249
- case "field": {
250
- const relatedId = entity[relation.via.field];
251
- if (typeof relatedId !== "string") return null;
252
- const related = store.readEntitySnapshot(relation.type, relatedId);
253
- return related ? projectRow(related, include, store) : null;
254
- }
255
- case "array": {
256
- const ids = entity[relation.via.field];
257
- if (!Array.isArray(ids)) return [];
258
- return ids.map((id) => typeof id === "string" ? store.readEntitySnapshot(relation.type, id) : null).filter((row) => row !== null).map((row) => projectRow(row, include, store));
259
- }
260
- case "list": {
261
- const key = typeof relation.via.key === "function" ? relation.via.key(entity) : relation.via.key;
262
- if (!key) return [];
263
- const ids = store.lists[key]?.ids ?? [];
264
- return ids.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
265
- }
266
- case "resolver": {
267
- const resolved = relation.via.resolve(entity, store);
268
- if (Array.isArray(resolved)) {
269
- return resolved.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
270
- }
271
- if (typeof resolved !== "string") return null;
272
- const related = store.readEntitySnapshot(relation.type, resolved);
273
- return related ? projectRow(related, include, store) : null;
274
- }
84
+ function clauseToPrismaLeaf(c) {
85
+ switch (c.op) {
86
+ case "eq":
87
+ return { equals: c.value };
88
+ case "neq":
89
+ return { not: c.value };
90
+ case "gt":
91
+ return { gt: c.value };
92
+ case "gte":
93
+ return { gte: c.value };
94
+ case "lt":
95
+ return { lt: c.value };
96
+ case "lte":
97
+ return { lte: c.value };
98
+ case "contains":
99
+ return { contains: c.value, mode: "insensitive" };
100
+ case "startsWith":
101
+ return { startsWith: c.value, mode: "insensitive" };
102
+ case "endsWith":
103
+ return { endsWith: c.value, mode: "insensitive" };
104
+ case "in":
105
+ return { in: c.value };
106
+ case "nin":
107
+ return { notIn: c.value };
108
+ case "arrayContains":
109
+ return { has: c.value };
110
+ case "between":
111
+ case "arrayOverlaps":
112
+ case "matches":
113
+ case "custom":
114
+ default:
115
+ return null;
275
116
  }
276
117
  }
277
- function applySelection(row, select) {
278
- if (!select) return row;
279
- if (typeof select === "function") {
280
- const result = select(row);
281
- return result && typeof result === "object" ? result : { value: result };
118
+ function clauseToPrismaEntry(c) {
119
+ const parts = c.field.split(".").filter(Boolean);
120
+ if (parts.length === 0) return null;
121
+ if (c.op === "isNull") {
122
+ const equalsNull = c.value === void 0 || c.value === true;
123
+ return nestWhereField(parts, equalsNull ? null : { not: null });
282
124
  }
283
- const picked = {};
284
- for (const key of select) {
285
- if (key in row) picked[key] = row[key];
125
+ if (c.op === "isNotNull") {
126
+ return nestWhereField(parts, { not: null });
286
127
  }
287
- return picked;
128
+ const leaf = clauseToPrismaLeaf(c);
129
+ if (leaf === null) return null;
130
+ return nestWhereField(parts, leaf);
288
131
  }
289
-
290
- // src/graph-actions.ts
291
- var graphActionListeners = /* @__PURE__ */ new Set();
292
- var graphActionReplayers = /* @__PURE__ */ new Map();
293
- function createGraphTransaction() {
294
- const baseline = cloneGraphData();
295
- let closed = false;
296
- const tx = {
297
- upsertEntity(type, id, data) {
298
- useGraphStore.getState().upsertEntity(type, id, data);
299
- return tx;
300
- },
301
- replaceEntity(type, id, data) {
302
- useGraphStore.getState().replaceEntity(type, id, data);
303
- return tx;
304
- },
305
- removeEntity(type, id) {
306
- useGraphStore.getState().removeEntity(type, id);
307
- return tx;
308
- },
309
- patchEntity(type, id, patch) {
310
- useGraphStore.getState().patchEntity(type, id, patch);
311
- return tx;
312
- },
313
- clearPatch(type, id) {
314
- useGraphStore.getState().clearPatch(type, id);
315
- return tx;
316
- },
317
- insertIdInList(key, id, position) {
318
- useGraphStore.getState().insertIdInList(key, id, position);
319
- return tx;
320
- },
321
- removeIdFromAllLists(type, id) {
322
- useGraphStore.getState().removeIdFromAllLists(type, id);
323
- return tx;
324
- },
325
- setEntitySyncMetadata(type, id, metadata) {
326
- useGraphStore.getState().setEntitySyncMetadata(type, id, metadata);
327
- return tx;
328
- },
329
- markEntityPending(type, id, origin = "optimistic") {
330
- useGraphStore.getState().setEntitySyncMetadata(type, id, {
331
- synced: false,
332
- origin,
333
- updatedAt: Date.now()
334
- });
335
- return tx;
336
- },
337
- markEntitySynced(type, id, origin = "server") {
338
- useGraphStore.getState().setEntitySyncMetadata(type, id, {
339
- synced: true,
340
- origin,
341
- updatedAt: Date.now()
342
- });
343
- return tx;
344
- },
345
- commit() {
346
- closed = true;
347
- },
348
- rollback() {
349
- if (closed) return;
350
- useGraphStore.setState(cloneGraphData(baseline));
351
- closed = true;
352
- },
353
- snapshot() {
354
- return cloneGraphData();
132
+ function groupToPrismaWhere(g) {
133
+ const parts = [];
134
+ for (const item of g.clauses) {
135
+ if ("logic" in item) {
136
+ const nested = groupToPrismaWhere(item);
137
+ if (Object.keys(nested).length > 0) parts.push(nested);
138
+ } else {
139
+ const entry = clauseToPrismaEntry(item);
140
+ if (entry) parts.push(entry);
355
141
  }
356
- };
357
- return tx;
358
- }
359
- function createGraphAction(opts) {
360
- if (opts.key) {
361
- graphActionReplayers.set(opts.key, async (record) => {
362
- const tx = createGraphTransaction();
363
- try {
364
- const result = await opts.run(tx, record.input);
365
- tx.commit();
366
- return result;
367
- } catch (error) {
368
- tx.rollback();
369
- throw error;
370
- }
371
- });
372
142
  }
373
- return async (input) => {
374
- const tx = createGraphTransaction();
375
- const record = opts.key ? {
376
- id: `${opts.key}:${Date.now()}`,
377
- key: opts.key,
378
- input: structuredClone(input),
379
- enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
380
- } : null;
381
- try {
382
- if (record) emitGraphActionEvent({ type: "enqueued", record });
383
- opts.optimistic?.(tx, input);
384
- const result = await opts.run(tx, input);
385
- opts.onSuccess?.(result, input, tx);
386
- tx.commit();
387
- if (record) emitGraphActionEvent({ type: "settled", record });
388
- return result;
389
- } catch (error) {
390
- tx.rollback();
391
- const normalized = error instanceof Error ? error : new Error(String(error));
392
- if (record) emitGraphActionEvent({ type: "settled", record });
393
- opts.onError?.(normalized, input);
394
- throw normalized;
395
- }
396
- };
397
- }
398
- function subscribeGraphActionEvents(listener) {
399
- graphActionListeners.add(listener);
400
- return () => graphActionListeners.delete(listener);
401
- }
402
- async function replayRegisteredGraphAction(record) {
403
- const replayer = graphActionReplayers.get(record.key);
404
- if (!replayer) throw new Error(`No graph action registered for key "${record.key}"`);
405
- return replayer(record);
143
+ if (parts.length === 0) return {};
144
+ if (parts.length === 1) return parts[0];
145
+ return g.logic === "or" ? { OR: parts } : { AND: parts };
406
146
  }
407
- function cloneGraphData(source = useGraphStore.getState()) {
408
- return {
409
- entities: structuredClone(source.entities),
410
- patches: structuredClone(source.patches),
411
- entityStates: structuredClone(source.entityStates),
412
- syncMetadata: structuredClone(source.syncMetadata),
413
- lists: structuredClone(source.lists)
414
- };
147
+ function toPrismaWhere(filter) {
148
+ if (Array.isArray(filter)) {
149
+ const parts = [];
150
+ for (const item of filter) {
151
+ const entry = clauseToPrismaEntry(item);
152
+ if (entry) parts.push(entry);
153
+ }
154
+ if (parts.length === 0) return {};
155
+ if (parts.length === 1) return parts[0];
156
+ return { AND: parts };
157
+ }
158
+ return groupToPrismaWhere(filter);
415
159
  }
416
- function emitGraphActionEvent(event) {
417
- for (const listener of graphActionListeners) listener(event);
160
+ function toPrismaOrderBy(sort) {
161
+ return sort.map((s) => ({ [s.field]: s.direction }));
418
162
  }
419
163
 
420
- // src/graph-effects.ts
421
- function createGraphEffect(opts) {
422
- const getKey = opts.getKey ?? defaultGetKey;
423
- const isEqual = opts.isEqual ?? defaultIsEqual;
424
- let initialized = false;
425
- let previous = /* @__PURE__ */ new Map();
426
- const evaluate = () => {
427
- const nextValues = normalizeQueryResult(opts.query());
428
- const next = /* @__PURE__ */ new Map();
429
- nextValues.forEach((value, index) => {
430
- next.set(getKey(value, index), value);
431
- });
432
- if (!initialized) {
433
- initialized = true;
434
- previous = next;
435
- if (opts.skipInitial) return;
436
- }
437
- for (const [key, value] of next.entries()) {
438
- const previousValue = previous.get(key);
439
- if (previousValue === void 0) {
440
- opts.onEnter?.({ key, value });
441
- continue;
442
- }
443
- if (!isEqual(previousValue, value)) {
444
- opts.onUpdate?.({ key, value, previousValue });
445
- }
446
- }
447
- for (const [key, previousValue] of previous.entries()) {
448
- if (!next.has(key)) opts.onExit?.({ key, previousValue });
164
+ // src/view/types.ts
165
+ function toRestParams(view) {
166
+ const params = {};
167
+ if (view.filter) {
168
+ const clauses = flattenClauses(view.filter);
169
+ for (const c of clauses) {
170
+ if (c.op === "custom") continue;
171
+ const key = c.op === "eq" ? c.field : `${c.field}[${c.op}]`;
172
+ params[key] = Array.isArray(c.value) ? c.value.join(",") : String(c.value ?? "");
449
173
  }
450
- previous = next;
451
- };
452
- evaluate();
453
- const unsubscribe = useGraphStore.subscribe(() => {
454
- evaluate();
455
- });
456
- return {
457
- dispose: () => {
458
- unsubscribe();
174
+ }
175
+ if (view.sort) params["sort"] = view.sort.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
176
+ if (view.search?.query) params["q"] = view.search.query;
177
+ return params;
178
+ }
179
+ function toSQLClauses(view) {
180
+ const params = [];
181
+ let paramIdx = 1;
182
+ function clauseToSQL(c) {
183
+ const col = `"${c.field}"`;
184
+ switch (c.op) {
185
+ case "eq":
186
+ params.push(c.value);
187
+ return `${col} = $${paramIdx++}`;
188
+ case "neq":
189
+ params.push(c.value);
190
+ return `${col} != $${paramIdx++}`;
191
+ case "gt":
192
+ params.push(c.value);
193
+ return `${col} > $${paramIdx++}`;
194
+ case "gte":
195
+ params.push(c.value);
196
+ return `${col} >= $${paramIdx++}`;
197
+ case "lt":
198
+ params.push(c.value);
199
+ return `${col} < $${paramIdx++}`;
200
+ case "lte":
201
+ params.push(c.value);
202
+ return `${col} <= $${paramIdx++}`;
203
+ case "in":
204
+ params.push(c.value);
205
+ return `${col} = ANY($${paramIdx++})`;
206
+ case "nin":
207
+ params.push(c.value);
208
+ return `${col} != ALL($${paramIdx++})`;
209
+ case "isNull":
210
+ return `${col} IS NULL`;
211
+ case "isNotNull":
212
+ return `${col} IS NOT NULL`;
213
+ case "contains":
214
+ params.push(`%${c.value}%`);
215
+ return `${col} ILIKE $${paramIdx++}`;
216
+ case "startsWith":
217
+ params.push(`${c.value}%`);
218
+ return `${col} ILIKE $${paramIdx++}`;
219
+ case "between": {
220
+ const [lo, hi] = c.value;
221
+ params.push(lo, hi);
222
+ return `${col} BETWEEN $${paramIdx++} AND $${paramIdx++}`;
223
+ }
224
+ case "arrayContains":
225
+ params.push(c.value);
226
+ return `$${paramIdx++} = ANY(${col})`;
227
+ default:
228
+ return "TRUE";
459
229
  }
460
- };
230
+ }
231
+ function groupToSQL(g) {
232
+ const parts = g.clauses.map((c) => "logic" in c ? `(${groupToSQL(c)})` : clauseToSQL(c));
233
+ return parts.join(` ${g.logic.toUpperCase()} `);
234
+ }
235
+ let where = "TRUE";
236
+ if (view.filter) {
237
+ if (Array.isArray(view.filter)) where = view.filter.map(clauseToSQL).join(" AND ") || "TRUE";
238
+ else where = groupToSQL(view.filter) || "TRUE";
239
+ }
240
+ if (view.search?.query) {
241
+ params.push(`%${view.search.query}%`);
242
+ where += ` AND (${view.search.fields.map((f) => `"${f}"`).join(" || ' ' || ")}) ILIKE $${paramIdx++}`;
243
+ }
244
+ const orderBy = view.sort ? view.sort.map((s) => `"${s.field}" ${s.direction.toUpperCase()}${s.nulls ? ` NULLS ${s.nulls.toUpperCase()}` : ""}`).join(", ") : "";
245
+ return { where, orderBy, params };
461
246
  }
462
- function normalizeQueryResult(value) {
463
- if (value == null) return [];
464
- return Array.isArray(value) ? value : [value];
247
+ function toGraphQLVariables(view) {
248
+ const result = {};
249
+ if (view.filter) {
250
+ const clauses = flattenClauses(view.filter);
251
+ const where = {};
252
+ for (const c of clauses) {
253
+ if (c.op === "custom") continue;
254
+ where[c.field] = { [`_${c.op}`]: c.value };
255
+ }
256
+ if (Object.keys(where).length) result.where = where;
257
+ }
258
+ if (view.sort) result.orderBy = view.sort.map((s) => ({ [s.field]: s.direction === "desc" ? "desc_nulls_last" : "asc_nulls_last" }));
259
+ if (view.search?.query) result.search = view.search.query;
260
+ return result;
465
261
  }
466
- function defaultGetKey(value, index) {
467
- if (value && typeof value === "object") {
468
- const record = value;
469
- if (typeof record.id === "string") return record.id;
470
- if (typeof record.$key === "string") return record.$key;
262
+ function flattenClauses(filter) {
263
+ if (Array.isArray(filter)) return filter;
264
+ function walk(g) {
265
+ return g.clauses.flatMap((c) => "logic" in c ? walk(c) : [c]);
471
266
  }
472
- return String(index);
267
+ return walk(filter);
473
268
  }
474
- function defaultIsEqual(previousValue, nextValue) {
475
- return JSON.stringify(previousValue) === JSON.stringify(nextValue);
269
+ function hasCustomPredicates(filter) {
270
+ return flattenClauses(filter).some((c) => c.op === "custom");
476
271
  }
477
272
 
478
- // src/object-path.ts
479
- function isObject(value) {
480
- return typeof value === "object" && value !== null && !Array.isArray(value);
273
+ // src/transport/rest.ts
274
+ function makeRestTransport(opts) {
275
+ const {
276
+ supabase,
277
+ table,
278
+ select = "*",
279
+ identify = (row) => String(row.id),
280
+ authoritative = false,
281
+ staleTime,
282
+ searchColumns = [],
283
+ defaultLimit = 200
284
+ } = opts;
285
+ return {
286
+ identify,
287
+ authoritative,
288
+ staleTime,
289
+ async list(q) {
290
+ const limit = q.limit ?? defaultLimit;
291
+ const offset = typeof q.cursor === "number" ? q.cursor : 0;
292
+ let builder = supabase.from(table).select(select, { count: offset === 0 ? "exact" : void 0 });
293
+ if (q.filter) builder = applyFilter(builder, q.filter);
294
+ if (q.search && searchColumns.length > 0) {
295
+ const escaped = q.search.replace(/[%,()]/g, "");
296
+ const orClause = searchColumns.map((c) => `${c}.ilike.*${escaped}*`).join(",");
297
+ builder = builder.or(orClause);
298
+ }
299
+ if (q.sort) {
300
+ for (const s of q.sort) {
301
+ builder = builder.order(s.field, {
302
+ ascending: s.direction === "asc",
303
+ nullsFirst: s.nulls === "first"
304
+ });
305
+ }
306
+ }
307
+ builder = builder.range(offset, offset + limit - 1);
308
+ if (q.signal && builder.abortSignal) builder = builder.abortSignal(q.signal);
309
+ const result = await builder;
310
+ if (result.error) {
311
+ throw mapPostgrestError(result.error, result.status);
312
+ }
313
+ const rows = result.data ?? [];
314
+ const total = typeof result.count === "number" ? result.count : null;
315
+ const nextCursor = rows.length === limit ? offset + limit : null;
316
+ return { rows, total, nextCursor };
317
+ }
318
+ };
481
319
  }
482
- function getValueAtPath(source, path) {
483
- if (!path) return source;
484
- const segments = path.split(".").filter(Boolean);
485
- let current = source;
486
- for (const segment of segments) {
487
- if (!isObject(current) && !Array.isArray(current)) return void 0;
488
- current = current[segment];
320
+ function applyFilter(builder, filter) {
321
+ if (!Array.isArray(filter) && filter.logic === "or") {
322
+ const parts = filter.clauses.filter((c) => !("logic" in c)).map((c) => clauseToOrFragment(c)).filter((s) => !!s);
323
+ if (parts.length > 0) return builder.or(parts.join(","));
324
+ return builder;
489
325
  }
490
- return current;
491
- }
492
- function setValueAtPath(source, path, value) {
493
- const segments = path.split(".").filter(Boolean);
494
- if (segments.length === 0) return source;
495
- const clone = structuredClone(source);
496
- let current = clone;
497
- for (let index = 0; index < segments.length - 1; index += 1) {
498
- const segment = segments[index];
499
- const next = current[segment];
500
- if (!isObject(next)) current[segment] = {};
501
- current = current[segment];
326
+ const clauses = flattenClauses(filter);
327
+ let b = builder;
328
+ for (const c of clauses) {
329
+ b = applyClause(b, c);
502
330
  }
503
- current[segments[segments.length - 1]] = value;
504
- return clone;
331
+ return b;
505
332
  }
506
- function collectDirtyPaths(current, original, prefix = "", acc = /* @__PURE__ */ new Set()) {
507
- if (isObject(current) && isObject(original)) {
508
- const keys = /* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(original)]);
509
- for (const key of keys) {
510
- const nextPrefix = prefix ? `${prefix}.${key}` : key;
511
- collectDirtyPaths(current[key], original[key], nextPrefix, acc);
512
- }
513
- return acc;
333
+ function applyClause(builder, c) {
334
+ switch (c.op) {
335
+ case "eq":
336
+ return builder.eq(c.field, c.value);
337
+ case "neq":
338
+ return builder.neq(c.field, c.value);
339
+ case "gt":
340
+ return builder.gt(c.field, c.value);
341
+ case "gte":
342
+ return builder.gte(c.field, c.value);
343
+ case "lt":
344
+ return builder.lt(c.field, c.value);
345
+ case "lte":
346
+ return builder.lte(c.field, c.value);
347
+ case "in":
348
+ return builder.in(c.field, c.value);
349
+ case "isNull":
350
+ return builder.is(c.field, null);
351
+ case "isNotNull":
352
+ return builder.neq(c.field, null);
353
+ case "contains":
354
+ return builder.ilike(c.field, `%${String(c.value)}%`);
355
+ case "startsWith":
356
+ return builder.ilike(c.field, `${String(c.value)}%`);
357
+ case "endsWith":
358
+ return builder.ilike(c.field, `%${String(c.value)}`);
359
+ case "matches":
360
+ return builder.ilike(c.field, String(c.value));
361
+ case "custom":
362
+ return builder;
363
+ // local-only predicate; skip on server
364
+ default:
365
+ return builder;
514
366
  }
515
- if (JSON.stringify(current) !== JSON.stringify(original) && prefix) acc.add(prefix);
516
- return acc;
517
- }
518
- var schemaRegistry = /* @__PURE__ */ new Map();
519
- function registerEntityJsonSchema(config) {
520
- const key = registryKey(config.entityType, config.field, config.schemaId);
521
- schemaRegistry.set(key, config);
522
- }
523
- function registerRuntimeSchema(config) {
524
- registerEntityJsonSchema(config);
525
367
  }
526
- function getEntityJsonSchema(opts) {
527
- const exact = schemaRegistry.get(registryKey(opts.entityType, opts.field, opts.schemaId));
528
- if (exact) return exact;
529
- if (opts.field) {
530
- const byField = schemaRegistry.get(registryKey(opts.entityType, opts.field));
531
- if (byField) return byField;
368
+ function clauseToOrFragment(c) {
369
+ const v = encodeURIComponent(String(c.value ?? ""));
370
+ switch (c.op) {
371
+ case "eq":
372
+ return `${c.field}.eq.${v}`;
373
+ case "neq":
374
+ return `${c.field}.neq.${v}`;
375
+ case "gt":
376
+ return `${c.field}.gt.${v}`;
377
+ case "gte":
378
+ return `${c.field}.gte.${v}`;
379
+ case "lt":
380
+ return `${c.field}.lt.${v}`;
381
+ case "lte":
382
+ return `${c.field}.lte.${v}`;
383
+ case "isNull":
384
+ return `${c.field}.is.null`;
385
+ case "isNotNull":
386
+ return `${c.field}.not.is.null`;
387
+ case "contains":
388
+ return `${c.field}.ilike.*${v}*`;
389
+ case "startsWith":
390
+ return `${c.field}.ilike.${v}*`;
391
+ case "endsWith":
392
+ return `${c.field}.ilike.*${v}`;
393
+ default:
394
+ return null;
532
395
  }
533
- if (opts.schemaId) {
534
- const byId = schemaRegistry.get(registryKey(opts.entityType, void 0, opts.schemaId));
535
- if (byId) return byId;
396
+ }
397
+ function mapPostgrestError(error, status) {
398
+ if (error instanceof TerminalError || error instanceof TransientError) return error;
399
+ const message = error && typeof error === "object" && "message" in error ? String(error.message ?? "Unknown REST error") : String(error);
400
+ if (typeof status === "number") {
401
+ if (status >= 400 && status < 500) {
402
+ return new TerminalError(message, { status, cause: error });
403
+ }
404
+ if (status >= 500) {
405
+ return new TransientError(message, { status, cause: error });
406
+ }
536
407
  }
537
- for (const schema of schemaRegistry.values()) {
538
- if (schema.entityType !== opts.entityType) continue;
539
- if (opts.field && schema.field !== opts.field) continue;
540
- return schema;
408
+ const code = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
409
+ if (code === "42P01" || code === "PGRST116" || code === "PGRST106") {
410
+ return new TerminalError(message, { cause: error });
541
411
  }
542
- return null;
412
+ return toEntityError(error);
543
413
  }
544
- function useSchemaEntityFields(opts) {
545
- return useMemo(() => {
546
- const schema = opts.schema ?? getEntityJsonSchema(opts)?.schema;
547
- if (!schema) return [];
548
- return buildEntityFieldsFromSchema({ schema, rootField: opts.rootField ?? opts.field });
549
- }, [opts.entityType, opts.field, opts.rootField, opts.schemaId, opts.schema]);
414
+ var EMPTY_IDS = [];
415
+ var EMPTY_ENTITY_STATE = {
416
+ isFetching: false,
417
+ lastFetched: null,
418
+ error: null,
419
+ stale: false
420
+ };
421
+ var EMPTY_SYNC_METADATA = {
422
+ synced: true,
423
+ origin: "server",
424
+ updatedAt: null
425
+ };
426
+ var EMPTY_LIST_STATE = {
427
+ ids: EMPTY_IDS,
428
+ total: null,
429
+ nextCursor: null,
430
+ prevCursor: null,
431
+ hasNextPage: false,
432
+ hasPrevPage: false,
433
+ isFetching: false,
434
+ isFetchingMore: false,
435
+ error: null,
436
+ lastError: null,
437
+ lastFetched: null,
438
+ stale: false,
439
+ currentPage: null,
440
+ pageSize: null
441
+ };
442
+ function defaultEntityState() {
443
+ return { ...EMPTY_ENTITY_STATE };
550
444
  }
551
- function buildEntityFieldsFromSchema(opts) {
552
- return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
445
+ function defaultSyncMetadata() {
446
+ return { ...EMPTY_SYNC_METADATA };
553
447
  }
554
- function exportGraphSnapshotWithSchemas(opts) {
555
- return JSON.stringify(
556
- {
557
- scope: opts.scope,
558
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
559
- data: opts.data,
560
- schemas: opts.schemas.filter(Boolean)
561
- },
562
- null,
563
- opts.pretty === false ? 0 : 2
564
- );
448
+ function defaultListState() {
449
+ return { ...EMPTY_LIST_STATE, ids: [] };
565
450
  }
566
- function escapeHtml(input) {
567
- return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
451
+ function ek(type, id) {
452
+ return `${type}:${id}`;
568
453
  }
569
- function renderMarkdownToHtml(value) {
570
- const escaped = escapeHtml(value);
571
- const blocks = escaped.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
572
- return blocks.map((block) => renderMarkdownBlock(block)).join("");
454
+ var snapshotCache = /* @__PURE__ */ new Map();
455
+ function readCachedEntitySnapshot(type, id, base, patch, metadata) {
456
+ const key = ek(type, id);
457
+ const cached = snapshotCache.get(key);
458
+ if (cached && cached.base === base && cached.patch === patch && cached.metadata === metadata) {
459
+ return cached.snapshot;
460
+ }
461
+ const snapshot = {
462
+ ...patch ? { ...base, ...patch } : base,
463
+ $synced: metadata.synced,
464
+ $origin: metadata.origin,
465
+ $updatedAt: metadata.updatedAt
466
+ };
467
+ snapshotCache.set(key, { base, patch, metadata, snapshot });
468
+ return snapshot;
573
469
  }
574
- function MarkdownFieldRenderer({ value, className }) {
575
- return /* @__PURE__ */ jsx(
576
- "div",
577
- {
578
- className,
579
- dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
470
+ var useGraphStore = create()(
471
+ subscribeWithSelector(
472
+ immer((set, get) => ({
473
+ entities: {},
474
+ patches: {},
475
+ entityStates: {},
476
+ syncMetadata: {},
477
+ lists: {},
478
+ upsertEntity: (type, id, data) => set((s) => {
479
+ if (!s.entities[type]) s.entities[type] = {};
480
+ s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
481
+ const key = ek(type, id);
482
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
483
+ }),
484
+ upsertEntities: (type, entries) => set((s) => {
485
+ if (!s.entities[type]) s.entities[type] = {};
486
+ for (const { id, data } of entries) {
487
+ s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
488
+ const key = ek(type, id);
489
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
490
+ }
491
+ }),
492
+ replaceEntity: (type, id, data) => set((s) => {
493
+ if (!s.entities[type]) s.entities[type] = {};
494
+ s.entities[type][id] = data;
495
+ const key = ek(type, id);
496
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
497
+ }),
498
+ removeEntity: (type, id) => set((s) => {
499
+ delete s.entities[type]?.[id];
500
+ delete s.patches[type]?.[id];
501
+ delete s.entityStates[ek(type, id)];
502
+ delete s.syncMetadata[ek(type, id)];
503
+ }),
504
+ patchEntity: (type, id, patch) => set((s) => {
505
+ if (!s.patches[type]) s.patches[type] = {};
506
+ s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
507
+ }),
508
+ unpatchEntity: (type, id, keys) => set((s) => {
509
+ const p = s.patches[type]?.[id];
510
+ if (!p) return;
511
+ for (const k of keys) delete p[k];
512
+ }),
513
+ clearPatch: (type, id) => set((s) => {
514
+ delete s.patches[type]?.[id];
515
+ }),
516
+ setEntityFetching: (type, id, fetching) => set((s) => {
517
+ const k = ek(type, id);
518
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
519
+ s.entityStates[k].isFetching = fetching;
520
+ }),
521
+ setEntityError: (type, id, error) => set((s) => {
522
+ const k = ek(type, id);
523
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
524
+ s.entityStates[k].error = error;
525
+ s.entityStates[k].isFetching = false;
526
+ }),
527
+ setEntityFetched: (type, id) => set((s) => {
528
+ const k = ek(type, id);
529
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
530
+ s.entityStates[k].lastFetched = Date.now();
531
+ s.entityStates[k].isFetching = false;
532
+ s.entityStates[k].error = null;
533
+ s.entityStates[k].stale = false;
534
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
535
+ }),
536
+ setEntityStale: (type, id, stale) => set((s) => {
537
+ const k = ek(type, id);
538
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
539
+ s.entityStates[k].stale = stale;
540
+ }),
541
+ setEntitySyncMetadata: (type, id, metadata) => set((s) => {
542
+ const k = ek(type, id);
543
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
544
+ }),
545
+ clearEntitySyncMetadata: (type, id) => set((s) => {
546
+ delete s.syncMetadata[ek(type, id)];
547
+ }),
548
+ setListResult: (key, ids, meta) => set((s) => {
549
+ const ex = s.lists[key] ?? defaultListState();
550
+ s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
551
+ }),
552
+ appendListResult: (key, ids, meta) => set((s) => {
553
+ const ex = s.lists[key] ?? defaultListState();
554
+ 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() };
555
+ }),
556
+ prependListResult: (key, ids, meta) => set((s) => {
557
+ const ex = s.lists[key] ?? defaultListState();
558
+ 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() };
559
+ }),
560
+ removeIdFromAllLists: (_type, id) => set((s) => {
561
+ for (const key of Object.keys(s.lists)) {
562
+ const list = s.lists[key];
563
+ const idx = list.ids.indexOf(id);
564
+ if (idx !== -1) {
565
+ list.ids.splice(idx, 1);
566
+ if (list.total !== null) list.total -= 1;
567
+ }
568
+ }
569
+ }),
570
+ insertIdInList: (key, id, position) => set((s) => {
571
+ if (!s.lists[key]) s.lists[key] = defaultListState();
572
+ const ids = s.lists[key].ids;
573
+ const ex = ids.indexOf(id);
574
+ if (ex !== -1) ids.splice(ex, 1);
575
+ if (position === "start") ids.unshift(id);
576
+ else if (position === "end") ids.push(id);
577
+ else ids.splice(position, 0, id);
578
+ }),
579
+ setListFetching: (key, fetching) => set((s) => {
580
+ if (!s.lists[key]) s.lists[key] = defaultListState();
581
+ s.lists[key].isFetching = fetching;
582
+ }),
583
+ setListFetchingMore: (key, fetchingMore) => set((s) => {
584
+ if (!s.lists[key]) s.lists[key] = defaultListState();
585
+ s.lists[key].isFetchingMore = fetchingMore;
586
+ }),
587
+ setListError: (key, error, typed) => set((s) => {
588
+ if (!s.lists[key]) s.lists[key] = defaultListState();
589
+ s.lists[key].error = error;
590
+ s.lists[key].lastError = typed ?? (error === null ? null : s.lists[key].lastError);
591
+ s.lists[key].isFetching = false;
592
+ s.lists[key].isFetchingMore = false;
593
+ s.lists[key].lastFetched = Date.now();
594
+ s.lists[key].stale = false;
595
+ }),
596
+ setListStale: (key, stale) => set((s) => {
597
+ if (!s.lists[key]) s.lists[key] = defaultListState();
598
+ s.lists[key].stale = stale;
599
+ }),
600
+ invalidateEntity: (type, id) => set((s) => {
601
+ if (id) {
602
+ const k = ek(type, id);
603
+ if (s.entityStates[k]) s.entityStates[k].stale = true;
604
+ } else {
605
+ for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
606
+ }
607
+ }),
608
+ invalidateLists: (matcher) => set((s) => {
609
+ const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
610
+ for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
611
+ }),
612
+ invalidateType: (type) => {
613
+ get().invalidateEntity(type);
614
+ get().invalidateLists(type);
615
+ },
616
+ readEntity: (type, id) => {
617
+ const s = get();
618
+ const base = s.entities[type]?.[id];
619
+ if (!base) return null;
620
+ const patch = s.patches[type]?.[id];
621
+ return patch ? { ...base, ...patch } : base;
622
+ },
623
+ readEntitySnapshot: (type, id) => {
624
+ const s = get();
625
+ const base = s.entities[type]?.[id];
626
+ if (!base) {
627
+ snapshotCache.delete(ek(type, id));
628
+ return null;
629
+ }
630
+ const patch = s.patches[type]?.[id];
631
+ const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
632
+ return readCachedEntitySnapshot(
633
+ type,
634
+ id,
635
+ base,
636
+ patch,
637
+ metadata
638
+ );
639
+ }
640
+ }))
641
+ )
642
+ );
643
+
644
+ // src/engine.ts
645
+ function serializeKey(key) {
646
+ return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
647
+ }
648
+ function sleep(ms) {
649
+ return new Promise((r) => setTimeout(r, ms));
650
+ }
651
+ var inflight = /* @__PURE__ */ new Map();
652
+ function dedupe(key, fn) {
653
+ if (inflight.has(key)) return inflight.get(key);
654
+ const p = fn().finally(() => inflight.delete(key));
655
+ inflight.set(key, p);
656
+ return p;
657
+ }
658
+ var subscriberStatsListeners = /* @__PURE__ */ new Set();
659
+ function emitSubscriberStatsChange() {
660
+ for (const cb of subscriberStatsListeners) cb();
661
+ }
662
+ var subscribers = /* @__PURE__ */ new Map();
663
+ function registerSubscriber(key) {
664
+ const token = Symbol(key);
665
+ if (!subscribers.has(key)) subscribers.set(key, /* @__PURE__ */ new Set());
666
+ subscribers.get(key).add(token);
667
+ emitSubscriberStatsChange();
668
+ return token;
669
+ }
670
+ function unregisterSubscriber(key, token) {
671
+ const set = subscribers.get(key);
672
+ if (!set) return;
673
+ set.delete(token);
674
+ if (set.size === 0) subscribers.delete(key);
675
+ emitSubscriberStatsChange();
676
+ }
677
+ function hasSubscribers(key) {
678
+ return (subscribers.get(key)?.size ?? 0) > 0;
679
+ }
680
+ var DEFAULT_OPTIONS = {
681
+ defaultStaleTime: 3e4,
682
+ defaultGcTime: 3e5,
683
+ gcInterval: 6e4,
684
+ maxRetries: 3,
685
+ retryBaseDelay: 1e3,
686
+ revalidateOnFocus: true,
687
+ revalidateOnReconnect: true
688
+ };
689
+ var engineOptions = { ...DEFAULT_OPTIONS };
690
+ function subscribeSubscriberStats(onChange) {
691
+ subscriberStatsListeners.add(onChange);
692
+ return () => subscriberStatsListeners.delete(onChange);
693
+ }
694
+ function getActiveSubscriberCount() {
695
+ let n = 0;
696
+ for (const set of subscribers.values()) n += set.size;
697
+ return n;
698
+ }
699
+ var gcIntervalId = null;
700
+ function runGarbageCollection() {
701
+ const store = useGraphStore.getState();
702
+ const { defaultGcTime: gcTime } = getEngineOptions();
703
+ const now = Date.now();
704
+ const toRemove = [];
705
+ for (const type of Object.keys(store.entities)) {
706
+ const bucket = store.entities[type];
707
+ if (!bucket) continue;
708
+ for (const id of Object.keys(bucket)) {
709
+ const key = `${type}:${id}`;
710
+ if (hasSubscribers(key)) continue;
711
+ const patch = store.patches[type]?.[id];
712
+ if (patch !== void 0 && Object.keys(patch).length > 0) continue;
713
+ const entityState = store.entityStates[key];
714
+ if (entityState?.isFetching) continue;
715
+ const lastFetched = entityState?.lastFetched;
716
+ if (lastFetched == null) continue;
717
+ if (now - lastFetched <= gcTime) continue;
718
+ toRemove.push({ type, id });
580
719
  }
581
- );
720
+ }
721
+ for (const { type, id } of toRemove) {
722
+ store.removeEntity(type, id);
723
+ store.removeIdFromAllLists(type, id);
724
+ }
582
725
  }
583
- function MarkdownFieldEditor({
584
- value,
585
- onChange,
586
- placeholder
587
- }) {
588
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
589
- /* @__PURE__ */ jsx(
590
- "textarea",
591
- {
592
- value,
593
- onChange: (event) => onChange(event.target.value),
594
- placeholder,
595
- className: "w-full min-h-[120px] rounded-md border bg-muted/50 px-3 py-2 text-sm resize-y focus:outline-none focus:ring-1 focus:ring-ring transition-colors"
596
- }
597
- ),
598
- /* @__PURE__ */ jsx("div", { className: "rounded-md border bg-background px-3 py-2", children: /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value, className: "prose prose-sm max-w-none" }) })
599
- ] });
726
+ function stopGarbageCollector() {
727
+ if (gcIntervalId != null && typeof clearInterval !== "undefined") {
728
+ clearInterval(gcIntervalId);
729
+ gcIntervalId = null;
730
+ }
600
731
  }
601
- function createMarkdownDetailRenderer(field) {
602
- return (value, entity) => /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value: String(value ?? getValueAtPath(entity, field) ?? ""), className: "prose prose-sm max-w-none" });
732
+ function startGarbageCollector() {
733
+ stopGarbageCollector();
734
+ if (typeof window === "undefined" || typeof setInterval === "undefined") return () => {
735
+ };
736
+ gcIntervalId = setInterval(() => runGarbageCollection(), getEngineOptions().gcInterval);
737
+ return () => stopGarbageCollector();
603
738
  }
604
- function buildSchemaFields(schema, pathPrefix, schemaPathPrefix) {
605
- if (schema.type === "object" && schema.properties) {
606
- const entries = Object.entries(schema.properties).sort(([, left], [, right]) => {
607
- const l = left["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
608
- const r = right["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
609
- return l - r;
610
- });
611
- return entries.flatMap(([key, childSchema]) => {
612
- if (childSchema["x-hidden"]) return [];
613
- const field = pathPrefix ? `${pathPrefix}.${key}` : key;
614
- const schemaPath = schemaPathPrefix ? `${schemaPathPrefix}.${key}` : key;
615
- if (childSchema.type === "object" && childSchema.properties) {
616
- return buildSchemaFields(childSchema, field, schemaPath);
739
+ function restartGarbageCollector() {
740
+ startGarbageCollector();
741
+ }
742
+ function configureEngine(opts) {
743
+ engineOptions = { ...DEFAULT_OPTIONS, ...opts };
744
+ restartGarbageCollector();
745
+ }
746
+ function getEngineOptions() {
747
+ return engineOptions;
748
+ }
749
+ async function fetchEntity(opts, engineOpts) {
750
+ const { type, id, fetch: fetch2, normalize, sideEffects, idField = "id" } = opts;
751
+ if (!id) return;
752
+ useGraphStore.getState().setEntityFetching(type, id, true);
753
+ const attempt = async (retries) => {
754
+ try {
755
+ const raw = await fetch2(id);
756
+ const normalized = normalize(raw);
757
+ const resolvedId = normalized[idField] ?? id;
758
+ useGraphStore.getState().upsertEntity(type, resolvedId, normalized);
759
+ useGraphStore.getState().setEntityFetched(type, resolvedId);
760
+ if (sideEffects) sideEffects(raw, useGraphStore);
761
+ opts.onSuccess?.(normalized);
762
+ } catch (err) {
763
+ const error = err instanceof Error ? err : new Error(String(err));
764
+ if (retries < engineOpts.maxRetries) {
765
+ await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
766
+ return attempt(retries + 1);
617
767
  }
618
- return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
768
+ useGraphStore.getState().setEntityError(type, id, error.message);
769
+ opts.onError?.(error);
770
+ }
771
+ };
772
+ await dedupe(`${type}:${id}`, () => attempt(0));
773
+ }
774
+ async function fetchList(opts, params, engineOpts, isLoadMore = false) {
775
+ const { type, queryKey, fetch: fetch2, normalize, sideEffects, mode = "replace" } = opts;
776
+ const key = serializeKey(queryKey);
777
+ const store = useGraphStore.getState();
778
+ if (isLoadMore) store.setListFetchingMore(key, true);
779
+ else store.setListFetching(key, true);
780
+ const attempt = async (retries) => {
781
+ try {
782
+ const response = await fetch2(params);
783
+ const normalized = response.items.map(normalize);
784
+ useGraphStore.getState().upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
785
+ for (const { id } of normalized) useGraphStore.getState().setEntityFetched(type, id);
786
+ const ids = normalized.map(({ id }) => id);
787
+ 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 };
788
+ if (mode === "append" && isLoadMore) useGraphStore.getState().appendListResult(key, ids, meta);
789
+ else useGraphStore.getState().setListResult(key, ids, meta);
790
+ if (sideEffects) sideEffects(response.items, useGraphStore);
791
+ opts.onSuccess?.(response);
792
+ } catch (err) {
793
+ const error = err instanceof Error ? err : new Error(String(err));
794
+ if (retries < engineOpts.maxRetries) {
795
+ await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
796
+ return attempt(retries + 1);
797
+ }
798
+ useGraphStore.getState().setListError(key, error.message);
799
+ opts.onError?.(error);
800
+ }
801
+ };
802
+ await dedupe(isLoadMore ? `${key}:more` : key, () => attempt(0));
803
+ }
804
+ var focusListenerAttached = false;
805
+ function attachGlobalListeners() {
806
+ if (typeof window === "undefined" || focusListenerAttached) return;
807
+ focusListenerAttached = true;
808
+ restartGarbageCollector();
809
+ const revalidateAll = () => {
810
+ const state = useGraphStore.getState();
811
+ for (const key of subscribers.keys()) {
812
+ if (!hasSubscribers(key)) continue;
813
+ const colonIdx = key.indexOf(":");
814
+ if (colonIdx === -1) continue;
815
+ const type = key.slice(0, colonIdx);
816
+ const id = key.slice(colonIdx + 1);
817
+ state.setEntityStale(type, id, true);
818
+ }
819
+ };
820
+ if (engineOptions.revalidateOnFocus) {
821
+ window.addEventListener("visibilitychange", () => {
822
+ if (document.visibilityState === "visible") revalidateAll();
619
823
  });
824
+ window.addEventListener("focus", revalidateAll);
620
825
  }
621
- return [];
826
+ if (engineOptions.revalidateOnReconnect) window.addEventListener("online", revalidateAll);
622
827
  }
623
- function schemaField(field, schemaPath, schema, required) {
624
- const type = inferFieldType(schema);
625
- const descriptor = {
626
- field,
627
- label: schema.title ?? humanize(field.split(".").pop() ?? field),
628
- type,
629
- required,
630
- hint: schema.description,
631
- schemaPath,
632
- schema,
633
- componentHint: schema["x-a2ui-component"]
828
+
829
+ // src/hooks/use-entities.ts
830
+ function sleep2(ms) {
831
+ return new Promise((r) => setTimeout(r, ms));
832
+ }
833
+ function useEntities(type, options = {}) {
834
+ const {
835
+ filter,
836
+ sort,
837
+ search,
838
+ limit,
839
+ cursor,
840
+ enabled = true
841
+ } = options;
842
+ const queryKey = useMemo(
843
+ () => serializeKey([type, { filter, sort, search, limit, cursor }]),
844
+ // eslint-disable-next-line react-hooks/exhaustive-deps
845
+ [type, JSON.stringify({ filter, sort, search, limit, cursor })]
846
+ );
847
+ const abortRef = useRef(null);
848
+ const fetchCountRef = useRef(0);
849
+ const [fetchTick, setFetchTick] = useState(0);
850
+ const listState = useStore(
851
+ useGraphStore,
852
+ useCallback((s) => s.lists[queryKey] ?? EMPTY_LIST_STATE, [queryKey])
853
+ );
854
+ const items = useMemo(() => {
855
+ const state = useGraphStore.getState();
856
+ return listState.ids.map((id) => state.readEntity(type, id)).filter((item) => item !== null);
857
+ }, [listState.ids, type]);
858
+ useEffect(() => {
859
+ if (!enabled) return;
860
+ const store = useGraphStore.getState();
861
+ const existing = store.lists[queryKey];
862
+ const engineOpts = getEngineOptions();
863
+ let transport;
864
+ try {
865
+ transport = getEntityTransport(type);
866
+ } catch {
867
+ const err = new TerminalError(`No transport registered for entity type "${type}"`);
868
+ store.setListError(queryKey, err.message, err);
869
+ return;
870
+ }
871
+ const effectiveStaleTime = transport.staleTime ?? engineOpts.defaultStaleTime;
872
+ if (existing?.lastFetched !== null && existing?.lastFetched !== void 0 && !existing.stale && Date.now() - existing.lastFetched < effectiveStaleTime) {
873
+ return;
874
+ }
875
+ abortRef.current?.abort();
876
+ const controller = new AbortController();
877
+ abortRef.current = controller;
878
+ const thisCount = ++fetchCountRef.current;
879
+ const query = {
880
+ filter: filter ?? void 0,
881
+ sort: sort ?? void 0,
882
+ search,
883
+ limit,
884
+ cursor: cursor ?? void 0,
885
+ signal: controller.signal
886
+ };
887
+ const maxRetries = engineOpts.maxRetries ?? 3;
888
+ const retryBaseDelay = engineOpts.retryBaseDelay ?? 1e3;
889
+ store.setListFetching(queryKey, true);
890
+ const attempt = async (retries) => {
891
+ try {
892
+ const result = await transport.list(query);
893
+ if (thisCount !== fetchCountRef.current) return;
894
+ if (controller.signal.aborted) return;
895
+ const graphStore = useGraphStore.getState();
896
+ const entries = result.rows.map((row) => ({
897
+ id: transport.identify(row),
898
+ data: row
899
+ }));
900
+ graphStore.upsertEntities(type, entries);
901
+ for (const { id } of entries) graphStore.setEntityFetched(type, id);
902
+ const ids = entries.map(({ id }) => id);
903
+ graphStore.setListResult(queryKey, ids, {
904
+ total: result.total,
905
+ nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
906
+ hasNextPage: result.nextCursor !== null && result.nextCursor !== void 0
907
+ });
908
+ } catch (err) {
909
+ if (thisCount !== fetchCountRef.current) return;
910
+ if (controller.signal.aborted) return;
911
+ const typed = toEntityError(err);
912
+ if (typed instanceof TransientError && retries < maxRetries) {
913
+ await sleep2(retryBaseDelay * Math.pow(2, retries));
914
+ if (controller.signal.aborted) return;
915
+ return attempt(retries + 1);
916
+ }
917
+ useGraphStore.getState().setListError(queryKey, typed.message, typed);
918
+ }
919
+ };
920
+ attempt(0);
921
+ return () => {
922
+ abortRef.current?.abort();
923
+ };
924
+ }, [queryKey, enabled, fetchTick]);
925
+ useEffect(() => {
926
+ let transport;
927
+ try {
928
+ transport = getEntityTransport(type);
929
+ } catch {
930
+ return;
931
+ }
932
+ if (!transport.subscribe || !enabled) return;
933
+ const unsub = transport.subscribe((ev) => {
934
+ const store = useGraphStore.getState();
935
+ if (ev.op === "delete") {
936
+ store.removeIdFromAllLists(type, ev.id);
937
+ store.removeEntity(type, ev.id);
938
+ } else if (ev.row) {
939
+ store.upsertEntity(type, ev.id, ev.row);
940
+ store.setEntityFetched(type, ev.id);
941
+ }
942
+ });
943
+ return () => unsub();
944
+ }, [type, enabled]);
945
+ const refetch = useCallback(() => {
946
+ abortRef.current?.abort();
947
+ useGraphStore.getState().setListStale(queryKey, true);
948
+ setFetchTick((n) => n + 1);
949
+ }, [queryKey]);
950
+ const typedError = listState.lastError ?? null;
951
+ return {
952
+ items,
953
+ // isLoading = first attempt in-flight with zero cached rows
954
+ isLoading: !enabled ? false : listState.lastFetched === null && listState.isFetching,
955
+ isError: typedError !== null,
956
+ error: typedError,
957
+ refetch
634
958
  };
635
- if (schema.enum) {
636
- descriptor.options = schema.enum.map((value) => ({
637
- value: String(value),
638
- label: String(value)
639
- }));
640
- }
641
- if (type === "markdown") {
642
- descriptor.render = createMarkdownDetailRenderer(field);
643
- }
644
- return descriptor;
645
959
  }
646
- function inferFieldType(schema) {
647
- const forced = schema["x-field-type"];
648
- if (forced === "markdown") return "markdown";
649
- if (schema.format === "markdown") return "markdown";
650
- if (schema.enum) return "enum";
651
- const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
652
- switch (type) {
653
- case "boolean":
654
- return "boolean";
655
- case "integer":
656
- case "number":
657
- return "number";
658
- case "string":
659
- if (schema.format === "email") return "email";
660
- if (schema.format === "uri" || schema.format === "url") return "url";
661
- if (schema.format === "date" || schema.format === "date-time") return "date";
662
- return "text";
663
- case "array":
664
- case "object":
665
- return "json";
960
+
961
+ // src/view/evaluator.ts
962
+ function matchesFilter(entity, filter) {
963
+ if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
964
+ return matchesGroup(entity, filter);
965
+ }
966
+ function matchesGroup(entity, group) {
967
+ const { logic, clauses } = group;
968
+ if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
969
+ return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
970
+ }
971
+ function matchesClause(entity, clause) {
972
+ const { field, op, value, predicate } = clause;
973
+ const fv = getNestedValue(entity, field);
974
+ switch (op) {
975
+ case "eq":
976
+ return fv === value;
977
+ case "neq":
978
+ return fv !== value;
979
+ case "gt":
980
+ return fv > value;
981
+ case "gte":
982
+ return fv >= value;
983
+ case "lt":
984
+ return fv < value;
985
+ case "lte":
986
+ return fv <= value;
987
+ case "in":
988
+ return Array.isArray(value) && value.includes(fv);
989
+ case "nin":
990
+ return Array.isArray(value) && !value.includes(fv);
991
+ case "isNull":
992
+ return fv == null;
993
+ case "isNotNull":
994
+ return fv != null;
995
+ case "contains":
996
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
997
+ case "startsWith":
998
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
999
+ case "endsWith":
1000
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
1001
+ case "between": {
1002
+ const [lo, hi] = value;
1003
+ return fv >= lo && fv <= hi;
1004
+ }
1005
+ case "arrayContains":
1006
+ return Array.isArray(fv) && fv.includes(value);
1007
+ case "arrayOverlaps":
1008
+ return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
1009
+ case "matches":
1010
+ return typeof fv === "string" && new RegExp(value).test(fv);
1011
+ case "custom":
1012
+ return predicate ? predicate(fv, entity) : true;
666
1013
  default:
667
- return "text";
1014
+ return true;
668
1015
  }
669
1016
  }
670
- function registryKey(entityType, field, schemaId) {
671
- return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
672
- }
673
- function humanize(value) {
674
- return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1017
+ function matchesSearch(entity, query, fields) {
1018
+ if (!query.trim()) return true;
1019
+ const lq = query.toLowerCase();
1020
+ return fields.some((field) => {
1021
+ const v = getNestedValue(entity, field);
1022
+ return typeof v === "string" && v.toLowerCase().includes(lq);
1023
+ });
675
1024
  }
676
- function renderMarkdownBlock(block) {
677
- if (block.startsWith("# ")) return `<h1>${renderInlineMarkdown(block.slice(2))}</h1>`;
678
- if (block.startsWith("## ")) return `<h2>${renderInlineMarkdown(block.slice(3))}</h2>`;
679
- return `<p>${renderInlineMarkdown(block).replaceAll("\n", "<br/>")}</p>`;
1025
+ function compareEntities(a, b, sort) {
1026
+ for (const clause of sort) {
1027
+ const r = compareByClause(a, b, clause);
1028
+ if (r !== 0) return r;
1029
+ }
1030
+ return 0;
680
1031
  }
681
- function renderInlineMarkdown(block) {
682
- return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
1032
+ function compareByClause(a, b, clause) {
1033
+ const { field, direction, nulls = "last", comparator } = clause;
1034
+ const av = getNestedValue(a, field);
1035
+ const bv = getNestedValue(b, field);
1036
+ const aNull = av == null;
1037
+ const bNull = bv == null;
1038
+ if (aNull && bNull) return 0;
1039
+ if (aNull) return nulls === "first" ? -1 : 1;
1040
+ if (bNull) return nulls === "first" ? 1 : -1;
1041
+ let cmp;
1042
+ if (comparator) cmp = comparator(av, bv);
1043
+ else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
1044
+ else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
1045
+ else cmp = String(av).localeCompare(String(bv));
1046
+ return direction === "desc" ? -cmp : cmp;
683
1047
  }
684
-
685
- // src/ai-interop.ts
686
- function exportGraphSnapshot(opts) {
687
- const payload = {
688
- scope: opts.scope,
689
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
690
- data: opts.data
691
- };
692
- return JSON.stringify(payload, null, opts.pretty === false ? 0 : 2);
1048
+ function findInsertionIndex(entity, sortedIds, getEntity, sort) {
1049
+ let lo = 0;
1050
+ let hi = sortedIds.length;
1051
+ while (lo < hi) {
1052
+ const mid = lo + hi >>> 1;
1053
+ const me = getEntity(sortedIds[mid]);
1054
+ if (!me) {
1055
+ lo = mid + 1;
1056
+ continue;
1057
+ }
1058
+ if (compareEntities(entity, me, sort) <= 0) hi = mid;
1059
+ else lo = mid + 1;
1060
+ }
1061
+ return lo;
693
1062
  }
694
- function createGraphTool(handler) {
695
- return (input) => handler(input, {
696
- store: useGraphStore.getState(),
697
- queryOnce,
698
- exportGraphSnapshot
699
- });
1063
+ function applyView(ids, getEntity, filter, sort, search) {
1064
+ let entries = [];
1065
+ for (const id of ids) {
1066
+ const entity = getEntity(id);
1067
+ if (!entity) continue;
1068
+ entries.push({ id, entity });
1069
+ }
1070
+ if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
1071
+ if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
1072
+ if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
1073
+ return entries.map((e) => e.id);
700
1074
  }
701
- function createSchemaGraphTool(handler) {
702
- return (input) => handler(input, {
703
- store: useGraphStore.getState(),
704
- queryOnce,
705
- exportGraphSnapshot,
706
- getEntityJsonSchema,
707
- exportGraphSnapshotWithSchemas
708
- });
1075
+ function checkCompleteness(loadedCount, total, hasNextPage) {
1076
+ if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
1077
+ if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
1078
+ return { isComplete: true, reason: "no-more-pages" };
709
1079
  }
710
- var DEFAULT_STORAGE_KEY = "prometheus:graph";
711
- var useGraphSyncStatusStore = create((set) => ({
712
- status: {
713
- phase: "idle",
714
- isOnline: true,
715
- isSynced: true,
716
- pendingActions: 0,
717
- lastHydratedAt: null,
718
- lastPersistedAt: null,
719
- storageKey: null,
720
- error: null
721
- },
722
- setStatus: (status) => set((state) => ({
723
- status: {
724
- ...state.status,
725
- ...status
726
- }
727
- }))
728
- }));
729
- var pendingActions = /* @__PURE__ */ new Map();
730
- function useGraphSyncStatus() {
731
- return useGraphSyncStatusStore((state) => state.status);
1080
+ function getNestedValue(obj, path) {
1081
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
1082
+ let current = obj;
1083
+ for (const part of parts) {
1084
+ if (current == null || typeof current !== "object") return void 0;
1085
+ current = current[part];
1086
+ }
1087
+ return current;
732
1088
  }
733
- async function persistGraphToStorage(opts) {
734
- const payload = {
735
- version: 1,
736
- snapshot: cloneGraphSnapshot(),
737
- pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
738
- };
739
- const json = JSON.stringify(payload);
740
- await opts.storage.set(opts.key, json);
741
- const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
742
- useGraphSyncStatusStore.getState().setStatus({
743
- lastPersistedAt: persistedAt,
744
- storageKey: opts.key,
745
- pendingActions: payload.pendingActions.length
746
- });
747
- return {
748
- ok: true,
749
- key: opts.key,
750
- bytes: json.length,
751
- persistedAt
752
- };
1089
+
1090
+ // src/hooks/use-entity-query.ts
1091
+ function sleep3(ms) {
1092
+ return new Promise((r) => setTimeout(r, ms));
753
1093
  }
754
- async function hydrateGraphFromStorage(opts) {
755
- const raw = await opts.storage.get(opts.key);
756
- if (!raw) {
757
- return {
758
- ok: false,
759
- key: opts.key,
760
- hydratedAt: null,
761
- entityCounts: {},
762
- error: "No persisted graph snapshot found"
763
- };
1094
+ var EMPTY_ENTITY_BUCKET = {};
1095
+ function useEntityQuery(type, opts = {}) {
1096
+ const {
1097
+ view: initialView = {},
1098
+ mode: forcedMode,
1099
+ enabled = true,
1100
+ initialIds,
1101
+ initialTotal,
1102
+ remoteDebounce = 300
1103
+ } = opts;
1104
+ const engineOpts = getEngineOptions();
1105
+ const optsRef = useRef(opts);
1106
+ optsRef.current = opts;
1107
+ const [liveView, setLiveView] = useState(initialView);
1108
+ const liveViewRef = useRef(liveView);
1109
+ liveViewRef.current = liveView;
1110
+ const abortRef = useRef(null);
1111
+ const fetchCountRef = useRef(0);
1112
+ const [fetchTick, setFetchTick] = useState(0);
1113
+ const [remoteResultKey, setRemoteResultKey] = useState(null);
1114
+ const [isFetchingState, setIsFetchingState] = useState(false);
1115
+ const baseKey = useMemo(
1116
+ () => serializeKey([type, "__base__"]),
1117
+ [type]
1118
+ );
1119
+ const seededRef = useRef(false);
1120
+ if (!seededRef.current && initialIds && initialIds.length > 0) {
1121
+ seededRef.current = true;
1122
+ const store = useGraphStore.getState();
1123
+ if (!store.lists[baseKey]) {
1124
+ store.setListResult(baseKey, initialIds, { total: initialTotal ?? null });
1125
+ }
764
1126
  }
1127
+ const listState = useStore(
1128
+ useGraphStore,
1129
+ useCallback((s) => s.lists[baseKey] ?? EMPTY_LIST_STATE, [baseKey])
1130
+ );
1131
+ const remoteListState = useStore(
1132
+ useGraphStore,
1133
+ useCallback(
1134
+ (s) => remoteResultKey ? s.lists[remoteResultKey] ?? null : null,
1135
+ [remoteResultKey]
1136
+ )
1137
+ );
1138
+ const { isComplete } = useMemo(() => {
1139
+ if (!listState) return { isComplete: false };
1140
+ return checkCompleteness(listState.ids.length, listState.total, listState.hasNextPage);
1141
+ }, [listState]);
1142
+ let transport = null;
765
1143
  try {
766
- const parsed = JSON.parse(raw);
767
- useGraphStore.setState(parsed.snapshot);
768
- pendingActions.clear();
769
- for (const action of parsed.pendingActions ?? []) pendingActions.set(action.id, action);
770
- const hydratedAt = (/* @__PURE__ */ new Date()).toISOString();
771
- useGraphSyncStatusStore.getState().setStatus({
772
- lastHydratedAt: hydratedAt,
773
- storageKey: opts.key,
774
- pendingActions: pendingActions.size,
775
- error: null
776
- });
777
- return {
778
- ok: true,
779
- key: opts.key,
780
- hydratedAt,
781
- entityCounts: Object.fromEntries(
782
- Object.entries(parsed.snapshot.entities).map(([type, entities]) => [type, Object.keys(entities).length])
783
- ),
784
- pendingActions: Array.from(pendingActions.values())
785
- };
786
- } catch (error) {
787
- const message = error instanceof Error ? error.message : String(error);
788
- useGraphSyncStatusStore.getState().setStatus({
789
- phase: "error",
790
- error: message,
791
- storageKey: opts.key
792
- });
793
- return {
794
- ok: false,
795
- key: opts.key,
796
- hydratedAt: null,
797
- entityCounts: {},
798
- error: message
799
- };
1144
+ transport = getEntityTransport(type);
1145
+ } catch {
800
1146
  }
801
- }
802
- function startLocalFirstGraph(opts) {
803
- const key = opts.key ?? DEFAULT_STORAGE_KEY;
804
- const persistDebounceMs = opts.persistDebounceMs ?? 50;
805
- const statusStore = useGraphSyncStatusStore.getState();
806
- statusStore.setStatus({
807
- phase: "hydrating",
808
- storageKey: key,
809
- isOnline: opts.onlineSource?.getIsOnline() ?? getDefaultOnlineSource().getIsOnline(),
810
- isSynced: pendingActions.size === 0,
811
- error: null
812
- });
813
- let persistTimer = null;
814
- const schedulePersist = () => {
815
- if (persistTimer) clearTimeout(persistTimer);
816
- persistTimer = setTimeout(() => {
817
- void persistGraphToStorage({ storage: opts.storage, key });
818
- }, persistDebounceMs);
819
- };
820
- const graphUnsub = useGraphStore.subscribe(() => {
821
- schedulePersist();
822
- });
823
- const actionUnsub = subscribeGraphActionEvents((event) => {
824
- if (event.type === "enqueued") pendingActions.set(event.record.id, event.record);
825
- if (event.type === "settled") pendingActions.delete(event.record.id);
826
- useGraphSyncStatusStore.getState().setStatus({
827
- pendingActions: pendingActions.size,
828
- isSynced: pendingActions.size === 0
829
- });
830
- schedulePersist();
831
- });
832
- const onlineSource = opts.onlineSource ?? getDefaultOnlineSource();
833
- const onlineUnsub = onlineSource.subscribe((online) => {
834
- useGraphSyncStatusStore.getState().setStatus({
835
- isOnline: online,
836
- phase: online ? "ready" : "offline"
837
- });
838
- });
839
- const ready = (async () => {
840
- const hydrated = await hydrateGraphFromStorage({ storage: opts.storage, key });
841
- if (opts.replayPendingActions && hydrated.ok && pendingActions.size > 0) {
842
- useGraphSyncStatusStore.getState().setStatus({
843
- phase: "syncing",
844
- isSynced: false
845
- });
846
- for (const action of Array.from(pendingActions.values())) {
847
- await replayRegisteredGraphAction(action);
848
- pendingActions.delete(action.id);
1147
+ const completenessMode = useMemo(() => {
1148
+ if (forcedMode) return forcedMode;
1149
+ if (liveView.filter && hasCustomPredicates(liveView.filter)) return "local";
1150
+ if (isComplete) return "local";
1151
+ if (!transport) return "local";
1152
+ return "hybrid";
1153
+ }, [forcedMode, isComplete, liveView.filter, transport?.authoritative]);
1154
+ const localViewIds = useStore(
1155
+ useGraphStore,
1156
+ useShallow((state) => {
1157
+ const list = state.lists[baseKey] ?? EMPTY_LIST_STATE;
1158
+ const sourceIds = completenessMode !== "remote" && remoteResultKey ? state.lists[remoteResultKey]?.ids ?? EMPTY_IDS : list.ids;
1159
+ const getEntity = (id) => state.readEntitySnapshot(type, id);
1160
+ return applyView(
1161
+ sourceIds,
1162
+ getEntity,
1163
+ liveView.filter,
1164
+ liveView.sort,
1165
+ liveView.search?.query ? { query: liveView.search.query, fields: liveView.search.fields } : null
1166
+ );
1167
+ })
1168
+ );
1169
+ const items = useMemo(
1170
+ () => localViewIds.map((id) => useGraphStore.getState().readEntitySnapshot(type, id)).filter((item) => item !== null),
1171
+ [localViewIds, type]
1172
+ );
1173
+ const fireRemoteFetch = useCallback(
1174
+ async (view, cursor) => {
1175
+ let tp;
1176
+ try {
1177
+ tp = getEntityTransport(type);
1178
+ } catch (e) {
1179
+ const err = toEntityError(e);
1180
+ useGraphStore.getState().setListError(baseKey, err.message, err);
1181
+ return;
849
1182
  }
850
- await persistGraphToStorage({ storage: opts.storage, key });
851
- }
852
- const online = onlineSource.getIsOnline();
853
- useGraphSyncStatusStore.getState().setStatus({
854
- phase: online ? "ready" : "offline",
855
- isOnline: online,
856
- isSynced: pendingActions.size === 0,
857
- pendingActions: pendingActions.size
858
- });
859
- })();
860
- return {
861
- ready,
862
- dispose() {
863
- graphUnsub();
864
- actionUnsub();
865
- onlineUnsub();
866
- if (persistTimer) clearTimeout(persistTimer);
867
- },
868
- async persistNow() {
869
- await persistGraphToStorage({ storage: opts.storage, key });
870
- },
871
- hydrate() {
872
- return hydrateGraphFromStorage({ storage: opts.storage, key });
1183
+ abortRef.current?.abort();
1184
+ const controller = new AbortController();
1185
+ abortRef.current = controller;
1186
+ const thisCount = ++fetchCountRef.current;
1187
+ const rKey = serializeKey([type, "__view__", view, cursor]);
1188
+ setRemoteResultKey(rKey);
1189
+ setIsFetchingState(true);
1190
+ const store = useGraphStore.getState();
1191
+ store.setListFetching(rKey, true);
1192
+ store.setListFetching(baseKey, true);
1193
+ tp.staleTime ?? engineOpts.defaultStaleTime;
1194
+ const maxRetries = engineOpts.maxRetries ?? 3;
1195
+ const retryBaseDelay = engineOpts.retryBaseDelay ?? 1e3;
1196
+ const query = {
1197
+ filter: view.filter,
1198
+ sort: view.sort,
1199
+ search: view.search?.query,
1200
+ cursor,
1201
+ signal: controller.signal
1202
+ };
1203
+ const attempt = async (retries) => {
1204
+ try {
1205
+ const result = await tp.list(query);
1206
+ if (thisCount !== fetchCountRef.current) return;
1207
+ if (controller.signal.aborted) return;
1208
+ const graphStore = useGraphStore.getState();
1209
+ const entries = result.rows.map((row) => ({
1210
+ id: tp.identify(row),
1211
+ data: row
1212
+ }));
1213
+ graphStore.upsertEntities(type, entries);
1214
+ for (const { id } of entries) graphStore.setEntityFetched(type, id);
1215
+ const ids = entries.map(({ id }) => id);
1216
+ if (cursor !== void 0) {
1217
+ graphStore.appendListResult(rKey, ids, {
1218
+ total: result.total,
1219
+ nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
1220
+ hasNextPage: result.nextCursor !== null && result.nextCursor !== void 0
1221
+ });
1222
+ } else {
1223
+ graphStore.setListResult(rKey, ids, {
1224
+ total: result.total,
1225
+ nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
1226
+ hasNextPage: result.nextCursor !== null && result.nextCursor !== void 0
1227
+ });
1228
+ graphStore.setListFetching(baseKey, false);
1229
+ }
1230
+ } catch (err) {
1231
+ if (thisCount !== fetchCountRef.current) return;
1232
+ if (controller.signal.aborted) return;
1233
+ const typed = toEntityError(err);
1234
+ if (typed instanceof TransientError && retries < maxRetries) {
1235
+ await sleep3(retryBaseDelay * Math.pow(2, retries));
1236
+ if (controller.signal.aborted) return;
1237
+ return attempt(retries + 1);
1238
+ }
1239
+ const gs = useGraphStore.getState();
1240
+ gs.setListError(rKey, typed.message, typed);
1241
+ gs.setListError(baseKey, typed.message, typed);
1242
+ } finally {
1243
+ if (thisCount === fetchCountRef.current) setIsFetchingState(false);
1244
+ }
1245
+ };
1246
+ void attempt(0);
873
1247
  },
874
- getStatus() {
875
- return useGraphSyncStatusStore.getState().status;
1248
+ [type, baseKey, engineOpts.defaultStaleTime, engineOpts.maxRetries, engineOpts.retryBaseDelay]
1249
+ );
1250
+ const debounceTimer = useRef(null);
1251
+ useEffect(() => {
1252
+ if (!enabled || completenessMode === "local" || !transport) return;
1253
+ const searchQuery = liveView.search?.query ?? "";
1254
+ const minChars = liveView.search?.minChars ?? 2;
1255
+ if (searchQuery.length > 0 && searchQuery.length < minChars) return;
1256
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
1257
+ debounceTimer.current = setTimeout(
1258
+ () => fireRemoteFetch(liveViewRef.current),
1259
+ remoteDebounce
1260
+ );
1261
+ return () => {
1262
+ if (debounceTimer.current) clearTimeout(debounceTimer.current);
1263
+ };
1264
+ }, [liveView, completenessMode, enabled, remoteDebounce]);
1265
+ useEffect(() => {
1266
+ if (!enabled || !transport) return;
1267
+ const store = useGraphStore.getState();
1268
+ const existing = store.lists[baseKey];
1269
+ const effectiveStaleTime = transport.staleTime ?? engineOpts.defaultStaleTime;
1270
+ const isStale = !existing?.lastFetched || existing.stale || Date.now() - (existing.lastFetched ?? 0) > effectiveStaleTime;
1271
+ if (!existing || isStale) {
1272
+ void fireRemoteFetch(liveViewRef.current);
876
1273
  }
877
- };
878
- }
879
- function cloneGraphSnapshot() {
880
- const state = useGraphStore.getState();
881
- return {
882
- entities: structuredClone(state.entities),
883
- patches: structuredClone(state.patches),
884
- entityStates: structuredClone(state.entityStates),
885
- syncMetadata: structuredClone(state.syncMetadata),
886
- lists: structuredClone(state.lists)
887
- };
888
- }
889
- function getDefaultOnlineSource() {
890
- if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
891
- return {
892
- getIsOnline: () => window.navigator.onLine,
893
- subscribe: (listener) => {
894
- const onlineHandler = () => listener(true);
895
- const offlineHandler = () => listener(false);
896
- window.addEventListener("online", onlineHandler);
897
- window.addEventListener("offline", offlineHandler);
898
- return () => {
899
- window.removeEventListener("online", onlineHandler);
900
- window.removeEventListener("offline", offlineHandler);
901
- };
1274
+ }, [baseKey, enabled, fetchTick]);
1275
+ useEffect(() => {
1276
+ const unsub = useGraphStore.subscribe(
1277
+ (state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET,
1278
+ (newEntities, prevEntities) => {
1279
+ const view = liveViewRef.current;
1280
+ const store = useGraphStore.getState();
1281
+ const list = store.lists[baseKey];
1282
+ if (!list) return;
1283
+ for (const id of /* @__PURE__ */ new Set([
1284
+ ...Object.keys(newEntities),
1285
+ ...Object.keys(prevEntities)
1286
+ ])) {
1287
+ const isPresent = id in newEntities;
1288
+ if (!isPresent) continue;
1289
+ const entity = newEntities[id];
1290
+ const merged = store.readEntitySnapshot(type, id) ?? entity;
1291
+ const matchesCurrentView = (!view.filter || matchesFilter(merged, view.filter)) && (!view.search?.query || matchesSearch(merged, view.search.query, view.search.fields));
1292
+ if (matchesCurrentView && !list.ids.includes(id)) {
1293
+ if (view.sort && view.sort.length > 0) {
1294
+ const idx = findInsertionIndex(
1295
+ merged,
1296
+ list.ids,
1297
+ (eid) => store.readEntitySnapshot(type, eid),
1298
+ view.sort
1299
+ );
1300
+ store.insertIdInList(baseKey, id, idx);
1301
+ } else {
1302
+ store.insertIdInList(baseKey, id, "start");
1303
+ }
1304
+ }
1305
+ }
902
1306
  }
903
- };
904
- }
1307
+ );
1308
+ return unsub;
1309
+ }, [type, baseKey]);
1310
+ useEffect(() => {
1311
+ let tp;
1312
+ try {
1313
+ tp = getEntityTransport(type);
1314
+ } catch {
1315
+ return;
1316
+ }
1317
+ if (!tp.subscribe || !enabled) return;
1318
+ const unsub = tp.subscribe((ev) => {
1319
+ const store = useGraphStore.getState();
1320
+ if (ev.op === "delete") {
1321
+ store.removeIdFromAllLists(type, ev.id);
1322
+ store.removeEntity(type, ev.id);
1323
+ } else if (ev.row) {
1324
+ store.upsertEntity(type, ev.id, ev.row);
1325
+ store.setEntityFetched(type, ev.id);
1326
+ }
1327
+ });
1328
+ return () => unsub();
1329
+ }, [type, enabled]);
1330
+ const setView = useCallback(
1331
+ (partial) => setLiveView((prev) => ({ ...prev, ...partial })),
1332
+ []
1333
+ );
1334
+ const setFilter = useCallback(
1335
+ (filter) => setLiveView((prev) => ({ ...prev, filter: filter ?? void 0 })),
1336
+ []
1337
+ );
1338
+ const setSort = useCallback(
1339
+ (sort) => setLiveView((prev) => ({ ...prev, sort: sort ?? void 0 })),
1340
+ []
1341
+ );
1342
+ const setSearch = useCallback(
1343
+ (query) => setLiveView((prev) => ({
1344
+ ...prev,
1345
+ search: prev.search ? { ...prev.search, query } : { query, fields: [] }
1346
+ })),
1347
+ []
1348
+ );
1349
+ const clearView = useCallback(() => setLiveView(initialView), [initialView]);
1350
+ const fetchNextPage = useCallback(() => {
1351
+ if (completenessMode === "local" || isFetchingState) return;
1352
+ const cursor = remoteListState?.nextCursor ?? void 0;
1353
+ void fireRemoteFetch(liveViewRef.current, cursor ?? void 0);
1354
+ }, [completenessMode, isFetchingState, remoteListState?.nextCursor, fireRemoteFetch]);
1355
+ const refetch = useCallback(() => {
1356
+ abortRef.current?.abort();
1357
+ useGraphStore.getState().setListStale(baseKey, true);
1358
+ setFetchTick((n) => n + 1);
1359
+ }, [baseKey]);
1360
+ const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
1361
+ const baseError = listState?.lastError ?? null;
1362
+ const remoteError = remoteListState?.lastError ?? null;
1363
+ const typedError = remoteError ?? baseError ?? null;
905
1364
  return {
906
- getIsOnline: () => true,
907
- subscribe: () => () => {
908
- }
1365
+ items,
1366
+ viewIds: localViewIds,
1367
+ viewTotal,
1368
+ isLoading: !enabled ? false : items.length === 0 && ((listState?.isFetching ?? false) || isFetchingState),
1369
+ isFetching: (listState?.isFetching ?? false) || isFetchingState,
1370
+ isFetchingMore: remoteListState?.isFetching ?? false,
1371
+ isError: typedError !== null,
1372
+ error: typedError,
1373
+ hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
1374
+ fetchNextPage,
1375
+ isShowingLocalPending: completenessMode === "hybrid" && isFetchingState && items.length > 0,
1376
+ isLocallyComplete: isComplete,
1377
+ completenessMode,
1378
+ setView,
1379
+ setFilter,
1380
+ setSort,
1381
+ setSearch,
1382
+ clearView,
1383
+ refetch
909
1384
  };
910
1385
  }
911
1386
 
912
- // src/engine.ts
913
- function serializeKey(key) {
914
- return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
915
- }
916
- function sleep(ms) {
917
- return new Promise((r) => setTimeout(r, ms));
918
- }
919
- var inflight = /* @__PURE__ */ new Map();
920
- function dedupe(key, fn) {
921
- if (inflight.has(key)) return inflight.get(key);
922
- const p = fn().finally(() => inflight.delete(key));
923
- inflight.set(key, p);
924
- return p;
925
- }
926
- var subscriberStatsListeners = /* @__PURE__ */ new Set();
927
- function emitSubscriberStatsChange() {
928
- for (const cb of subscriberStatsListeners) cb();
1387
+ // src/graph-query.ts
1388
+ function queryOnce(opts) {
1389
+ const store = useGraphStore.getState();
1390
+ const ids = resolveCandidateIds(store, opts);
1391
+ let rows = ids.map((id) => store.readEntitySnapshot(opts.type, id)).filter((row) => row !== null);
1392
+ if (opts.where) rows = rows.filter(opts.where);
1393
+ if (opts.sort) rows = [...rows].sort(opts.sort);
1394
+ const projected = rows.map((row) => applySelection(projectRow(row, opts.include, store), opts.select));
1395
+ if (opts.id) return projected[0] ?? null;
1396
+ return projected;
929
1397
  }
930
- var subscribers = /* @__PURE__ */ new Map();
931
- function registerSubscriber(key) {
932
- const token = Symbol(key);
933
- if (!subscribers.has(key)) subscribers.set(key, /* @__PURE__ */ new Set());
934
- subscribers.get(key).add(token);
935
- emitSubscriberStatsChange();
936
- return token;
1398
+ var selectGraph = queryOnce;
1399
+ function resolveCandidateIds(store, opts) {
1400
+ if (opts.id) return [opts.id];
1401
+ if (opts.ids) return opts.ids;
1402
+ if (opts.listKey) return store.lists[opts.listKey]?.ids ?? [];
1403
+ return Object.keys(store.entities[opts.type] ?? {});
937
1404
  }
938
- function unregisterSubscriber(key, token) {
939
- const set = subscribers.get(key);
940
- if (!set) return;
941
- set.delete(token);
942
- if (set.size === 0) subscribers.delete(key);
943
- emitSubscriberStatsChange();
1405
+ function projectRow(row, include, store) {
1406
+ if (!include) return row;
1407
+ const projected = { ...row };
1408
+ for (const [key, relation] of Object.entries(include)) {
1409
+ const related = resolveRelation(row, relation, store);
1410
+ projected[key] = related;
1411
+ }
1412
+ return projected;
944
1413
  }
945
- function hasSubscribers(key) {
946
- return (subscribers.get(key)?.size ?? 0) > 0;
1414
+ function resolveRelation(entity, relation, store) {
1415
+ const include = relation.include;
1416
+ switch (relation.via.kind) {
1417
+ case "field": {
1418
+ const relatedId = entity[relation.via.field];
1419
+ if (typeof relatedId !== "string") return null;
1420
+ const related = store.readEntitySnapshot(relation.type, relatedId);
1421
+ return related ? projectRow(related, include, store) : null;
1422
+ }
1423
+ case "array": {
1424
+ const ids = entity[relation.via.field];
1425
+ if (!Array.isArray(ids)) return [];
1426
+ return ids.map((id) => typeof id === "string" ? store.readEntitySnapshot(relation.type, id) : null).filter((row) => row !== null).map((row) => projectRow(row, include, store));
1427
+ }
1428
+ case "list": {
1429
+ const key = typeof relation.via.key === "function" ? relation.via.key(entity) : relation.via.key;
1430
+ if (!key) return [];
1431
+ const ids = store.lists[key]?.ids ?? [];
1432
+ return ids.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
1433
+ }
1434
+ case "resolver": {
1435
+ const resolved = relation.via.resolve(entity, store);
1436
+ if (Array.isArray(resolved)) {
1437
+ return resolved.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
1438
+ }
1439
+ if (typeof resolved !== "string") return null;
1440
+ const related = store.readEntitySnapshot(relation.type, resolved);
1441
+ return related ? projectRow(related, include, store) : null;
1442
+ }
1443
+ }
947
1444
  }
948
- var DEFAULT_OPTIONS = {
949
- defaultStaleTime: 3e4,
950
- defaultGcTime: 3e5,
951
- gcInterval: 6e4,
952
- maxRetries: 3,
953
- retryBaseDelay: 1e3,
954
- revalidateOnFocus: true,
955
- revalidateOnReconnect: true
956
- };
957
- var engineOptions = { ...DEFAULT_OPTIONS };
958
- function subscribeSubscriberStats(onChange) {
959
- subscriberStatsListeners.add(onChange);
960
- return () => subscriberStatsListeners.delete(onChange);
1445
+ function applySelection(row, select) {
1446
+ if (!select) return row;
1447
+ if (typeof select === "function") {
1448
+ const result = select(row);
1449
+ return result && typeof result === "object" ? result : { value: result };
1450
+ }
1451
+ const picked = {};
1452
+ for (const key of select) {
1453
+ if (key in row) picked[key] = row[key];
1454
+ }
1455
+ return picked;
961
1456
  }
962
- function getActiveSubscriberCount() {
963
- let n = 0;
964
- for (const set of subscribers.values()) n += set.size;
965
- return n;
1457
+
1458
+ // src/graph-actions.ts
1459
+ var graphActionListeners = /* @__PURE__ */ new Set();
1460
+ var graphActionReplayers = /* @__PURE__ */ new Map();
1461
+ function createGraphTransaction() {
1462
+ const baseline = cloneGraphData();
1463
+ let closed = false;
1464
+ const tx = {
1465
+ upsertEntity(type, id, data) {
1466
+ useGraphStore.getState().upsertEntity(type, id, data);
1467
+ return tx;
1468
+ },
1469
+ replaceEntity(type, id, data) {
1470
+ useGraphStore.getState().replaceEntity(type, id, data);
1471
+ return tx;
1472
+ },
1473
+ removeEntity(type, id) {
1474
+ useGraphStore.getState().removeEntity(type, id);
1475
+ return tx;
1476
+ },
1477
+ patchEntity(type, id, patch) {
1478
+ useGraphStore.getState().patchEntity(type, id, patch);
1479
+ return tx;
1480
+ },
1481
+ clearPatch(type, id) {
1482
+ useGraphStore.getState().clearPatch(type, id);
1483
+ return tx;
1484
+ },
1485
+ insertIdInList(key, id, position) {
1486
+ useGraphStore.getState().insertIdInList(key, id, position);
1487
+ return tx;
1488
+ },
1489
+ removeIdFromAllLists(type, id) {
1490
+ useGraphStore.getState().removeIdFromAllLists(type, id);
1491
+ return tx;
1492
+ },
1493
+ setEntitySyncMetadata(type, id, metadata) {
1494
+ useGraphStore.getState().setEntitySyncMetadata(type, id, metadata);
1495
+ return tx;
1496
+ },
1497
+ markEntityPending(type, id, origin = "optimistic") {
1498
+ useGraphStore.getState().setEntitySyncMetadata(type, id, {
1499
+ synced: false,
1500
+ origin,
1501
+ updatedAt: Date.now()
1502
+ });
1503
+ return tx;
1504
+ },
1505
+ markEntitySynced(type, id, origin = "server") {
1506
+ useGraphStore.getState().setEntitySyncMetadata(type, id, {
1507
+ synced: true,
1508
+ origin,
1509
+ updatedAt: Date.now()
1510
+ });
1511
+ return tx;
1512
+ },
1513
+ commit() {
1514
+ closed = true;
1515
+ },
1516
+ rollback() {
1517
+ if (closed) return;
1518
+ useGraphStore.setState(cloneGraphData(baseline));
1519
+ closed = true;
1520
+ },
1521
+ snapshot() {
1522
+ return cloneGraphData();
1523
+ }
1524
+ };
1525
+ return tx;
966
1526
  }
967
- var gcIntervalId = null;
968
- function runGarbageCollection() {
969
- const store = useGraphStore.getState();
970
- const { defaultGcTime: gcTime } = getEngineOptions();
971
- const now = Date.now();
972
- const toRemove = [];
973
- for (const type of Object.keys(store.entities)) {
974
- const bucket = store.entities[type];
975
- if (!bucket) continue;
976
- for (const id of Object.keys(bucket)) {
977
- const key = `${type}:${id}`;
978
- if (hasSubscribers(key)) continue;
979
- const patch = store.patches[type]?.[id];
980
- if (patch !== void 0 && Object.keys(patch).length > 0) continue;
981
- const entityState = store.entityStates[key];
982
- if (entityState?.isFetching) continue;
983
- const lastFetched = entityState?.lastFetched;
984
- if (lastFetched == null) continue;
985
- if (now - lastFetched <= gcTime) continue;
986
- toRemove.push({ type, id });
1527
+ function createGraphAction(opts) {
1528
+ if (opts.key) {
1529
+ graphActionReplayers.set(opts.key, async (record) => {
1530
+ const tx = createGraphTransaction();
1531
+ try {
1532
+ const result = await opts.run(tx, record.input);
1533
+ tx.commit();
1534
+ return result;
1535
+ } catch (error) {
1536
+ tx.rollback();
1537
+ throw error;
1538
+ }
1539
+ });
1540
+ }
1541
+ return async (input) => {
1542
+ const tx = createGraphTransaction();
1543
+ const record = opts.key ? {
1544
+ id: `${opts.key}:${Date.now()}`,
1545
+ key: opts.key,
1546
+ input: structuredClone(input),
1547
+ enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
1548
+ } : null;
1549
+ try {
1550
+ if (record) emitGraphActionEvent({ type: "enqueued", record });
1551
+ opts.optimistic?.(tx, input);
1552
+ const result = await opts.run(tx, input);
1553
+ opts.onSuccess?.(result, input, tx);
1554
+ tx.commit();
1555
+ if (record) emitGraphActionEvent({ type: "settled", record });
1556
+ return result;
1557
+ } catch (error) {
1558
+ tx.rollback();
1559
+ const normalized = error instanceof Error ? error : new Error(String(error));
1560
+ if (record) emitGraphActionEvent({ type: "settled", record });
1561
+ opts.onError?.(normalized, input);
1562
+ throw normalized;
987
1563
  }
988
- }
989
- for (const { type, id } of toRemove) {
990
- store.removeEntity(type, id);
991
- store.removeIdFromAllLists(type, id);
992
- }
993
- }
994
- function stopGarbageCollector() {
995
- if (gcIntervalId != null && typeof clearInterval !== "undefined") {
996
- clearInterval(gcIntervalId);
997
- gcIntervalId = null;
998
- }
999
- }
1000
- function startGarbageCollector() {
1001
- stopGarbageCollector();
1002
- if (typeof window === "undefined" || typeof setInterval === "undefined") return () => {
1003
1564
  };
1004
- gcIntervalId = setInterval(() => runGarbageCollection(), getEngineOptions().gcInterval);
1005
- return () => stopGarbageCollector();
1006
- }
1007
- function restartGarbageCollector() {
1008
- startGarbageCollector();
1009
1565
  }
1010
- function configureEngine(opts) {
1011
- engineOptions = { ...DEFAULT_OPTIONS, ...opts };
1012
- restartGarbageCollector();
1566
+ function subscribeGraphActionEvents(listener) {
1567
+ graphActionListeners.add(listener);
1568
+ return () => graphActionListeners.delete(listener);
1013
1569
  }
1014
- function getEngineOptions() {
1015
- return engineOptions;
1570
+ async function replayRegisteredGraphAction(record) {
1571
+ const replayer = graphActionReplayers.get(record.key);
1572
+ if (!replayer) throw new Error(`No graph action registered for key "${record.key}"`);
1573
+ return replayer(record);
1016
1574
  }
1017
- async function fetchEntity(opts, engineOpts) {
1018
- const { type, id, fetch: fetch2, normalize, sideEffects, idField = "id" } = opts;
1019
- if (!id) return;
1020
- useGraphStore.getState().setEntityFetching(type, id, true);
1021
- const attempt = async (retries) => {
1022
- try {
1023
- const raw = await fetch2(id);
1024
- const normalized = normalize(raw);
1025
- const resolvedId = normalized[idField] ?? id;
1026
- useGraphStore.getState().upsertEntity(type, resolvedId, normalized);
1027
- useGraphStore.getState().setEntityFetched(type, resolvedId);
1028
- if (sideEffects) sideEffects(raw, useGraphStore);
1029
- opts.onSuccess?.(normalized);
1030
- } catch (err) {
1031
- const error = err instanceof Error ? err : new Error(String(err));
1032
- if (retries < engineOpts.maxRetries) {
1033
- await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
1034
- return attempt(retries + 1);
1035
- }
1036
- useGraphStore.getState().setEntityError(type, id, error.message);
1037
- opts.onError?.(error);
1038
- }
1575
+ function cloneGraphData(source = useGraphStore.getState()) {
1576
+ return {
1577
+ entities: structuredClone(source.entities),
1578
+ patches: structuredClone(source.patches),
1579
+ entityStates: structuredClone(source.entityStates),
1580
+ syncMetadata: structuredClone(source.syncMetadata),
1581
+ lists: structuredClone(source.lists)
1039
1582
  };
1040
- await dedupe(`${type}:${id}`, () => attempt(0));
1041
1583
  }
1042
- async function fetchList(opts, params, engineOpts, isLoadMore = false) {
1043
- const { type, queryKey, fetch: fetch2, normalize, sideEffects, mode = "replace" } = opts;
1044
- const key = serializeKey(queryKey);
1045
- const store = useGraphStore.getState();
1046
- if (isLoadMore) store.setListFetchingMore(key, true);
1047
- else store.setListFetching(key, true);
1048
- const attempt = async (retries) => {
1049
- try {
1050
- const response = await fetch2(params);
1051
- const normalized = response.items.map(normalize);
1052
- useGraphStore.getState().upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
1053
- for (const { id } of normalized) useGraphStore.getState().setEntityFetched(type, id);
1054
- const ids = normalized.map(({ id }) => id);
1055
- 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 };
1056
- if (mode === "append" && isLoadMore) useGraphStore.getState().appendListResult(key, ids, meta);
1057
- else useGraphStore.getState().setListResult(key, ids, meta);
1058
- if (sideEffects) sideEffects(response.items, useGraphStore);
1059
- opts.onSuccess?.(response);
1060
- } catch (err) {
1061
- const error = err instanceof Error ? err : new Error(String(err));
1062
- if (retries < engineOpts.maxRetries) {
1063
- await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
1064
- return attempt(retries + 1);
1584
+ function emitGraphActionEvent(event) {
1585
+ for (const listener of graphActionListeners) listener(event);
1586
+ }
1587
+
1588
+ // src/graph-effects.ts
1589
+ function createGraphEffect(opts) {
1590
+ const getKey = opts.getKey ?? defaultGetKey;
1591
+ const isEqual = opts.isEqual ?? defaultIsEqual;
1592
+ let initialized = false;
1593
+ let previous = /* @__PURE__ */ new Map();
1594
+ const evaluate = () => {
1595
+ const nextValues = normalizeQueryResult(opts.query());
1596
+ const next = /* @__PURE__ */ new Map();
1597
+ nextValues.forEach((value, index) => {
1598
+ next.set(getKey(value, index), value);
1599
+ });
1600
+ if (!initialized) {
1601
+ initialized = true;
1602
+ previous = next;
1603
+ if (opts.skipInitial) return;
1604
+ }
1605
+ for (const [key, value] of next.entries()) {
1606
+ const previousValue = previous.get(key);
1607
+ if (previousValue === void 0) {
1608
+ opts.onEnter?.({ key, value });
1609
+ continue;
1065
1610
  }
1066
- useGraphStore.getState().setListError(key, error.message);
1067
- opts.onError?.(error);
1611
+ if (!isEqual(previousValue, value)) {
1612
+ opts.onUpdate?.({ key, value, previousValue });
1613
+ }
1614
+ }
1615
+ for (const [key, previousValue] of previous.entries()) {
1616
+ if (!next.has(key)) opts.onExit?.({ key, previousValue });
1068
1617
  }
1618
+ previous = next;
1069
1619
  };
1070
- await dedupe(isLoadMore ? `${key}:more` : key, () => attempt(0));
1071
- }
1072
- var focusListenerAttached = false;
1073
- function attachGlobalListeners() {
1074
- if (typeof window === "undefined" || focusListenerAttached) return;
1075
- focusListenerAttached = true;
1076
- restartGarbageCollector();
1077
- const revalidateAll = () => {
1078
- const state = useGraphStore.getState();
1079
- for (const key of subscribers.keys()) {
1080
- if (!hasSubscribers(key)) continue;
1081
- const colonIdx = key.indexOf(":");
1082
- if (colonIdx === -1) continue;
1083
- const type = key.slice(0, colonIdx);
1084
- const id = key.slice(colonIdx + 1);
1085
- state.setEntityStale(type, id, true);
1620
+ evaluate();
1621
+ const unsubscribe = useGraphStore.subscribe(() => {
1622
+ evaluate();
1623
+ });
1624
+ return {
1625
+ dispose: () => {
1626
+ unsubscribe();
1086
1627
  }
1087
1628
  };
1088
- if (engineOptions.revalidateOnFocus) {
1089
- window.addEventListener("visibilitychange", () => {
1090
- if (document.visibilityState === "visible") revalidateAll();
1091
- });
1092
- window.addEventListener("focus", revalidateAll);
1629
+ }
1630
+ function normalizeQueryResult(value) {
1631
+ if (value == null) return [];
1632
+ return Array.isArray(value) ? value : [value];
1633
+ }
1634
+ function defaultGetKey(value, index) {
1635
+ if (value && typeof value === "object") {
1636
+ const record = value;
1637
+ if (typeof record.id === "string") return record.id;
1638
+ if (typeof record.$key === "string") return record.$key;
1093
1639
  }
1094
- if (engineOptions.revalidateOnReconnect) window.addEventListener("online", revalidateAll);
1640
+ return String(index);
1095
1641
  }
1096
- function collectGraphDevStats(entities, patches, entityStates, listsState) {
1097
- const entityCounts = {};
1098
- let totalEntities = 0;
1099
- for (const type of Object.keys(entities)) {
1100
- const bucket = entities[type];
1101
- if (!bucket) continue;
1102
- const n = Object.keys(bucket).length;
1103
- if (n > 0) entityCounts[type] = n;
1104
- totalEntities += n;
1642
+ function defaultIsEqual(previousValue, nextValue) {
1643
+ return JSON.stringify(previousValue) === JSON.stringify(nextValue);
1644
+ }
1645
+
1646
+ // src/object-path.ts
1647
+ function isObject(value) {
1648
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1649
+ }
1650
+ function getValueAtPath(source, path) {
1651
+ if (!path) return source;
1652
+ const segments = path.split(".").filter(Boolean);
1653
+ let current = source;
1654
+ for (const segment of segments) {
1655
+ if (!isObject(current) && !Array.isArray(current)) return void 0;
1656
+ current = current[segment];
1105
1657
  }
1106
- const listKeys = Object.keys(listsState);
1107
- const listCount = listKeys.length;
1108
- const patchedEntities = [];
1109
- for (const type of Object.keys(patches)) {
1110
- const bucket = patches[type];
1111
- if (!bucket) continue;
1112
- for (const id of Object.keys(bucket)) {
1113
- const p = bucket[id];
1114
- if (p && Object.keys(p).length > 0) patchedEntities.push({ type, id });
1658
+ return current;
1659
+ }
1660
+ function setValueAtPath(source, path, value) {
1661
+ const segments = path.split(".").filter(Boolean);
1662
+ if (segments.length === 0) return source;
1663
+ const clone = structuredClone(source);
1664
+ let current = clone;
1665
+ for (let index = 0; index < segments.length - 1; index += 1) {
1666
+ const segment = segments[index];
1667
+ const next = current[segment];
1668
+ if (!isObject(next)) current[segment] = {};
1669
+ current = current[segment];
1670
+ }
1671
+ current[segments[segments.length - 1]] = value;
1672
+ return clone;
1673
+ }
1674
+ function collectDirtyPaths(current, original, prefix = "", acc = /* @__PURE__ */ new Set()) {
1675
+ if (isObject(current) && isObject(original)) {
1676
+ const keys = /* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(original)]);
1677
+ for (const key of keys) {
1678
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
1679
+ collectDirtyPaths(current[key], original[key], nextPrefix, acc);
1115
1680
  }
1681
+ return acc;
1116
1682
  }
1117
- const staleEntities = [];
1118
- const fetchingEntities = [];
1119
- for (const key of Object.keys(entityStates)) {
1120
- const colon = key.indexOf(":");
1121
- if (colon === -1) continue;
1122
- const type = key.slice(0, colon);
1123
- const id = key.slice(colon + 1);
1124
- const es = entityStates[key];
1125
- if (es.stale) staleEntities.push({ type, id });
1126
- if (es.isFetching) fetchingEntities.push({ type, id });
1683
+ if (JSON.stringify(current) !== JSON.stringify(original) && prefix) acc.add(prefix);
1684
+ return acc;
1685
+ }
1686
+ var schemaRegistry = /* @__PURE__ */ new Map();
1687
+ function registerEntityJsonSchema(config) {
1688
+ const key = registryKey(config.entityType, config.field, config.schemaId);
1689
+ schemaRegistry.set(key, config);
1690
+ }
1691
+ function registerRuntimeSchema(config) {
1692
+ registerEntityJsonSchema(config);
1693
+ }
1694
+ function getEntityJsonSchema(opts) {
1695
+ const exact = schemaRegistry.get(registryKey(opts.entityType, opts.field, opts.schemaId));
1696
+ if (exact) return exact;
1697
+ if (opts.field) {
1698
+ const byField = schemaRegistry.get(registryKey(opts.entityType, opts.field));
1699
+ if (byField) return byField;
1700
+ }
1701
+ if (opts.schemaId) {
1702
+ const byId = schemaRegistry.get(registryKey(opts.entityType, void 0, opts.schemaId));
1703
+ if (byId) return byId;
1127
1704
  }
1128
- const lists = listKeys.map((key) => ({
1129
- key,
1130
- idCount: listsState[key]?.ids.length ?? 0,
1131
- isFetching: Boolean(listsState[key]?.isFetching || listsState[key]?.isFetchingMore),
1132
- isStale: Boolean(listsState[key]?.stale)
1133
- }));
1134
- return {
1135
- entityCounts,
1136
- totalEntities,
1137
- listCount,
1138
- patchedEntities,
1139
- staleEntities,
1140
- fetchingEntities,
1141
- lists
1142
- };
1705
+ for (const schema of schemaRegistry.values()) {
1706
+ if (schema.entityType !== opts.entityType) continue;
1707
+ if (opts.field && schema.field !== opts.field) continue;
1708
+ return schema;
1709
+ }
1710
+ return null;
1143
1711
  }
1144
- function subscriberCountServerSnapshot() {
1145
- return 0;
1712
+ function useSchemaEntityFields(opts) {
1713
+ return useMemo(() => {
1714
+ const schema = opts.schema ?? getEntityJsonSchema(opts)?.schema;
1715
+ if (!schema) return [];
1716
+ return buildEntityFieldsFromSchema({ schema, rootField: opts.rootField ?? opts.field });
1717
+ }, [opts.entityType, opts.field, opts.rootField, opts.schemaId, opts.schema]);
1146
1718
  }
1147
- function useGraphDevTools() {
1148
- const subscriberCount = useSyncExternalStore(
1149
- subscribeSubscriberStats,
1150
- getActiveSubscriberCount,
1151
- subscriberCountServerSnapshot
1152
- );
1153
- const entities = useStore(useGraphStore, (state) => state.entities);
1154
- const patches = useStore(useGraphStore, (state) => state.patches);
1155
- const entityStates = useStore(useGraphStore, (state) => state.entityStates);
1156
- const listsState = useStore(useGraphStore, (state) => state.lists);
1157
- const graphPart = useMemo(
1158
- () => collectGraphDevStats(entities, patches, entityStates, listsState),
1159
- [entities, patches, entityStates, listsState]
1160
- );
1161
- return { ...graphPart, subscriberCount };
1719
+ function buildEntityFieldsFromSchema(opts) {
1720
+ return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
1162
1721
  }
1163
- var listenersAttached = false;
1164
- function ensureListeners() {
1165
- if (!listenersAttached) {
1166
- attachGlobalListeners();
1167
- listenersAttached = true;
1168
- }
1722
+ function exportGraphSnapshotWithSchemas(opts) {
1723
+ return JSON.stringify(
1724
+ {
1725
+ scope: opts.scope,
1726
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1727
+ data: opts.data,
1728
+ schemas: opts.schemas.filter(Boolean)
1729
+ },
1730
+ null,
1731
+ opts.pretty === false ? 0 : 2
1732
+ );
1169
1733
  }
1170
- function useEntity(opts) {
1171
- const { type, id, staleTime = getEngineOptions().defaultStaleTime, enabled = true } = opts;
1172
- ensureListeners();
1173
- const fetchRef = useRef(opts.fetch);
1174
- fetchRef.current = opts.fetch;
1175
- const normalizeRef = useRef(opts.normalize);
1176
- normalizeRef.current = opts.normalize;
1177
- const dataSelector = useCallback((state) => {
1178
- if (!id) return null;
1179
- return state.readEntitySnapshot(type, id);
1180
- }, [id, type]);
1181
- const data = useStore(useGraphStore, useShallow(dataSelector));
1182
- const entityState = useStore(useGraphStore, useCallback(
1183
- (state) => state.entityStates[`${type}:${id}`] ?? EMPTY_ENTITY_STATE,
1184
- [type, id]
1185
- ));
1186
- const doFetch = useCallback(() => {
1187
- if (!id || !enabled) return;
1188
- fetchEntity({ type, id, fetch: fetchRef.current, normalize: normalizeRef.current }, getEngineOptions());
1189
- }, [id, enabled, type]);
1190
- useEffect(() => {
1191
- if (!id || !enabled) return;
1192
- const token = registerSubscriber(`${type}:${id}`);
1193
- const state = useGraphStore.getState();
1194
- const existingState = state.entityStates[`${type}:${id}`];
1195
- const hasData = !!state.entities[type]?.[id];
1196
- const isStale = !existingState?.lastFetched || existingState.stale || Date.now() - (existingState.lastFetched ?? 0) > staleTime;
1197
- if (!hasData || isStale) doFetch();
1198
- return () => unregisterSubscriber(`${type}:${id}`, token);
1199
- }, [id, type, enabled, staleTime, doFetch]);
1200
- useEffect(() => {
1201
- if (entityState.stale && id && enabled && !entityState.isFetching) doFetch();
1202
- }, [entityState.stale, id, enabled, entityState.isFetching, doFetch]);
1203
- return { data, isLoading: !data && entityState.isFetching, isFetching: entityState.isFetching, error: entityState.error, isStale: entityState.stale, refetch: doFetch };
1734
+ function escapeHtml(input) {
1735
+ return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1204
1736
  }
1205
- function useEntityList(opts) {
1206
- const { type, queryKey, staleTime = getEngineOptions().defaultStaleTime, enabled = true, mode = "replace" } = opts;
1207
- ensureListeners();
1208
- const key = useMemo(() => serializeKey(queryKey), [queryKey]);
1209
- const fetchRef = useRef(opts.fetch);
1210
- fetchRef.current = opts.fetch;
1211
- const normalizeRef = useRef(opts.normalize);
1212
- normalizeRef.current = opts.normalize;
1213
- const listState = useStore(useGraphStore, useCallback((state) => state.lists[key] ?? EMPTY_LIST_STATE, [key]));
1214
- const itemsSelector = useCallback((state) => {
1215
- const ids = state.lists[key]?.ids ?? EMPTY_IDS;
1216
- return ids.map((id) => state.readEntitySnapshot(type, id)).filter((x) => x !== null);
1217
- }, [key, type]);
1218
- const items = useStore(useGraphStore, useShallow(itemsSelector));
1219
- const doFetch = useCallback((params = {}) => {
1220
- if (!enabled) return;
1221
- fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, params, getEngineOptions(), false);
1222
- }, [enabled, type, queryKey, mode]);
1223
- const fetchNextPage = useCallback(() => {
1224
- if (!listState.hasNextPage || listState.isFetchingMore || !enabled) return;
1225
- fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, { cursor: listState.nextCursor ?? void 0, page: (listState.currentPage ?? 0) + 1, pageSize: listState.pageSize ?? void 0 }, getEngineOptions(), true);
1226
- }, [listState.hasNextPage, listState.isFetchingMore, listState.nextCursor, listState.currentPage, listState.pageSize, enabled, type, queryKey, mode]);
1227
- useEffect(() => {
1228
- if (!enabled) return;
1229
- const state = useGraphStore.getState();
1230
- const existing = state.lists[key];
1231
- const isStale = !existing?.lastFetched || existing.stale || Date.now() - (existing.lastFetched ?? 0) > staleTime;
1232
- if (!existing || isStale) doFetch({ page: 1, pageSize: listState.pageSize ?? void 0 });
1233
- }, [key, enabled, staleTime, doFetch, listState.pageSize]);
1234
- useEffect(() => {
1235
- if (listState.stale && enabled && !listState.isFetching) doFetch();
1236
- }, [listState.stale, enabled, listState.isFetching, doFetch]);
1237
- return { items, ids: listState.ids, isLoading: listState.ids.length === 0 && listState.isFetching, isFetching: listState.isFetching, isFetchingMore: listState.isFetchingMore, error: listState.error, hasNextPage: listState.hasNextPage, hasPrevPage: listState.hasPrevPage, total: listState.total, currentPage: listState.currentPage, fetchNextPage, refetch: doFetch };
1737
+ function renderMarkdownToHtml(value) {
1738
+ const escaped = escapeHtml(value);
1739
+ const blocks = escaped.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
1740
+ return blocks.map((block) => renderMarkdownBlock(block)).join("");
1238
1741
  }
1239
- function useEntityMutation(opts) {
1240
- const [state, setState] = useState({ isPending: false, isSuccess: false, isError: false, error: null });
1241
- const optsRef = useRef(opts);
1242
- optsRef.current = opts;
1243
- const mutate = useCallback(async (input) => {
1244
- const { type, mutate: apiFn, normalize, optimistic, invalidateLists, invalidateEntities, onSuccess, onError } = optsRef.current;
1245
- setState({ isPending: true, isSuccess: false, isError: false, error: null });
1246
- let rollback = null;
1247
- if (optimistic) {
1248
- const opt = optimistic(input);
1249
- if (opt) {
1250
- const { id, patch } = opt;
1251
- const store = useGraphStore.getState();
1252
- const previous = { ...store.patches[type]?.[id] };
1253
- const previousSync = store.syncMetadata[`${type}:${id}`];
1254
- store.patchEntity(type, id, patch);
1255
- store.setEntitySyncMetadata(type, id, { synced: false, origin: "optimistic", updatedAt: Date.now() });
1256
- rollback = () => {
1257
- const currentStore = useGraphStore.getState();
1258
- if (Object.keys(previous).length > 0) currentStore.patchEntity(type, id, previous);
1259
- else currentStore.clearPatch(type, id);
1260
- if (previousSync) currentStore.setEntitySyncMetadata(type, id, previousSync);
1261
- else currentStore.clearEntitySyncMetadata(type, id);
1262
- };
1263
- }
1264
- }
1265
- try {
1266
- const result = await apiFn(input);
1267
- if (normalize) {
1268
- const { id, data } = normalize(result, input);
1269
- const store = useGraphStore.getState();
1270
- store.upsertEntity(type, id, data);
1271
- store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
1272
- if (optimistic) {
1273
- const opt = optimistic(input);
1274
- if (opt) store.clearPatch(type, opt.id);
1275
- }
1276
- }
1277
- if (invalidateLists) for (const k of invalidateLists) useGraphStore.getState().invalidateLists(k);
1278
- if (invalidateEntities) for (const { type: t, id } of invalidateEntities) useGraphStore.getState().invalidateEntity(t, id);
1279
- setState({ isPending: false, isSuccess: true, isError: false, error: null });
1280
- onSuccess?.(result, input);
1281
- return result;
1282
- } catch (err) {
1283
- const error = err instanceof Error ? err : new Error(String(err));
1284
- rollback?.();
1285
- setState({ isPending: false, isSuccess: false, isError: true, error: error.message });
1286
- onError?.(error, input);
1287
- return null;
1742
+ function MarkdownFieldRenderer({ value, className }) {
1743
+ return /* @__PURE__ */ jsx(
1744
+ "div",
1745
+ {
1746
+ className,
1747
+ dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
1288
1748
  }
1289
- }, []);
1290
- const trigger = useCallback((input) => {
1291
- void mutate(input);
1292
- }, [mutate]);
1293
- const reset = useCallback(() => setState({ isPending: false, isSuccess: false, isError: false, error: null }), []);
1294
- return { mutate, trigger, reset, state };
1295
- }
1296
- function useEntityAugment(type, id) {
1297
- const patch = useStore(useGraphStore, useCallback((state) => id ? state.patches[type]?.[id] ?? null : null, [type, id]));
1298
- const augment = useCallback((fields) => {
1299
- if (!id) return;
1300
- useGraphStore.getState().patchEntity(type, id, fields);
1301
- }, [type, id]);
1302
- const unaugment = useCallback((keys) => {
1303
- if (!id) return;
1304
- useGraphStore.getState().unpatchEntity(type, id, keys);
1305
- }, [type, id]);
1306
- const clear = useCallback(() => {
1307
- if (!id) return;
1308
- useGraphStore.getState().clearPatch(type, id);
1309
- }, [type, id]);
1310
- return { patch, augment, unaugment, clear };
1749
+ );
1311
1750
  }
1312
- var suspenseEntityPromises = /* @__PURE__ */ new Map();
1313
- var suspenseListPromises = /* @__PURE__ */ new Map();
1314
- function getEntitySuspensePromise(type, id) {
1315
- const key = `${type}:${id}`;
1316
- const existing = suspenseEntityPromises.get(key);
1317
- if (existing) return existing;
1318
- let unsub = null;
1319
- let settled = false;
1320
- const promise = new Promise((resolve, reject) => {
1321
- const settle = (fn) => {
1322
- if (settled) return;
1323
- settled = true;
1324
- unsub?.();
1325
- unsub = null;
1326
- fn();
1327
- };
1328
- const inspect = (state) => {
1329
- if (settled) return;
1330
- const hasData = !!state.entities[type]?.[id];
1331
- const es = state.entityStates[key];
1332
- if (hasData) settle(() => resolve());
1333
- else if (es != null && es.error != null && !es.isFetching) {
1334
- const msg = es.error;
1335
- settle(() => reject(new Error(msg)));
1336
- }
1337
- };
1338
- inspect(useGraphStore.getState());
1339
- if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
1340
- });
1341
- const tracked = promise.finally(() => {
1342
- suspenseEntityPromises.delete(key);
1343
- });
1344
- suspenseEntityPromises.set(key, tracked);
1345
- return tracked;
1751
+ function MarkdownFieldEditor({
1752
+ value,
1753
+ onChange,
1754
+ placeholder
1755
+ }) {
1756
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1757
+ /* @__PURE__ */ jsx(
1758
+ "textarea",
1759
+ {
1760
+ value,
1761
+ onChange: (event) => onChange(event.target.value),
1762
+ placeholder,
1763
+ className: "w-full min-h-[120px] rounded-md border bg-muted/50 px-3 py-2 text-sm resize-y focus:outline-none focus:ring-1 focus:ring-ring transition-colors"
1764
+ }
1765
+ ),
1766
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border bg-background px-3 py-2", children: /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value, className: "prose prose-sm max-w-none" }) })
1767
+ ] });
1346
1768
  }
1347
- function getListSuspensePromise(listKey) {
1348
- const existing = suspenseListPromises.get(listKey);
1349
- if (existing) return existing;
1350
- let unsub = null;
1351
- let settled = false;
1352
- const promise = new Promise((resolve, reject) => {
1353
- const settle = (fn) => {
1354
- if (settled) return;
1355
- settled = true;
1356
- unsub?.();
1357
- unsub = null;
1358
- fn();
1359
- };
1360
- const inspect = (state) => {
1361
- if (settled) return;
1362
- const list = state.lists[listKey] ?? EMPTY_LIST_STATE;
1363
- if (list.ids.length > 0) settle(() => resolve());
1364
- else if (list.error != null && !list.isFetching) {
1365
- const msg = list.error;
1366
- settle(() => reject(new Error(msg)));
1367
- } else if (list.ids.length === 0 && !list.isFetching && list.lastFetched != null) settle(() => resolve());
1368
- };
1369
- inspect(useGraphStore.getState());
1370
- if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
1371
- });
1372
- const tracked = promise.finally(() => {
1373
- suspenseListPromises.delete(listKey);
1374
- });
1375
- suspenseListPromises.set(listKey, tracked);
1376
- return tracked;
1769
+ function createMarkdownDetailRenderer(field) {
1770
+ return (value, entity) => /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value: String(value ?? getValueAtPath(entity, field) ?? ""), className: "prose prose-sm max-w-none" });
1377
1771
  }
1378
- function useSuspenseEntity(opts) {
1379
- const result = useEntity(opts);
1380
- const { type, id } = opts;
1381
- if (result.isLoading) {
1382
- if (!id) throw new Error("useSuspenseEntity requires a non-null entity id");
1383
- throw getEntitySuspensePromise(type, id);
1772
+ function buildSchemaFields(schema, pathPrefix, schemaPathPrefix) {
1773
+ if (schema.type === "object" && schema.properties) {
1774
+ const entries = Object.entries(schema.properties).sort(([, left], [, right]) => {
1775
+ const l = left["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
1776
+ const r = right["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
1777
+ return l - r;
1778
+ });
1779
+ return entries.flatMap(([key, childSchema]) => {
1780
+ if (childSchema["x-hidden"]) return [];
1781
+ const field = pathPrefix ? `${pathPrefix}.${key}` : key;
1782
+ const schemaPath = schemaPathPrefix ? `${schemaPathPrefix}.${key}` : key;
1783
+ if (childSchema.type === "object" && childSchema.properties) {
1784
+ return buildSchemaFields(childSchema, field, schemaPath);
1785
+ }
1786
+ return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
1787
+ });
1384
1788
  }
1385
- if (result.error != null && result.data == null) {
1386
- throw new Error(result.error);
1789
+ return [];
1790
+ }
1791
+ function schemaField(field, schemaPath, schema, required) {
1792
+ const type = inferFieldType(schema);
1793
+ const descriptor = {
1794
+ field,
1795
+ label: schema.title ?? humanize(field.split(".").pop() ?? field),
1796
+ type,
1797
+ required,
1798
+ hint: schema.description,
1799
+ schemaPath,
1800
+ schema,
1801
+ componentHint: schema["x-a2ui-component"]
1802
+ };
1803
+ if (schema.enum) {
1804
+ descriptor.options = schema.enum.map((value) => ({
1805
+ value: String(value),
1806
+ label: String(value)
1807
+ }));
1387
1808
  }
1388
- if (result.data == null) {
1389
- throw new Error(!id ? "useSuspenseEntity requires a non-null entity id" : "Entity not found");
1809
+ if (type === "markdown") {
1810
+ descriptor.render = createMarkdownDetailRenderer(field);
1390
1811
  }
1391
- return {
1392
- data: result.data,
1393
- isFetching: result.isFetching,
1394
- isStale: result.isStale,
1395
- refetch: result.refetch
1396
- };
1812
+ return descriptor;
1397
1813
  }
1398
- function useSuspenseEntityList(opts) {
1399
- const key = useMemo(() => serializeKey(opts.queryKey), [opts.queryKey]);
1400
- const result = useEntityList(opts);
1401
- if (result.isLoading) throw getListSuspensePromise(key);
1402
- if (result.error != null && result.items.length === 0) {
1403
- throw new Error(result.error);
1814
+ function inferFieldType(schema) {
1815
+ const forced = schema["x-field-type"];
1816
+ if (forced === "markdown") return "markdown";
1817
+ if (schema.format === "markdown") return "markdown";
1818
+ if (schema.enum) return "enum";
1819
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
1820
+ switch (type) {
1821
+ case "boolean":
1822
+ return "boolean";
1823
+ case "integer":
1824
+ case "number":
1825
+ return "number";
1826
+ case "string":
1827
+ if (schema.format === "email") return "email";
1828
+ if (schema.format === "uri" || schema.format === "url") return "url";
1829
+ if (schema.format === "date" || schema.format === "date-time") return "date";
1830
+ return "text";
1831
+ case "array":
1832
+ case "object":
1833
+ return "json";
1834
+ default:
1835
+ return "text";
1404
1836
  }
1405
- const { isLoading: _isLoading, ...rest } = result;
1406
- return rest;
1407
1837
  }
1408
-
1409
- // src/view/evaluator.ts
1410
- function matchesFilter(entity, filter) {
1411
- if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
1412
- return matchesGroup(entity, filter);
1838
+ function registryKey(entityType, field, schemaId) {
1839
+ return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
1413
1840
  }
1414
- function matchesGroup(entity, group) {
1415
- const { logic, clauses } = group;
1416
- if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
1417
- return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
1841
+ function humanize(value) {
1842
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
1418
1843
  }
1419
- function matchesClause(entity, clause) {
1420
- const { field, op, value, predicate } = clause;
1421
- const fv = getNestedValue(entity, field);
1422
- switch (op) {
1423
- case "eq":
1424
- return fv === value;
1425
- case "neq":
1426
- return fv !== value;
1427
- case "gt":
1428
- return fv > value;
1429
- case "gte":
1430
- return fv >= value;
1431
- case "lt":
1432
- return fv < value;
1433
- case "lte":
1434
- return fv <= value;
1435
- case "in":
1436
- return Array.isArray(value) && value.includes(fv);
1437
- case "nin":
1438
- return Array.isArray(value) && !value.includes(fv);
1439
- case "isNull":
1440
- return fv == null;
1441
- case "isNotNull":
1442
- return fv != null;
1443
- case "contains":
1444
- return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
1445
- case "startsWith":
1446
- return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
1447
- case "endsWith":
1448
- return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
1449
- case "between": {
1450
- const [lo, hi] = value;
1451
- return fv >= lo && fv <= hi;
1452
- }
1453
- case "arrayContains":
1454
- return Array.isArray(fv) && fv.includes(value);
1455
- case "arrayOverlaps":
1456
- return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
1457
- case "matches":
1458
- return typeof fv === "string" && new RegExp(value).test(fv);
1459
- case "custom":
1460
- return predicate ? predicate(fv, entity) : true;
1461
- default:
1462
- return true;
1463
- }
1844
+ function renderMarkdownBlock(block) {
1845
+ if (block.startsWith("# ")) return `<h1>${renderInlineMarkdown(block.slice(2))}</h1>`;
1846
+ if (block.startsWith("## ")) return `<h2>${renderInlineMarkdown(block.slice(3))}</h2>`;
1847
+ return `<p>${renderInlineMarkdown(block).replaceAll("\n", "<br/>")}</p>`;
1464
1848
  }
1465
- function matchesSearch(entity, query, fields) {
1466
- if (!query.trim()) return true;
1467
- const lq = query.toLowerCase();
1468
- return fields.some((field) => {
1469
- const v = getNestedValue(entity, field);
1470
- return typeof v === "string" && v.toLowerCase().includes(lq);
1471
- });
1849
+ function renderInlineMarkdown(block) {
1850
+ return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
1472
1851
  }
1473
- function compareEntities(a, b, sort) {
1474
- for (const clause of sort) {
1475
- const r = compareByClause(a, b, clause);
1476
- if (r !== 0) return r;
1477
- }
1478
- return 0;
1852
+
1853
+ // src/ai-interop.ts
1854
+ function exportGraphSnapshot(opts) {
1855
+ const payload = {
1856
+ scope: opts.scope,
1857
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1858
+ data: opts.data
1859
+ };
1860
+ return JSON.stringify(payload, null, opts.pretty === false ? 0 : 2);
1479
1861
  }
1480
- function compareByClause(a, b, clause) {
1481
- const { field, direction, nulls = "last", comparator } = clause;
1482
- const av = getNestedValue(a, field);
1483
- const bv = getNestedValue(b, field);
1484
- const aNull = av == null;
1485
- const bNull = bv == null;
1486
- if (aNull && bNull) return 0;
1487
- if (aNull) return nulls === "first" ? -1 : 1;
1488
- if (bNull) return nulls === "first" ? 1 : -1;
1489
- let cmp;
1490
- if (comparator) cmp = comparator(av, bv);
1491
- else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
1492
- else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
1493
- else cmp = String(av).localeCompare(String(bv));
1494
- return direction === "desc" ? -cmp : cmp;
1862
+ function createGraphTool(handler) {
1863
+ return (input) => handler(input, {
1864
+ store: useGraphStore.getState(),
1865
+ queryOnce,
1866
+ exportGraphSnapshot
1867
+ });
1495
1868
  }
1496
- function findInsertionIndex(entity, sortedIds, getEntity, sort) {
1497
- let lo = 0;
1498
- let hi = sortedIds.length;
1499
- while (lo < hi) {
1500
- const mid = lo + hi >>> 1;
1501
- const me = getEntity(sortedIds[mid]);
1502
- if (!me) {
1503
- lo = mid + 1;
1504
- continue;
1869
+ function createSchemaGraphTool(handler) {
1870
+ return (input) => handler(input, {
1871
+ store: useGraphStore.getState(),
1872
+ queryOnce,
1873
+ exportGraphSnapshot,
1874
+ getEntityJsonSchema,
1875
+ exportGraphSnapshotWithSchemas
1876
+ });
1877
+ }
1878
+ var DEFAULT_STORAGE_KEY = "prometheus:graph";
1879
+ var useGraphSyncStatusStore = create((set) => ({
1880
+ status: {
1881
+ phase: "idle",
1882
+ isOnline: true,
1883
+ isSynced: true,
1884
+ pendingActions: 0,
1885
+ lastHydratedAt: null,
1886
+ lastPersistedAt: null,
1887
+ storageKey: null,
1888
+ error: null
1889
+ },
1890
+ setStatus: (status) => set((state) => ({
1891
+ status: {
1892
+ ...state.status,
1893
+ ...status
1505
1894
  }
1506
- if (compareEntities(entity, me, sort) <= 0) hi = mid;
1507
- else lo = mid + 1;
1508
- }
1509
- return lo;
1895
+ }))
1896
+ }));
1897
+ var pendingActions = /* @__PURE__ */ new Map();
1898
+ function useGraphSyncStatus() {
1899
+ return useGraphSyncStatusStore((state) => state.status);
1510
1900
  }
1511
- function applyView(ids, getEntity, filter, sort, search) {
1512
- let entries = [];
1513
- for (const id of ids) {
1514
- const entity = getEntity(id);
1515
- if (!entity) continue;
1516
- entries.push({ id, entity });
1901
+ async function persistGraphToStorage(opts) {
1902
+ const payload = {
1903
+ version: 1,
1904
+ snapshot: cloneGraphSnapshot(),
1905
+ pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
1906
+ };
1907
+ const json = JSON.stringify(payload);
1908
+ await opts.storage.set(opts.key, json);
1909
+ const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
1910
+ useGraphSyncStatusStore.getState().setStatus({
1911
+ lastPersistedAt: persistedAt,
1912
+ storageKey: opts.key,
1913
+ pendingActions: payload.pendingActions.length
1914
+ });
1915
+ return {
1916
+ ok: true,
1917
+ key: opts.key,
1918
+ bytes: json.length,
1919
+ persistedAt
1920
+ };
1921
+ }
1922
+ async function hydrateGraphFromStorage(opts) {
1923
+ const raw = await opts.storage.get(opts.key);
1924
+ if (!raw) {
1925
+ return {
1926
+ ok: false,
1927
+ key: opts.key,
1928
+ hydratedAt: null,
1929
+ entityCounts: {},
1930
+ error: "No persisted graph snapshot found"
1931
+ };
1932
+ }
1933
+ try {
1934
+ const parsed = JSON.parse(raw);
1935
+ useGraphStore.setState(parsed.snapshot);
1936
+ pendingActions.clear();
1937
+ for (const action of parsed.pendingActions ?? []) pendingActions.set(action.id, action);
1938
+ const hydratedAt = (/* @__PURE__ */ new Date()).toISOString();
1939
+ useGraphSyncStatusStore.getState().setStatus({
1940
+ lastHydratedAt: hydratedAt,
1941
+ storageKey: opts.key,
1942
+ pendingActions: pendingActions.size,
1943
+ error: null
1944
+ });
1945
+ return {
1946
+ ok: true,
1947
+ key: opts.key,
1948
+ hydratedAt,
1949
+ entityCounts: Object.fromEntries(
1950
+ Object.entries(parsed.snapshot.entities).map(([type, entities]) => [type, Object.keys(entities).length])
1951
+ ),
1952
+ pendingActions: Array.from(pendingActions.values())
1953
+ };
1954
+ } catch (error) {
1955
+ const message = error instanceof Error ? error.message : String(error);
1956
+ useGraphSyncStatusStore.getState().setStatus({
1957
+ phase: "error",
1958
+ error: message,
1959
+ storageKey: opts.key
1960
+ });
1961
+ return {
1962
+ ok: false,
1963
+ key: opts.key,
1964
+ hydratedAt: null,
1965
+ entityCounts: {},
1966
+ error: message
1967
+ };
1517
1968
  }
1518
- if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
1519
- if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
1520
- if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
1521
- return entries.map((e) => e.id);
1522
1969
  }
1523
- function checkCompleteness(loadedCount, total, hasNextPage) {
1524
- if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
1525
- if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
1526
- return { isComplete: true, reason: "no-more-pages" };
1970
+ function startLocalFirstGraph(opts) {
1971
+ const key = opts.key ?? DEFAULT_STORAGE_KEY;
1972
+ const persistDebounceMs = opts.persistDebounceMs ?? 50;
1973
+ const statusStore = useGraphSyncStatusStore.getState();
1974
+ statusStore.setStatus({
1975
+ phase: "hydrating",
1976
+ storageKey: key,
1977
+ isOnline: opts.onlineSource?.getIsOnline() ?? getDefaultOnlineSource().getIsOnline(),
1978
+ isSynced: pendingActions.size === 0,
1979
+ error: null
1980
+ });
1981
+ let persistTimer = null;
1982
+ const schedulePersist = () => {
1983
+ if (persistTimer) clearTimeout(persistTimer);
1984
+ persistTimer = setTimeout(() => {
1985
+ void persistGraphToStorage({ storage: opts.storage, key });
1986
+ }, persistDebounceMs);
1987
+ };
1988
+ const graphUnsub = useGraphStore.subscribe(() => {
1989
+ schedulePersist();
1990
+ });
1991
+ const actionUnsub = subscribeGraphActionEvents((event) => {
1992
+ if (event.type === "enqueued") pendingActions.set(event.record.id, event.record);
1993
+ if (event.type === "settled") pendingActions.delete(event.record.id);
1994
+ useGraphSyncStatusStore.getState().setStatus({
1995
+ pendingActions: pendingActions.size,
1996
+ isSynced: pendingActions.size === 0
1997
+ });
1998
+ schedulePersist();
1999
+ });
2000
+ const onlineSource = opts.onlineSource ?? getDefaultOnlineSource();
2001
+ const onlineUnsub = onlineSource.subscribe((online) => {
2002
+ useGraphSyncStatusStore.getState().setStatus({
2003
+ isOnline: online,
2004
+ phase: online ? "ready" : "offline"
2005
+ });
2006
+ });
2007
+ const ready = (async () => {
2008
+ const hydrated = await hydrateGraphFromStorage({ storage: opts.storage, key });
2009
+ if (opts.replayPendingActions && hydrated.ok && pendingActions.size > 0) {
2010
+ useGraphSyncStatusStore.getState().setStatus({
2011
+ phase: "syncing",
2012
+ isSynced: false
2013
+ });
2014
+ const policy = resolveRetryPolicy(opts.retryPolicy);
2015
+ for (const action of Array.from(pendingActions.values())) {
2016
+ await replayActionWithRetry(action, policy);
2017
+ pendingActions.delete(action.id);
2018
+ }
2019
+ await persistGraphToStorage({ storage: opts.storage, key });
2020
+ }
2021
+ const online = onlineSource.getIsOnline();
2022
+ useGraphSyncStatusStore.getState().setStatus({
2023
+ phase: online ? "ready" : "offline",
2024
+ isOnline: online,
2025
+ isSynced: pendingActions.size === 0,
2026
+ pendingActions: pendingActions.size
2027
+ });
2028
+ })();
2029
+ return {
2030
+ ready,
2031
+ dispose() {
2032
+ graphUnsub();
2033
+ actionUnsub();
2034
+ onlineUnsub();
2035
+ if (persistTimer) clearTimeout(persistTimer);
2036
+ },
2037
+ async persistNow() {
2038
+ await persistGraphToStorage({ storage: opts.storage, key });
2039
+ },
2040
+ hydrate() {
2041
+ return hydrateGraphFromStorage({ storage: opts.storage, key });
2042
+ },
2043
+ getStatus() {
2044
+ return useGraphSyncStatusStore.getState().status;
2045
+ }
2046
+ };
1527
2047
  }
1528
- function getNestedValue(obj, path) {
1529
- const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
1530
- let current = obj;
1531
- for (const part of parts) {
1532
- if (current == null || typeof current !== "object") return void 0;
1533
- current = current[part];
1534
- }
1535
- return current;
2048
+ function cloneGraphSnapshot() {
2049
+ const state = useGraphStore.getState();
2050
+ return {
2051
+ entities: structuredClone(state.entities),
2052
+ patches: structuredClone(state.patches),
2053
+ entityStates: structuredClone(state.entityStates),
2054
+ syncMetadata: structuredClone(state.syncMetadata),
2055
+ lists: structuredClone(state.lists)
2056
+ };
1536
2057
  }
1537
-
1538
- // src/view/prisma-compile.ts
1539
- function nestWhereField(parts, leaf) {
1540
- if (parts.length === 0) return {};
1541
- if (parts.length === 1) return { [parts[0]]: leaf };
1542
- return { [parts[0]]: nestWhereField(parts.slice(1), leaf) };
2058
+ function resolveRetryPolicy(policy) {
2059
+ return {
2060
+ maxAttempts: policy?.maxAttempts ?? 5,
2061
+ initialDelayMs: policy?.initialDelayMs ?? 500,
2062
+ maxDelayMs: policy?.maxDelayMs ?? 3e4,
2063
+ backoffFactor: policy?.backoffFactor ?? 2,
2064
+ jitter: policy?.jitter ?? "equal",
2065
+ poisonHandler: policy?.poisonHandler
2066
+ };
1543
2067
  }
1544
- function clauseToPrismaLeaf(c) {
1545
- switch (c.op) {
1546
- case "eq":
1547
- return { equals: c.value };
1548
- case "neq":
1549
- return { not: c.value };
1550
- case "gt":
1551
- return { gt: c.value };
1552
- case "gte":
1553
- return { gte: c.value };
1554
- case "lt":
1555
- return { lt: c.value };
1556
- case "lte":
1557
- return { lte: c.value };
1558
- case "contains":
1559
- return { contains: c.value, mode: "insensitive" };
1560
- case "startsWith":
1561
- return { startsWith: c.value, mode: "insensitive" };
1562
- case "endsWith":
1563
- return { endsWith: c.value, mode: "insensitive" };
1564
- case "in":
1565
- return { in: c.value };
1566
- case "nin":
1567
- return { notIn: c.value };
1568
- case "arrayContains":
1569
- return { has: c.value };
1570
- case "between":
1571
- case "arrayOverlaps":
1572
- case "matches":
1573
- case "custom":
2068
+ function computeDelay(policy, attempt) {
2069
+ const base = Math.min(
2070
+ policy.initialDelayMs * Math.pow(policy.backoffFactor, Math.max(0, attempt - 1)),
2071
+ policy.maxDelayMs
2072
+ );
2073
+ switch (policy.jitter) {
2074
+ case "none":
2075
+ return base;
2076
+ case "full":
2077
+ return Math.random() * base;
2078
+ case "equal":
1574
2079
  default:
1575
- return null;
2080
+ return base / 2 + Math.random() * (base / 2);
1576
2081
  }
1577
2082
  }
1578
- function clauseToPrismaEntry(c) {
1579
- const parts = c.field.split(".").filter(Boolean);
1580
- if (parts.length === 0) return null;
1581
- if (c.op === "isNull") {
1582
- const equalsNull = c.value === void 0 || c.value === true;
1583
- return nestWhereField(parts, equalsNull ? null : { not: null });
1584
- }
1585
- if (c.op === "isNotNull") {
1586
- return nestWhereField(parts, { not: null });
1587
- }
1588
- const leaf = clauseToPrismaLeaf(c);
1589
- if (leaf === null) return null;
1590
- return nestWhereField(parts, leaf);
2083
+ function sleep4(ms) {
2084
+ if (ms <= 0) return Promise.resolve();
2085
+ return new Promise((r) => setTimeout(r, ms));
1591
2086
  }
1592
- function groupToPrismaWhere(g) {
1593
- const parts = [];
1594
- for (const item of g.clauses) {
1595
- if ("logic" in item) {
1596
- const nested = groupToPrismaWhere(item);
1597
- if (Object.keys(nested).length > 0) parts.push(nested);
1598
- } else {
1599
- const entry = clauseToPrismaEntry(item);
1600
- if (entry) parts.push(entry);
2087
+ async function replayActionWithRetry(action, policy) {
2088
+ let lastError = null;
2089
+ for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
2090
+ try {
2091
+ await replayRegisteredGraphAction(action);
2092
+ return { ok: true };
2093
+ } catch (error) {
2094
+ lastError = error;
2095
+ if (attempt >= policy.maxAttempts) break;
2096
+ await sleep4(computeDelay(policy, attempt));
1601
2097
  }
1602
2098
  }
1603
- if (parts.length === 0) return {};
1604
- if (parts.length === 1) return parts[0];
1605
- return g.logic === "or" ? { OR: parts } : { AND: parts };
2099
+ try {
2100
+ await policy.poisonHandler?.(action, lastError);
2101
+ } catch {
2102
+ }
2103
+ return { ok: false, poisoned: true, error: lastError };
1606
2104
  }
1607
- function toPrismaWhere(filter) {
1608
- if (Array.isArray(filter)) {
1609
- const parts = [];
1610
- for (const item of filter) {
1611
- const entry = clauseToPrismaEntry(item);
1612
- if (entry) parts.push(entry);
2105
+ function getDefaultOnlineSource() {
2106
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
2107
+ return {
2108
+ getIsOnline: () => window.navigator.onLine,
2109
+ subscribe: (listener) => {
2110
+ const onlineHandler = () => listener(true);
2111
+ const offlineHandler = () => listener(false);
2112
+ window.addEventListener("online", onlineHandler);
2113
+ window.addEventListener("offline", offlineHandler);
2114
+ return () => {
2115
+ window.removeEventListener("online", onlineHandler);
2116
+ window.removeEventListener("offline", offlineHandler);
2117
+ };
2118
+ }
2119
+ };
2120
+ }
2121
+ return {
2122
+ getIsOnline: () => true,
2123
+ subscribe: () => () => {
1613
2124
  }
1614
- if (parts.length === 0) return {};
1615
- if (parts.length === 1) return parts[0];
1616
- return { AND: parts };
2125
+ };
2126
+ }
2127
+ function collectGraphDevStats(entities, patches, entityStates, listsState) {
2128
+ const entityCounts = {};
2129
+ let totalEntities = 0;
2130
+ for (const type of Object.keys(entities)) {
2131
+ const bucket = entities[type];
2132
+ if (!bucket) continue;
2133
+ const n = Object.keys(bucket).length;
2134
+ if (n > 0) entityCounts[type] = n;
2135
+ totalEntities += n;
1617
2136
  }
1618
- return groupToPrismaWhere(filter);
2137
+ const listKeys = Object.keys(listsState);
2138
+ const listCount = listKeys.length;
2139
+ const patchedEntities = [];
2140
+ for (const type of Object.keys(patches)) {
2141
+ const bucket = patches[type];
2142
+ if (!bucket) continue;
2143
+ for (const id of Object.keys(bucket)) {
2144
+ const p = bucket[id];
2145
+ if (p && Object.keys(p).length > 0) patchedEntities.push({ type, id });
2146
+ }
2147
+ }
2148
+ const staleEntities = [];
2149
+ const fetchingEntities = [];
2150
+ for (const key of Object.keys(entityStates)) {
2151
+ const colon = key.indexOf(":");
2152
+ if (colon === -1) continue;
2153
+ const type = key.slice(0, colon);
2154
+ const id = key.slice(colon + 1);
2155
+ const es = entityStates[key];
2156
+ if (es.stale) staleEntities.push({ type, id });
2157
+ if (es.isFetching) fetchingEntities.push({ type, id });
2158
+ }
2159
+ const lists = listKeys.map((key) => ({
2160
+ key,
2161
+ idCount: listsState[key]?.ids.length ?? 0,
2162
+ isFetching: Boolean(listsState[key]?.isFetching || listsState[key]?.isFetchingMore),
2163
+ isStale: Boolean(listsState[key]?.stale)
2164
+ }));
2165
+ return {
2166
+ entityCounts,
2167
+ totalEntities,
2168
+ listCount,
2169
+ patchedEntities,
2170
+ staleEntities,
2171
+ fetchingEntities,
2172
+ lists
2173
+ };
1619
2174
  }
1620
- function toPrismaOrderBy(sort) {
1621
- return sort.map((s) => ({ [s.field]: s.direction }));
2175
+ function subscriberCountServerSnapshot() {
2176
+ return 0;
1622
2177
  }
1623
-
1624
- // src/view/types.ts
1625
- function toRestParams(view) {
1626
- const params = {};
1627
- if (view.filter) {
1628
- const clauses = flattenClauses(view.filter);
1629
- for (const c of clauses) {
1630
- if (c.op === "custom") continue;
1631
- const key = c.op === "eq" ? c.field : `${c.field}[${c.op}]`;
1632
- params[key] = Array.isArray(c.value) ? c.value.join(",") : String(c.value ?? "");
1633
- }
2178
+ function useGraphDevTools() {
2179
+ const subscriberCount = useSyncExternalStore(
2180
+ subscribeSubscriberStats,
2181
+ getActiveSubscriberCount,
2182
+ subscriberCountServerSnapshot
2183
+ );
2184
+ const entities = useStore(useGraphStore, (state) => state.entities);
2185
+ const patches = useStore(useGraphStore, (state) => state.patches);
2186
+ const entityStates = useStore(useGraphStore, (state) => state.entityStates);
2187
+ const listsState = useStore(useGraphStore, (state) => state.lists);
2188
+ const graphPart = useMemo(
2189
+ () => collectGraphDevStats(entities, patches, entityStates, listsState),
2190
+ [entities, patches, entityStates, listsState]
2191
+ );
2192
+ return { ...graphPart, subscriberCount };
2193
+ }
2194
+ var listenersAttached = false;
2195
+ function ensureListeners() {
2196
+ if (!listenersAttached) {
2197
+ attachGlobalListeners();
2198
+ listenersAttached = true;
1634
2199
  }
1635
- if (view.sort) params["sort"] = view.sort.map((s) => `${s.direction === "desc" ? "-" : ""}${s.field}`).join(",");
1636
- if (view.search?.query) params["q"] = view.search.query;
1637
- return params;
1638
2200
  }
1639
- function toSQLClauses(view) {
1640
- const params = [];
1641
- let paramIdx = 1;
1642
- function clauseToSQL(c) {
1643
- const col = `"${c.field}"`;
1644
- switch (c.op) {
1645
- case "eq":
1646
- params.push(c.value);
1647
- return `${col} = $${paramIdx++}`;
1648
- case "neq":
1649
- params.push(c.value);
1650
- return `${col} != $${paramIdx++}`;
1651
- case "gt":
1652
- params.push(c.value);
1653
- return `${col} > $${paramIdx++}`;
1654
- case "gte":
1655
- params.push(c.value);
1656
- return `${col} >= $${paramIdx++}`;
1657
- case "lt":
1658
- params.push(c.value);
1659
- return `${col} < $${paramIdx++}`;
1660
- case "lte":
1661
- params.push(c.value);
1662
- return `${col} <= $${paramIdx++}`;
1663
- case "in":
1664
- params.push(c.value);
1665
- return `${col} = ANY($${paramIdx++})`;
1666
- case "nin":
1667
- params.push(c.value);
1668
- return `${col} != ALL($${paramIdx++})`;
1669
- case "isNull":
1670
- return `${col} IS NULL`;
1671
- case "isNotNull":
1672
- return `${col} IS NOT NULL`;
1673
- case "contains":
1674
- params.push(`%${c.value}%`);
1675
- return `${col} ILIKE $${paramIdx++}`;
1676
- case "startsWith":
1677
- params.push(`${c.value}%`);
1678
- return `${col} ILIKE $${paramIdx++}`;
1679
- case "between": {
1680
- const [lo, hi] = c.value;
1681
- params.push(lo, hi);
1682
- return `${col} BETWEEN $${paramIdx++} AND $${paramIdx++}`;
2201
+ function useEntity(opts) {
2202
+ const { type, id, staleTime = getEngineOptions().defaultStaleTime, enabled = true } = opts;
2203
+ ensureListeners();
2204
+ const fetchRef = useRef(opts.fetch);
2205
+ fetchRef.current = opts.fetch;
2206
+ const normalizeRef = useRef(opts.normalize);
2207
+ normalizeRef.current = opts.normalize;
2208
+ const dataSelector = useCallback((state) => {
2209
+ if (!id) return null;
2210
+ return state.readEntitySnapshot(type, id);
2211
+ }, [id, type]);
2212
+ const data = useStore(useGraphStore, useShallow(dataSelector));
2213
+ const entityState = useStore(useGraphStore, useCallback(
2214
+ (state) => state.entityStates[`${type}:${id}`] ?? EMPTY_ENTITY_STATE,
2215
+ [type, id]
2216
+ ));
2217
+ const doFetch = useCallback(() => {
2218
+ if (!id || !enabled) return;
2219
+ fetchEntity({ type, id, fetch: fetchRef.current, normalize: normalizeRef.current }, getEngineOptions());
2220
+ }, [id, enabled, type]);
2221
+ useEffect(() => {
2222
+ if (!id || !enabled) return;
2223
+ const token = registerSubscriber(`${type}:${id}`);
2224
+ const state = useGraphStore.getState();
2225
+ const existingState = state.entityStates[`${type}:${id}`];
2226
+ const hasData = !!state.entities[type]?.[id];
2227
+ const isStale = !existingState?.lastFetched || existingState.stale || Date.now() - (existingState.lastFetched ?? 0) > staleTime;
2228
+ if (!hasData || isStale) doFetch();
2229
+ return () => unregisterSubscriber(`${type}:${id}`, token);
2230
+ }, [id, type, enabled, staleTime, doFetch]);
2231
+ useEffect(() => {
2232
+ if (entityState.stale && id && enabled && !entityState.isFetching) doFetch();
2233
+ }, [entityState.stale, id, enabled, entityState.isFetching, doFetch]);
2234
+ return { data, isLoading: !data && entityState.isFetching, isFetching: entityState.isFetching, error: entityState.error, isStale: entityState.stale, refetch: doFetch };
2235
+ }
2236
+ function useEntityList(opts) {
2237
+ console.warn(
2238
+ `[entity-management] useEntityList("${String(opts.type)}") is deprecated in 2.0.
2239
+ Register a transport at boot: registerEntityTransport("${String(opts.type)}", makeRestTransport(...))
2240
+ Then replace this call with: useEntities<T>("${String(opts.type)}")`
2241
+ );
2242
+ const { type, queryKey, staleTime = getEngineOptions().defaultStaleTime, enabled = true, mode = "replace" } = opts;
2243
+ ensureListeners();
2244
+ const key = useMemo(() => serializeKey(queryKey), [queryKey]);
2245
+ const fetchRef = useRef(opts.fetch);
2246
+ fetchRef.current = opts.fetch;
2247
+ const normalizeRef = useRef(opts.normalize);
2248
+ normalizeRef.current = opts.normalize;
2249
+ const listState = useStore(useGraphStore, useCallback((state) => state.lists[key] ?? EMPTY_LIST_STATE, [key]));
2250
+ const itemsSelector = useCallback((state) => {
2251
+ const ids = state.lists[key]?.ids ?? EMPTY_IDS;
2252
+ return ids.map((id) => state.readEntitySnapshot(type, id)).filter((x) => x !== null);
2253
+ }, [key, type]);
2254
+ const items = useStore(useGraphStore, useShallow(itemsSelector));
2255
+ const doFetch = useCallback((params = {}) => {
2256
+ if (!enabled) return;
2257
+ fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, params, getEngineOptions(), false);
2258
+ }, [enabled, type, queryKey, mode]);
2259
+ const fetchNextPage = useCallback(() => {
2260
+ if (!listState.hasNextPage || listState.isFetchingMore || !enabled) return;
2261
+ fetchList({ type, queryKey, mode, fetch: fetchRef.current, normalize: normalizeRef.current }, { cursor: listState.nextCursor ?? void 0, page: (listState.currentPage ?? 0) + 1, pageSize: listState.pageSize ?? void 0 }, getEngineOptions(), true);
2262
+ }, [listState.hasNextPage, listState.isFetchingMore, listState.nextCursor, listState.currentPage, listState.pageSize, enabled, type, queryKey, mode]);
2263
+ useEffect(() => {
2264
+ if (!enabled) return;
2265
+ const state = useGraphStore.getState();
2266
+ const existing = state.lists[key];
2267
+ const isStale = !existing?.lastFetched || existing.stale || Date.now() - (existing.lastFetched ?? 0) > staleTime;
2268
+ if (!existing || isStale) doFetch({ page: 1, pageSize: listState.pageSize ?? void 0 });
2269
+ }, [key, enabled, staleTime, doFetch, listState.pageSize]);
2270
+ useEffect(() => {
2271
+ if (listState.stale && enabled && !listState.isFetching) doFetch();
2272
+ }, [listState.stale, enabled, listState.isFetching, doFetch]);
2273
+ return useMemo(
2274
+ () => ({
2275
+ items,
2276
+ ids: listState.ids,
2277
+ isLoading: listState.ids.length === 0 && listState.isFetching,
2278
+ isFetching: listState.isFetching,
2279
+ isFetchingMore: listState.isFetchingMore,
2280
+ error: listState.error,
2281
+ isError: listState.error !== null,
2282
+ hasNextPage: listState.hasNextPage,
2283
+ hasPrevPage: listState.hasPrevPage,
2284
+ total: listState.total,
2285
+ currentPage: listState.currentPage,
2286
+ fetchNextPage,
2287
+ refetch: doFetch
2288
+ }),
2289
+ [items, listState, fetchNextPage, doFetch]
2290
+ );
2291
+ }
2292
+ function useEntityMutation(opts) {
2293
+ const [state, setState] = useState({ isPending: false, isSuccess: false, isError: false, error: null });
2294
+ const optsRef = useRef(opts);
2295
+ optsRef.current = opts;
2296
+ const mutate = useCallback(async (input) => {
2297
+ const { type, mutate: apiFn, normalize, optimistic, invalidateLists, invalidateEntities, onSuccess, onError } = optsRef.current;
2298
+ setState({ isPending: true, isSuccess: false, isError: false, error: null });
2299
+ let rollback = null;
2300
+ if (optimistic) {
2301
+ const opt = optimistic(input);
2302
+ if (opt) {
2303
+ const { id, patch } = opt;
2304
+ const store = useGraphStore.getState();
2305
+ const previous = { ...store.patches[type]?.[id] };
2306
+ const previousSync = store.syncMetadata[`${type}:${id}`];
2307
+ store.patchEntity(type, id, patch);
2308
+ store.setEntitySyncMetadata(type, id, { synced: false, origin: "optimistic", updatedAt: Date.now() });
2309
+ rollback = () => {
2310
+ const currentStore = useGraphStore.getState();
2311
+ if (Object.keys(previous).length > 0) currentStore.patchEntity(type, id, previous);
2312
+ else currentStore.clearPatch(type, id);
2313
+ if (previousSync) currentStore.setEntitySyncMetadata(type, id, previousSync);
2314
+ else currentStore.clearEntitySyncMetadata(type, id);
2315
+ };
1683
2316
  }
1684
- case "arrayContains":
1685
- params.push(c.value);
1686
- return `$${paramIdx++} = ANY(${col})`;
1687
- default:
1688
- return "TRUE";
1689
2317
  }
2318
+ try {
2319
+ const result = await apiFn(input);
2320
+ if (normalize) {
2321
+ const { id, data } = normalize(result, input);
2322
+ const store = useGraphStore.getState();
2323
+ store.upsertEntity(type, id, data);
2324
+ store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
2325
+ if (optimistic) {
2326
+ const opt = optimistic(input);
2327
+ if (opt) store.clearPatch(type, opt.id);
2328
+ }
2329
+ }
2330
+ if (invalidateLists) for (const k of invalidateLists) useGraphStore.getState().invalidateLists(k);
2331
+ if (invalidateEntities) for (const { type: t, id } of invalidateEntities) useGraphStore.getState().invalidateEntity(t, id);
2332
+ setState({ isPending: false, isSuccess: true, isError: false, error: null });
2333
+ onSuccess?.(result, input);
2334
+ return result;
2335
+ } catch (err) {
2336
+ const error = err instanceof Error ? err : new Error(String(err));
2337
+ rollback?.();
2338
+ setState({ isPending: false, isSuccess: false, isError: true, error: error.message });
2339
+ onError?.(error, input);
2340
+ return null;
2341
+ }
2342
+ }, []);
2343
+ const trigger = useCallback((input) => {
2344
+ void mutate(input);
2345
+ }, [mutate]);
2346
+ const reset = useCallback(() => setState({ isPending: false, isSuccess: false, isError: false, error: null }), []);
2347
+ return { mutate, trigger, reset, state };
2348
+ }
2349
+ function useEntityAugment(type, id) {
2350
+ const patch = useStore(useGraphStore, useCallback((state) => id ? state.patches[type]?.[id] ?? null : null, [type, id]));
2351
+ const augment = useCallback((fields) => {
2352
+ if (!id) return;
2353
+ useGraphStore.getState().patchEntity(type, id, fields);
2354
+ }, [type, id]);
2355
+ const unaugment = useCallback((keys) => {
2356
+ if (!id) return;
2357
+ useGraphStore.getState().unpatchEntity(type, id, keys);
2358
+ }, [type, id]);
2359
+ const clear = useCallback(() => {
2360
+ if (!id) return;
2361
+ useGraphStore.getState().clearPatch(type, id);
2362
+ }, [type, id]);
2363
+ return { patch, augment, unaugment, clear };
2364
+ }
2365
+ var suspenseEntityPromises = /* @__PURE__ */ new Map();
2366
+ var suspenseListPromises = /* @__PURE__ */ new Map();
2367
+ function getEntitySuspensePromise(type, id) {
2368
+ const key = `${type}:${id}`;
2369
+ const existing = suspenseEntityPromises.get(key);
2370
+ if (existing) return existing;
2371
+ let unsub = null;
2372
+ let settled = false;
2373
+ const promise = new Promise((resolve, reject) => {
2374
+ const settle = (fn) => {
2375
+ if (settled) return;
2376
+ settled = true;
2377
+ unsub?.();
2378
+ unsub = null;
2379
+ fn();
2380
+ };
2381
+ const inspect = (state) => {
2382
+ if (settled) return;
2383
+ const hasData = !!state.entities[type]?.[id];
2384
+ const es = state.entityStates[key];
2385
+ if (hasData) settle(() => resolve());
2386
+ else if (es != null && es.error != null && !es.isFetching) {
2387
+ const msg = es.error;
2388
+ settle(() => reject(new Error(msg)));
2389
+ }
2390
+ };
2391
+ inspect(useGraphStore.getState());
2392
+ if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
2393
+ });
2394
+ const tracked = promise.finally(() => {
2395
+ suspenseEntityPromises.delete(key);
2396
+ });
2397
+ suspenseEntityPromises.set(key, tracked);
2398
+ return tracked;
2399
+ }
2400
+ function getListSuspensePromise(listKey) {
2401
+ const existing = suspenseListPromises.get(listKey);
2402
+ if (existing) return existing;
2403
+ let unsub = null;
2404
+ let settled = false;
2405
+ const promise = new Promise((resolve, reject) => {
2406
+ const settle = (fn) => {
2407
+ if (settled) return;
2408
+ settled = true;
2409
+ unsub?.();
2410
+ unsub = null;
2411
+ fn();
2412
+ };
2413
+ const inspect = (state) => {
2414
+ if (settled) return;
2415
+ const list = state.lists[listKey] ?? EMPTY_LIST_STATE;
2416
+ if (list.ids.length > 0) settle(() => resolve());
2417
+ else if (list.error != null && !list.isFetching) {
2418
+ const msg = list.error;
2419
+ settle(() => reject(new Error(msg)));
2420
+ } else if (list.ids.length === 0 && !list.isFetching && list.lastFetched != null) settle(() => resolve());
2421
+ };
2422
+ inspect(useGraphStore.getState());
2423
+ if (!settled) unsub = useGraphStore.subscribe((state) => inspect(state));
2424
+ });
2425
+ const tracked = promise.finally(() => {
2426
+ suspenseListPromises.delete(listKey);
2427
+ });
2428
+ suspenseListPromises.set(listKey, tracked);
2429
+ return tracked;
2430
+ }
2431
+ function useSuspenseEntity(opts) {
2432
+ const result = useEntity(opts);
2433
+ const { type, id } = opts;
2434
+ if (result.isLoading) {
2435
+ if (!id) throw new Error("useSuspenseEntity requires a non-null entity id");
2436
+ throw getEntitySuspensePromise(type, id);
1690
2437
  }
1691
- function groupToSQL(g) {
1692
- const parts = g.clauses.map((c) => "logic" in c ? `(${groupToSQL(c)})` : clauseToSQL(c));
1693
- return parts.join(` ${g.logic.toUpperCase()} `);
1694
- }
1695
- let where = "TRUE";
1696
- if (view.filter) {
1697
- if (Array.isArray(view.filter)) where = view.filter.map(clauseToSQL).join(" AND ") || "TRUE";
1698
- else where = groupToSQL(view.filter) || "TRUE";
1699
- }
1700
- if (view.search?.query) {
1701
- params.push(`%${view.search.query}%`);
1702
- where += ` AND (${view.search.fields.map((f) => `"${f}"`).join(" || ' ' || ")}) ILIKE $${paramIdx++}`;
2438
+ if (result.error != null && result.data == null) {
2439
+ throw new Error(result.error);
1703
2440
  }
1704
- const orderBy = view.sort ? view.sort.map((s) => `"${s.field}" ${s.direction.toUpperCase()}${s.nulls ? ` NULLS ${s.nulls.toUpperCase()}` : ""}`).join(", ") : "";
1705
- return { where, orderBy, params };
1706
- }
1707
- function toGraphQLVariables(view) {
1708
- const result = {};
1709
- if (view.filter) {
1710
- const clauses = flattenClauses(view.filter);
1711
- const where = {};
1712
- for (const c of clauses) {
1713
- if (c.op === "custom") continue;
1714
- where[c.field] = { [`_${c.op}`]: c.value };
1715
- }
1716
- if (Object.keys(where).length) result.where = where;
2441
+ if (result.data == null) {
2442
+ throw new Error(!id ? "useSuspenseEntity requires a non-null entity id" : "Entity not found");
1717
2443
  }
1718
- if (view.sort) result.orderBy = view.sort.map((s) => ({ [s.field]: s.direction === "desc" ? "desc_nulls_last" : "asc_nulls_last" }));
1719
- if (view.search?.query) result.search = view.search.query;
1720
- return result;
2444
+ return {
2445
+ data: result.data,
2446
+ isFetching: result.isFetching,
2447
+ isStale: result.isStale,
2448
+ refetch: result.refetch
2449
+ };
1721
2450
  }
1722
- function flattenClauses(filter) {
1723
- if (Array.isArray(filter)) return filter;
1724
- function walk(g) {
1725
- return g.clauses.flatMap((c) => "logic" in c ? walk(c) : [c]);
2451
+ function useSuspenseEntityList(opts) {
2452
+ const key = useMemo(() => serializeKey(opts.queryKey), [opts.queryKey]);
2453
+ const result = useEntityList(opts);
2454
+ if (result.isLoading) throw getListSuspensePromise(key);
2455
+ if (result.error != null && result.items.length === 0) {
2456
+ throw new Error(result.error);
1726
2457
  }
1727
- return walk(filter);
1728
- }
1729
- function hasCustomPredicates(filter) {
1730
- return flattenClauses(filter).some((c) => c.op === "custom");
2458
+ const { isLoading: _isLoading, ...rest } = result;
2459
+ return rest;
1731
2460
  }
1732
-
1733
- // src/view/use-entity-view.ts
1734
- var EMPTY_ENTITY_BUCKET = {};
2461
+ var EMPTY_ENTITY_BUCKET2 = {};
1735
2462
  function useEntityView(opts) {
2463
+ console.warn(
2464
+ `[entity-management] useEntityView("${String(opts.type)}") is deprecated in 2.0.
2465
+ Register a transport at boot: registerEntityTransport("${String(opts.type)}", makeRestTransport(...))
2466
+ Then replace this call with: useEntityQuery<T>("${String(opts.type)}", { view })`
2467
+ );
1736
2468
  const { type, baseQueryKey, mode: forcedMode, remoteFetch, remoteDebounce = 300, staleTime = getEngineOptions().defaultStaleTime, enabled = true, initialIds, initialTotal } = opts;
1737
2469
  const optsRef = useRef(opts);
1738
2470
  optsRef.current = opts;
@@ -1797,16 +2529,19 @@ function useEntityView(opts) {
1797
2529
  setRemoteError(null);
1798
2530
  const store = useGraphStore.getState();
1799
2531
  store.setListFetching(rKey, true);
2532
+ const baseKeyStr = serializeKey(bqk);
1800
2533
  try {
1801
2534
  const response = await rf(params);
1802
2535
  const normalized = norm ? response.items.map(norm) : response.items.map((item) => ({ id: String(item.id), data: item }));
1803
- store.upsertEntities(type, normalized);
2536
+ store.upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
1804
2537
  for (const { id } of normalized) store.setEntityFetched(type, id);
1805
2538
  store.setListResult(rKey, normalized.map(({ id }) => id), { total: response.total ?? null, nextCursor: response.nextCursor ?? null, hasNextPage: response.hasNextPage ?? !!response.nextCursor });
2539
+ store.setListFetching(baseKeyStr, false);
1806
2540
  } catch (err) {
1807
2541
  const msg = err instanceof Error ? err.message : String(err);
1808
2542
  setRemoteError(msg);
1809
2543
  store.setListError(rKey, msg);
2544
+ store.setListError(baseKeyStr, msg);
1810
2545
  } finally {
1811
2546
  setIsRemoteFetching(false);
1812
2547
  }
@@ -1830,7 +2565,7 @@ function useEntityView(opts) {
1830
2565
  if (!existing || isStale) fireRemoteFetch(liveViewRef.current);
1831
2566
  }, [baseKey, enabled, staleTime, fireRemoteFetch]);
1832
2567
  useEffect(() => {
1833
- const unsub = useGraphStore.subscribe((state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET, (newEntities, prevEntities) => {
2568
+ const unsub = useGraphStore.subscribe((state) => state.entities[type] ?? EMPTY_ENTITY_BUCKET2, (newEntities, prevEntities) => {
1834
2569
  const view = liveViewRef.current;
1835
2570
  const store = useGraphStore.getState();
1836
2571
  const list = store.lists[baseKey];
@@ -1862,15 +2597,17 @@ function useEntityView(opts) {
1862
2597
  }, [completenessMode, isRemoteFetching, remoteListState?.nextCursor, fireRemoteFetch]);
1863
2598
  const refetch = useCallback(() => fireRemoteFetch(liveViewRef.current), [fireRemoteFetch]);
1864
2599
  const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
2600
+ const error = remoteError ?? listState?.error ?? null;
1865
2601
  return {
1866
2602
  items,
1867
2603
  viewIds: localViewIds,
1868
2604
  viewTotal,
1869
- isLoading: items.length === 0 && (listState?.isFetching ?? true) && !isRemoteFetching,
2605
+ isLoading: items.length === 0 && ((listState?.isFetching ?? false) || isRemoteFetching),
1870
2606
  isFetching: (listState?.isFetching ?? false) || isRemoteFetching,
1871
2607
  isRemoteFetching,
1872
2608
  isShowingLocalPending: completenessMode === "hybrid" && isRemoteFetching && items.length > 0,
1873
- error: remoteError ?? listState?.error ?? null,
2609
+ error,
2610
+ isError: error !== null,
1874
2611
  hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
1875
2612
  fetchNextPage,
1876
2613
  isLocallyComplete: isComplete,
@@ -2814,6 +3551,255 @@ function usePGliteQuery(opts) {
2814
3551
  return { isLoading, error };
2815
3552
  }
2816
3553
 
3554
+ // src/adapters/electricsql-tenant.ts
3555
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3556
+ function assertUuid(companyId) {
3557
+ if (typeof companyId !== "string" || !UUID_RE.test(companyId)) {
3558
+ throw new Error(
3559
+ `tenant-scoped adapter: companyId must be a UUID, received "${companyId}".`
3560
+ );
3561
+ }
3562
+ }
3563
+ function assertSafeColumn(column) {
3564
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(column)) {
3565
+ throw new Error(`tenant-scoped adapter: unsafe tenantColumn "${column}".`);
3566
+ }
3567
+ }
3568
+ function buildTenantWhere(tenantColumn, companyId, tableLabel) {
3569
+ if (typeof tenantColumn === "undefined") {
3570
+ throw new Error(
3571
+ `shape "${tableLabel}" lacks tenantColumn \u2014 tenant-scoped adapter refuses to attach unscoped shapes.`
3572
+ );
3573
+ }
3574
+ assertUuid(companyId);
3575
+ if (tenantColumn === null) {
3576
+ return `id = '${companyId}'`;
3577
+ }
3578
+ assertSafeColumn(tenantColumn);
3579
+ return `${tenantColumn} = '${companyId}'`;
3580
+ }
3581
+ function createTenantScopedElectricAdapter(opts) {
3582
+ const { pglite, tenantClaim, tables, onSynced } = opts;
3583
+ assertUuid(tenantClaim.companyId);
3584
+ const wired = tables.map((tc) => {
3585
+ const where = buildTenantWhere(tc.tenantColumn, tenantClaim.companyId, tc.table);
3586
+ const shapeStream = tc.shapeStreamFactory({ table: tc.table, where, tenantClaim });
3587
+ return {
3588
+ type: tc.type,
3589
+ table: tc.table,
3590
+ where,
3591
+ idColumn: tc.primaryKey?.[0] ?? "id",
3592
+ normalize: tc.normalize,
3593
+ shapeStream
3594
+ };
3595
+ });
3596
+ return createElectricAdapter({ pglite, tables: wired, onSynced });
3597
+ }
3598
+
3599
+ // src/adapters/pglite-persistence.ts
3600
+ var DEFAULT_TABLE = "_graph_snapshot";
3601
+ async function createPGlitePersistenceAdapter(pglite, options = {}) {
3602
+ const tableName = options.tableName ?? DEFAULT_TABLE;
3603
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tableName)) {
3604
+ throw new Error(`createPGlitePersistenceAdapter: invalid tableName "${tableName}"`);
3605
+ }
3606
+ await pglite.exec(
3607
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
3608
+ key TEXT PRIMARY KEY,
3609
+ value TEXT NOT NULL,
3610
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
3611
+ )`
3612
+ );
3613
+ return {
3614
+ async get(key) {
3615
+ const result = await pglite.query(
3616
+ `SELECT value FROM ${tableName} WHERE key = $1`,
3617
+ [key]
3618
+ );
3619
+ const row = result.rows[0];
3620
+ return row?.value ?? null;
3621
+ },
3622
+ async set(key, value) {
3623
+ await pglite.query(
3624
+ `INSERT INTO ${tableName} (key, value, updated_at)
3625
+ VALUES ($1, $2, now())
3626
+ ON CONFLICT (key) DO UPDATE
3627
+ SET value = EXCLUDED.value,
3628
+ updated_at = now()`,
3629
+ [key, value]
3630
+ );
3631
+ },
3632
+ async remove(key) {
3633
+ await pglite.query(`DELETE FROM ${tableName} WHERE key = $1`, [key]);
3634
+ }
3635
+ };
3636
+ }
3637
+
3638
+ // src/schema-from-sql.ts
3639
+ function parseCreateTable(sql) {
3640
+ const headerMatch = sql.match(
3641
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["]?([A-Za-z_][A-Za-z0-9_]*)["]?\s*\(([\s\S]*)\)\s*;?\s*$/im
3642
+ );
3643
+ if (!headerMatch) {
3644
+ throw new Error("parseCreateTable: could not locate a CREATE TABLE block");
3645
+ }
3646
+ const tableName = headerMatch[1];
3647
+ const body = headerMatch[2];
3648
+ const columns = [];
3649
+ for (const rawLine of splitTopLevelCommas(body)) {
3650
+ const line = rawLine.trim();
3651
+ if (!line) continue;
3652
+ if (/^(?:PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT|LIKE|EXCLUDE)\b/i.test(line)) {
3653
+ continue;
3654
+ }
3655
+ const colMatch = line.match(/^["]?([A-Za-z_][A-Za-z0-9_]*)["]?\s+([A-Za-z][A-Za-z0-9_]*(?:\s*\([^)]*\))?(?:\s*\[\s*\])?)([\s\S]*)$/);
3656
+ if (!colMatch) continue;
3657
+ const name = colMatch[1];
3658
+ const sqlType = colMatch[2].replace(/\s+/g, "").toUpperCase();
3659
+ const rest = colMatch[3].toUpperCase();
3660
+ const notNull = /\bNOT\s+NULL\b/.test(rest);
3661
+ const hasDefault = /\bDEFAULT\b/.test(rest);
3662
+ columns.push({ name, sqlType, notNull, hasDefault });
3663
+ }
3664
+ return { tableName, columns };
3665
+ }
3666
+ function splitTopLevelCommas(body) {
3667
+ const parts = [];
3668
+ let depth = 0;
3669
+ let current = "";
3670
+ for (const ch of body) {
3671
+ if (ch === "(") depth++;
3672
+ else if (ch === ")") depth--;
3673
+ if (ch === "," && depth === 0) {
3674
+ parts.push(current);
3675
+ current = "";
3676
+ } else {
3677
+ current += ch;
3678
+ }
3679
+ }
3680
+ if (current.trim()) parts.push(current);
3681
+ return parts;
3682
+ }
3683
+ function sqlTypeToJsonSchema(sqlType) {
3684
+ const t = sqlType.replace(/\s+/g, "").toUpperCase();
3685
+ if (/^TEXT\[\]$/.test(t)) {
3686
+ return { schema: { type: "array", items: { type: "string" } } };
3687
+ }
3688
+ if (t === "UUID" || t === "TEXT" || /^VARCHAR(\(.*\))?$/.test(t) || /^CHAR(\(.*\))?$/.test(t)) {
3689
+ return { schema: { type: "string" } };
3690
+ }
3691
+ if (t === "INTEGER" || t === "INT" || t === "BIGINT" || t === "SMALLINT" || t === "INT4" || t === "INT8" || t === "INT2") {
3692
+ return { schema: { type: "integer" } };
3693
+ }
3694
+ if (/^NUMERIC(\(.*\))?$/.test(t) || /^DECIMAL(\(.*\))?$/.test(t) || t === "REAL" || t === "DOUBLEPRECISION" || t === "FLOAT") {
3695
+ return { schema: { type: "number" } };
3696
+ }
3697
+ if (t === "BOOLEAN" || t === "BOOL") {
3698
+ return { schema: { type: "boolean" } };
3699
+ }
3700
+ if (t === "TIMESTAMPTZ" || t === "TIMESTAMP" || t === "TIMESTAMPWITHTIMEZONE" || t === "TIMESTAMPWITHOUTTIMEZONE") {
3701
+ return { schema: { type: "string", format: "date-time" } };
3702
+ }
3703
+ if (t === "DATE") {
3704
+ return { schema: { type: "string", format: "date" } };
3705
+ }
3706
+ if (t === "JSONB" || t === "JSON") {
3707
+ return { schema: { type: "object" } };
3708
+ }
3709
+ return {
3710
+ schema: { type: "string" },
3711
+ warning: `sql type "${sqlType}" not explicitly mapped; defaulting to string`
3712
+ };
3713
+ }
3714
+ function deepMergeUnknown(base, override) {
3715
+ const out = { ...base };
3716
+ for (const [key, value] of Object.entries(override)) {
3717
+ const baseValue = base[key];
3718
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && baseValue !== null && typeof baseValue === "object" && !Array.isArray(baseValue)) {
3719
+ out[key] = deepMergeUnknown(
3720
+ baseValue,
3721
+ value
3722
+ );
3723
+ } else {
3724
+ out[key] = value;
3725
+ }
3726
+ }
3727
+ return out;
3728
+ }
3729
+ function mergeEntityConfig(generated, overrides) {
3730
+ if (!overrides) return generated;
3731
+ const merged = deepMergeUnknown(
3732
+ generated,
3733
+ overrides
3734
+ );
3735
+ const refined = merged;
3736
+ return {
3737
+ ...refined,
3738
+ entityType: generated.entityType,
3739
+ schema: refined.schema ?? generated.schema
3740
+ };
3741
+ }
3742
+ function registerEntityFromSql(opts) {
3743
+ const parsed = parseCreateTable(opts.createTableSql);
3744
+ const properties = {};
3745
+ const required = [];
3746
+ for (const col of parsed.columns) {
3747
+ const { schema, warning } = sqlTypeToJsonSchema(col.sqlType);
3748
+ const propSchema = { ...schema };
3749
+ if (warning) {
3750
+ propSchema["x-warning"] = warning;
3751
+ }
3752
+ properties[col.name] = propSchema;
3753
+ if (col.notNull && !col.hasDefault) {
3754
+ required.push(col.name);
3755
+ }
3756
+ }
3757
+ const generated = {
3758
+ entityType: opts.entityType,
3759
+ source: "runtime",
3760
+ schema: {
3761
+ type: "object",
3762
+ title: parsed.tableName,
3763
+ properties,
3764
+ required
3765
+ }
3766
+ };
3767
+ const config = mergeEntityConfig(generated, opts.overrides);
3768
+ registerEntityJsonSchema(config);
3769
+ return config;
3770
+ }
3771
+ function useEntityListAsTable(opts) {
3772
+ const queryKey = opts.queryKey ?? ["entity-list-as-table", opts.type];
3773
+ const list = useEntityList({
3774
+ ...opts,
3775
+ queryKey
3776
+ });
3777
+ const lastDataRef = useRef(null);
3778
+ const data = useMemo(() => {
3779
+ const prev = lastDataRef.current;
3780
+ if (prev && prev.length === list.items.length) {
3781
+ let same = true;
3782
+ for (let i = 0; i < prev.length; i++) {
3783
+ if (prev[i] !== list.items[i]) {
3784
+ same = false;
3785
+ break;
3786
+ }
3787
+ }
3788
+ if (same) return prev;
3789
+ }
3790
+ lastDataRef.current = list.items;
3791
+ return list.items;
3792
+ }, [list.items]);
3793
+ return {
3794
+ data,
3795
+ rowCount: list.total ?? list.ids.length,
3796
+ isLoading: list.isLoading,
3797
+ isFetching: list.isFetching,
3798
+ error: list.error,
3799
+ refetch: list.refetch
3800
+ };
3801
+ }
3802
+
2817
3803
  // src/graphql/client.ts
2818
3804
  async function executeGQL(cfg, document2, variables) {
2819
3805
  const headers = { "Content-Type": "application/json", ...cfg.headers?.() ?? {} };
@@ -2854,6 +3840,7 @@ var GQLClient = class {
2854
3840
  constructor(cfg) {
2855
3841
  this.cfg = cfg;
2856
3842
  }
3843
+ cfg;
2857
3844
  async query(opts) {
2858
3845
  const key = opts.cacheKey ?? `gql:${opts.document.slice(0, 60)}:${JSON.stringify(opts.variables ?? {})}`;
2859
3846
  return dedupe(key, async () => {
@@ -6641,10 +7628,11 @@ function DefaultListItemContent({
6641
7628
  descriptor
6642
7629
  }) {
6643
7630
  if (!descriptor) {
6644
- const keys = Object.keys(item);
7631
+ const rec = item;
7632
+ const keys = Object.keys(rec);
6645
7633
  return /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
6646
- /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: String(item[keys[0]] ?? "") }),
6647
- keys[1] && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: String(item[keys[1]] ?? "") })
7634
+ /* @__PURE__ */ jsx("p", { className: "truncate text-sm font-medium", children: String(rec[keys[0]] ?? "") }),
7635
+ keys[1] && /* @__PURE__ */ jsx("p", { className: "truncate text-xs text-muted-foreground", children: String(rec[keys[1]] ?? "") })
6648
7636
  ] });
6649
7637
  }
6650
7638
  const IconComponent = typeof descriptor.icon === "function" ? descriptor.icon : null;
@@ -8659,6 +9647,6 @@ function actionsColumn2() {
8659
9647
  };
8660
9648
  }
8661
9649
 
8662
- export { ActionButtonRow, ActionDropdown, ColumnPresetDialog, DataTable, DataTableColumnHeader, DataTableFilter, DataTablePagination, DataTableToolbar, ElectricSQLAdapter as ElectricSQLPresetAdapter, EmptyState, EntityDetailSheet, EntityFormSheet, EntityListView, EntityTable, FilterPresetDialog, GQLClient, GalleryView, InlineCellEditor, InlineItemEditor, ListView, MarkdownFieldEditor, MarkdownFieldRenderer, MemoryAdapter, MultiSelectBar, PresetPicker, InlineCellEditor2 as PureInlineCellEditor, RealtimeManager, RestApiAdapter, SelectionContext, Sheet, SortHeader, SupabaseRealtimeAdapter as SupabasePresetAdapter, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableStorageProvider, ViewModeSwitcher, ZustandPersistAdapter, actionsColumn, applyView, booleanColumn, buildEntityFieldsFromSchema, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createConvexAdapter, createElectricAdapter, createGQLClient, createGraphAction, createGraphEffect, createGraphQLSubscriptionAdapter, createGraphTool, createGraphTransaction, createPresetStore, createPrismaEntityConfig, createRow, createSchemaGraphTool, createSelectionStore, createSupabaseRealtimeAdapter, createWebSocketAdapter, dateColumn, dedupe, deleteAction, editAction, enumColumn, executeGQL, exportGraphSnapshot, exportGraphSnapshotWithSchemas, fetchEntity, fetchList, flattenClauses, getCoreRowModel2 as getCoreRowModel, getEntityJsonSchema, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginatedRowModel, getRealtimeManager, getSchema, getSelectedRowModel, getSortedRowModel2 as getSortedRowModel, hasCustomPredicates, hydrateGraphFromStorage, matchesFilter, matchesSearch, normalizeGQLResponse, numberColumn, persistGraphToStorage, prismaRelationsToSchema, actionsColumn2 as pureActionsColumn, booleanColumn2 as pureBooleanColumn, dateColumn2 as pureDateColumn, enumColumn2 as pureEnumColumn, numberColumn2 as pureNumberColumn, selectionColumn2 as pureSelectionColumn, textColumn2 as pureTextColumn, queryOnce, readRelations, registerEntityJsonSchema, registerRuntimeSchema, registerSchema, renderMarkdownToHtml, resetRealtimeManager, selectGraph, selectionColumn, serializeKey, startGarbageCollector, startLocalFirstGraph, stopGarbageCollector, textColumn, toGraphQLVariables, toPrismaInclude, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, useEntity, useEntityAugment, useEntityCRUD, useEntityList, useEntityMutation, useEntityView, useGQLEntity, useGQLList, useGQLMutation, useGQLSubscription, useGraphDevTools, useGraphStore, useGraphSyncStatus, useLocalFirst, usePGliteQuery, useSchemaEntityFields, useSelectionContext, useSelectionStore, useSuspenseEntity, useSuspenseEntityList, useTable, useTablePresets, useTableRealtimeMode, useTableStorageAdapter, viewAction };
9650
+ export { ActionButtonRow, ActionDropdown, ColumnPresetDialog, DataTable, DataTableColumnHeader, DataTableFilter, DataTablePagination, DataTableToolbar, ElectricSQLAdapter as ElectricSQLPresetAdapter, EmptyState, EntityDetailSheet, EntityFormSheet, EntityListView, EntityTable, FilterPresetDialog, GQLClient, GalleryView, InlineCellEditor, InlineItemEditor, ListView, MarkdownFieldEditor, MarkdownFieldRenderer, MemoryAdapter, MultiSelectBar, PresetPicker, InlineCellEditor2 as PureInlineCellEditor, RealtimeManager, RestApiAdapter, SelectionContext, Sheet, SortHeader, SupabaseRealtimeAdapter as SupabasePresetAdapter, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableStorageProvider, TerminalError, TransientError, ViewModeSwitcher, ZustandPersistAdapter, __resetEntityTransports, actionsColumn, applyView, booleanColumn, buildEntityFieldsFromSchema, buildTenantWhere, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createConvexAdapter, createElectricAdapter, createGQLClient, createGraphAction, createGraphEffect, createGraphQLSubscriptionAdapter, createGraphTool, createGraphTransaction, createPGlitePersistenceAdapter, createPresetStore, createPrismaEntityConfig, createRow, createSchemaGraphTool, createSelectionStore, createSupabaseRealtimeAdapter, createTenantScopedElectricAdapter, createWebSocketAdapter, dateColumn, dedupe, deleteAction, editAction, enumColumn, executeGQL, exportGraphSnapshot, exportGraphSnapshotWithSchemas, fetchEntity, fetchList, flattenClauses, getCoreRowModel2 as getCoreRowModel, getEntityJsonSchema, getEntityTransport, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginatedRowModel, getRealtimeManager, getRegisteredEntityTypes, getSchema, getSelectedRowModel, getSortedRowModel2 as getSortedRowModel, hasCustomPredicates, hydrateGraphFromStorage, makeRestTransport, matchesFilter, matchesSearch, normalizeGQLResponse, numberColumn, parseCreateTable, persistGraphToStorage, prismaRelationsToSchema, actionsColumn2 as pureActionsColumn, booleanColumn2 as pureBooleanColumn, dateColumn2 as pureDateColumn, enumColumn2 as pureEnumColumn, numberColumn2 as pureNumberColumn, selectionColumn2 as pureSelectionColumn, textColumn2 as pureTextColumn, queryOnce, readRelations, registerEntityFromSql, registerEntityJsonSchema, registerEntityTransport, registerRuntimeSchema, registerSchema, renderMarkdownToHtml, replayActionWithRetry, resetRealtimeManager, selectGraph, selectionColumn, serializeKey, sqlTypeToJsonSchema, startGarbageCollector, startLocalFirstGraph, stopGarbageCollector, textColumn, toEntityError, toGraphQLVariables, toPrismaInclude, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, useEntities, useEntity, useEntityAugment, useEntityCRUD, useEntityList, useEntityListAsTable, useEntityMutation, useEntityQuery, useEntityView, useGQLEntity, useGQLList, useGQLMutation, useGQLSubscription, useGraphDevTools, useGraphStore, useGraphSyncStatus, useLocalFirst, usePGliteQuery, useSchemaEntityFields, useSelectionContext, useSelectionStore, useSuspenseEntity, useSuspenseEntityList, useTable, useTablePresets, useTableRealtimeMode, useTableStorageAdapter, viewAction };
8663
9651
  //# sourceMappingURL=index.mjs.map
8664
9652
  //# sourceMappingURL=index.mjs.map