@prometheus-ags/prometheus-entity-management 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  import { create, createStore, useStore } from 'zustand';
2
2
  import { subscribeWithSelector, persist } from 'zustand/middleware';
3
3
  import { immer } from 'zustand/middleware/immer';
4
- import React5, { createContext, useSyncExternalStore, useMemo, useRef, useCallback, useEffect, useState, useContext, useId } from 'react';
4
+ import React6, { createContext, useMemo, useSyncExternalStore, useRef, useCallback, useEffect, useState, useContext, useId } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
6
  import { useShallow } from 'zustand/react/shallow';
6
7
  import { useReactTable, getSortedRowModel, getCoreRowModel, flexRender } from '@tanstack/react-table';
7
8
  import { Search, X, Loader2, RefreshCw, ChevronLeft, ChevronRight, Pencil, Trash2 } from 'lucide-react';
8
9
  import { clsx } from 'clsx';
9
10
  import { twMerge } from 'tailwind-merge';
10
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
11
 
12
12
  // src/graph.ts
13
13
  var EMPTY_IDS = [];
@@ -17,6 +17,11 @@ var EMPTY_ENTITY_STATE = {
17
17
  error: null,
18
18
  stale: false
19
19
  };
20
+ var EMPTY_SYNC_METADATA = {
21
+ synced: true,
22
+ origin: "server",
23
+ updatedAt: null
24
+ };
20
25
  var EMPTY_LIST_STATE = {
21
26
  ids: EMPTY_IDS,
22
27
  total: null,
@@ -35,6 +40,9 @@ var EMPTY_LIST_STATE = {
35
40
  function defaultEntityState() {
36
41
  return { ...EMPTY_ENTITY_STATE };
37
42
  }
43
+ function defaultSyncMetadata() {
44
+ return { ...EMPTY_SYNC_METADATA };
45
+ }
38
46
  function defaultListState() {
39
47
  return { ...EMPTY_LIST_STATE, ids: [] };
40
48
  }
@@ -47,24 +55,33 @@ var useGraphStore = create()(
47
55
  entities: {},
48
56
  patches: {},
49
57
  entityStates: {},
58
+ syncMetadata: {},
50
59
  lists: {},
51
60
  upsertEntity: (type, id, data) => set((s) => {
52
61
  if (!s.entities[type]) s.entities[type] = {};
53
62
  s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
63
+ const key = ek(type, id);
64
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
54
65
  }),
55
66
  upsertEntities: (type, entries) => set((s) => {
56
67
  if (!s.entities[type]) s.entities[type] = {};
57
- for (const { id, data } of entries)
68
+ for (const { id, data } of entries) {
58
69
  s.entities[type][id] = { ...s.entities[type][id] ?? {}, ...data };
70
+ const key = ek(type, id);
71
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
72
+ }
59
73
  }),
60
74
  replaceEntity: (type, id, data) => set((s) => {
61
75
  if (!s.entities[type]) s.entities[type] = {};
62
76
  s.entities[type][id] = data;
77
+ const key = ek(type, id);
78
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
63
79
  }),
64
80
  removeEntity: (type, id) => set((s) => {
65
81
  delete s.entities[type]?.[id];
66
82
  delete s.patches[type]?.[id];
67
83
  delete s.entityStates[ek(type, id)];
84
+ delete s.syncMetadata[ek(type, id)];
68
85
  }),
69
86
  patchEntity: (type, id, patch) => set((s) => {
70
87
  if (!s.patches[type]) s.patches[type] = {};
@@ -96,12 +113,20 @@ var useGraphStore = create()(
96
113
  s.entityStates[k].isFetching = false;
97
114
  s.entityStates[k].error = null;
98
115
  s.entityStates[k].stale = false;
116
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
99
117
  }),
100
118
  setEntityStale: (type, id, stale) => set((s) => {
101
119
  const k = ek(type, id);
102
120
  if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
103
121
  s.entityStates[k].stale = stale;
104
122
  }),
123
+ setEntitySyncMetadata: (type, id, metadata) => set((s) => {
124
+ const k = ek(type, id);
125
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
126
+ }),
127
+ clearEntitySyncMetadata: (type, id) => set((s) => {
128
+ delete s.syncMetadata[ek(type, id)];
129
+ }),
105
130
  setListResult: (key, ids, meta) => set((s) => {
106
131
  const ex = s.lists[key] ?? defaultListState();
107
132
  s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, stale: false, lastFetched: Date.now() };
@@ -173,11 +198,717 @@ var useGraphStore = create()(
173
198
  if (!base) return null;
174
199
  const patch = s.patches[type]?.[id];
175
200
  return patch ? { ...base, ...patch } : base;
201
+ },
202
+ readEntitySnapshot: (type, id) => {
203
+ const s = get();
204
+ const base = s.entities[type]?.[id];
205
+ if (!base) return null;
206
+ const patch = s.patches[type]?.[id];
207
+ const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
208
+ return {
209
+ ...patch ? { ...base, ...patch } : base,
210
+ $synced: metadata.synced,
211
+ $origin: metadata.origin,
212
+ $updatedAt: metadata.updatedAt
213
+ };
176
214
  }
177
215
  }))
178
216
  )
179
217
  );
180
218
 
219
+ // src/graph-query.ts
220
+ function queryOnce(opts) {
221
+ const store = useGraphStore.getState();
222
+ const ids = resolveCandidateIds(store, opts);
223
+ let rows = ids.map((id) => store.readEntitySnapshot(opts.type, id)).filter((row) => row !== null);
224
+ if (opts.where) rows = rows.filter(opts.where);
225
+ if (opts.sort) rows = [...rows].sort(opts.sort);
226
+ const projected = rows.map((row) => applySelection(projectRow(row, opts.include, store), opts.select));
227
+ if (opts.id) return projected[0] ?? null;
228
+ return projected;
229
+ }
230
+ var selectGraph = queryOnce;
231
+ function resolveCandidateIds(store, opts) {
232
+ if (opts.id) return [opts.id];
233
+ if (opts.ids) return opts.ids;
234
+ if (opts.listKey) return store.lists[opts.listKey]?.ids ?? [];
235
+ return Object.keys(store.entities[opts.type] ?? {});
236
+ }
237
+ function projectRow(row, include, store) {
238
+ if (!include) return row;
239
+ const projected = { ...row };
240
+ for (const [key, relation] of Object.entries(include)) {
241
+ const related = resolveRelation(row, relation, store);
242
+ projected[key] = related;
243
+ }
244
+ return projected;
245
+ }
246
+ function resolveRelation(entity, relation, store) {
247
+ const include = relation.include;
248
+ switch (relation.via.kind) {
249
+ case "field": {
250
+ const relatedId = entity[relation.via.field];
251
+ if (typeof relatedId !== "string") return null;
252
+ const related = store.readEntitySnapshot(relation.type, relatedId);
253
+ return related ? projectRow(related, include, store) : null;
254
+ }
255
+ case "array": {
256
+ const ids = entity[relation.via.field];
257
+ if (!Array.isArray(ids)) return [];
258
+ return ids.map((id) => typeof id === "string" ? store.readEntitySnapshot(relation.type, id) : null).filter((row) => row !== null).map((row) => projectRow(row, include, store));
259
+ }
260
+ case "list": {
261
+ const key = typeof relation.via.key === "function" ? relation.via.key(entity) : relation.via.key;
262
+ if (!key) return [];
263
+ const ids = store.lists[key]?.ids ?? [];
264
+ return ids.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
265
+ }
266
+ case "resolver": {
267
+ const resolved = relation.via.resolve(entity, store);
268
+ if (Array.isArray(resolved)) {
269
+ return resolved.map((id) => store.readEntitySnapshot(relation.type, id)).filter((row) => row !== null).map((row) => projectRow(row, include, store));
270
+ }
271
+ if (typeof resolved !== "string") return null;
272
+ const related = store.readEntitySnapshot(relation.type, resolved);
273
+ return related ? projectRow(related, include, store) : null;
274
+ }
275
+ }
276
+ }
277
+ function applySelection(row, select) {
278
+ if (!select) return row;
279
+ if (typeof select === "function") {
280
+ const result = select(row);
281
+ return result && typeof result === "object" ? result : { value: result };
282
+ }
283
+ const picked = {};
284
+ for (const key of select) {
285
+ if (key in row) picked[key] = row[key];
286
+ }
287
+ return picked;
288
+ }
289
+
290
+ // src/graph-actions.ts
291
+ var graphActionListeners = /* @__PURE__ */ new Set();
292
+ var graphActionReplayers = /* @__PURE__ */ new Map();
293
+ function createGraphTransaction() {
294
+ const baseline = cloneGraphData();
295
+ let closed = false;
296
+ const tx = {
297
+ upsertEntity(type, id, data) {
298
+ useGraphStore.getState().upsertEntity(type, id, data);
299
+ return tx;
300
+ },
301
+ replaceEntity(type, id, data) {
302
+ useGraphStore.getState().replaceEntity(type, id, data);
303
+ return tx;
304
+ },
305
+ removeEntity(type, id) {
306
+ useGraphStore.getState().removeEntity(type, id);
307
+ return tx;
308
+ },
309
+ patchEntity(type, id, patch) {
310
+ useGraphStore.getState().patchEntity(type, id, patch);
311
+ return tx;
312
+ },
313
+ clearPatch(type, id) {
314
+ useGraphStore.getState().clearPatch(type, id);
315
+ return tx;
316
+ },
317
+ insertIdInList(key, id, position) {
318
+ useGraphStore.getState().insertIdInList(key, id, position);
319
+ return tx;
320
+ },
321
+ removeIdFromAllLists(type, id) {
322
+ useGraphStore.getState().removeIdFromAllLists(type, id);
323
+ return tx;
324
+ },
325
+ setEntitySyncMetadata(type, id, metadata) {
326
+ useGraphStore.getState().setEntitySyncMetadata(type, id, metadata);
327
+ return tx;
328
+ },
329
+ markEntityPending(type, id, origin = "optimistic") {
330
+ useGraphStore.getState().setEntitySyncMetadata(type, id, {
331
+ synced: false,
332
+ origin,
333
+ updatedAt: Date.now()
334
+ });
335
+ return tx;
336
+ },
337
+ markEntitySynced(type, id, origin = "server") {
338
+ useGraphStore.getState().setEntitySyncMetadata(type, id, {
339
+ synced: true,
340
+ origin,
341
+ updatedAt: Date.now()
342
+ });
343
+ return tx;
344
+ },
345
+ commit() {
346
+ closed = true;
347
+ },
348
+ rollback() {
349
+ if (closed) return;
350
+ useGraphStore.setState(cloneGraphData(baseline));
351
+ closed = true;
352
+ },
353
+ snapshot() {
354
+ return cloneGraphData();
355
+ }
356
+ };
357
+ return tx;
358
+ }
359
+ function createGraphAction(opts) {
360
+ if (opts.key) {
361
+ graphActionReplayers.set(opts.key, async (record) => {
362
+ const tx = createGraphTransaction();
363
+ try {
364
+ const result = await opts.run(tx, record.input);
365
+ tx.commit();
366
+ return result;
367
+ } catch (error) {
368
+ tx.rollback();
369
+ throw error;
370
+ }
371
+ });
372
+ }
373
+ return async (input) => {
374
+ const tx = createGraphTransaction();
375
+ const record = opts.key ? {
376
+ id: `${opts.key}:${Date.now()}`,
377
+ key: opts.key,
378
+ input: structuredClone(input),
379
+ enqueuedAt: (/* @__PURE__ */ new Date()).toISOString()
380
+ } : null;
381
+ try {
382
+ if (record) emitGraphActionEvent({ type: "enqueued", record });
383
+ opts.optimistic?.(tx, input);
384
+ const result = await opts.run(tx, input);
385
+ opts.onSuccess?.(result, input, tx);
386
+ tx.commit();
387
+ if (record) emitGraphActionEvent({ type: "settled", record });
388
+ return result;
389
+ } catch (error) {
390
+ tx.rollback();
391
+ const normalized = error instanceof Error ? error : new Error(String(error));
392
+ if (record) emitGraphActionEvent({ type: "settled", record });
393
+ opts.onError?.(normalized, input);
394
+ throw normalized;
395
+ }
396
+ };
397
+ }
398
+ function subscribeGraphActionEvents(listener) {
399
+ graphActionListeners.add(listener);
400
+ return () => graphActionListeners.delete(listener);
401
+ }
402
+ async function replayRegisteredGraphAction(record) {
403
+ const replayer = graphActionReplayers.get(record.key);
404
+ if (!replayer) throw new Error(`No graph action registered for key "${record.key}"`);
405
+ return replayer(record);
406
+ }
407
+ function cloneGraphData(source = useGraphStore.getState()) {
408
+ return {
409
+ entities: structuredClone(source.entities),
410
+ patches: structuredClone(source.patches),
411
+ entityStates: structuredClone(source.entityStates),
412
+ syncMetadata: structuredClone(source.syncMetadata),
413
+ lists: structuredClone(source.lists)
414
+ };
415
+ }
416
+ function emitGraphActionEvent(event) {
417
+ for (const listener of graphActionListeners) listener(event);
418
+ }
419
+
420
+ // src/graph-effects.ts
421
+ function createGraphEffect(opts) {
422
+ const getKey = opts.getKey ?? defaultGetKey;
423
+ const isEqual = opts.isEqual ?? defaultIsEqual;
424
+ let initialized = false;
425
+ let previous = /* @__PURE__ */ new Map();
426
+ const evaluate = () => {
427
+ const nextValues = normalizeQueryResult(opts.query());
428
+ const next = /* @__PURE__ */ new Map();
429
+ nextValues.forEach((value, index) => {
430
+ next.set(getKey(value, index), value);
431
+ });
432
+ if (!initialized) {
433
+ initialized = true;
434
+ previous = next;
435
+ if (opts.skipInitial) return;
436
+ }
437
+ for (const [key, value] of next.entries()) {
438
+ const previousValue = previous.get(key);
439
+ if (previousValue === void 0) {
440
+ opts.onEnter?.({ key, value });
441
+ continue;
442
+ }
443
+ if (!isEqual(previousValue, value)) {
444
+ opts.onUpdate?.({ key, value, previousValue });
445
+ }
446
+ }
447
+ for (const [key, previousValue] of previous.entries()) {
448
+ if (!next.has(key)) opts.onExit?.({ key, previousValue });
449
+ }
450
+ previous = next;
451
+ };
452
+ evaluate();
453
+ const unsubscribe = useGraphStore.subscribe(() => {
454
+ evaluate();
455
+ });
456
+ return {
457
+ dispose: () => {
458
+ unsubscribe();
459
+ }
460
+ };
461
+ }
462
+ function normalizeQueryResult(value) {
463
+ if (value == null) return [];
464
+ return Array.isArray(value) ? value : [value];
465
+ }
466
+ function defaultGetKey(value, index) {
467
+ if (value && typeof value === "object") {
468
+ const record = value;
469
+ if (typeof record.id === "string") return record.id;
470
+ if (typeof record.$key === "string") return record.$key;
471
+ }
472
+ return String(index);
473
+ }
474
+ function defaultIsEqual(previousValue, nextValue) {
475
+ return JSON.stringify(previousValue) === JSON.stringify(nextValue);
476
+ }
477
+
478
+ // src/object-path.ts
479
+ function isObject(value) {
480
+ return typeof value === "object" && value !== null && !Array.isArray(value);
481
+ }
482
+ function getValueAtPath(source, path) {
483
+ if (!path) return source;
484
+ const segments = path.split(".").filter(Boolean);
485
+ let current = source;
486
+ for (const segment of segments) {
487
+ if (!isObject(current) && !Array.isArray(current)) return void 0;
488
+ current = current[segment];
489
+ }
490
+ return current;
491
+ }
492
+ function setValueAtPath(source, path, value) {
493
+ const segments = path.split(".").filter(Boolean);
494
+ if (segments.length === 0) return source;
495
+ const clone = structuredClone(source);
496
+ let current = clone;
497
+ for (let index = 0; index < segments.length - 1; index += 1) {
498
+ const segment = segments[index];
499
+ const next = current[segment];
500
+ if (!isObject(next)) current[segment] = {};
501
+ current = current[segment];
502
+ }
503
+ current[segments[segments.length - 1]] = value;
504
+ return clone;
505
+ }
506
+ function collectDirtyPaths(current, original, prefix = "", acc = /* @__PURE__ */ new Set()) {
507
+ if (isObject(current) && isObject(original)) {
508
+ const keys = /* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(original)]);
509
+ for (const key of keys) {
510
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
511
+ collectDirtyPaths(current[key], original[key], nextPrefix, acc);
512
+ }
513
+ return acc;
514
+ }
515
+ if (JSON.stringify(current) !== JSON.stringify(original) && prefix) acc.add(prefix);
516
+ return acc;
517
+ }
518
+ var schemaRegistry = /* @__PURE__ */ new Map();
519
+ function registerEntityJsonSchema(config) {
520
+ const key = registryKey(config.entityType, config.field, config.schemaId);
521
+ schemaRegistry.set(key, config);
522
+ }
523
+ function registerRuntimeSchema(config) {
524
+ registerEntityJsonSchema(config);
525
+ }
526
+ function getEntityJsonSchema(opts) {
527
+ const exact = schemaRegistry.get(registryKey(opts.entityType, opts.field, opts.schemaId));
528
+ if (exact) return exact;
529
+ if (opts.field) {
530
+ const byField = schemaRegistry.get(registryKey(opts.entityType, opts.field));
531
+ if (byField) return byField;
532
+ }
533
+ if (opts.schemaId) {
534
+ const byId = schemaRegistry.get(registryKey(opts.entityType, void 0, opts.schemaId));
535
+ if (byId) return byId;
536
+ }
537
+ for (const schema of schemaRegistry.values()) {
538
+ if (schema.entityType !== opts.entityType) continue;
539
+ if (opts.field && schema.field !== opts.field) continue;
540
+ return schema;
541
+ }
542
+ return null;
543
+ }
544
+ function useSchemaEntityFields(opts) {
545
+ return useMemo(() => {
546
+ const schema = opts.schema ?? getEntityJsonSchema(opts)?.schema;
547
+ if (!schema) return [];
548
+ return buildEntityFieldsFromSchema({ schema, rootField: opts.rootField ?? opts.field });
549
+ }, [opts.entityType, opts.field, opts.rootField, opts.schemaId, opts.schema]);
550
+ }
551
+ function buildEntityFieldsFromSchema(opts) {
552
+ return buildSchemaFields(opts.schema, opts.rootField ?? "", "");
553
+ }
554
+ function exportGraphSnapshotWithSchemas(opts) {
555
+ return JSON.stringify(
556
+ {
557
+ scope: opts.scope,
558
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
559
+ data: opts.data,
560
+ schemas: opts.schemas.filter(Boolean)
561
+ },
562
+ null,
563
+ opts.pretty === false ? 0 : 2
564
+ );
565
+ }
566
+ function escapeHtml(input) {
567
+ return input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
568
+ }
569
+ function renderMarkdownToHtml(value) {
570
+ const escaped = escapeHtml(value);
571
+ const blocks = escaped.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
572
+ return blocks.map((block) => renderMarkdownBlock(block)).join("");
573
+ }
574
+ function MarkdownFieldRenderer({ value, className }) {
575
+ return /* @__PURE__ */ jsx(
576
+ "div",
577
+ {
578
+ className,
579
+ dangerouslySetInnerHTML: { __html: renderMarkdownToHtml(value ?? "") }
580
+ }
581
+ );
582
+ }
583
+ function MarkdownFieldEditor({
584
+ value,
585
+ onChange,
586
+ placeholder
587
+ }) {
588
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
589
+ /* @__PURE__ */ jsx(
590
+ "textarea",
591
+ {
592
+ value,
593
+ onChange: (event) => onChange(event.target.value),
594
+ placeholder,
595
+ className: "w-full min-h-[120px] rounded-md border bg-muted/50 px-3 py-2 text-sm resize-y focus:outline-none focus:ring-1 focus:ring-ring transition-colors"
596
+ }
597
+ ),
598
+ /* @__PURE__ */ jsx("div", { className: "rounded-md border bg-background px-3 py-2", children: /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value, className: "prose prose-sm max-w-none" }) })
599
+ ] });
600
+ }
601
+ function createMarkdownDetailRenderer(field) {
602
+ return (value, entity) => /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value: String(value ?? getValueAtPath(entity, field) ?? ""), className: "prose prose-sm max-w-none" });
603
+ }
604
+ function buildSchemaFields(schema, pathPrefix, schemaPathPrefix) {
605
+ if (schema.type === "object" && schema.properties) {
606
+ const entries = Object.entries(schema.properties).sort(([, left], [, right]) => {
607
+ const l = left["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
608
+ const r = right["x-display-order"] ?? Number.MAX_SAFE_INTEGER;
609
+ return l - r;
610
+ });
611
+ return entries.flatMap(([key, childSchema]) => {
612
+ if (childSchema["x-hidden"]) return [];
613
+ const field = pathPrefix ? `${pathPrefix}.${key}` : key;
614
+ const schemaPath = schemaPathPrefix ? `${schemaPathPrefix}.${key}` : key;
615
+ if (childSchema.type === "object" && childSchema.properties) {
616
+ return buildSchemaFields(childSchema, field, schemaPath);
617
+ }
618
+ return [schemaField(field, schemaPath, childSchema, schema.required?.includes(key) ?? false)];
619
+ });
620
+ }
621
+ return [];
622
+ }
623
+ function schemaField(field, schemaPath, schema, required) {
624
+ const type = inferFieldType(schema);
625
+ const descriptor = {
626
+ field,
627
+ label: schema.title ?? humanize(field.split(".").pop() ?? field),
628
+ type,
629
+ required,
630
+ hint: schema.description,
631
+ schemaPath,
632
+ schema,
633
+ componentHint: schema["x-a2ui-component"]
634
+ };
635
+ if (schema.enum) {
636
+ descriptor.options = schema.enum.map((value) => ({
637
+ value: String(value),
638
+ label: String(value)
639
+ }));
640
+ }
641
+ if (type === "markdown") {
642
+ descriptor.render = createMarkdownDetailRenderer(field);
643
+ }
644
+ return descriptor;
645
+ }
646
+ function inferFieldType(schema) {
647
+ const forced = schema["x-field-type"];
648
+ if (forced === "markdown") return "markdown";
649
+ if (schema.format === "markdown") return "markdown";
650
+ if (schema.enum) return "enum";
651
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
652
+ switch (type) {
653
+ case "boolean":
654
+ return "boolean";
655
+ case "integer":
656
+ case "number":
657
+ return "number";
658
+ case "string":
659
+ if (schema.format === "email") return "email";
660
+ if (schema.format === "uri" || schema.format === "url") return "url";
661
+ if (schema.format === "date" || schema.format === "date-time") return "date";
662
+ return "text";
663
+ case "array":
664
+ case "object":
665
+ return "json";
666
+ default:
667
+ return "text";
668
+ }
669
+ }
670
+ function registryKey(entityType, field, schemaId) {
671
+ return `${entityType}::${field ?? "*"}::${schemaId ?? "*"}`;
672
+ }
673
+ function humanize(value) {
674
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
675
+ }
676
+ function renderMarkdownBlock(block) {
677
+ if (block.startsWith("# ")) return `<h1>${renderInlineMarkdown(block.slice(2))}</h1>`;
678
+ if (block.startsWith("## ")) return `<h2>${renderInlineMarkdown(block.slice(3))}</h2>`;
679
+ return `<p>${renderInlineMarkdown(block).replaceAll("\n", "<br/>")}</p>`;
680
+ }
681
+ function renderInlineMarkdown(block) {
682
+ return block.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
683
+ }
684
+
685
+ // src/ai-interop.ts
686
+ function exportGraphSnapshot(opts) {
687
+ const payload = {
688
+ scope: opts.scope,
689
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
690
+ data: opts.data
691
+ };
692
+ return JSON.stringify(payload, null, opts.pretty === false ? 0 : 2);
693
+ }
694
+ function createGraphTool(handler) {
695
+ return (input) => handler(input, {
696
+ store: useGraphStore.getState(),
697
+ queryOnce,
698
+ exportGraphSnapshot
699
+ });
700
+ }
701
+ function createSchemaGraphTool(handler) {
702
+ return (input) => handler(input, {
703
+ store: useGraphStore.getState(),
704
+ queryOnce,
705
+ exportGraphSnapshot,
706
+ getEntityJsonSchema,
707
+ exportGraphSnapshotWithSchemas
708
+ });
709
+ }
710
+ var DEFAULT_STORAGE_KEY = "prometheus:graph";
711
+ var useGraphSyncStatusStore = create((set) => ({
712
+ status: {
713
+ phase: "idle",
714
+ isOnline: true,
715
+ isSynced: true,
716
+ pendingActions: 0,
717
+ lastHydratedAt: null,
718
+ lastPersistedAt: null,
719
+ storageKey: null,
720
+ error: null
721
+ },
722
+ setStatus: (status) => set((state) => ({
723
+ status: {
724
+ ...state.status,
725
+ ...status
726
+ }
727
+ }))
728
+ }));
729
+ var pendingActions = /* @__PURE__ */ new Map();
730
+ function useGraphSyncStatus() {
731
+ return useGraphSyncStatusStore((state) => state.status);
732
+ }
733
+ async function persistGraphToStorage(opts) {
734
+ const payload = {
735
+ version: 1,
736
+ snapshot: cloneGraphSnapshot(),
737
+ pendingActions: opts.pendingActions ?? Array.from(pendingActions.values())
738
+ };
739
+ const json = JSON.stringify(payload);
740
+ await opts.storage.set(opts.key, json);
741
+ const persistedAt = (/* @__PURE__ */ new Date()).toISOString();
742
+ useGraphSyncStatusStore.getState().setStatus({
743
+ lastPersistedAt: persistedAt,
744
+ storageKey: opts.key,
745
+ pendingActions: payload.pendingActions.length
746
+ });
747
+ return {
748
+ ok: true,
749
+ key: opts.key,
750
+ bytes: json.length,
751
+ persistedAt
752
+ };
753
+ }
754
+ async function hydrateGraphFromStorage(opts) {
755
+ const raw = await opts.storage.get(opts.key);
756
+ if (!raw) {
757
+ return {
758
+ ok: false,
759
+ key: opts.key,
760
+ hydratedAt: null,
761
+ entityCounts: {},
762
+ error: "No persisted graph snapshot found"
763
+ };
764
+ }
765
+ try {
766
+ const parsed = JSON.parse(raw);
767
+ useGraphStore.setState(parsed.snapshot);
768
+ pendingActions.clear();
769
+ for (const action of parsed.pendingActions ?? []) pendingActions.set(action.id, action);
770
+ const hydratedAt = (/* @__PURE__ */ new Date()).toISOString();
771
+ useGraphSyncStatusStore.getState().setStatus({
772
+ lastHydratedAt: hydratedAt,
773
+ storageKey: opts.key,
774
+ pendingActions: pendingActions.size,
775
+ error: null
776
+ });
777
+ return {
778
+ ok: true,
779
+ key: opts.key,
780
+ hydratedAt,
781
+ entityCounts: Object.fromEntries(
782
+ Object.entries(parsed.snapshot.entities).map(([type, entities]) => [type, Object.keys(entities).length])
783
+ ),
784
+ pendingActions: Array.from(pendingActions.values())
785
+ };
786
+ } catch (error) {
787
+ const message = error instanceof Error ? error.message : String(error);
788
+ useGraphSyncStatusStore.getState().setStatus({
789
+ phase: "error",
790
+ error: message,
791
+ storageKey: opts.key
792
+ });
793
+ return {
794
+ ok: false,
795
+ key: opts.key,
796
+ hydratedAt: null,
797
+ entityCounts: {},
798
+ error: message
799
+ };
800
+ }
801
+ }
802
+ function startLocalFirstGraph(opts) {
803
+ const key = opts.key ?? DEFAULT_STORAGE_KEY;
804
+ const persistDebounceMs = opts.persistDebounceMs ?? 50;
805
+ const statusStore = useGraphSyncStatusStore.getState();
806
+ statusStore.setStatus({
807
+ phase: "hydrating",
808
+ storageKey: key,
809
+ isOnline: opts.onlineSource?.getIsOnline() ?? getDefaultOnlineSource().getIsOnline(),
810
+ isSynced: pendingActions.size === 0,
811
+ error: null
812
+ });
813
+ let persistTimer = null;
814
+ const schedulePersist = () => {
815
+ if (persistTimer) clearTimeout(persistTimer);
816
+ persistTimer = setTimeout(() => {
817
+ void persistGraphToStorage({ storage: opts.storage, key });
818
+ }, persistDebounceMs);
819
+ };
820
+ const graphUnsub = useGraphStore.subscribe(() => {
821
+ schedulePersist();
822
+ });
823
+ const actionUnsub = subscribeGraphActionEvents((event) => {
824
+ if (event.type === "enqueued") pendingActions.set(event.record.id, event.record);
825
+ if (event.type === "settled") pendingActions.delete(event.record.id);
826
+ useGraphSyncStatusStore.getState().setStatus({
827
+ pendingActions: pendingActions.size,
828
+ isSynced: pendingActions.size === 0
829
+ });
830
+ schedulePersist();
831
+ });
832
+ const onlineSource = opts.onlineSource ?? getDefaultOnlineSource();
833
+ const onlineUnsub = onlineSource.subscribe((online) => {
834
+ useGraphSyncStatusStore.getState().setStatus({
835
+ isOnline: online,
836
+ phase: online ? "ready" : "offline"
837
+ });
838
+ });
839
+ const ready = (async () => {
840
+ const hydrated = await hydrateGraphFromStorage({ storage: opts.storage, key });
841
+ if (opts.replayPendingActions && hydrated.ok && pendingActions.size > 0) {
842
+ useGraphSyncStatusStore.getState().setStatus({
843
+ phase: "syncing",
844
+ isSynced: false
845
+ });
846
+ for (const action of Array.from(pendingActions.values())) {
847
+ await replayRegisteredGraphAction(action);
848
+ pendingActions.delete(action.id);
849
+ }
850
+ await persistGraphToStorage({ storage: opts.storage, key });
851
+ }
852
+ const online = onlineSource.getIsOnline();
853
+ useGraphSyncStatusStore.getState().setStatus({
854
+ phase: online ? "ready" : "offline",
855
+ isOnline: online,
856
+ isSynced: pendingActions.size === 0,
857
+ pendingActions: pendingActions.size
858
+ });
859
+ })();
860
+ return {
861
+ ready,
862
+ dispose() {
863
+ graphUnsub();
864
+ actionUnsub();
865
+ onlineUnsub();
866
+ if (persistTimer) clearTimeout(persistTimer);
867
+ },
868
+ async persistNow() {
869
+ await persistGraphToStorage({ storage: opts.storage, key });
870
+ },
871
+ hydrate() {
872
+ return hydrateGraphFromStorage({ storage: opts.storage, key });
873
+ },
874
+ getStatus() {
875
+ return useGraphSyncStatusStore.getState().status;
876
+ }
877
+ };
878
+ }
879
+ function cloneGraphSnapshot() {
880
+ const state = useGraphStore.getState();
881
+ return {
882
+ entities: structuredClone(state.entities),
883
+ patches: structuredClone(state.patches),
884
+ entityStates: structuredClone(state.entityStates),
885
+ syncMetadata: structuredClone(state.syncMetadata),
886
+ lists: structuredClone(state.lists)
887
+ };
888
+ }
889
+ function getDefaultOnlineSource() {
890
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
891
+ return {
892
+ getIsOnline: () => window.navigator.onLine,
893
+ subscribe: (listener) => {
894
+ const onlineHandler = () => listener(true);
895
+ const offlineHandler = () => listener(false);
896
+ window.addEventListener("online", onlineHandler);
897
+ window.addEventListener("offline", offlineHandler);
898
+ return () => {
899
+ window.removeEventListener("online", onlineHandler);
900
+ window.removeEventListener("offline", offlineHandler);
901
+ };
902
+ }
903
+ };
904
+ }
905
+ return {
906
+ getIsOnline: () => true,
907
+ subscribe: () => () => {
908
+ }
909
+ };
910
+ }
911
+
181
912
  // src/engine.ts
182
913
  function serializeKey(key) {
183
914
  return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
@@ -445,10 +1176,7 @@ function useEntity(opts) {
445
1176
  normalizeRef.current = opts.normalize;
446
1177
  const data = useStore(useGraphStore, useShallow((state) => {
447
1178
  if (!id) return null;
448
- const base = state.entities[type]?.[id];
449
- if (!base) return null;
450
- const patch = state.patches[type]?.[id];
451
- return patch ? { ...base, ...patch } : base;
1179
+ return state.readEntitySnapshot(type, id);
452
1180
  }));
453
1181
  const entityState = useStore(useGraphStore, useCallback(
454
1182
  (state) => state.entityStates[`${type}:${id}`] ?? EMPTY_ENTITY_STATE,
@@ -486,12 +1214,7 @@ function useEntityList(opts) {
486
1214
  useGraphStore,
487
1215
  useShallow((state) => {
488
1216
  const ids = state.lists[key]?.ids ?? EMPTY_IDS;
489
- return ids.map((id) => {
490
- const base = state.entities[type]?.[id];
491
- if (!base) return null;
492
- const patch = state.patches[type]?.[id];
493
- return patch ? { ...base, ...patch } : base;
494
- }).filter((x) => x !== null);
1217
+ return ids.map((id) => state.readEntitySnapshot(type, id)).filter((x) => x !== null);
495
1218
  })
496
1219
  );
497
1220
  const doFetch = useCallback((params = {}) => {
@@ -528,18 +1251,28 @@ function useEntityMutation(opts) {
528
1251
  const { id, patch } = opt;
529
1252
  const store = useGraphStore.getState();
530
1253
  const previous = { ...store.patches[type]?.[id] };
1254
+ const previousSync = store.syncMetadata[`${type}:${id}`];
531
1255
  store.patchEntity(type, id, patch);
532
- rollback = () => Object.keys(previous).length > 0 ? useGraphStore.getState().patchEntity(type, id, previous) : useGraphStore.getState().clearPatch(type, id);
1256
+ store.setEntitySyncMetadata(type, id, { synced: false, origin: "optimistic", updatedAt: Date.now() });
1257
+ rollback = () => {
1258
+ const currentStore = useGraphStore.getState();
1259
+ if (Object.keys(previous).length > 0) currentStore.patchEntity(type, id, previous);
1260
+ else currentStore.clearPatch(type, id);
1261
+ if (previousSync) currentStore.setEntitySyncMetadata(type, id, previousSync);
1262
+ else currentStore.clearEntitySyncMetadata(type, id);
1263
+ };
533
1264
  }
534
1265
  }
535
1266
  try {
536
1267
  const result = await apiFn(input);
537
1268
  if (normalize) {
538
1269
  const { id, data } = normalize(result, input);
539
- useGraphStore.getState().upsertEntity(type, id, data);
1270
+ const store = useGraphStore.getState();
1271
+ store.upsertEntity(type, id, data);
1272
+ store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
540
1273
  if (optimistic) {
541
1274
  const opt = optimistic(input);
542
- if (opt) useGraphStore.getState().clearPatch(type, opt.id);
1275
+ if (opt) store.clearPatch(type, opt.id);
543
1276
  }
544
1277
  }
545
1278
  if (invalidateLists) for (const k of invalidateLists) useGraphStore.getState().invalidateLists(k);
@@ -1041,12 +1774,7 @@ function useEntityView(opts) {
1041
1774
  useShallow((state) => {
1042
1775
  const list = state.lists[baseKey] ?? EMPTY_LIST_STATE;
1043
1776
  const sourceIds = completenessMode !== "remote" && remoteResultKey ? state.lists[remoteResultKey]?.ids ?? EMPTY_IDS : list.ids;
1044
- const getEntity = (id) => {
1045
- const base = state.entities[type]?.[id];
1046
- if (!base) return null;
1047
- const patch = state.patches[type]?.[id];
1048
- return patch ? { ...base, ...patch } : base;
1049
- };
1777
+ const getEntity = (id) => state.readEntitySnapshot(type, id);
1050
1778
  return applyView(
1051
1779
  sourceIds,
1052
1780
  getEntity,
@@ -1059,12 +1787,7 @@ function useEntityView(opts) {
1059
1787
  const items = useStore(
1060
1788
  useGraphStore,
1061
1789
  useShallow(
1062
- (state) => localViewIds.map((id) => {
1063
- const base = state.entities[type]?.[id];
1064
- if (!base) return null;
1065
- const patch = state.patches[type]?.[id];
1066
- return patch ? { ...base, ...patch } : base;
1067
- }).filter((item) => item !== null)
1790
+ (state) => localViewIds.map((id) => state.readEntitySnapshot(type, id)).filter((item) => item !== null)
1068
1791
  )
1069
1792
  );
1070
1793
  const fireRemoteFetch = useCallback(async (view, cursor) => {
@@ -1119,17 +1842,11 @@ function useEntityView(opts) {
1119
1842
  const isPresent = id in newEntities;
1120
1843
  if (!isPresent) continue;
1121
1844
  const entity = newEntities[id];
1122
- const patch = store.patches[type]?.[id];
1123
- const merged = patch ? { ...entity, ...patch } : entity;
1845
+ const merged = store.readEntitySnapshot(type, id) ?? entity;
1124
1846
  const matches = (!view.filter || matchesFilter(merged, view.filter)) && (!view.search?.query || matchesSearch(merged, view.search.query, view.search.fields));
1125
1847
  if (matches && !list.ids.includes(id)) {
1126
1848
  if (view.sort && view.sort.length > 0) {
1127
- const idx = findInsertionIndex(merged, list.ids, (eid) => {
1128
- const b = store.entities[type]?.[eid];
1129
- if (!b) return null;
1130
- const p = store.patches[type]?.[eid];
1131
- return p ? { ...b, ...p } : b;
1132
- }, view.sort);
1849
+ const idx = findInsertionIndex(merged, list.ids, (eid) => store.readEntitySnapshot(type, eid), view.sort);
1133
1850
  store.insertIdInList(baseKey, id, idx);
1134
1851
  } else store.insertIdInList(baseKey, id, "start");
1135
1852
  }
@@ -1172,15 +1889,15 @@ function useEntityView(opts) {
1172
1889
  }
1173
1890
 
1174
1891
  // src/crud/relations.ts
1175
- var schemaRegistry = /* @__PURE__ */ new Map();
1892
+ var schemaRegistry2 = /* @__PURE__ */ new Map();
1176
1893
  function registerSchema(schema) {
1177
- schemaRegistry.set(schema.type, schema);
1894
+ schemaRegistry2.set(schema.type, schema);
1178
1895
  }
1179
1896
  function getSchema(type) {
1180
- return schemaRegistry.get(type) ?? null;
1897
+ return schemaRegistry2.get(type) ?? null;
1181
1898
  }
1182
1899
  function cascadeInvalidation(ctx) {
1183
- const schema = schemaRegistry.get(ctx.type);
1900
+ const schema = schemaRegistry2.get(ctx.type);
1184
1901
  if (!schema) return;
1185
1902
  const store = useGraphStore.getState();
1186
1903
  if (schema.globalListKeys) for (const key of schema.globalListKeys) store.invalidateLists(key);
@@ -1207,7 +1924,7 @@ function cascadeInvalidation(ctx) {
1207
1924
  }
1208
1925
  }
1209
1926
  }
1210
- for (const [, otherSchema] of schemaRegistry) {
1927
+ for (const [, otherSchema] of schemaRegistry2) {
1211
1928
  if (!otherSchema.relations) continue;
1212
1929
  for (const [, rel] of Object.entries(otherSchema.relations)) {
1213
1930
  if (rel.targetType !== ctx.type) continue;
@@ -1216,7 +1933,7 @@ function cascadeInvalidation(ctx) {
1216
1933
  }
1217
1934
  }
1218
1935
  function readRelations(type, entity) {
1219
- const schema = schemaRegistry.get(type);
1936
+ const schema = schemaRegistry2.get(type);
1220
1937
  if (!schema?.relations) return {};
1221
1938
  const store = useGraphStore.getState();
1222
1939
  const result = {};
@@ -1277,7 +1994,7 @@ function useEntityCRUD(opts) {
1277
1994
  useEffect(() => {
1278
1995
  if (detail) setEditBuffer({ ...detail });
1279
1996
  }, [selectedId]);
1280
- const setField = useCallback((field, value) => setEditBuffer((prev) => ({ ...prev, [field]: value })), []);
1997
+ const setField = useCallback((field, value) => setEditBuffer((prev) => setValueAtPath(prev, String(field), value)), []);
1281
1998
  const setFields = useCallback((fields) => setEditBuffer((prev) => ({ ...prev, ...fields })), []);
1282
1999
  const resetBuffer = useCallback(() => {
1283
2000
  const current = selectedId ? useGraphStore.getState().readEntity(type, selectedId) : null;
@@ -1285,10 +2002,7 @@ function useEntityCRUD(opts) {
1285
2002
  }, [type, selectedId]);
1286
2003
  const dirty = useMemo(() => {
1287
2004
  if (!detail) return { changed: /* @__PURE__ */ new Set(), isDirty: false };
1288
- const changed = /* @__PURE__ */ new Set();
1289
- for (const key of Object.keys(editBuffer)) {
1290
- if (JSON.stringify(editBuffer[key]) !== JSON.stringify(detail[key])) changed.add(key);
1291
- }
2005
+ const changed = collectDirtyPaths(editBuffer, detail);
1292
2006
  return { changed, isDirty: changed.size > 0 };
1293
2007
  }, [editBuffer, detail]);
1294
2008
  const startEdit = useCallback((id) => {
@@ -1307,25 +2021,33 @@ function useEntityCRUD(opts) {
1307
2021
  }, [resetBuffer, selectedId]);
1308
2022
  const applyOptimistic = useCallback(() => {
1309
2023
  if (!selectedId) return;
1310
- useGraphStore.getState().patchEntity(type, selectedId, editBuffer);
2024
+ const store = useGraphStore.getState();
2025
+ store.patchEntity(type, selectedId, editBuffer);
2026
+ store.setEntitySyncMetadata(type, selectedId, { synced: false, origin: "optimistic", updatedAt: Date.now() });
1311
2027
  }, [type, selectedId, editBuffer]);
1312
2028
  const save = useCallback(async () => {
1313
2029
  if (!selectedId || !onUpdate) return null;
1314
2030
  setIsSaving(true);
1315
2031
  setSaveError(null);
1316
- const previous = useGraphStore.getState().readEntity(type, selectedId);
1317
- useGraphStore.getState().upsertEntity(type, selectedId, editBuffer);
2032
+ const store = useGraphStore.getState();
2033
+ const previous = store.readEntity(type, selectedId);
2034
+ const previousSync = store.syncMetadata[`${type}:${selectedId}`];
2035
+ store.upsertEntity(type, selectedId, editBuffer);
2036
+ store.setEntitySyncMetadata(type, selectedId, { synced: false, origin: "optimistic", updatedAt: Date.now() });
1318
2037
  try {
1319
2038
  const result = await onUpdate(selectedId, editBuffer);
1320
2039
  const { id, data } = normalize(result);
1321
- useGraphStore.getState().replaceEntity(type, id, data);
1322
- useGraphStore.getState().clearPatch(type, id);
2040
+ store.replaceEntity(type, id, data);
2041
+ store.clearPatch(type, id);
2042
+ store.setEntitySyncMetadata(type, id, { synced: true, origin: "server", updatedAt: Date.now() });
1323
2043
  cascadeInvalidation({ type, id: selectedId, previous, next: data, op: "update" });
1324
2044
  setMode("detail");
1325
2045
  optsRef.current.onUpdateSuccess?.(result);
1326
2046
  return result;
1327
2047
  } catch (err) {
1328
- if (previous) useGraphStore.getState().replaceEntity(type, selectedId, previous);
2048
+ if (previous) store.replaceEntity(type, selectedId, previous);
2049
+ if (previousSync) store.setEntitySyncMetadata(type, selectedId, previousSync);
2050
+ else store.clearEntitySyncMetadata(type, selectedId);
1329
2051
  const error = err instanceof Error ? err : new Error(String(err));
1330
2052
  setSaveError(error.message);
1331
2053
  optsRef.current.onError?.("update", error);
@@ -1337,7 +2059,7 @@ function useEntityCRUD(opts) {
1337
2059
  const [createBuffer, setCreateBuffer] = useState({ ...createDefaults });
1338
2060
  const [isCreating, setIsCreating] = useState(false);
1339
2061
  const [createError, setCreateError] = useState(null);
1340
- const setCreateField = useCallback((field, value) => setCreateBuffer((prev) => ({ ...prev, [field]: value })), []);
2062
+ const setCreateField = useCallback((field, value) => setCreateBuffer((prev) => setValueAtPath(prev, String(field), value)), []);
1341
2063
  const setCreateFields = useCallback((fields) => setCreateBuffer((prev) => ({ ...prev, ...fields })), []);
1342
2064
  const resetCreateBuffer = useCallback(() => setCreateBuffer({ ...optsRef.current.createDefaults ?? {} }), []);
1343
2065
  const startCreate = useCallback(() => {
@@ -1350,21 +2072,23 @@ function useEntityCRUD(opts) {
1350
2072
  setMode("list");
1351
2073
  setCreateError(null);
1352
2074
  }, [resetCreateBuffer]);
1353
- const create2 = useCallback(async () => {
2075
+ const create3 = useCallback(async () => {
1354
2076
  if (!onCreate) return null;
1355
2077
  setIsCreating(true);
1356
2078
  setCreateError(null);
1357
2079
  const tempId = `__temp__${Date.now()}`;
1358
2080
  const optimisticData = { ...createBuffer, id: tempId, _optimistic: true };
1359
- useGraphStore.getState().upsertEntity(type, tempId, optimisticData);
1360
- useGraphStore.getState().insertIdInList(serializeKey(listQueryKey), tempId, "start");
2081
+ const store = useGraphStore.getState();
2082
+ store.upsertEntity(type, tempId, optimisticData);
2083
+ store.setEntitySyncMetadata(type, tempId, { synced: false, origin: "optimistic", updatedAt: Date.now() });
2084
+ store.insertIdInList(serializeKey(listQueryKey), tempId, "start");
1361
2085
  try {
1362
2086
  const result = await onCreate(createBuffer);
1363
2087
  const { id: realId, data } = normalize(result);
1364
- useGraphStore.getState().removeEntity(type, tempId);
1365
- useGraphStore.getState().upsertEntity(type, realId, data);
1366
- useGraphStore.getState().setEntityFetched(type, realId);
1367
- const store = useGraphStore.getState();
2088
+ store.removeEntity(type, tempId);
2089
+ store.upsertEntity(type, realId, data);
2090
+ store.setEntityFetched(type, realId);
2091
+ store.setEntitySyncMetadata(type, realId, { synced: true, origin: "server", updatedAt: Date.now() });
1368
2092
  for (const key of Object.keys(store.lists)) {
1369
2093
  const list2 = store.lists[key];
1370
2094
  const idx = list2.ids.indexOf(tempId);
@@ -1382,8 +2106,8 @@ function useEntityCRUD(opts) {
1382
2106
  optsRef.current.onCreateSuccess?.(result);
1383
2107
  return result;
1384
2108
  } catch (err) {
1385
- useGraphStore.getState().removeEntity(type, tempId);
1386
- useGraphStore.getState().removeIdFromAllLists(type, tempId);
2109
+ store.removeEntity(type, tempId);
2110
+ store.removeIdFromAllLists(type, tempId);
1387
2111
  const error = err instanceof Error ? err : new Error(String(err));
1388
2112
  setCreateError(error.message);
1389
2113
  optsRef.current.onError?.("create", error);
@@ -1422,7 +2146,7 @@ function useEntityCRUD(opts) {
1422
2146
  setIsDeleting(false);
1423
2147
  }
1424
2148
  }, [type, selectedId, listQueryKey, clearSelectionAfterDelete]);
1425
- return { mode, setMode, list, selectedId, select, openDetail, detail: detail ?? null, detailIsLoading, detailError: detailError ?? null, relations, editBuffer, setField, setFields, resetBuffer, dirty, startEdit, cancelEdit, save, isSaving, saveError, applyOptimistic, createBuffer, setCreateField, setCreateFields, resetCreateBuffer, startCreate, cancelCreate, create: create2, isCreating, createError, deleteEntity, isDeleting, deleteError, isEditing: mode === "edit" || mode === "create" };
2149
+ return { mode, setMode, list, selectedId, select, openDetail, detail: detail ?? null, detailIsLoading, detailError: detailError ?? null, relations, editBuffer, setField, setFields, resetBuffer, dirty, startEdit, cancelEdit, save, isSaving, saveError, applyOptimistic, createBuffer, setCreateField, setCreateFields, resetCreateBuffer, startCreate, cancelCreate, create: create3, isCreating, createError, deleteEntity, isDeleting, deleteError, isEditing: mode === "edit" || mode === "create" };
1426
2150
  }
1427
2151
 
1428
2152
  // src/adapters/realtime-manager.ts
@@ -2187,10 +2911,7 @@ function useGQLEntity(opts) {
2187
2911
  optsRef.current = opts;
2188
2912
  const data = useStore(useGraphStore, useShallow((s) => {
2189
2913
  if (!id) return null;
2190
- const base = s.entities[type]?.[id];
2191
- if (!base) return null;
2192
- const patch = s.patches[type]?.[id];
2193
- return patch ? { ...base, ...patch } : base;
2914
+ return s.readEntitySnapshot(type, id);
2194
2915
  }));
2195
2916
  const entityState = useStore(useGraphStore, useCallback(
2196
2917
  (s) => s.entityStates[`${type}:${id}`] ?? EMPTY_ENTITY_STATE,
@@ -2236,12 +2957,7 @@ function useGQLList(opts) {
2236
2957
  useGraphStore,
2237
2958
  useShallow((s) => {
2238
2959
  const ids = s.lists[key]?.ids ?? EMPTY_IDS;
2239
- return ids.map((id) => {
2240
- const base = s.entities[type]?.[id];
2241
- if (!base) return null;
2242
- const p = s.patches[type]?.[id];
2243
- return p ? { ...base, ...p } : base;
2244
- }).filter((x) => x !== null);
2960
+ return ids.map((id) => s.readEntitySnapshot(type, id)).filter((x) => x !== null);
2245
2961
  })
2246
2962
  );
2247
2963
  const doFetch = useCallback((cursor, append = false) => {
@@ -2478,7 +3194,7 @@ function EntityTable({ viewResult, columns, getRowId = (r) => String(r.id), sele
2478
3194
  ] });
2479
3195
  }
2480
3196
  function Sheet({ open, onClose, title, subtitle, children, footer, width = "w-[480px]" }) {
2481
- React5.useEffect(() => {
3197
+ React6.useEffect(() => {
2482
3198
  const h = (e) => {
2483
3199
  if (e.key === "Escape") onClose();
2484
3200
  };
@@ -2514,6 +3230,8 @@ function FieldControl({ descriptor, value, onChange, entity, readonly }) {
2514
3230
  return /* @__PURE__ */ jsx("input", { type: "number", value: String(value ?? ""), onChange: (e) => onChange(e.target.valueAsNumber), placeholder: descriptor.placeholder, className: base });
2515
3231
  case "textarea":
2516
3232
  return /* @__PURE__ */ jsx("textarea", { value: String(value ?? ""), onChange: (e) => onChange(e.target.value), placeholder: descriptor.placeholder, className: "w-full min-h-[80px] rounded-md border bg-muted/50 px-3 py-2 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-ring transition-colors" });
3233
+ case "markdown":
3234
+ return /* @__PURE__ */ jsx(MarkdownFieldEditor, { value: String(value ?? ""), onChange: (nextValue) => onChange(nextValue), placeholder: descriptor.placeholder });
2517
3235
  case "date":
2518
3236
  return /* @__PURE__ */ jsx("input", { type: "date", value: value ? new Date(value).toISOString().split("T")[0] : "", onChange: (e) => onChange(e.target.value ? new Date(e.target.value).toISOString() : null), className: base });
2519
3237
  case "boolean":
@@ -2533,12 +3251,35 @@ function FieldControl({ descriptor, value, onChange, entity, readonly }) {
2533
3251
  !value && /* @__PURE__ */ jsx("option", { value: "", children: "Select\u2026" }),
2534
3252
  (descriptor.options ?? []).map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
2535
3253
  ] });
3254
+ case "json":
3255
+ return /* @__PURE__ */ jsx(
3256
+ "textarea",
3257
+ {
3258
+ value: value != null ? JSON.stringify(value, null, 2) : "",
3259
+ onChange: (event) => {
3260
+ const nextValue = event.target.value;
3261
+ try {
3262
+ onChange(nextValue ? JSON.parse(nextValue) : null);
3263
+ } catch {
3264
+ onChange(nextValue);
3265
+ }
3266
+ },
3267
+ placeholder: descriptor.placeholder,
3268
+ className: "w-full min-h-[120px] rounded-md border bg-muted/50 px-3 py-2 text-sm font-mono resize-y focus:outline-none focus:ring-1 focus:ring-ring transition-colors"
3269
+ }
3270
+ );
2536
3271
  default:
2537
3272
  return /* @__PURE__ */ jsx("input", { value: String(value ?? ""), onChange: (e) => onChange(e.target.value), className: base });
2538
3273
  }
2539
3274
  }
3275
+ function FieldReadonlyValue({ descriptor, value, entity }) {
3276
+ if (descriptor.render) return /* @__PURE__ */ jsx(Fragment, { children: descriptor.render(value, entity) });
3277
+ if (descriptor.type === "markdown") return /* @__PURE__ */ jsx(MarkdownFieldRenderer, { value: String(value ?? ""), className: "prose prose-sm max-w-none py-1" });
3278
+ if (descriptor.type === "json") return /* @__PURE__ */ jsx("pre", { className: "text-xs py-1 whitespace-pre-wrap break-words", children: JSON.stringify(value ?? null, null, 2) });
3279
+ return /* @__PURE__ */ jsx("p", { className: "text-sm py-1", children: value != null && value !== "" ? String(value) : "\u2014" });
3280
+ }
2540
3281
  function EntityDetailSheet({ crud, fields, title = "Details", description, children, showEditButton = true, showDeleteButton = true, deleteConfirmMessage = "This action cannot be undone." }) {
2541
- const [confirmDelete, setConfirmDelete] = React5.useState(false);
3282
+ const [confirmDelete, setConfirmDelete] = React6.useState(false);
2542
3283
  const open = crud.mode === "detail" && !!crud.selectedId;
2543
3284
  const entity = crud.detail;
2544
3285
  const resolvedTitle = entity && typeof title === "function" ? title(entity) : String(title);
@@ -2562,8 +3303,7 @@ function EntityDetailSheet({ crud, fields, title = "Details", description, child
2562
3303
  entity && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
2563
3304
  fields.map((f) => /* @__PURE__ */ jsxs("div", { children: [
2564
3305
  /* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-muted-foreground uppercase tracking-wide mb-1", children: f.label }),
2565
- /* @__PURE__ */ jsx(FieldControl, { descriptor: f, value: entity[f.field], onChange: () => {
2566
- }, entity, readonly: true })
3306
+ /* @__PURE__ */ jsx(FieldReadonlyValue, { descriptor: f, value: getValueAtPath(entity, f.field), entity })
2567
3307
  ] }, f.field)),
2568
3308
  children && /* @__PURE__ */ jsxs(Fragment, { children: [
2569
3309
  /* @__PURE__ */ jsx("div", { className: "border-t my-1" }),
@@ -2633,13 +3373,14 @@ function EntityFormSheet({ crud, fields, createTitle = "Create", editTitle = "Ed
2633
3373
  error && /* @__PURE__ */ jsx("div", { className: "px-3 py-2 rounded-md bg-destructive/10 border border-destructive/20 text-xs text-destructive", children: error }),
2634
3374
  visibleFields.map((f) => {
2635
3375
  const isDirty = !isCreate && crud.dirty.changed.has(f.field);
3376
+ const currentValue = getValueAtPath(buf, f.field);
2636
3377
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
2637
3378
  /* @__PURE__ */ jsxs("label", { className: cn("text-xs font-medium", isDirty ? "text-primary" : "text-muted-foreground"), children: [
2638
3379
  f.label,
2639
3380
  f.required && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-0.5", children: "*" }),
2640
3381
  isDirty && /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-[10px] font-normal opacity-70", children: "modified" })
2641
3382
  ] }),
2642
- /* @__PURE__ */ jsx(FieldControl, { descriptor: f, value: buf[f.field], onChange: (v) => setField(f.field, v), entity: buf, readonly: f.readonlyOnEdit && isEdit }),
3383
+ /* @__PURE__ */ jsx(FieldControl, { descriptor: f, value: currentValue, onChange: (v) => setField(f.field, v), entity: buf, readonly: f.readonlyOnEdit && isEdit }),
2643
3384
  f.hint && /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: f.hint })
2644
3385
  ] }, f.field);
2645
3386
  })
@@ -3840,9 +4581,9 @@ function createSelectionStore() {
3840
4581
  function useSelectionStore(store, selector) {
3841
4582
  return useStore(store, selector);
3842
4583
  }
3843
- var SelectionContext = React5.createContext(null);
4584
+ var SelectionContext = React6.createContext(null);
3844
4585
  function useSelectionContext() {
3845
- const store = React5.useContext(SelectionContext);
4586
+ const store = React6.useContext(SelectionContext);
3846
4587
  if (!store) throw new Error("useSelectionContext must be used within a SelectionContext.Provider");
3847
4588
  return store;
3848
4589
  }
@@ -4757,7 +5498,7 @@ function useTableStorageAdapter() {
4757
5498
  function useTableRealtimeMode() {
4758
5499
  return useContext(TableStorageContext).realtimeMode;
4759
5500
  }
4760
- var Table = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsx(
5501
+ var Table = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsx(
4761
5502
  "table",
4762
5503
  {
4763
5504
  ref,
@@ -4766,11 +5507,11 @@ var Table = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */
4766
5507
  }
4767
5508
  ) }));
4768
5509
  Table.displayName = "Table";
4769
- var TableHeader = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("thead", { ref, className: cn("bg-muted/60", className), ...props }));
5510
+ var TableHeader = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("thead", { ref, className: cn("bg-muted/60", className), ...props }));
4770
5511
  TableHeader.displayName = "TableHeader";
4771
- var TableBody = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("tbody", { ref, className: cn("bg-background", className), ...props }));
5512
+ var TableBody = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("tbody", { ref, className: cn("bg-background", className), ...props }));
4772
5513
  TableBody.displayName = "TableBody";
4773
- var TableFooter = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
5514
+ var TableFooter = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
4774
5515
  "tfoot",
4775
5516
  {
4776
5517
  ref,
@@ -4779,7 +5520,7 @@ var TableFooter = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE
4779
5520
  }
4780
5521
  ));
4781
5522
  TableFooter.displayName = "TableFooter";
4782
- var TableRow = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
5523
+ var TableRow = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
4783
5524
  "tr",
4784
5525
  {
4785
5526
  ref,
@@ -4791,7 +5532,7 @@ var TableRow = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__
4791
5532
  }
4792
5533
  ));
4793
5534
  TableRow.displayName = "TableRow";
4794
- var TableHead = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
5535
+ var TableHead = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
4795
5536
  "th",
4796
5537
  {
4797
5538
  ref,
@@ -4803,7 +5544,7 @@ var TableHead = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__
4803
5544
  }
4804
5545
  ));
4805
5546
  TableHead.displayName = "TableHead";
4806
- var TableCell = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
5547
+ var TableCell = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
4807
5548
  "td",
4808
5549
  {
4809
5550
  ref,
@@ -4815,7 +5556,7 @@ var TableCell = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__
4815
5556
  }
4816
5557
  ));
4817
5558
  TableCell.displayName = "TableCell";
4818
- var TableCaption = React5.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
5559
+ var TableCaption = React6.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
4819
5560
  "caption",
4820
5561
  {
4821
5562
  ref,
@@ -6334,7 +7075,7 @@ function ChevronsRightIcon({ className }) {
6334
7075
  ] });
6335
7076
  }
6336
7077
  function EmptyState({ config, isFiltered = false, className }) {
6337
- if (React5.isValidElement(config)) {
7078
+ if (React6.isValidElement(config)) {
6338
7079
  return /* @__PURE__ */ jsx(Fragment, { children: config });
6339
7080
  }
6340
7081
  const cfg = config ?? {};
@@ -7734,13 +8475,13 @@ function selectionColumn2() {
7734
8475
  enableFiltering: false,
7735
8476
  enableHiding: false,
7736
8477
  enableResizing: false,
7737
- header: ({ table }) => React5.createElement("input", {
8478
+ header: ({ table }) => React6.createElement("input", {
7738
8479
  type: "checkbox",
7739
8480
  checked: table.getIsAllPageRowsSelected(),
7740
8481
  onChange: table.getToggleAllPageRowsSelectedHandler(),
7741
8482
  className: "h-4 w-4 rounded border-primary text-primary focus:ring-ring"
7742
8483
  }),
7743
- cell: ({ row }) => React5.createElement("input", {
8484
+ cell: ({ row }) => React6.createElement("input", {
7744
8485
  type: "checkbox",
7745
8486
  checked: row.getIsSelected(),
7746
8487
  onChange: row.getToggleSelectedHandler(),
@@ -7876,7 +8617,7 @@ function enumColumn2(options) {
7876
8617
  const opt = options.options.find((o) => o.value === val);
7877
8618
  if (!opt) return val;
7878
8619
  if (opt.badgeClassName) {
7879
- return React5.createElement(
8620
+ return React6.createElement(
7880
8621
  "span",
7881
8622
  {
7882
8623
  className: `inline-flex items-center rounded px-1.5 py-0.5 text-[11px] font-medium capitalize ${opt.badgeClassName}`
@@ -7884,7 +8625,7 @@ function enumColumn2(options) {
7884
8625
  opt.label
7885
8626
  );
7886
8627
  }
7887
- return React5.createElement(
8628
+ return React6.createElement(
7888
8629
  "span",
7889
8630
  {
7890
8631
  className: "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
@@ -7921,6 +8662,6 @@ function actionsColumn2() {
7921
8662
  };
7922
8663
  }
7923
8664
 
7924
- export { ActionButtonRow, ActionDropdown, ColumnPresetDialog, DataTable, DataTableColumnHeader, DataTableFilter, DataTablePagination, DataTableToolbar, ElectricSQLAdapter as ElectricSQLPresetAdapter, EmptyState, EntityDetailSheet, EntityFormSheet, EntityListView, EntityTable, FilterPresetDialog, GQLClient, GalleryView, InlineCellEditor, InlineItemEditor, ListView, MemoryAdapter, MultiSelectBar, PresetPicker, InlineCellEditor2 as PureInlineCellEditor, RealtimeManager, RestApiAdapter, SelectionContext, Sheet, SortHeader, SupabaseRealtimeAdapter as SupabasePresetAdapter, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableStorageProvider, ViewModeSwitcher, ZustandPersistAdapter, actionsColumn, applyView, booleanColumn, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createConvexAdapter, createElectricAdapter, createGQLClient, createGraphQLSubscriptionAdapter, createPresetStore, createPrismaEntityConfig, createRow, createSelectionStore, createSupabaseRealtimeAdapter, createWebSocketAdapter, dateColumn, dedupe, deleteAction, editAction, enumColumn, executeGQL, fetchEntity, fetchList, flattenClauses, getCoreRowModel2 as getCoreRowModel, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginatedRowModel, getRealtimeManager, getSchema, getSelectedRowModel, getSortedRowModel2 as getSortedRowModel, hasCustomPredicates, matchesFilter, matchesSearch, normalizeGQLResponse, numberColumn, prismaRelationsToSchema, actionsColumn2 as pureActionsColumn, booleanColumn2 as pureBooleanColumn, dateColumn2 as pureDateColumn, enumColumn2 as pureEnumColumn, numberColumn2 as pureNumberColumn, selectionColumn2 as pureSelectionColumn, textColumn2 as pureTextColumn, readRelations, registerSchema, resetRealtimeManager, selectionColumn, serializeKey, startGarbageCollector, stopGarbageCollector, textColumn, toGraphQLVariables, toPrismaInclude, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, useEntity, useEntityAugment, useEntityCRUD, useEntityList, useEntityMutation, useEntityView, useGQLEntity, useGQLList, useGQLMutation, useGQLSubscription, useGraphDevTools, useGraphStore, useLocalFirst, usePGliteQuery, useSelectionContext, useSelectionStore, useSuspenseEntity, useSuspenseEntityList, useTable, useTablePresets, useTableRealtimeMode, useTableStorageAdapter, viewAction };
8665
+ export { ActionButtonRow, ActionDropdown, ColumnPresetDialog, DataTable, DataTableColumnHeader, DataTableFilter, DataTablePagination, DataTableToolbar, ElectricSQLAdapter as ElectricSQLPresetAdapter, EmptyState, EntityDetailSheet, EntityFormSheet, EntityListView, EntityTable, FilterPresetDialog, GQLClient, GalleryView, InlineCellEditor, InlineItemEditor, ListView, MarkdownFieldEditor, MarkdownFieldRenderer, MemoryAdapter, MultiSelectBar, PresetPicker, InlineCellEditor2 as PureInlineCellEditor, RealtimeManager, RestApiAdapter, SelectionContext, Sheet, SortHeader, SupabaseRealtimeAdapter as SupabasePresetAdapter, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, TableStorageProvider, ViewModeSwitcher, ZustandPersistAdapter, actionsColumn, applyView, booleanColumn, buildEntityFieldsFromSchema, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createConvexAdapter, createElectricAdapter, createGQLClient, createGraphAction, createGraphEffect, createGraphQLSubscriptionAdapter, createGraphTool, createGraphTransaction, createPresetStore, createPrismaEntityConfig, createRow, createSchemaGraphTool, createSelectionStore, createSupabaseRealtimeAdapter, createWebSocketAdapter, dateColumn, dedupe, deleteAction, editAction, enumColumn, executeGQL, exportGraphSnapshot, exportGraphSnapshotWithSchemas, fetchEntity, fetchList, flattenClauses, getCoreRowModel2 as getCoreRowModel, getEntityJsonSchema, getExpandedRowModel, getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getGroupedRowModel, getPaginatedRowModel, getRealtimeManager, getSchema, getSelectedRowModel, getSortedRowModel2 as getSortedRowModel, hasCustomPredicates, hydrateGraphFromStorage, matchesFilter, matchesSearch, normalizeGQLResponse, numberColumn, persistGraphToStorage, prismaRelationsToSchema, actionsColumn2 as pureActionsColumn, booleanColumn2 as pureBooleanColumn, dateColumn2 as pureDateColumn, enumColumn2 as pureEnumColumn, numberColumn2 as pureNumberColumn, selectionColumn2 as pureSelectionColumn, textColumn2 as pureTextColumn, queryOnce, readRelations, registerEntityJsonSchema, registerRuntimeSchema, registerSchema, renderMarkdownToHtml, resetRealtimeManager, selectGraph, selectionColumn, serializeKey, startGarbageCollector, startLocalFirstGraph, stopGarbageCollector, textColumn, toGraphQLVariables, toPrismaInclude, toPrismaOrderBy, toPrismaWhere, toRestParams, toSQLClauses, useEntity, useEntityAugment, useEntityCRUD, useEntityList, useEntityMutation, useEntityView, useGQLEntity, useGQLList, useGQLMutation, useGQLSubscription, useGraphDevTools, useGraphStore, useGraphSyncStatus, useLocalFirst, usePGliteQuery, useSchemaEntityFields, useSelectionContext, useSelectionStore, useSuspenseEntity, useSuspenseEntityList, useTable, useTablePresets, useTableRealtimeMode, useTableStorageAdapter, viewAction };
7925
8666
  //# sourceMappingURL=index.mjs.map
7926
8667
  //# sourceMappingURL=index.mjs.map