@prometheus-ags/prometheus-entity-management 1.2.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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)];
774
+ useGraphStore.getState().setEntityError(type, id, error.message);
775
+ opts.onError?.(error);
776
+ }
777
+ };
778
+ await dedupe(`${type}:${id}`, () => attempt(0));
779
+ }
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
+ }
825
+ };
826
+ if (engineOptions.revalidateOnFocus) {
827
+ window.addEventListener("visibilitychange", () => {
828
+ if (document.visibilityState === "visible") revalidateAll();
625
829
  });
830
+ window.addEventListener("focus", revalidateAll);
626
831
  }
627
- return [];
832
+ if (engineOptions.revalidateOnReconnect) window.addEventListener("online", revalidateAll);
628
833
  }
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"]
834
+
835
+ // src/hooks/use-entities.ts
836
+ function sleep2(ms) {
837
+ return new Promise((r) => setTimeout(r, ms));
838
+ }
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
640
964
  };
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
965
  }
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";
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);
971
+ }
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));
976
+ }
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;
672
1019
  default:
673
- return "text";
1020
+ return true;
674
1021
  }
675
1022
  }
676
- function registryKey(entityType, field, schemaId) {
677
- return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
678
- }
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());
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);
1029
+ });
681
1030
  }
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>`;
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;
686
1037
  }
687
- function renderInlineMarkdown(block) {
688
- return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
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;
689
1053
  }
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);
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;
1063
+ }
1064
+ if (compareEntities(entity, me, sort) <= 0) hi = mid;
1065
+ else lo = mid + 1;
1066
+ }
1067
+ return lo;
699
1068
  }
700
- function createGraphTool(handler) {
701
- return (input) => handler(input, {
702
- store: useGraphStore.getState(),
703
- queryOnce,
704
- exportGraphSnapshot
705
- });
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);
706
1080
  }
707
- function createSchemaGraphTool(handler) {
708
- return (input) => handler(input, {
709
- store: useGraphStore.getState(),
710
- queryOnce,
711
- exportGraphSnapshot,
712
- getEntityJsonSchema,
713
- exportGraphSnapshotWithSchemas
714
- });
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" };
715
1085
  }
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
732
- }
733
- }))
734
- }));
735
- var pendingActions = /* @__PURE__ */ new Map();
736
- function useGraphSyncStatus() {
737
- return useGraphSyncStatusStore((state) => state.status);
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;
738
1094
  }
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
- };
1095
+
1096
+ // src/hooks/use-entity-query.ts
1097
+ function sleep3(ms) {
1098
+ return new Promise((r) => setTimeout(r, ms));
759
1099
  }
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
- };
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 });
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];
1676
+ }
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);
1121
1686
  }
1687
+ return acc;
1122
1688
  }
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 });
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;
1133
1710
  }
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
- };
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]
1166
- );
1167
- return { ...graphPart, subscriberCount };
1725
+ function buildEntityFieldsFromSchema(opts) {
1726
+ return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
1168
1727
  }
1169
- var listenersAttached = false;
1170
- function ensureListeners() {
1171
- if (!listenersAttached) {
1172
- attachGlobalListeners();
1173
- listenersAttached = true;
1174
- }
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
1738
+ );
1175
1739
  }
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 };
1740
+ function escapeHtml(input) {
1741
+ return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1210
1742
  }
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 };
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("");
1244
1747
  }
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
- };
1269
- }
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
- }
1282
- }
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;
1748
+ function MarkdownFieldRenderer({ value, className }) {
1749
+ return /* @__PURE__ */ jsxRuntime.jsx(
1750
+ "div",
1751
+ {
1752
+ className,
1753
+ dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
1294
1754
  }
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 };
1301
- }
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 };
1755
+ );
1317
1756
  }
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);
1349
- });
1350
- suspenseEntityPromises.set(key, tracked);
1351
- return tracked;
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"
1770
+ }
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
+ ] });
1352
1774
  }
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));
1377
- });
1378
- const tracked = promise.finally(() => {
1379
- suspenseListPromises.delete(listKey);
1380
- });
1381
- suspenseListPromises.set(listKey, tracked);
1382
- return tracked;
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" });
1383
1777
  }
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);
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);
1791
+ }
1792
+ return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
1793
+ });
1390
1794
  }
1391
- if (result.error != null && result.data == null) {
1392
- throw new Error(result.error);
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
+ }));
1393
1814
  }
1394
- if (result.data == null) {
1395
- throw new Error(!id ? "useSuspenseEntity requires a non-null entity id" : "Entity not found");
1815
+ if (type === "markdown") {
1816
+ descriptor.render = createMarkdownDetailRenderer(field);
1396
1817
  }
1397
- return {
1398
- data: result.data,
1399
- isFetching: result.isFetching,
1400
- isStale: result.isStale,
1401
- refetch: result.refetch
1402
- };
1818
+ return descriptor;
1403
1819
  }
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);
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";
1410
1842
  }
1411
- const { isLoading: _isLoading, ...rest } = result;
1412
- return rest;
1413
1843
  }
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);
1844
+ function registryKey(entityType, field, schemaId) {
1845
+ return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
1419
1846
  }
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));
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());
1424
1849
  }
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;
1467
- default:
1468
- return true;
1469
- }
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>`;
1470
1854
  }
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
- });
1855
+ function renderInlineMarkdown(block) {
1856
+ return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
1478
1857
  }
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;
1483
- }
1484
- return 0;
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);
1485
1867
  }
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;
1868
+ function createGraphTool(handler) {
1869
+ return (input) => handler(input, {
1870
+ store: useGraphStore.getState(),
1871
+ queryOnce,
1872
+ exportGraphSnapshot
1873
+ });
1501
1874
  }
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;
1875
+ function createSchemaGraphTool(handler) {
1876
+ return (input) => handler(input, {
1877
+ store: useGraphStore.getState(),
1878
+ queryOnce,
1879
+ exportGraphSnapshot,
1880
+ getEntityJsonSchema,
1881
+ exportGraphSnapshotWithSchemas
1882
+ });
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
1511
1900
  }
1512
- if (compareEntities(entity, me, sort) <= 0) hi = mid;
1513
- else lo = mid + 1;
1514
- }
1515
- return lo;
1901
+ }))
1902
+ }));
1903
+ var pendingActions = /* @__PURE__ */ new Map();
1904
+ function useGraphSyncStatus() {
1905
+ return useGraphSyncStatusStore((state) => state.status);
1516
1906
  }
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 });
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
1920
+ });
1921
+ return {
1922
+ ok: true,
1923
+ key: opts.key,
1924
+ bytes: json.length,
1925
+ persistedAt
1926
+ };
1927
+ }
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
+ };
1938
+ }
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
+ };
1523
1974
  }
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
1975
  }
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" };
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
+ })();
2035
+ return {
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
+ }
2052
+ };
1533
2053
  }
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
- }
1541
- return current;
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
+ };
1542
2063
  }
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) };
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
+ };
1549
2073
  }
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":
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":
1580
2085
  default:
1581
- return null;
2086
+ return base / 2 + Math.random() * (base / 2);
1582
2087
  }
1583
2088
  }
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 });
1593
- }
1594
- const leaf = clauseToPrismaLeaf(c);
1595
- if (leaf === null) return null;
1596
- return nestWhereField(parts, leaf);
2089
+ function sleep4(ms) {
2090
+ if (ms <= 0) return Promise.resolve();
2091
+ return new Promise((r) => setTimeout(r, ms));
1597
2092
  }
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);
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));
1607
2103
  }
1608
2104
  }
1609
- if (parts.length === 0) return {};
1610
- if (parts.length === 1) return parts[0];
1611
- return g.logic === "or" ? { OR: parts } : { AND: parts };
2105
+ try {
2106
+ await policy.poisonHandler?.(action, lastError);
2107
+ } catch {
2108
+ }
2109
+ return { ok: false, poisoned: true, error: lastError };
1612
2110
  }
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);
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
+ };
2126
+ }
2127
+ return {
2128
+ getIsOnline: () => true,
2129
+ subscribe: () => () => {
1619
2130
  }
1620
- if (parts.length === 0) return {};
1621
- if (parts.length === 1) return parts[0];
1622
- return { AND: parts };
2131
+ };
2132
+ }
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;
1623
2142
  }
1624
- return groupToPrismaWhere(filter);
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 });
2152
+ }
2153
+ }
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;
@@ -1803,16 +2535,19 @@ function useEntityView(opts) {
1803
2535
  setRemoteError(null);
1804
2536
  const store = useGraphStore.getState();
1805
2537
  store.setListFetching(rKey, true);
2538
+ const baseKeyStr = serializeKey(bqk);
1806
2539
  try {
1807
2540
  const response = await rf(params);
1808
2541
  const normalized = norm ? response.items.map(norm) : response.items.map((item) => ({ id: String(item.id), data: item }));
1809
- store.upsertEntities(type, normalized);
2542
+ store.upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
1810
2543
  for (const { id } of normalized) store.setEntityFetched(type, id);
1811
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);
1812
2546
  } catch (err) {
1813
2547
  const msg = err instanceof Error ? err.message : String(err);
1814
2548
  setRemoteError(msg);
1815
2549
  store.setListError(rKey, msg);
2550
+ store.setListError(baseKeyStr, msg);
1816
2551
  } finally {
1817
2552
  setIsRemoteFetching(false);
1818
2553
  }
@@ -1836,7 +2571,7 @@ function useEntityView(opts) {
1836
2571
  if (!existing || isStale) fireRemoteFetch(liveViewRef.current);
1837
2572
  }, [baseKey, enabled, staleTime, fireRemoteFetch]);
1838
2573
  React6.useEffect(() => {
1839
- 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) => {
1840
2575
  const view = liveViewRef.current;
1841
2576
  const store = useGraphStore.getState();
1842
2577
  const list = store.lists[baseKey];
@@ -1868,15 +2603,17 @@ function useEntityView(opts) {
1868
2603
  }, [completenessMode, isRemoteFetching, remoteListState?.nextCursor, fireRemoteFetch]);
1869
2604
  const refetch = React6.useCallback(() => fireRemoteFetch(liveViewRef.current), [fireRemoteFetch]);
1870
2605
  const viewTotal = remoteListState?.total ?? (isComplete ? localViewIds.length : listState?.total ?? null);
2606
+ const error = remoteError ?? listState?.error ?? null;
1871
2607
  return {
1872
2608
  items,
1873
2609
  viewIds: localViewIds,
1874
2610
  viewTotal,
1875
- isLoading: items.length === 0 && (listState?.isFetching ?? true) && !isRemoteFetching,
2611
+ isLoading: items.length === 0 && ((listState?.isFetching ?? false) || isRemoteFetching),
1876
2612
  isFetching: (listState?.isFetching ?? false) || isRemoteFetching,
1877
2613
  isRemoteFetching,
1878
2614
  isShowingLocalPending: completenessMode === "hybrid" && isRemoteFetching && items.length > 0,
1879
- error: remoteError ?? listState?.error ?? null,
2615
+ error,
2616
+ isError: error !== null,
1880
2617
  hasNextPage: completenessMode === "local" ? false : remoteListState?.hasNextPage ?? listState?.hasNextPage ?? false,
1881
2618
  fetchNextPage,
1882
2619
  isLocallyComplete: isComplete,
@@ -2820,6 +3557,255 @@ function usePGliteQuery(opts) {
2820
3557
  return { isLoading, error };
2821
3558
  }
2822
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
+
2823
3809
  // src/graphql/client.ts
2824
3810
  async function executeGQL(cfg, document2, variables) {
2825
3811
  const headers = { "Content-Type": "application/json", ...cfg.headers?.() ?? {} };
@@ -2860,6 +3846,7 @@ var GQLClient = class {
2860
3846
  constructor(cfg) {
2861
3847
  this.cfg = cfg;
2862
3848
  }
3849
+ cfg;
2863
3850
  async query(opts) {
2864
3851
  const key = opts.cacheKey ?? `gql:${opts.document.slice(0, 60)}:${JSON.stringify(opts.variables ?? {})}`;
2865
3852
  return dedupe(key, async () => {
@@ -6647,10 +7634,11 @@ function DefaultListItemContent({
6647
7634
  descriptor
6648
7635
  }) {
6649
7636
  if (!descriptor) {
6650
- const keys = Object.keys(item);
7637
+ const rec = item;
7638
+ const keys = Object.keys(rec);
6651
7639
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
6652
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium", children: String(item[keys[0]] ?? "") }),
6653
- 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]] ?? "") })
6654
7642
  ] });
6655
7643
  }
6656
7644
  const IconComponent = typeof descriptor.icon === "function" ? descriptor.icon : null;
@@ -8706,12 +9694,16 @@ exports.TableHead = TableHead;
8706
9694
  exports.TableHeader = TableHeader;
8707
9695
  exports.TableRow = TableRow;
8708
9696
  exports.TableStorageProvider = TableStorageProvider;
9697
+ exports.TerminalError = TerminalError;
9698
+ exports.TransientError = TransientError;
8709
9699
  exports.ViewModeSwitcher = ViewModeSwitcher;
8710
9700
  exports.ZustandPersistAdapter = ZustandPersistAdapter;
9701
+ exports.__resetEntityTransports = __resetEntityTransports;
8711
9702
  exports.actionsColumn = actionsColumn;
8712
9703
  exports.applyView = applyView;
8713
9704
  exports.booleanColumn = booleanColumn;
8714
9705
  exports.buildEntityFieldsFromSchema = buildEntityFieldsFromSchema;
9706
+ exports.buildTenantWhere = buildTenantWhere;
8715
9707
  exports.cascadeInvalidation = cascadeInvalidation;
8716
9708
  exports.checkCompleteness = checkCompleteness;
8717
9709
  exports.compareEntities = compareEntities;
@@ -8724,12 +9716,14 @@ exports.createGraphEffect = createGraphEffect;
8724
9716
  exports.createGraphQLSubscriptionAdapter = createGraphQLSubscriptionAdapter;
8725
9717
  exports.createGraphTool = createGraphTool;
8726
9718
  exports.createGraphTransaction = createGraphTransaction;
9719
+ exports.createPGlitePersistenceAdapter = createPGlitePersistenceAdapter;
8727
9720
  exports.createPresetStore = createPresetStore;
8728
9721
  exports.createPrismaEntityConfig = createPrismaEntityConfig;
8729
9722
  exports.createRow = createRow;
8730
9723
  exports.createSchemaGraphTool = createSchemaGraphTool;
8731
9724
  exports.createSelectionStore = createSelectionStore;
8732
9725
  exports.createSupabaseRealtimeAdapter = createSupabaseRealtimeAdapter;
9726
+ exports.createTenantScopedElectricAdapter = createTenantScopedElectricAdapter;
8733
9727
  exports.createWebSocketAdapter = createWebSocketAdapter;
8734
9728
  exports.dateColumn = dateColumn;
8735
9729
  exports.dedupe = dedupe;
@@ -8744,6 +9738,7 @@ exports.fetchList = fetchList;
8744
9738
  exports.flattenClauses = flattenClauses;
8745
9739
  exports.getCoreRowModel = getCoreRowModel2;
8746
9740
  exports.getEntityJsonSchema = getEntityJsonSchema;
9741
+ exports.getEntityTransport = getEntityTransport;
8747
9742
  exports.getExpandedRowModel = getExpandedRowModel;
8748
9743
  exports.getFacetedMinMaxValues = getFacetedMinMaxValues;
8749
9744
  exports.getFacetedRowModel = getFacetedRowModel;
@@ -8752,15 +9747,18 @@ exports.getFilteredRowModel = getFilteredRowModel;
8752
9747
  exports.getGroupedRowModel = getGroupedRowModel;
8753
9748
  exports.getPaginatedRowModel = getPaginatedRowModel;
8754
9749
  exports.getRealtimeManager = getRealtimeManager;
9750
+ exports.getRegisteredEntityTypes = getRegisteredEntityTypes;
8755
9751
  exports.getSchema = getSchema;
8756
9752
  exports.getSelectedRowModel = getSelectedRowModel;
8757
9753
  exports.getSortedRowModel = getSortedRowModel2;
8758
9754
  exports.hasCustomPredicates = hasCustomPredicates;
8759
9755
  exports.hydrateGraphFromStorage = hydrateGraphFromStorage;
9756
+ exports.makeRestTransport = makeRestTransport;
8760
9757
  exports.matchesFilter = matchesFilter;
8761
9758
  exports.matchesSearch = matchesSearch;
8762
9759
  exports.normalizeGQLResponse = normalizeGQLResponse;
8763
9760
  exports.numberColumn = numberColumn;
9761
+ exports.parseCreateTable = parseCreateTable;
8764
9762
  exports.persistGraphToStorage = persistGraphToStorage;
8765
9763
  exports.prismaRelationsToSchema = prismaRelationsToSchema;
8766
9764
  exports.pureActionsColumn = actionsColumn2;
@@ -8772,29 +9770,37 @@ exports.pureSelectionColumn = selectionColumn2;
8772
9770
  exports.pureTextColumn = textColumn2;
8773
9771
  exports.queryOnce = queryOnce;
8774
9772
  exports.readRelations = readRelations;
9773
+ exports.registerEntityFromSql = registerEntityFromSql;
8775
9774
  exports.registerEntityJsonSchema = registerEntityJsonSchema;
9775
+ exports.registerEntityTransport = registerEntityTransport;
8776
9776
  exports.registerRuntimeSchema = registerRuntimeSchema;
8777
9777
  exports.registerSchema = registerSchema;
8778
9778
  exports.renderMarkdownToHtml = renderMarkdownToHtml;
9779
+ exports.replayActionWithRetry = replayActionWithRetry;
8779
9780
  exports.resetRealtimeManager = resetRealtimeManager;
8780
9781
  exports.selectGraph = selectGraph;
8781
9782
  exports.selectionColumn = selectionColumn;
8782
9783
  exports.serializeKey = serializeKey;
9784
+ exports.sqlTypeToJsonSchema = sqlTypeToJsonSchema;
8783
9785
  exports.startGarbageCollector = startGarbageCollector;
8784
9786
  exports.startLocalFirstGraph = startLocalFirstGraph;
8785
9787
  exports.stopGarbageCollector = stopGarbageCollector;
8786
9788
  exports.textColumn = textColumn;
9789
+ exports.toEntityError = toEntityError;
8787
9790
  exports.toGraphQLVariables = toGraphQLVariables;
8788
9791
  exports.toPrismaInclude = toPrismaInclude;
8789
9792
  exports.toPrismaOrderBy = toPrismaOrderBy;
8790
9793
  exports.toPrismaWhere = toPrismaWhere;
8791
9794
  exports.toRestParams = toRestParams;
8792
9795
  exports.toSQLClauses = toSQLClauses;
9796
+ exports.useEntities = useEntities;
8793
9797
  exports.useEntity = useEntity;
8794
9798
  exports.useEntityAugment = useEntityAugment;
8795
9799
  exports.useEntityCRUD = useEntityCRUD;
8796
9800
  exports.useEntityList = useEntityList;
9801
+ exports.useEntityListAsTable = useEntityListAsTable;
8797
9802
  exports.useEntityMutation = useEntityMutation;
9803
+ exports.useEntityQuery = useEntityQuery;
8798
9804
  exports.useEntityView = useEntityView;
8799
9805
  exports.useGQLEntity = useGQLEntity;
8800
9806
  exports.useGQLList = useGQLList;