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