@prometheus-ags/entity-graph-core 3.0.5 → 3.1.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.
@@ -0,0 +1,2561 @@
1
+ 'use strict';
2
+
3
+ var vanilla = require('zustand/vanilla');
4
+ var middleware = require('zustand/middleware');
5
+ var immer = require('zustand/middleware/immer');
6
+
7
+ // src/devtools/protocol.ts
8
+ var GRAPH_DEVTOOLS_PROTOCOL = "prometheus.entity-graph.devtools";
9
+ var GRAPH_DEVTOOLS_PROTOCOL_VERSION = 1;
10
+
11
+ // src/merge/lww.ts
12
+ var lwwStrategy = (prev, next) => {
13
+ return prev ? { ...prev, ...next } : { ...next };
14
+ };
15
+
16
+ // src/merge/registry.ts
17
+ var byType = /* @__PURE__ */ new Map();
18
+ var globalDefault = null;
19
+ function getMergeStrategy(type) {
20
+ return byType.get(type) ?? globalDefault ?? lwwStrategy;
21
+ }
22
+
23
+ // src/view/evaluator.ts
24
+ function matchesFilter(entity, filter) {
25
+ if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
26
+ return matchesGroup(entity, filter);
27
+ }
28
+ function matchesGroup(entity, group) {
29
+ const { logic, clauses } = group;
30
+ if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
31
+ return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
32
+ }
33
+ function matchesClause(entity, clause) {
34
+ const { field, op, value, predicate } = clause;
35
+ const fv = getNestedValue(entity, field);
36
+ switch (op) {
37
+ case "eq":
38
+ return fv === value;
39
+ case "neq":
40
+ return fv !== value;
41
+ case "gt":
42
+ return fv > value;
43
+ case "gte":
44
+ return fv >= value;
45
+ case "lt":
46
+ return fv < value;
47
+ case "lte":
48
+ return fv <= value;
49
+ case "in":
50
+ return Array.isArray(value) && value.includes(fv);
51
+ case "nin":
52
+ return Array.isArray(value) && !value.includes(fv);
53
+ case "isNull":
54
+ return fv == null;
55
+ case "isNotNull":
56
+ return fv != null;
57
+ case "contains":
58
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
59
+ case "startsWith":
60
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
61
+ case "endsWith":
62
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
63
+ case "between": {
64
+ const [lo, hi] = value;
65
+ return fv >= lo && fv <= hi;
66
+ }
67
+ case "arrayContains":
68
+ return Array.isArray(fv) && fv.includes(value);
69
+ case "arrayOverlaps":
70
+ return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
71
+ case "matches":
72
+ return typeof fv === "string" && new RegExp(value).test(fv);
73
+ case "custom":
74
+ return predicate ? predicate(fv, entity) : true;
75
+ default:
76
+ return true;
77
+ }
78
+ }
79
+ function matchesSearch(entity, query, fields) {
80
+ if (!query.trim()) return true;
81
+ const lq = query.toLowerCase();
82
+ return fields.some((field) => {
83
+ const v = getNestedValue(entity, field);
84
+ return typeof v === "string" && v.toLowerCase().includes(lq);
85
+ });
86
+ }
87
+ function compareEntities(a, b, sort) {
88
+ for (const clause of sort) {
89
+ const r = compareByClause(a, b, clause);
90
+ if (r !== 0) return r;
91
+ }
92
+ return 0;
93
+ }
94
+ function compareByClause(a, b, clause) {
95
+ const { field, direction, nulls = "last", comparator } = clause;
96
+ const av = getNestedValue(a, field);
97
+ const bv = getNestedValue(b, field);
98
+ const aNull = av == null;
99
+ const bNull = bv == null;
100
+ if (aNull && bNull) return 0;
101
+ if (aNull) return nulls === "first" ? -1 : 1;
102
+ if (bNull) return nulls === "first" ? 1 : -1;
103
+ let cmp;
104
+ if (comparator) cmp = comparator(av, bv);
105
+ else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
106
+ else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
107
+ else cmp = String(av).localeCompare(String(bv));
108
+ return direction === "desc" ? -cmp : cmp;
109
+ }
110
+ function findInsertionIndex(entity, sortedIds, getEntity, sort) {
111
+ let lo = 0;
112
+ let hi = sortedIds.length;
113
+ while (lo < hi) {
114
+ const mid = lo + hi >>> 1;
115
+ const me = getEntity(sortedIds[mid]);
116
+ if (!me) {
117
+ lo = mid + 1;
118
+ continue;
119
+ }
120
+ if (compareEntities(entity, me, sort) <= 0) hi = mid;
121
+ else lo = mid + 1;
122
+ }
123
+ return lo;
124
+ }
125
+ function getNestedValue(obj, path) {
126
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
127
+ let current = obj;
128
+ for (const part of parts) {
129
+ if (current == null || typeof current !== "object") return void 0;
130
+ current = current[part];
131
+ }
132
+ return current;
133
+ }
134
+
135
+ // src/graph.ts
136
+ var EMPTY_IDS = [];
137
+ var EMPTY_ENTITY_STATE = {
138
+ isFetching: false,
139
+ lastFetched: null,
140
+ error: null,
141
+ stale: false
142
+ };
143
+ var EMPTY_SYNC_METADATA = {
144
+ synced: true,
145
+ origin: "server",
146
+ updatedAt: null
147
+ };
148
+ var EMPTY_LIST_STATE = {
149
+ ids: EMPTY_IDS,
150
+ total: null,
151
+ nextCursor: null,
152
+ prevCursor: null,
153
+ hasNextPage: false,
154
+ hasPrevPage: false,
155
+ isFetching: false,
156
+ isFetchingMore: false,
157
+ error: null,
158
+ lastError: null,
159
+ lastFetched: null,
160
+ stale: false,
161
+ currentPage: null,
162
+ pageSize: null
163
+ };
164
+ function defaultEntityState() {
165
+ return { ...EMPTY_ENTITY_STATE };
166
+ }
167
+ function defaultSyncMetadata() {
168
+ return { ...EMPTY_SYNC_METADATA };
169
+ }
170
+ function defaultListState() {
171
+ return { ...EMPTY_LIST_STATE, ids: [] };
172
+ }
173
+ function ek(type, id) {
174
+ return `${type}:${id}`;
175
+ }
176
+ var snapshotCache = /* @__PURE__ */ new Map();
177
+ function readCachedEntitySnapshot(type, id, base, patch, metadata) {
178
+ const key = ek(type, id);
179
+ const cached = snapshotCache.get(key);
180
+ if (cached && cached.base === base && cached.patch === patch && cached.metadata === metadata) {
181
+ return cached.snapshot;
182
+ }
183
+ const snapshot = {
184
+ ...patch ? { ...base, ...patch } : base,
185
+ $synced: metadata.synced,
186
+ $origin: metadata.origin,
187
+ $updatedAt: metadata.updatedAt
188
+ };
189
+ snapshotCache.set(key, { base, patch, metadata, snapshot });
190
+ return snapshot;
191
+ }
192
+ function createGraphStore() {
193
+ return vanilla.createStore()(
194
+ middleware.subscribeWithSelector(
195
+ immer.immer((set, get) => ({
196
+ entities: {},
197
+ patches: {},
198
+ entityStates: {},
199
+ syncMetadata: {},
200
+ lists: {},
201
+ upsertEntity: (type, id, data) => set((s) => {
202
+ if (!s.entities[type]) s.entities[type] = {};
203
+ const key = ek(type, id);
204
+ const strategy = getMergeStrategy(type);
205
+ const ctx = { type, id, origin: s.syncMetadata[key]?.origin ?? "server", updatedAt: Date.now() };
206
+ s.entities[type][id] = strategy(s.entities[type][id], data, ctx);
207
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
208
+ }),
209
+ upsertEntities: (type, entries) => set((s) => {
210
+ if (!s.entities[type]) s.entities[type] = {};
211
+ const strategy = getMergeStrategy(type);
212
+ for (const { id, data } of entries) {
213
+ const key = ek(type, id);
214
+ const ctx = { type, id, origin: s.syncMetadata[key]?.origin ?? "server", updatedAt: Date.now() };
215
+ s.entities[type][id] = strategy(s.entities[type][id], data, ctx);
216
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
217
+ }
218
+ }),
219
+ ingestFetchedList: (type, entries, options = {}) => set((s) => {
220
+ const fetchedAt = Date.now();
221
+ const ids = entries.map(({ id }) => id);
222
+ const batches = [
223
+ { type, entries },
224
+ ...options.sideBatches ?? []
225
+ ];
226
+ const origins = /* @__PURE__ */ new Map();
227
+ for (const batch of batches) {
228
+ if (!s.entities[batch.type]) s.entities[batch.type] = {};
229
+ for (const { id } of batch.entries) {
230
+ const key = ek(batch.type, id);
231
+ if (!origins.has(key)) {
232
+ origins.set(key, s.syncMetadata[key]?.origin ?? "server");
233
+ }
234
+ }
235
+ }
236
+ for (const batch of batches) {
237
+ const strategy = getMergeStrategy(batch.type);
238
+ for (const { id, data } of batch.entries) {
239
+ const key = ek(batch.type, id);
240
+ const ctx = {
241
+ type: batch.type,
242
+ id,
243
+ origin: origins.get(key) ?? "server",
244
+ updatedAt: fetchedAt
245
+ };
246
+ s.entities[batch.type][id] = strategy(s.entities[batch.type][id], data, ctx);
247
+ }
248
+ }
249
+ const completed = /* @__PURE__ */ new Set();
250
+ for (const batch of batches) {
251
+ for (const { id } of batch.entries) {
252
+ const key = ek(batch.type, id);
253
+ if (completed.has(key)) continue;
254
+ completed.add(key);
255
+ if (!s.entityStates[key]) s.entityStates[key] = defaultEntityState();
256
+ s.entityStates[key].lastFetched = fetchedAt;
257
+ s.entityStates[key].isFetching = false;
258
+ s.entityStates[key].error = null;
259
+ s.entityStates[key].stale = false;
260
+ s.syncMetadata[key] = {
261
+ ...s.syncMetadata[key] ?? defaultSyncMetadata(),
262
+ synced: true,
263
+ origin: "server",
264
+ updatedAt: fetchedAt
265
+ };
266
+ }
267
+ }
268
+ const readDraftEntity = (id) => {
269
+ const base = s.entities[type]?.[id];
270
+ if (!base) return null;
271
+ const patch = s.patches[type]?.[id];
272
+ const metadata = s.syncMetadata[ek(type, id)] ?? defaultSyncMetadata();
273
+ return {
274
+ ...base,
275
+ ...patch ?? {},
276
+ $synced: metadata.synced,
277
+ $origin: metadata.origin,
278
+ $updatedAt: metadata.updatedAt
279
+ };
280
+ };
281
+ for (const target of options.lists ?? []) {
282
+ const existing = s.lists[target.key] ?? defaultListState();
283
+ const targetIds = target.ids ?? ids;
284
+ const nextIds = target.mode === "append" ? Array.from(/* @__PURE__ */ new Set([...existing.ids, ...targetIds])) : targetIds;
285
+ s.lists[target.key] = {
286
+ ...existing,
287
+ ...target.meta ?? {},
288
+ ids: nextIds,
289
+ isFetching: false,
290
+ isFetchingMore: false,
291
+ error: null,
292
+ lastError: null,
293
+ stale: false,
294
+ lastFetched: fetchedAt
295
+ };
296
+ }
297
+ for (const projection of options.projections ?? []) {
298
+ const existing = s.lists[projection.key] ?? defaultListState();
299
+ const nextIds = [...existing.ids];
300
+ const { filter, sort, search } = projection.view;
301
+ for (const id of new Set(ids)) {
302
+ if (nextIds.includes(id)) continue;
303
+ const entity = readDraftEntity(id);
304
+ if (!entity) continue;
305
+ const matches = (!filter || matchesFilter(entity, filter)) && (!search?.query || matchesSearch(entity, search.query, search.fields));
306
+ if (!matches) continue;
307
+ if (sort && sort.length > 0) {
308
+ const index = findInsertionIndex(entity, nextIds, readDraftEntity, sort);
309
+ nextIds.splice(index, 0, id);
310
+ } else {
311
+ nextIds.unshift(id);
312
+ }
313
+ }
314
+ s.lists[projection.key] = projection.completeFetch ? {
315
+ ...existing,
316
+ ids: nextIds,
317
+ isFetching: false,
318
+ isFetchingMore: false,
319
+ error: null,
320
+ lastError: null,
321
+ stale: false,
322
+ lastFetched: fetchedAt
323
+ } : { ...existing, ids: nextIds };
324
+ }
325
+ for (const key of options.finishListFetches ?? []) {
326
+ if (!s.lists[key]) s.lists[key] = defaultListState();
327
+ s.lists[key].isFetching = false;
328
+ }
329
+ }),
330
+ replaceEntity: (type, id, data) => set((s) => {
331
+ if (!s.entities[type]) s.entities[type] = {};
332
+ s.entities[type][id] = data;
333
+ const key = ek(type, id);
334
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
335
+ }),
336
+ removeEntity: (type, id) => set((s) => {
337
+ delete s.entities[type]?.[id];
338
+ delete s.patches[type]?.[id];
339
+ delete s.entityStates[ek(type, id)];
340
+ delete s.syncMetadata[ek(type, id)];
341
+ }),
342
+ patchEntity: (type, id, patch) => set((s) => {
343
+ if (!s.patches[type]) s.patches[type] = {};
344
+ s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
345
+ }),
346
+ unpatchEntity: (type, id, keys) => set((s) => {
347
+ const p = s.patches[type]?.[id];
348
+ if (!p) return;
349
+ for (const k of keys) delete p[k];
350
+ }),
351
+ clearPatch: (type, id) => set((s) => {
352
+ delete s.patches[type]?.[id];
353
+ }),
354
+ setEntityFetching: (type, id, fetching) => set((s) => {
355
+ const k = ek(type, id);
356
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
357
+ s.entityStates[k].isFetching = fetching;
358
+ }),
359
+ setEntityError: (type, id, error) => set((s) => {
360
+ const k = ek(type, id);
361
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
362
+ s.entityStates[k].error = error;
363
+ s.entityStates[k].isFetching = false;
364
+ }),
365
+ setEntityFetched: (type, id) => set((s) => {
366
+ const k = ek(type, id);
367
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
368
+ s.entityStates[k].lastFetched = Date.now();
369
+ s.entityStates[k].isFetching = false;
370
+ s.entityStates[k].error = null;
371
+ s.entityStates[k].stale = false;
372
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
373
+ }),
374
+ setEntityStale: (type, id, stale) => set((s) => {
375
+ const k = ek(type, id);
376
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
377
+ s.entityStates[k].stale = stale;
378
+ }),
379
+ setEntitySyncMetadata: (type, id, metadata) => set((s) => {
380
+ const k = ek(type, id);
381
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
382
+ }),
383
+ clearEntitySyncMetadata: (type, id) => set((s) => {
384
+ delete s.syncMetadata[ek(type, id)];
385
+ }),
386
+ setListResult: (key, ids, meta) => set((s) => {
387
+ const ex = s.lists[key] ?? defaultListState();
388
+ s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
389
+ }),
390
+ appendListResult: (key, ids, meta) => set((s) => {
391
+ const ex = s.lists[key] ?? defaultListState();
392
+ 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() };
393
+ }),
394
+ prependListResult: (key, ids, meta) => set((s) => {
395
+ const ex = s.lists[key] ?? defaultListState();
396
+ 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() };
397
+ }),
398
+ removeIdFromAllLists: (_type, id) => set((s) => {
399
+ for (const key of Object.keys(s.lists)) {
400
+ const list = s.lists[key];
401
+ const idx = list.ids.indexOf(id);
402
+ if (idx !== -1) {
403
+ list.ids.splice(idx, 1);
404
+ if (list.total !== null) list.total -= 1;
405
+ }
406
+ }
407
+ }),
408
+ insertIdInList: (key, id, position) => set((s) => {
409
+ if (!s.lists[key]) s.lists[key] = defaultListState();
410
+ const ids = s.lists[key].ids;
411
+ const ex = ids.indexOf(id);
412
+ if (ex !== -1) ids.splice(ex, 1);
413
+ if (position === "start") ids.unshift(id);
414
+ else if (position === "end") ids.push(id);
415
+ else ids.splice(position, 0, id);
416
+ }),
417
+ setListFetching: (key, fetching) => set((s) => {
418
+ if (!s.lists[key]) s.lists[key] = defaultListState();
419
+ s.lists[key].isFetching = fetching;
420
+ }),
421
+ setListFetchingMore: (key, fetchingMore) => set((s) => {
422
+ if (!s.lists[key]) s.lists[key] = defaultListState();
423
+ s.lists[key].isFetchingMore = fetchingMore;
424
+ }),
425
+ setListError: (key, error, typed) => set((s) => {
426
+ if (!s.lists[key]) s.lists[key] = defaultListState();
427
+ s.lists[key].error = error;
428
+ s.lists[key].lastError = typed ?? (error === null ? null : s.lists[key].lastError);
429
+ s.lists[key].isFetching = false;
430
+ s.lists[key].isFetchingMore = false;
431
+ s.lists[key].lastFetched = Date.now();
432
+ s.lists[key].stale = false;
433
+ }),
434
+ setListStale: (key, stale) => set((s) => {
435
+ if (!s.lists[key]) s.lists[key] = defaultListState();
436
+ s.lists[key].stale = stale;
437
+ }),
438
+ invalidateEntity: (type, id) => set((s) => {
439
+ if (id) {
440
+ const k = ek(type, id);
441
+ if (s.entityStates[k]) s.entityStates[k].stale = true;
442
+ } else {
443
+ for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
444
+ }
445
+ }),
446
+ invalidateLists: (matcher) => set((s) => {
447
+ const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
448
+ for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
449
+ }),
450
+ invalidateType: (type) => {
451
+ get().invalidateEntity(type);
452
+ get().invalidateLists(type);
453
+ },
454
+ readEntity: (type, id) => {
455
+ const s = get();
456
+ const base = s.entities[type]?.[id];
457
+ if (!base) return null;
458
+ const patch = s.patches[type]?.[id];
459
+ return patch ? { ...base, ...patch } : base;
460
+ },
461
+ readEntitySnapshot: (type, id) => {
462
+ const s = get();
463
+ const base = s.entities[type]?.[id];
464
+ if (!base) {
465
+ snapshotCache.delete(ek(type, id));
466
+ return null;
467
+ }
468
+ const patch = s.patches[type]?.[id];
469
+ const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
470
+ return readCachedEntitySnapshot(
471
+ type,
472
+ id,
473
+ base,
474
+ patch,
475
+ metadata
476
+ );
477
+ }
478
+ }))
479
+ )
480
+ );
481
+ }
482
+ createGraphStore();
483
+
484
+ // src/devtools/projection.ts
485
+ function countRecordEntries(record) {
486
+ return Object.keys(record).length;
487
+ }
488
+ function collectGraphDevtoolsCounts(state) {
489
+ let entities = 0;
490
+ let entityTypes = 0;
491
+ for (const bucket of Object.values(state.entities)) {
492
+ const count = Object.keys(bucket).length;
493
+ if (count > 0) entityTypes += 1;
494
+ entities += count;
495
+ }
496
+ let patchedEntities = 0;
497
+ for (const bucket of Object.values(state.patches)) {
498
+ for (const patch of Object.values(bucket)) {
499
+ if (Object.keys(patch).length > 0) patchedEntities += 1;
500
+ }
501
+ }
502
+ let listMemberships = 0;
503
+ let fetching = 0;
504
+ let stale = 0;
505
+ let errors = 0;
506
+ for (const list of Object.values(state.lists)) {
507
+ listMemberships += list.ids.length;
508
+ if (list.isFetching || list.isFetchingMore) fetching += 1;
509
+ if (list.stale) stale += 1;
510
+ if (list.error !== null || list.lastError !== null) errors += 1;
511
+ }
512
+ for (const entityState of Object.values(state.entityStates)) {
513
+ if (entityState.isFetching) fetching += 1;
514
+ if (entityState.stale) stale += 1;
515
+ if (entityState.error !== null) errors += 1;
516
+ }
517
+ return {
518
+ entityTypes,
519
+ entities,
520
+ patchedEntities,
521
+ entityStates: countRecordEntries(state.entityStates),
522
+ syncMetadata: countRecordEntries(state.syncMetadata),
523
+ lists: countRecordEntries(state.lists),
524
+ listMemberships,
525
+ fetching,
526
+ stale,
527
+ errors
528
+ };
529
+ }
530
+ function withValues(change, before, after, policy, storeId) {
531
+ if (policy.mode !== "include") return { ...change, valueState: "hidden-by-policy" };
532
+ const redact = policy.redact ?? ((value) => value);
533
+ const baseContext = {
534
+ storeId,
535
+ category: change.category,
536
+ key: change.key,
537
+ ...change.id !== void 0 ? { id: change.id } : {},
538
+ fieldPath: [],
539
+ destination: "history"
540
+ };
541
+ const projectValue = (value, side) => {
542
+ try {
543
+ return { value: toGraphDevtoolsTransportValue(redact(value, { ...baseContext, side })), failed: false };
544
+ } catch {
545
+ return { value: { $type: "redaction-error" }, failed: true };
546
+ }
547
+ };
548
+ const projectedBefore = before !== void 0 ? projectValue(before, "before") : void 0;
549
+ const projectedAfter = after !== void 0 ? projectValue(after, "after") : void 0;
550
+ return {
551
+ ...change,
552
+ valueState: projectedBefore?.failed || projectedAfter?.failed ? "redaction-error" : policy.redact ? "redacted" : "included",
553
+ ...projectedBefore ? { before: projectedBefore.value } : {},
554
+ ...projectedAfter ? { after: projectedAfter.value } : {}
555
+ };
556
+ }
557
+ function toGraphDevtoolsTransportValue(value, seen = /* @__PURE__ */ new WeakSet()) {
558
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
559
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
560
+ if (typeof value === "bigint") return { $type: "bigint", value: value.toString() };
561
+ if (typeof value === "undefined") return { $type: "undefined" };
562
+ if (typeof value === "function" || typeof value === "symbol") return { $type: typeof value };
563
+ if (value instanceof Date) return { $type: "date", value: value.toISOString() };
564
+ if (value instanceof Error) return { $type: "error", name: value.name, message: value.message };
565
+ if (typeof value !== "object") return String(value);
566
+ if (seen.has(value)) return { $type: "circular" };
567
+ seen.add(value);
568
+ try {
569
+ if (Array.isArray(value)) return value.map((item) => toGraphDevtoolsTransportValue(item, seen));
570
+ const normalized = {};
571
+ for (const [key, item] of Object.entries(value)) {
572
+ normalized[key] = toGraphDevtoolsTransportValue(item, seen);
573
+ }
574
+ return normalized;
575
+ } finally {
576
+ seen.delete(value);
577
+ }
578
+ }
579
+ function actionFor(before, after) {
580
+ if (before === void 0) return "added";
581
+ if (after === void 0) return "removed";
582
+ return "updated";
583
+ }
584
+ function diffNestedRecords(category, before, after, policy, storeId) {
585
+ const changes = [];
586
+ for (const type of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
587
+ const beforeBucket = before[type] ?? {};
588
+ const afterBucket = after[type] ?? {};
589
+ if (beforeBucket === afterBucket) continue;
590
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(beforeBucket), ...Object.keys(afterBucket)])) {
591
+ const previous = beforeBucket[id];
592
+ const current = afterBucket[id];
593
+ if (previous === current) continue;
594
+ changes.push(withValues({
595
+ category,
596
+ action: actionFor(previous, current),
597
+ key: type,
598
+ id
599
+ }, previous, current, policy, storeId));
600
+ }
601
+ }
602
+ return changes;
603
+ }
604
+ function diffFlatRecords(category, before, after, policy, storeId) {
605
+ const changes = [];
606
+ if (before === after) return changes;
607
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
608
+ const previous = before[key];
609
+ const current = after[key];
610
+ if (previous === current) continue;
611
+ changes.push(withValues({
612
+ category,
613
+ action: actionFor(previous, current),
614
+ key
615
+ }, previous, current, policy, storeId));
616
+ }
617
+ return changes;
618
+ }
619
+ function diffLists(before, after, policy, storeId) {
620
+ const changes = [];
621
+ if (before === after) return changes;
622
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
623
+ const previous = before[key];
624
+ const current = after[key];
625
+ if (previous === current) continue;
626
+ changes.push(withValues({
627
+ category: "list",
628
+ action: actionFor(previous, current),
629
+ key,
630
+ beforeCount: previous?.ids.length ?? 0,
631
+ afterCount: current?.ids.length ?? 0
632
+ }, previous, current, policy, storeId));
633
+ }
634
+ return changes;
635
+ }
636
+ function projectGraphDevtoolsChanges(before, after, policy, storeId) {
637
+ return [
638
+ ...diffNestedRecords("entity", before.entities, after.entities, policy, storeId),
639
+ ...diffNestedRecords("patch", before.patches, after.patches, policy, storeId),
640
+ ...diffFlatRecords("entity-state", before.entityStates, after.entityStates, policy, storeId),
641
+ ...diffFlatRecords("sync", before.syncMetadata, after.syncMetadata, policy, storeId),
642
+ ...diffLists(before.lists, after.lists, policy, storeId)
643
+ ];
644
+ }
645
+
646
+ // src/devtools/inspection.ts
647
+ function wireEntityKey(type, id) {
648
+ return `${type}:${id}`;
649
+ }
650
+ function graphDevtoolsEntityIdentityKey(type, id) {
651
+ return JSON.stringify([type, id]);
652
+ }
653
+ function parseGraphDevtoolsEntityIdentityKey(key) {
654
+ try {
655
+ const value = JSON.parse(key);
656
+ if (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string") return { type: value[0], id: value[1] };
657
+ } catch {
658
+ }
659
+ return null;
660
+ }
661
+ function splitEntityKey(key) {
662
+ const separator = key.indexOf(":");
663
+ if (separator < 1 || separator === key.length - 1) return null;
664
+ return { type: key.slice(0, separator), id: key.slice(separator + 1) };
665
+ }
666
+ function graphDevtoolsEntityIdentitiesForStateKey(state, key) {
667
+ const matches = [];
668
+ const types = /* @__PURE__ */ new Set([...Object.keys(state.entities), ...Object.keys(state.patches)]);
669
+ for (const type of types) {
670
+ const ids = /* @__PURE__ */ new Set([
671
+ ...Object.keys(state.entities[type] ?? {}),
672
+ ...Object.keys(state.patches[type] ?? {})
673
+ ]);
674
+ for (const id of ids) {
675
+ if (wireEntityKey(type, id) === key) matches.push({ type, id });
676
+ }
677
+ }
678
+ if (matches.length > 0) return matches;
679
+ const fallback = splitEntityKey(key);
680
+ return fallback ? [fallback] : [];
681
+ }
682
+ function toIsoTimestamp(value) {
683
+ return value === null ? null : new Date(value).toISOString();
684
+ }
685
+ function compareEntityIdentity(left, right) {
686
+ if (left.type !== right.type) return left.type < right.type ? -1 : 1;
687
+ if (left.id === right.id) return 0;
688
+ return left.id < right.id ? -1 : 1;
689
+ }
690
+ function collectEntityIdentities(state) {
691
+ const identities = /* @__PURE__ */ new Map();
692
+ for (const [type, bucket] of Object.entries(state.entities)) {
693
+ for (const id of Object.keys(bucket)) {
694
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
695
+ }
696
+ }
697
+ for (const [type, bucket] of Object.entries(state.patches)) {
698
+ for (const id of Object.keys(bucket)) {
699
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
700
+ }
701
+ }
702
+ for (const key of [...Object.keys(state.entityStates), ...Object.keys(state.syncMetadata)]) {
703
+ for (const identity of graphDevtoolsEntityIdentitiesForStateKey(state, key)) {
704
+ identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
705
+ }
706
+ }
707
+ return [...identities.values()].sort(compareEntityIdentity);
708
+ }
709
+ function dirtyReasonsFor(canonical, patch, synced) {
710
+ const reasons = [];
711
+ for (const field of Object.keys(patch ?? {}).sort()) {
712
+ reasons.push({
713
+ kind: "local-patch",
714
+ field,
715
+ change: canonical && Object.hasOwn(canonical, field) ? "changed" : "added"
716
+ });
717
+ }
718
+ if (!synced) {
719
+ reasons.push({ kind: "sync-state", field: null, change: "unsynced" });
720
+ }
721
+ return reasons;
722
+ }
723
+ function projectGraphDevtoolsInspectionValue(value, policy, context) {
724
+ if (policy.mode !== "include") return { $type: "hidden-by-policy" };
725
+ try {
726
+ const projected = policy.redact ? policy.redact(value, {
727
+ ...context,
728
+ fieldPath: [],
729
+ side: "after",
730
+ destination: "inspection"
731
+ }) : value;
732
+ return toGraphDevtoolsTransportValue(projected);
733
+ } catch {
734
+ return { $type: "redaction-error" };
735
+ }
736
+ }
737
+ function recordFor(state, storeId, type, id, revisions, valuePolicy) {
738
+ const key = wireEntityKey(type, id);
739
+ const identityKey = graphDevtoolsEntityIdentityKey(type, id);
740
+ const canonical = state.entities[type]?.[id];
741
+ const storedPatch = state.patches[type]?.[id];
742
+ const patch = storedPatch && Object.keys(storedPatch).length > 0 ? storedPatch : void 0;
743
+ const entityState = state.entityStates[key] ?? EMPTY_ENTITY_STATE;
744
+ const sync = state.syncMetadata[key] ?? EMPTY_SYNC_METADATA;
745
+ const dirtyReasons = dirtyReasonsFor(canonical, patch, sync.synced);
746
+ return {
747
+ key,
748
+ type,
749
+ id,
750
+ presence: canonical ? "present" : "missing-canonical",
751
+ canonical: canonical ? projectGraphDevtoolsInspectionValue(canonical, valuePolicy, { storeId, category: "entity", key: type, id }) : null,
752
+ patch: patch ? projectGraphDevtoolsInspectionValue(patch, valuePolicy, { storeId, category: "patch", key: type, id }) : null,
753
+ merged: canonical ? projectGraphDevtoolsInspectionValue(
754
+ { ...canonical, ...patch ?? {} },
755
+ valuePolicy,
756
+ { storeId, category: "entity", key: type, id }
757
+ ) : null,
758
+ dirty: dirtyReasons.length > 0,
759
+ dirtyReasons,
760
+ entityState: {
761
+ isFetching: entityState.isFetching,
762
+ lastFetchedAt: toIsoTimestamp(entityState.lastFetched),
763
+ stale: entityState.stale,
764
+ error: entityState.error === null ? null : {
765
+ kind: "entity-fetch",
766
+ message: entityState.error,
767
+ retryable: null
768
+ }
769
+ },
770
+ sync: {
771
+ synced: sync.synced,
772
+ origin: sync.origin,
773
+ updatedAt: toIsoTimestamp(sync.updatedAt)
774
+ },
775
+ revision: revisions.get(identityKey) ?? 0,
776
+ viewIds: []
777
+ };
778
+ }
779
+ function projectGraphDevtoolsEntityRecords(state, storeId, revisions, valuePolicy = { mode: "metadata-only" }, viewIdsByEntity = /* @__PURE__ */ new Map()) {
780
+ return {
781
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
782
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
783
+ storeId,
784
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
785
+ entityRecords: collectEntityIdentities(state).map(({ type, id }) => {
786
+ const record = recordFor(state, storeId, type, id, revisions, valuePolicy);
787
+ return {
788
+ ...record,
789
+ viewIds: [...viewIdsByEntity.get(graphDevtoolsEntityIdentityKey(type, id)) ?? []]
790
+ };
791
+ })
792
+ };
793
+ }
794
+ function advanceGraphDevtoolsEntityRevisions(changes, revisions, state, valueRevisions) {
795
+ const touched = /* @__PURE__ */ new Set();
796
+ const valueTouched = /* @__PURE__ */ new Set();
797
+ for (const change of changes) {
798
+ if ((change.category === "entity" || change.category === "patch") && change.id !== void 0) {
799
+ const key = graphDevtoolsEntityIdentityKey(change.key, change.id);
800
+ touched.add(key);
801
+ valueTouched.add(key);
802
+ continue;
803
+ }
804
+ if (change.category === "entity-state" || change.category === "sync") {
805
+ for (const identity of graphDevtoolsEntityIdentitiesForStateKey(state, change.key)) {
806
+ touched.add(graphDevtoolsEntityIdentityKey(identity.type, identity.id));
807
+ }
808
+ }
809
+ }
810
+ for (const key of touched) revisions.set(key, (revisions.get(key) ?? 0) + 1);
811
+ if (valueRevisions) {
812
+ for (const key of valueTouched) valueRevisions.set(key, (valueRevisions.get(key) ?? 0) + 1);
813
+ }
814
+ }
815
+
816
+ // src/engine.ts
817
+ function serializeKey(key) {
818
+ return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
819
+ }
820
+
821
+ // src/crud/relations.ts
822
+ var schemaRegistry = /* @__PURE__ */ new Map();
823
+ function getRegisteredSchemas() {
824
+ return [...schemaRegistry.values()];
825
+ }
826
+
827
+ // src/devtools/relationships.ts
828
+ function mergedEntity(state, type, id) {
829
+ const canonical = state.entities[type]?.[id];
830
+ if (!canonical) return null;
831
+ return { ...canonical, ...state.patches[type]?.[id] ?? {} };
832
+ }
833
+ function statusFor(state, type, id) {
834
+ return state.entities[type]?.[id] ? "resolved" : "missing-target";
835
+ }
836
+ function edge(relation, descriptor, direction, sourceType, sourceId, sourceField, targetType, targetId, state) {
837
+ return {
838
+ relation,
839
+ cardinality: descriptor.cardinality,
840
+ direction,
841
+ source: { type: sourceType, id: sourceId, field: sourceField },
842
+ target: { type: targetType, id: targetId },
843
+ status: statusFor(state, targetType, targetId)
844
+ };
845
+ }
846
+ function belongsToEdges(state, schemaType, relation, descriptor) {
847
+ const edges = [];
848
+ for (const id of Object.keys(state.entities[schemaType] ?? {})) {
849
+ const entity = mergedEntity(state, schemaType, id);
850
+ const targetId = entity?.[descriptor.foreignKey];
851
+ if (typeof targetId !== "string" || targetId.length === 0) continue;
852
+ edges.push(edge(
853
+ relation,
854
+ descriptor,
855
+ "outgoing",
856
+ schemaType,
857
+ id,
858
+ descriptor.foreignKey,
859
+ descriptor.targetType,
860
+ targetId,
861
+ state
862
+ ));
863
+ }
864
+ return edges;
865
+ }
866
+ function hasManyEdges(state, schemaType, relation, descriptor) {
867
+ const edges = [];
868
+ for (const parentId of Object.keys(state.entities[schemaType] ?? {})) {
869
+ const childIds = /* @__PURE__ */ new Set();
870
+ for (const childId of Object.keys(state.entities[descriptor.targetType] ?? {})) {
871
+ const child = mergedEntity(state, descriptor.targetType, childId);
872
+ if (child?.[descriptor.foreignKey] === parentId) childIds.add(childId);
873
+ }
874
+ const listKey = serializeKey(descriptor.listKeyPrefix(parentId));
875
+ for (const childId of state.lists[listKey]?.ids ?? []) {
876
+ if (!state.entities[descriptor.targetType]?.[childId]) childIds.add(childId);
877
+ }
878
+ for (const childId of childIds) {
879
+ edges.push(edge(
880
+ relation,
881
+ descriptor,
882
+ "reverse",
883
+ schemaType,
884
+ parentId,
885
+ null,
886
+ descriptor.targetType,
887
+ childId,
888
+ state
889
+ ));
890
+ }
891
+ }
892
+ return edges;
893
+ }
894
+ function manyToManyEdges(state, schemaType, relation, descriptor) {
895
+ const field = descriptor.localArrayField;
896
+ if (!field) return [];
897
+ const edges = [];
898
+ for (const id of Object.keys(state.entities[schemaType] ?? {})) {
899
+ const entity = mergedEntity(state, schemaType, id);
900
+ const targetIds = entity?.[field];
901
+ if (!Array.isArray(targetIds)) continue;
902
+ for (const targetId of new Set(targetIds)) {
903
+ if (typeof targetId !== "string" || targetId.length === 0) continue;
904
+ edges.push(edge(
905
+ relation,
906
+ descriptor,
907
+ "outgoing",
908
+ schemaType,
909
+ id,
910
+ field,
911
+ descriptor.targetType,
912
+ targetId,
913
+ state
914
+ ));
915
+ }
916
+ }
917
+ return edges;
918
+ }
919
+ function compareRelationships(left, right) {
920
+ const leftKey = [
921
+ left.source.type,
922
+ left.source.id,
923
+ left.relation,
924
+ left.direction,
925
+ left.target.type,
926
+ left.target.id
927
+ ].join("\0");
928
+ const rightKey = [
929
+ right.source.type,
930
+ right.source.id,
931
+ right.relation,
932
+ right.direction,
933
+ right.target.type,
934
+ right.target.id
935
+ ].join("\0");
936
+ if (leftKey === rightKey) return 0;
937
+ return leftKey < rightKey ? -1 : 1;
938
+ }
939
+ function projectGraphDevtoolsRelationships(state, storeId) {
940
+ const relationships = [];
941
+ for (const schema of getRegisteredSchemas()) {
942
+ for (const [name, descriptor] of Object.entries(schema.relations ?? {})) {
943
+ switch (descriptor.cardinality) {
944
+ case "belongsTo":
945
+ relationships.push(...belongsToEdges(state, schema.type, name, descriptor));
946
+ break;
947
+ case "hasMany":
948
+ relationships.push(...hasManyEdges(state, schema.type, name, descriptor));
949
+ break;
950
+ case "manyToMany":
951
+ relationships.push(...manyToManyEdges(state, schema.type, name, descriptor));
952
+ break;
953
+ }
954
+ }
955
+ }
956
+ relationships.sort(compareRelationships);
957
+ return {
958
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
959
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
960
+ storeId,
961
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
962
+ relationships
963
+ };
964
+ }
965
+
966
+ // src/devtools-compatibility-bridge.ts
967
+ var BRIDGE_KEY = /* @__PURE__ */ Symbol.for("prometheus.entity-graph.devtools.compatibility.v1");
968
+ var bridgeHost = globalThis;
969
+ var existingBridge = bridgeHost[BRIDGE_KEY];
970
+ var bridgeState = existingBridge?.version === 1 ? existingBridge : { version: 1, entries: /* @__PURE__ */ new WeakMap(), factory: null };
971
+ bridgeHost[BRIDGE_KEY] = bridgeState;
972
+ function getOrCreateEntry(store) {
973
+ let entry = bridgeState.entries.get(store);
974
+ if (!entry) {
975
+ entry = {
976
+ delegate: null,
977
+ listeners: /* @__PURE__ */ new Set(),
978
+ unsubscribeDelegate: null,
979
+ compatibilityDetach: null
980
+ };
981
+ bridgeState.entries.set(store, entry);
982
+ }
983
+ return entry;
984
+ }
985
+ function registerGraphDevtoolsCompatibilityFactory(factory) {
986
+ bridgeState.factory = factory;
987
+ }
988
+ function notify(entry) {
989
+ for (const listener of [...entry.listeners]) {
990
+ try {
991
+ listener();
992
+ } catch {
993
+ }
994
+ }
995
+ }
996
+ function registerGraphDevtoolsCompatibilityDelegate(store, delegate) {
997
+ const entry = getOrCreateEntry(store);
998
+ entry.unsubscribeDelegate?.();
999
+ entry.delegate = delegate;
1000
+ entry.unsubscribeDelegate = delegate.subscribe(() => notify(entry));
1001
+ notify(entry);
1002
+ let unregistered = false;
1003
+ return () => {
1004
+ if (unregistered || entry.delegate !== delegate) return;
1005
+ unregistered = true;
1006
+ entry.unsubscribeDelegate?.();
1007
+ entry.unsubscribeDelegate = null;
1008
+ entry.delegate = null;
1009
+ notify(entry);
1010
+ if (entry.listeners.size === 0) bridgeState.entries.delete(store);
1011
+ };
1012
+ }
1013
+
1014
+ // src/devtools/time-travel.ts
1015
+ function cloneGraphData(data) {
1016
+ return structuredClone({
1017
+ entities: data.entities,
1018
+ patches: data.patches,
1019
+ entityStates: data.entityStates,
1020
+ syncMetadata: data.syncMetadata,
1021
+ lists: data.lists
1022
+ });
1023
+ }
1024
+ function encodedBytes(value) {
1025
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1026
+ }
1027
+ function isRecord(value) {
1028
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
1029
+ const prototype = Object.getPrototypeOf(value);
1030
+ return prototype === Object.prototype || prototype === null;
1031
+ }
1032
+ function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
1033
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1034
+ if (typeof value === "number") return Number.isFinite(value);
1035
+ if (typeof value !== "object") return false;
1036
+ if (ancestors.has(value)) return false;
1037
+ ancestors.add(value);
1038
+ const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : isRecord(value) && Object.values(value).every((item) => isJsonValue(item, ancestors));
1039
+ ancestors.delete(value);
1040
+ return valid;
1041
+ }
1042
+ function isNullableString(value) {
1043
+ return value === null || typeof value === "string";
1044
+ }
1045
+ function isNullableFiniteNumber(value) {
1046
+ return value === null || typeof value === "number" && Number.isFinite(value);
1047
+ }
1048
+ function isTimestamp(value) {
1049
+ return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
1050
+ }
1051
+ function isEntityTable(value) {
1052
+ return isRecord(value) && Object.values(value).every(
1053
+ (entities) => isRecord(entities) && Object.values(entities).every(isRecord)
1054
+ );
1055
+ }
1056
+ function isEntityStates(value) {
1057
+ return isRecord(value) && Object.values(value).every((state) => isRecord(state) && typeof state.isFetching === "boolean" && isNullableFiniteNumber(state.lastFetched) && isNullableString(state.error) && typeof state.stale === "boolean");
1058
+ }
1059
+ function isSyncMetadata(value) {
1060
+ return isRecord(value) && Object.values(value).every((metadata) => isRecord(metadata) && typeof metadata.synced === "boolean" && (metadata.origin === "server" || metadata.origin === "client" || metadata.origin === "optimistic") && isNullableFiniteNumber(metadata.updatedAt));
1061
+ }
1062
+ function isLists(value) {
1063
+ return isRecord(value) && Object.values(value).every((list) => isRecord(list) && Array.isArray(list.ids) && list.ids.every((id) => typeof id === "string") && isNullableFiniteNumber(list.total) && isNullableString(list.nextCursor) && isNullableString(list.prevCursor) && typeof list.hasNextPage === "boolean" && typeof list.hasPrevPage === "boolean" && typeof list.isFetching === "boolean" && typeof list.isFetchingMore === "boolean" && isNullableString(list.error) && (list.lastError === null || isRecord(list.lastError)) && isNullableFiniteNumber(list.lastFetched) && typeof list.stale === "boolean" && isNullableFiniteNumber(list.currentPage) && isNullableFiniteNumber(list.pageSize));
1064
+ }
1065
+ function isGraphData(value) {
1066
+ return isRecord(value) && isEntityTable(value.entities) && isEntityTable(value.patches) && isEntityStates(value.entityStates) && isSyncMetadata(value.syncMetadata) && isLists(value.lists);
1067
+ }
1068
+ function createGraphDevtoolsSnapshotHistory(options) {
1069
+ const retained = [];
1070
+ const compatibilityLabels = /* @__PURE__ */ new Map();
1071
+ const listeners = /* @__PURE__ */ new Set();
1072
+ let snapshotLimit = options.snapshotLimit;
1073
+ let retainedBytes = 0;
1074
+ let latestCursor = 0;
1075
+ let baselineCursor = null;
1076
+ let lastUnavailable = null;
1077
+ const unavailableReferences = /* @__PURE__ */ new Map();
1078
+ let clearedThroughCursor = 0;
1079
+ let mode = "live";
1080
+ let activeCursor = null;
1081
+ let activeSource = null;
1082
+ let protectedLiveHead = null;
1083
+ let importedCandidate = null;
1084
+ let nextImportCandidate = 1;
1085
+ let disposed = false;
1086
+ const notify2 = () => {
1087
+ for (const listener of [...listeners]) {
1088
+ try {
1089
+ listener();
1090
+ } catch {
1091
+ }
1092
+ }
1093
+ };
1094
+ const unavailable = (cursor, capturedAt, eventSequence, reason, label) => {
1095
+ const reference = {
1096
+ cursor,
1097
+ capturedAt,
1098
+ eventSequence,
1099
+ ...label !== void 0 ? { label } : {},
1100
+ status: "unavailable",
1101
+ reason
1102
+ };
1103
+ lastUnavailable = reference;
1104
+ unavailableReferences.set(cursor, reference);
1105
+ while (unavailableReferences.size > Math.max(1, snapshotLimit)) {
1106
+ const oldest = unavailableReferences.keys().next().value;
1107
+ if (oldest === void 0) break;
1108
+ unavailableReferences.delete(oldest);
1109
+ }
1110
+ return reference;
1111
+ };
1112
+ const expired = (cursor) => {
1113
+ if (cursor > latestCursor) return null;
1114
+ const status = history.getStatus();
1115
+ const unavailableReference = unavailableReferences.get(cursor);
1116
+ if (unavailableReference) {
1117
+ return {
1118
+ status: "expired-history",
1119
+ cursor,
1120
+ reason: "unavailable",
1121
+ unavailableReason: unavailableReference.reason,
1122
+ oldestCursor: status.oldestCursor,
1123
+ newestCursor: status.newestCursor,
1124
+ latestCursor: status.latestCursor
1125
+ };
1126
+ }
1127
+ return {
1128
+ status: "expired-history",
1129
+ cursor,
1130
+ reason: cursor <= clearedThroughCursor ? "cleared" : "evicted",
1131
+ oldestCursor: status.oldestCursor,
1132
+ newestCursor: status.newestCursor,
1133
+ latestCursor: status.latestCursor
1134
+ };
1135
+ };
1136
+ const enterRewind = (cursor, source, data, liveState, restore) => {
1137
+ if (disposed || mode === "rewound" && protectedLiveHead === null) return null;
1138
+ const previousCursor = activeCursor;
1139
+ const previousSource = activeSource;
1140
+ let target;
1141
+ let nextLiveHead = protectedLiveHead;
1142
+ try {
1143
+ target = cloneGraphData(data);
1144
+ if (mode === "live") nextLiveHead = cloneGraphData(liveState);
1145
+ restore(target);
1146
+ } catch {
1147
+ return null;
1148
+ }
1149
+ protectedLiveHead = nextLiveHead;
1150
+ mode = "rewound";
1151
+ activeCursor = cursor;
1152
+ activeSource = source;
1153
+ return { status: "rewound", previousCursor, previousSource };
1154
+ };
1155
+ const history = {
1156
+ capture(state, eventSequence, label) {
1157
+ importedCandidate = null;
1158
+ const cursor = ++latestCursor;
1159
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
1160
+ if (baselineCursor === null) baselineCursor = cursor;
1161
+ if (disposed || snapshotLimit === 0 || options.snapshotBytesLimit === 0) {
1162
+ const reference2 = unavailable(cursor, capturedAt, eventSequence, "retention-disabled", label);
1163
+ notify2();
1164
+ return reference2;
1165
+ }
1166
+ let data;
1167
+ let bytes;
1168
+ try {
1169
+ data = cloneGraphData(state);
1170
+ bytes = encodedBytes(data);
1171
+ } catch {
1172
+ const reference2 = unavailable(cursor, capturedAt, eventSequence, "capture-failed", label);
1173
+ notify2();
1174
+ return reference2;
1175
+ }
1176
+ if (bytes > options.snapshotBytesLimit) {
1177
+ const unavailableReference = unavailable(cursor, capturedAt, eventSequence, "oversize", label);
1178
+ notify2();
1179
+ return unavailableReference;
1180
+ }
1181
+ const reference = {
1182
+ cursor,
1183
+ capturedAt,
1184
+ eventSequence,
1185
+ ...label !== void 0 ? { label } : {},
1186
+ status: "retained",
1187
+ bytes
1188
+ };
1189
+ retained.push({ reference, data });
1190
+ retainedBytes += bytes;
1191
+ while (retained.length > snapshotLimit || retainedBytes > options.snapshotBytesLimit) {
1192
+ const removed = retained.shift();
1193
+ if (removed) compatibilityLabels.delete(removed.reference.cursor);
1194
+ retainedBytes -= removed?.reference.bytes ?? 0;
1195
+ }
1196
+ notify2();
1197
+ return reference;
1198
+ },
1199
+ captureCompatibility(state, label) {
1200
+ const candidateCursor = mode === "live" ? latestCursor : activeSource === "retained" ? activeCursor : null;
1201
+ const current = candidateCursor === null ? void 0 : retained.find(({ reference: reference2 }) => reference2.cursor === candidateCursor);
1202
+ if (current && !compatibilityLabels.has(current.reference.cursor)) {
1203
+ compatibilityLabels.set(current.reference.cursor, label);
1204
+ notify2();
1205
+ return {
1206
+ ...current.reference,
1207
+ ...label !== void 0 ? { label } : {}
1208
+ };
1209
+ }
1210
+ const reference = history.capture(state, null, label);
1211
+ if (reference.status === "retained") {
1212
+ compatibilityLabels.set(reference.cursor, label);
1213
+ }
1214
+ return reference;
1215
+ },
1216
+ getStatus() {
1217
+ return {
1218
+ mode,
1219
+ cursor: activeCursor,
1220
+ source: activeSource,
1221
+ retainedSnapshots: retained.length,
1222
+ retainedBytes,
1223
+ snapshotLimit,
1224
+ byteLimit: options.snapshotBytesLimit,
1225
+ baselineCursor,
1226
+ oldestCursor: retained[0]?.reference.cursor ?? null,
1227
+ newestCursor: retained.length > 0 ? retained[retained.length - 1].reference.cursor : null,
1228
+ latestCursor: latestCursor === 0 ? null : latestCursor,
1229
+ lastUnavailable,
1230
+ importCandidate: importedCandidate ? {
1231
+ candidateId: importedCandidate.candidateId,
1232
+ snapshots: importedCandidate.snapshots.length,
1233
+ bytes: importedCandidate.bytes
1234
+ } : null
1235
+ };
1236
+ },
1237
+ getReferences() {
1238
+ return retained.map(({ reference }) => ({ ...reference }));
1239
+ },
1240
+ getCompatibilityReferences() {
1241
+ return retained.filter(({ reference }) => compatibilityLabels.has(reference.cursor)).map(({ reference }) => {
1242
+ const label = compatibilityLabels.get(reference.cursor);
1243
+ return { ...reference, ...label !== void 0 ? { label } : {} };
1244
+ });
1245
+ },
1246
+ read(cursor) {
1247
+ const snapshot = retained.find((candidate) => candidate.reference.cursor === cursor);
1248
+ return snapshot ? cloneGraphData(snapshot.data) : null;
1249
+ },
1250
+ rewind(cursor, liveState, restore) {
1251
+ if (disposed) return null;
1252
+ const snapshot = retained.find((candidate) => candidate.reference.cursor === cursor);
1253
+ if (!snapshot) return expired(cursor);
1254
+ const result = enterRewind(cursor, "retained", snapshot.data, liveState, restore);
1255
+ if (result) notify2();
1256
+ return result;
1257
+ },
1258
+ inspectImport(candidate, storeId) {
1259
+ importedCandidate = null;
1260
+ const rejected = (reason, message) => {
1261
+ notify2();
1262
+ return { status: "rejected", reason, message };
1263
+ };
1264
+ try {
1265
+ if (!isRecord(candidate) || !isJsonValue(candidate) || candidate.protocol !== GRAPH_DEVTOOLS_PROTOCOL) {
1266
+ return rejected("invalid-envelope", "Import must use the Prometheus entity-graph DevTools protocol");
1267
+ }
1268
+ if (candidate.version !== GRAPH_DEVTOOLS_PROTOCOL_VERSION) {
1269
+ return rejected("unsupported-version", `Import protocol version ${String(candidate.version)} is unsupported`);
1270
+ }
1271
+ if (candidate.storeId !== storeId) {
1272
+ return rejected("wrong-store", `Import targets ${String(candidate.storeId)}, not ${storeId}`);
1273
+ }
1274
+ if (!isTimestamp(candidate.exportedAt) || !Array.isArray(candidate.snapshots) || candidate.snapshots.length === 0) {
1275
+ return rejected("invalid-envelope", "Import metadata and at least one snapshot are required");
1276
+ }
1277
+ if (candidate.snapshots.length > snapshotLimit) {
1278
+ return rejected("snapshot-limit-exceeded", `Import contains ${candidate.snapshots.length} snapshots; limit is ${snapshotLimit}`);
1279
+ }
1280
+ const importBytes = encodedBytes(candidate);
1281
+ if (importBytes > options.snapshotBytesLimit) {
1282
+ return rejected("byte-limit-exceeded", `Import contains ${importBytes} bytes; limit is ${options.snapshotBytesLimit}`);
1283
+ }
1284
+ const snapshots = [];
1285
+ let previousCursor = 0;
1286
+ for (const value of candidate.snapshots) {
1287
+ if (!isRecord(value)) return rejected("invalid-envelope", "Every imported snapshot must be an object");
1288
+ const cursor = value.cursor;
1289
+ const eventSequence = value.eventSequence;
1290
+ if (typeof cursor !== "number" || !Number.isSafeInteger(cursor) || cursor <= previousCursor || !isTimestamp(value.capturedAt) || !(eventSequence === null || typeof eventSequence === "number" && Number.isSafeInteger(eventSequence) && eventSequence > 0) || !isGraphData(value.data)) {
1291
+ return rejected("invalid-envelope", "Imported snapshots must have ordered stable cursors and valid graph data");
1292
+ }
1293
+ const data = cloneGraphData(value.data);
1294
+ snapshots.push({
1295
+ cursor,
1296
+ capturedAt: value.capturedAt,
1297
+ eventSequence,
1298
+ bytes: encodedBytes(data),
1299
+ data
1300
+ });
1301
+ previousCursor = cursor;
1302
+ }
1303
+ const candidateBytes = snapshots.reduce((total, snapshot) => total + snapshot.bytes, 0);
1304
+ while (retained.length + snapshots.length > snapshotLimit || retainedBytes + candidateBytes > options.snapshotBytesLimit) {
1305
+ const removed = retained.shift();
1306
+ if (!removed) break;
1307
+ compatibilityLabels.delete(removed.reference.cursor);
1308
+ retainedBytes -= removed.reference.bytes;
1309
+ }
1310
+ const candidateId = `import-${nextImportCandidate++}`;
1311
+ importedCandidate = { candidateId, bytes: candidateBytes, snapshots };
1312
+ notify2();
1313
+ const receipt = {
1314
+ status: "awaiting-confirmation",
1315
+ candidateId,
1316
+ storeId,
1317
+ protocolVersion: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1318
+ inspectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1319
+ bytes: importBytes,
1320
+ snapshots: snapshots.map(({ data: _data, ...metadata }) => metadata)
1321
+ };
1322
+ return receipt;
1323
+ } catch {
1324
+ return rejected("invalid-envelope", "Import could not be safely inspected or cloned");
1325
+ }
1326
+ },
1327
+ restoreImport(candidateId, cursor, liveState, restore) {
1328
+ if (!importedCandidate || importedCandidate.candidateId !== candidateId) {
1329
+ return { status: "candidate-not-found" };
1330
+ }
1331
+ const snapshot = importedCandidate.snapshots.find((candidate) => candidate.cursor === cursor);
1332
+ if (!snapshot) return { status: "snapshot-not-found" };
1333
+ const restored = enterRewind(cursor, "import", snapshot.data, liveState, restore);
1334
+ if (!restored) return { status: "restore-failed" };
1335
+ importedCandidate = null;
1336
+ notify2();
1337
+ return restored;
1338
+ },
1339
+ returnToLive(restore) {
1340
+ if (disposed || mode !== "rewound" || protectedLiveHead === null) return null;
1341
+ const previousCursor = activeCursor;
1342
+ const previousSource = activeSource;
1343
+ if (previousCursor === null || previousSource === null) return null;
1344
+ try {
1345
+ restore(cloneGraphData(protectedLiveHead));
1346
+ } catch {
1347
+ return null;
1348
+ }
1349
+ mode = "live";
1350
+ activeCursor = null;
1351
+ activeSource = null;
1352
+ protectedLiveHead = null;
1353
+ notify2();
1354
+ return { previousCursor, previousSource };
1355
+ },
1356
+ leaveRewindForMutation() {
1357
+ if (mode !== "rewound") return null;
1358
+ const previousCursor = activeCursor;
1359
+ const previousSource = activeSource;
1360
+ if (previousCursor === null || previousSource === null) return null;
1361
+ mode = "live";
1362
+ activeCursor = null;
1363
+ activeSource = null;
1364
+ protectedLiveHead = null;
1365
+ notify2();
1366
+ return { previousCursor, previousSource };
1367
+ },
1368
+ configureLimit(nextLimit) {
1369
+ if (!Number.isFinite(nextLimit) || nextLimit <= 0) return;
1370
+ snapshotLimit = Math.max(1, Math.floor(nextLimit));
1371
+ while (retained.length > snapshotLimit) {
1372
+ const removed = retained.shift();
1373
+ if (removed) compatibilityLabels.delete(removed.reference.cursor);
1374
+ retainedBytes -= removed?.reference.bytes ?? 0;
1375
+ }
1376
+ while (unavailableReferences.size > Math.max(1, snapshotLimit)) {
1377
+ const oldest = unavailableReferences.keys().next().value;
1378
+ if (oldest === void 0) break;
1379
+ unavailableReferences.delete(oldest);
1380
+ }
1381
+ if (importedCandidate && retained.length + importedCandidate.snapshots.length > snapshotLimit) {
1382
+ importedCandidate = null;
1383
+ }
1384
+ notify2();
1385
+ },
1386
+ subscribe(listener) {
1387
+ if (disposed) return () => {
1388
+ };
1389
+ listeners.add(listener);
1390
+ return () => listeners.delete(listener);
1391
+ },
1392
+ clear() {
1393
+ retained.length = 0;
1394
+ compatibilityLabels.clear();
1395
+ retainedBytes = 0;
1396
+ clearedThroughCursor = latestCursor;
1397
+ importedCandidate = null;
1398
+ notify2();
1399
+ },
1400
+ dispose() {
1401
+ disposed = true;
1402
+ retained.length = 0;
1403
+ compatibilityLabels.clear();
1404
+ retainedBytes = 0;
1405
+ lastUnavailable = null;
1406
+ unavailableReferences.clear();
1407
+ mode = "live";
1408
+ activeCursor = null;
1409
+ activeSource = null;
1410
+ protectedLiveHead = null;
1411
+ importedCandidate = null;
1412
+ listeners.clear();
1413
+ }
1414
+ };
1415
+ return history;
1416
+ }
1417
+
1418
+ // src/devtools/views.ts
1419
+ function orderedUnique(values) {
1420
+ return [...new Set(values)];
1421
+ }
1422
+ function sameValues(left, right) {
1423
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1424
+ }
1425
+ function membershipFor(entry) {
1426
+ const entityIds = /* @__PURE__ */ new Set();
1427
+ for (const registration of entry.registrations.values()) {
1428
+ for (const id of registration.entityIds) entityIds.add(id);
1429
+ }
1430
+ return [...entityIds].map((id) => ({ type: entry.definition.entityType, id }));
1431
+ }
1432
+ function viewRecord(entry, state) {
1433
+ const membership = membershipFor(entry);
1434
+ const registrations = [...entry.registrations.values()];
1435
+ const lastRenderedAt = registrations.map((registration) => registration.lastRenderedAt).filter((value) => value !== null).sort().at(-1) ?? null;
1436
+ const queryKey = entry.definition.queryKey ?? null;
1437
+ const listState = queryKey === null ? EMPTY_LIST_STATE : state.lists[queryKey] ?? EMPTY_LIST_STATE;
1438
+ return {
1439
+ ...entry.definition,
1440
+ queryKey,
1441
+ registeredAt: entry.registeredAt,
1442
+ lastRenderedAt,
1443
+ renderCount: registrations.reduce((total, registration) => total + registration.renderCount, 0),
1444
+ subscriberCount: registrations.length,
1445
+ membership,
1446
+ list: entry.definition.kind === "list" ? {
1447
+ visibleCount: membership.length,
1448
+ graphCount: listState.ids.length,
1449
+ total: listState.total,
1450
+ isFetching: listState.isFetching,
1451
+ isFetchingMore: listState.isFetchingMore,
1452
+ stale: listState.stale,
1453
+ hasNextPage: listState.hasNextPage,
1454
+ hasPreviousPage: listState.hasPrevPage
1455
+ } : null
1456
+ };
1457
+ }
1458
+ function allEntityIdentities(state, memberships) {
1459
+ const identities = /* @__PURE__ */ new Map();
1460
+ for (const [type, bucket] of Object.entries(state.entities)) {
1461
+ for (const id of Object.keys(bucket)) {
1462
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
1463
+ }
1464
+ }
1465
+ for (const [type, bucket] of Object.entries(state.patches)) {
1466
+ for (const id of Object.keys(bucket)) {
1467
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
1468
+ }
1469
+ }
1470
+ for (const key of [
1471
+ ...Object.keys(state.entityStates),
1472
+ ...Object.keys(state.syncMetadata)
1473
+ ]) {
1474
+ for (const identity of graphDevtoolsEntityIdentitiesForStateKey(state, key)) {
1475
+ identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
1476
+ }
1477
+ }
1478
+ for (const key of memberships.keys()) {
1479
+ const identity = parseGraphDevtoolsEntityIdentityKey(key);
1480
+ if (identity) identities.set(key, identity);
1481
+ }
1482
+ return [...identities.values()].sort((left, right) => {
1483
+ if (left.type !== right.type) return left.type < right.type ? -1 : 1;
1484
+ if (left.id === right.id) return 0;
1485
+ return left.id < right.id ? -1 : 1;
1486
+ });
1487
+ }
1488
+ function createGraphDevtoolsViewRegistry(storeId, getState, onViewChange) {
1489
+ const entries = /* @__PURE__ */ new Map();
1490
+ let disposed = false;
1491
+ const viewIdsByEntity = () => {
1492
+ const result = /* @__PURE__ */ new Map();
1493
+ for (const [viewId, entry] of entries) {
1494
+ for (const membership of membershipFor(entry)) {
1495
+ const key = graphDevtoolsEntityIdentityKey(membership.type, membership.id);
1496
+ const viewIds = result.get(key) ?? [];
1497
+ viewIds.push(viewId);
1498
+ result.set(key, viewIds);
1499
+ }
1500
+ }
1501
+ for (const viewIds of result.values()) viewIds.sort();
1502
+ return result;
1503
+ };
1504
+ return {
1505
+ register(definition) {
1506
+ if (disposed) return { viewId: definition.viewId, updateMembership() {
1507
+ }, unregister() {
1508
+ } };
1509
+ const token = Symbol(definition.viewId);
1510
+ let entry = entries.get(definition.viewId);
1511
+ const isNew = entry === void 0;
1512
+ if (!entry) {
1513
+ entry = {
1514
+ definition: { ...definition, queryKey: definition.queryKey ?? null },
1515
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
1516
+ registrations: /* @__PURE__ */ new Map()
1517
+ };
1518
+ entries.set(definition.viewId, entry);
1519
+ }
1520
+ entry.registrations.set(token, { entityIds: [], renderCount: 0, lastRenderedAt: null });
1521
+ if (isNew) onViewChange({ state: "registered", viewId: definition.viewId, membershipCount: 0 });
1522
+ let unregistered = false;
1523
+ return {
1524
+ viewId: definition.viewId,
1525
+ updateMembership(entityIds) {
1526
+ if (disposed || unregistered) return;
1527
+ const currentEntry = entries.get(definition.viewId);
1528
+ const registration = currentEntry?.registrations.get(token);
1529
+ if (!currentEntry || !registration) return;
1530
+ const before = membershipFor(currentEntry);
1531
+ registration.entityIds = orderedUnique(entityIds);
1532
+ registration.renderCount += 1;
1533
+ registration.lastRenderedAt = (/* @__PURE__ */ new Date()).toISOString();
1534
+ const after = membershipFor(currentEntry);
1535
+ const beforeKeys = before.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1536
+ const afterKeys = after.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1537
+ if (!sameValues(beforeKeys, afterKeys)) {
1538
+ onViewChange({
1539
+ state: "membership-changed",
1540
+ viewId: definition.viewId,
1541
+ membershipCount: after.length
1542
+ });
1543
+ }
1544
+ },
1545
+ unregister() {
1546
+ if (disposed || unregistered) return;
1547
+ unregistered = true;
1548
+ const currentEntry = entries.get(definition.viewId);
1549
+ if (!currentEntry) return;
1550
+ const before = membershipFor(currentEntry);
1551
+ currentEntry.registrations.delete(token);
1552
+ if (currentEntry.registrations.size === 0) {
1553
+ entries.delete(definition.viewId);
1554
+ onViewChange({ state: "unregistered", viewId: definition.viewId, membershipCount: 0 });
1555
+ return;
1556
+ }
1557
+ const after = membershipFor(currentEntry);
1558
+ const beforeKeys = before.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1559
+ const afterKeys = after.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1560
+ if (!sameValues(beforeKeys, afterKeys)) {
1561
+ onViewChange({
1562
+ state: "membership-changed",
1563
+ viewId: definition.viewId,
1564
+ membershipCount: after.length
1565
+ });
1566
+ }
1567
+ }
1568
+ };
1569
+ },
1570
+ getSnapshot() {
1571
+ const state = getState();
1572
+ const memberships = viewIdsByEntity();
1573
+ return {
1574
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1575
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1576
+ storeId,
1577
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1578
+ views: [...entries.values()].map((entry) => viewRecord(entry, state)).sort((left, right) => left.viewId < right.viewId ? -1 : left.viewId > right.viewId ? 1 : 0),
1579
+ entityViewMembership: allEntityIdentities(state, memberships).map(({ type, id }) => ({
1580
+ type,
1581
+ id,
1582
+ viewIds: [...memberships.get(graphDevtoolsEntityIdentityKey(type, id)) ?? []]
1583
+ }))
1584
+ };
1585
+ },
1586
+ getViewIdsByEntity() {
1587
+ return viewIdsByEntity();
1588
+ },
1589
+ dispose() {
1590
+ disposed = true;
1591
+ entries.clear();
1592
+ }
1593
+ };
1594
+ }
1595
+
1596
+ // src/devtools/previews.ts
1597
+ function replaceEntityPatch(store, type, id, patch) {
1598
+ store.setState((state) => {
1599
+ const patches = { ...state.patches };
1600
+ const bucket = { ...patches[type] ?? {} };
1601
+ if (patch === void 0) delete bucket[id];
1602
+ else bucket[id] = { ...patch };
1603
+ if (Object.keys(bucket).length === 0) delete patches[type];
1604
+ else patches[type] = bucket;
1605
+ return { patches };
1606
+ });
1607
+ }
1608
+ function createGraphDevtoolsPreviewController(store, storeId, valuePolicy, valueRevisionFor) {
1609
+ const receipts = /* @__PURE__ */ new Map();
1610
+ const activeByEntity = /* @__PURE__ */ new Map();
1611
+ let nextPreview = 1;
1612
+ let disposed = false;
1613
+ const projectPatch = (type, id, value) => projectGraphDevtoolsInspectionValue(
1614
+ value,
1615
+ valuePolicy,
1616
+ { storeId, category: "patch", key: type, id }
1617
+ );
1618
+ const removeReceipt = (receipt) => {
1619
+ receipts.delete(receipt.previewId);
1620
+ const key = graphDevtoolsEntityIdentityKey(receipt.type, receipt.id);
1621
+ if (activeByEntity.get(key) === receipt.previewId) activeByEntity.delete(key);
1622
+ };
1623
+ return {
1624
+ apply(type, id, patch) {
1625
+ if (disposed || !store.getState().entities[type]?.[id]) return null;
1626
+ const key = graphDevtoolsEntityIdentityKey(type, id);
1627
+ const previousPreviewId = activeByEntity.get(key);
1628
+ if (previousPreviewId) receipts.delete(previousPreviewId);
1629
+ const storedPriorPatch = store.getState().patches[type]?.[id];
1630
+ const priorPatch = storedPriorPatch === void 0 ? void 0 : { ...storedPriorPatch };
1631
+ const previewPatch = { ...patch };
1632
+ const baselineRevision = valueRevisionFor(type, id);
1633
+ store.getState().patchEntity(type, id, previewPatch);
1634
+ const appliedPatch = { ...store.getState().patches[type]?.[id] ?? {} };
1635
+ const previewRevision = valueRevisionFor(type, id);
1636
+ const receipt = {
1637
+ previewId: `preview-${storeId}-${nextPreview++}`,
1638
+ type,
1639
+ id,
1640
+ priorPatch,
1641
+ previewPatch,
1642
+ appliedPatch,
1643
+ baselineRevision,
1644
+ previewRevision,
1645
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString()
1646
+ };
1647
+ receipts.set(receipt.previewId, receipt);
1648
+ activeByEntity.set(key, receipt.previewId);
1649
+ return {
1650
+ previewId: receipt.previewId,
1651
+ entity: { type, id },
1652
+ priorPatch: priorPatch === void 0 ? null : projectPatch(type, id, priorPatch),
1653
+ previewPatch: projectPatch(type, id, previewPatch),
1654
+ appliedPatch: projectPatch(type, id, appliedPatch),
1655
+ baselineRevision,
1656
+ previewRevision,
1657
+ appliedAt: receipt.appliedAt
1658
+ };
1659
+ },
1660
+ restore(previewId) {
1661
+ if (disposed) return null;
1662
+ const receipt = receipts.get(previewId);
1663
+ if (!receipt) return null;
1664
+ const observedRevision = valueRevisionFor(receipt.type, receipt.id);
1665
+ if (observedRevision !== receipt.previewRevision) {
1666
+ const currentPatch = store.getState().patches[receipt.type]?.[receipt.id];
1667
+ const conflict = {
1668
+ previewId,
1669
+ status: "conflict",
1670
+ reason: "entity-changed-since-preview",
1671
+ expectedRevision: receipt.previewRevision,
1672
+ observedRevision,
1673
+ currentPatch: currentPatch === void 0 ? null : projectPatch(receipt.type, receipt.id, currentPatch),
1674
+ priorPatch: receipt.priorPatch === void 0 ? null : projectPatch(receipt.type, receipt.id, receipt.priorPatch)
1675
+ };
1676
+ removeReceipt(receipt);
1677
+ return conflict;
1678
+ }
1679
+ replaceEntityPatch(store, receipt.type, receipt.id, receipt.priorPatch);
1680
+ removeReceipt(receipt);
1681
+ return {
1682
+ previewId,
1683
+ status: "restored",
1684
+ restoredPatch: receipt.priorPatch === void 0 ? null : projectPatch(receipt.type, receipt.id, receipt.priorPatch),
1685
+ observedRevision,
1686
+ restoredAt: (/* @__PURE__ */ new Date()).toISOString()
1687
+ };
1688
+ },
1689
+ dispose() {
1690
+ disposed = true;
1691
+ receipts.clear();
1692
+ activeByEntity.clear();
1693
+ }
1694
+ };
1695
+ }
1696
+
1697
+ // src/devtools/controller.ts
1698
+ var controllers = /* @__PURE__ */ new WeakMap();
1699
+ var storeNumbers = /* @__PURE__ */ new WeakMap();
1700
+ var nextStoreNumber = 1;
1701
+ function nowDuration() {
1702
+ return globalThis.performance?.now() ?? Date.now();
1703
+ }
1704
+ function boundedLimit(value, fallback, minimum = 0) {
1705
+ const candidate = value ?? fallback;
1706
+ return Number.isFinite(candidate) ? Math.max(minimum, Math.floor(candidate)) : fallback;
1707
+ }
1708
+ function storeIdFor(store) {
1709
+ let number = storeNumbers.get(store);
1710
+ if (number === void 0) {
1711
+ number = nextStoreNumber++;
1712
+ storeNumbers.set(store, number);
1713
+ }
1714
+ return `graph-${number}`;
1715
+ }
1716
+ function resultError(storeId, requestId, code, message) {
1717
+ return {
1718
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1719
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1720
+ requestId,
1721
+ storeId,
1722
+ ok: false,
1723
+ error: { code, message }
1724
+ };
1725
+ }
1726
+ function parseCommand(command) {
1727
+ if (typeof command !== "object" || command === null) return null;
1728
+ const candidate = command;
1729
+ if (candidate.protocol !== GRAPH_DEVTOOLS_PROTOCOL || typeof candidate.version !== "number" || typeof candidate.requestId !== "string" || typeof candidate.storeId !== "string" || typeof candidate.command !== "string") return null;
1730
+ return candidate;
1731
+ }
1732
+ function parsePreviewPayload(payload) {
1733
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1734
+ const candidate = payload;
1735
+ if (typeof candidate.type !== "string" || candidate.type.length === 0 || typeof candidate.id !== "string" || candidate.id.length === 0 || typeof candidate.patch !== "object" || candidate.patch === null || Array.isArray(candidate.patch) || Object.keys(candidate.patch).length === 0) return null;
1736
+ return candidate;
1737
+ }
1738
+ function parseRestorePreviewPayload(payload) {
1739
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1740
+ const previewId = payload.previewId;
1741
+ return typeof previewId === "string" && previewId.length > 0 ? { previewId } : null;
1742
+ }
1743
+ function parseRewindPayload(payload) {
1744
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1745
+ const cursor = payload.cursor;
1746
+ return typeof cursor === "number" && Number.isSafeInteger(cursor) && cursor > 0 ? { cursor } : null;
1747
+ }
1748
+ function parseInspectHistoryImportPayload(payload) {
1749
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1750
+ if (!Object.prototype.hasOwnProperty.call(payload, "candidate")) return null;
1751
+ return { candidate: payload.candidate };
1752
+ }
1753
+ function parseConfirmHistoryImportPayload(payload) {
1754
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1755
+ const candidate = payload;
1756
+ if (typeof candidate.candidateId !== "string" || candidate.candidateId.length === 0 || typeof candidate.cursor !== "number" || !Number.isSafeInteger(candidate.cursor) || candidate.cursor <= 0 || typeof candidate.confirm !== "boolean") return null;
1757
+ return candidate;
1758
+ }
1759
+ function createController(store, options) {
1760
+ const storeId = options.storeId ?? storeIdFor(store);
1761
+ const historyLimit = boundedLimit(options.historyLimit, 500);
1762
+ const historyBytesLimit = boundedLimit(options.historyBytesLimit, 5 * 1024 * 1024);
1763
+ const eventBytesLimit = boundedLimit(options.eventBytesLimit, 256 * 1024, 1024);
1764
+ const snapshotLimit = boundedLimit(options.snapshotLimit, 50);
1765
+ const snapshotBytesLimit = boundedLimit(options.snapshotBytesLimit, 10 * 1024 * 1024);
1766
+ const timeTravelEnabled = snapshotLimit > 0 && snapshotBytesLimit > 0;
1767
+ const timeTravelCommands = timeTravelEnabled ? [
1768
+ "get-time-travel-status",
1769
+ "rewind",
1770
+ "return-to-live",
1771
+ "inspect-history-import",
1772
+ "confirm-history-import"
1773
+ ] : [];
1774
+ const timeTravelFeatures = timeTravelEnabled ? ["snapshot-history", "time-travel", "history-import"] : [];
1775
+ const valuePolicy = options.values ?? { mode: "metadata-only" };
1776
+ const capabilities = {
1777
+ protocolVersion: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1778
+ metadataOnlyByDefault: true,
1779
+ commands: [
1780
+ "get-capabilities",
1781
+ "get-snapshot",
1782
+ "get-history",
1783
+ "get-history-status",
1784
+ "get-entity-records",
1785
+ "get-views",
1786
+ "get-relationships",
1787
+ "preview-entity-patch",
1788
+ "restore-entity-preview",
1789
+ ...timeTravelCommands,
1790
+ "clear-history"
1791
+ ],
1792
+ features: [
1793
+ "semantic-events",
1794
+ "diagnostic-events",
1795
+ "bounded-history",
1796
+ "multi-client",
1797
+ "multi-store",
1798
+ "entity-inspection",
1799
+ "view-inspection",
1800
+ "relationship-inspection",
1801
+ "local-preview",
1802
+ ...timeTravelFeatures
1803
+ ],
1804
+ limits: {
1805
+ historyEvents: historyLimit,
1806
+ historyBytes: historyBytesLimit,
1807
+ eventBytes: eventBytesLimit,
1808
+ snapshots: snapshotLimit,
1809
+ snapshotBytes: snapshotBytesLimit
1810
+ }
1811
+ };
1812
+ const listeners = /* @__PURE__ */ new Set();
1813
+ const history = [];
1814
+ const historySizes = [];
1815
+ const entityRevisions = /* @__PURE__ */ new Map();
1816
+ const entityValueRevisions = /* @__PURE__ */ new Map();
1817
+ let retainedBytes = 0;
1818
+ let sequence = 0;
1819
+ let activeClients = 0;
1820
+ let nextClientNumber = 1;
1821
+ let disposed = false;
1822
+ let projectionFailureRevision = 0;
1823
+ let internalReplayDepth = 0;
1824
+ const snapshotHistory = createGraphDevtoolsSnapshotHistory({
1825
+ snapshotLimit,
1826
+ snapshotBytesLimit
1827
+ });
1828
+ snapshotHistory.capture(store.getState(), null);
1829
+ const nextBase = () => {
1830
+ const current = ++sequence;
1831
+ const eventId = `${storeId}:${current}`;
1832
+ return {
1833
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1834
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1835
+ storeId,
1836
+ sequence: current,
1837
+ eventId,
1838
+ correlationId: eventId,
1839
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
1840
+ };
1841
+ };
1842
+ const encodedBytes2 = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
1843
+ const boundEvent = (event) => {
1844
+ if (event.type !== "mutation") return event;
1845
+ if (encodedBytes2(event) <= eventBytesLimit) return event;
1846
+ const valuesTruncated = event.payload.changes.some(
1847
+ (change) => "before" in change || "after" in change
1848
+ );
1849
+ const withoutValues = {
1850
+ ...event,
1851
+ payload: {
1852
+ ...event.payload,
1853
+ valuesTruncated,
1854
+ changes: event.payload.changes.map(({ before: _before, after: _after, ...change }) => ({
1855
+ ...change,
1856
+ valueState: change.valueState === "hidden-by-policy" || change.valueState === "redaction-error" ? change.valueState : "truncated"
1857
+ }))
1858
+ }
1859
+ };
1860
+ if (encodedBytes2(withoutValues) <= eventBytesLimit) return withoutValues;
1861
+ let lower = 0;
1862
+ let upper = withoutValues.payload.changes.length;
1863
+ let bounded = {
1864
+ ...withoutValues,
1865
+ payload: {
1866
+ ...withoutValues.payload,
1867
+ changes: [],
1868
+ changesOmitted: withoutValues.payload.changes.length
1869
+ }
1870
+ };
1871
+ while (lower <= upper) {
1872
+ const count = Math.floor((lower + upper) / 2);
1873
+ const candidate = {
1874
+ ...withoutValues,
1875
+ payload: {
1876
+ ...withoutValues.payload,
1877
+ changes: withoutValues.payload.changes.slice(0, count),
1878
+ changesOmitted: withoutValues.payload.changes.length - count
1879
+ }
1880
+ };
1881
+ if (encodedBytes2(candidate) <= eventBytesLimit) {
1882
+ bounded = candidate;
1883
+ lower = count + 1;
1884
+ } else {
1885
+ upper = count - 1;
1886
+ }
1887
+ }
1888
+ return bounded;
1889
+ };
1890
+ const publish = (candidate) => {
1891
+ if (disposed) return;
1892
+ const event = boundEvent(candidate);
1893
+ if (historyLimit > 0 && historyBytesLimit > 0) {
1894
+ const size = encodedBytes2(event);
1895
+ history.push(event);
1896
+ historySizes.push(size);
1897
+ retainedBytes += size;
1898
+ while (history.length > historyLimit || retainedBytes > historyBytesLimit) {
1899
+ history.shift();
1900
+ retainedBytes -= historySizes.shift() ?? 0;
1901
+ }
1902
+ }
1903
+ for (const listener of [...listeners]) {
1904
+ try {
1905
+ listener(event);
1906
+ } catch {
1907
+ }
1908
+ }
1909
+ };
1910
+ const publishLifecycle = (state, clientId) => {
1911
+ publish({
1912
+ ...nextBase(),
1913
+ type: "lifecycle",
1914
+ payload: {
1915
+ state,
1916
+ ...clientId !== void 0 ? { clientId } : {},
1917
+ activeClients
1918
+ }
1919
+ });
1920
+ };
1921
+ const publishTimeTravel = (state, cursor, previousCursor, source, previousSource, reason) => {
1922
+ const event = {
1923
+ ...nextBase(),
1924
+ type: "time-travel",
1925
+ payload: { state, cursor, previousCursor, source, previousSource, reason }
1926
+ };
1927
+ publish(event);
1928
+ return event;
1929
+ };
1930
+ const restoreGraphData = (data) => {
1931
+ internalReplayDepth += 1;
1932
+ try {
1933
+ store.setState(data);
1934
+ } finally {
1935
+ internalReplayDepth -= 1;
1936
+ }
1937
+ };
1938
+ const leaveRewindForMutation = () => {
1939
+ const previous = snapshotHistory.leaveRewindForMutation();
1940
+ if (previous === null) return;
1941
+ projectionFailureRevision += 1;
1942
+ publishTimeTravel(
1943
+ "live",
1944
+ null,
1945
+ previous.previousCursor,
1946
+ null,
1947
+ previous.previousSource,
1948
+ "mutation"
1949
+ );
1950
+ };
1951
+ const viewRegistry = createGraphDevtoolsViewRegistry(storeId, () => store.getState(), (payload) => {
1952
+ const event = { ...nextBase(), type: "view", payload };
1953
+ publish(event);
1954
+ });
1955
+ const affectedProjection = (changes, previous, current) => {
1956
+ const identities = /* @__PURE__ */ new Map();
1957
+ const changedListKeys = /* @__PURE__ */ new Set();
1958
+ for (const change of changes) {
1959
+ if ((change.category === "entity" || change.category === "patch") && change.id !== void 0) {
1960
+ identities.set(graphDevtoolsEntityIdentityKey(change.key, change.id), {
1961
+ type: change.key,
1962
+ id: change.id
1963
+ });
1964
+ } else if (change.category === "entity-state" || change.category === "sync") {
1965
+ const stateIdentities = [
1966
+ ...graphDevtoolsEntityIdentitiesForStateKey(previous, change.key),
1967
+ ...graphDevtoolsEntityIdentitiesForStateKey(current, change.key)
1968
+ ];
1969
+ for (const identity of stateIdentities) {
1970
+ identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
1971
+ }
1972
+ } else if (change.category === "list") {
1973
+ changedListKeys.add(change.key);
1974
+ }
1975
+ }
1976
+ const views = viewRegistry.getSnapshot().views;
1977
+ const affectedViewIds = /* @__PURE__ */ new Set();
1978
+ for (const view of views) {
1979
+ if (view.queryKey !== null && changedListKeys.has(view.queryKey)) {
1980
+ affectedViewIds.add(view.viewId);
1981
+ const previousIds = previous.lists[view.queryKey]?.ids ?? [];
1982
+ const currentIds = current.lists[view.queryKey]?.ids ?? [];
1983
+ const previousPositions = new Map(previousIds.map((id, index) => [id, index]));
1984
+ const currentPositions = new Map(currentIds.map((id, index) => [id, index]));
1985
+ const ids = /* @__PURE__ */ new Set([...previousIds, ...currentIds]);
1986
+ for (const id of ids) {
1987
+ if (previousPositions.get(id) === currentPositions.get(id)) continue;
1988
+ identities.set(graphDevtoolsEntityIdentityKey(view.entityType, id), {
1989
+ type: view.entityType,
1990
+ id
1991
+ });
1992
+ }
1993
+ }
1994
+ if (view.membership.some((member) => identities.has(graphDevtoolsEntityIdentityKey(member.type, member.id)))) {
1995
+ affectedViewIds.add(view.viewId);
1996
+ }
1997
+ }
1998
+ return {
1999
+ affectedEntities: [...identities.values()],
2000
+ affectedViewIds: [...affectedViewIds].sort()
2001
+ };
2002
+ };
2003
+ const unsubscribeStore = store.subscribe((current, previous) => {
2004
+ if (disposed || internalReplayDepth > 0) return;
2005
+ const startedAt = nowDuration();
2006
+ let changes;
2007
+ try {
2008
+ changes = projectGraphDevtoolsChanges(previous, current, valuePolicy, storeId);
2009
+ } catch {
2010
+ leaveRewindForMutation();
2011
+ projectionFailureRevision += 1;
2012
+ const base2 = nextBase();
2013
+ const event2 = {
2014
+ ...base2,
2015
+ type: "diagnostic",
2016
+ payload: {
2017
+ code: "projection-failed",
2018
+ message: "A graph publication could not be projected for DevTools.",
2019
+ snapshot: snapshotHistory.capture(current, base2.sequence)
2020
+ }
2021
+ };
2022
+ publish(event2);
2023
+ return;
2024
+ }
2025
+ if (changes.length === 0) return;
2026
+ leaveRewindForMutation();
2027
+ advanceGraphDevtoolsEntityRevisions(changes, entityRevisions, current, entityValueRevisions);
2028
+ const affected = affectedProjection(changes, previous, current);
2029
+ const base = nextBase();
2030
+ const event = {
2031
+ ...base,
2032
+ type: "mutation",
2033
+ payload: {
2034
+ snapshot: snapshotHistory.capture(current, base.sequence),
2035
+ valuesTruncated: false,
2036
+ changesOmitted: 0,
2037
+ changes,
2038
+ ...affected,
2039
+ before: collectGraphDevtoolsCounts(previous),
2040
+ after: collectGraphDevtoolsCounts(current),
2041
+ projectionDurationMs: Math.max(0, nowDuration() - startedAt)
2042
+ }
2043
+ };
2044
+ publish(event);
2045
+ });
2046
+ const previewController = createGraphDevtoolsPreviewController(
2047
+ store,
2048
+ storeId,
2049
+ valuePolicy,
2050
+ (type, id) => (entityValueRevisions.get(graphDevtoolsEntityIdentityKey(type, id)) ?? 0) + projectionFailureRevision
2051
+ );
2052
+ const controller = {
2053
+ storeId,
2054
+ capabilities,
2055
+ getSnapshot() {
2056
+ return {
2057
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
2058
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
2059
+ storeId,
2060
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
2061
+ counts: collectGraphDevtoolsCounts(store.getState()),
2062
+ history: controller.getHistoryStatus(),
2063
+ snapshots: controller.getSnapshotHistoryStatus()
2064
+ };
2065
+ },
2066
+ getHistory() {
2067
+ return [...history];
2068
+ },
2069
+ getHistoryStatus() {
2070
+ return {
2071
+ retainedEvents: history.length,
2072
+ retainedBytes,
2073
+ eventLimit: historyLimit,
2074
+ byteLimit: historyBytesLimit,
2075
+ oldestSequence: history[0]?.sequence ?? null,
2076
+ newestSequence: history.length > 0 ? history[history.length - 1].sequence : null
2077
+ };
2078
+ },
2079
+ getSnapshotHistoryStatus() {
2080
+ return snapshotHistory.getStatus();
2081
+ },
2082
+ getSnapshotReferences() {
2083
+ return snapshotHistory.getReferences();
2084
+ },
2085
+ getCompatibilitySnapshotReferences() {
2086
+ return snapshotHistory.getCompatibilityReferences();
2087
+ },
2088
+ captureCompatibilitySnapshot(label) {
2089
+ const reference = snapshotHistory.captureCompatibility(store.getState(), label);
2090
+ return reference.status === "retained" ? reference.cursor : -1;
2091
+ },
2092
+ configureSnapshotLimit(nextSnapshotLimit) {
2093
+ snapshotHistory.configureLimit(nextSnapshotLimit);
2094
+ capabilities.limits.snapshots = snapshotHistory.getStatus().snapshotLimit;
2095
+ },
2096
+ subscribeSnapshotHistory(listener) {
2097
+ return snapshotHistory.subscribe(listener);
2098
+ },
2099
+ clearSnapshotHistory() {
2100
+ snapshotHistory.clear();
2101
+ },
2102
+ getEntityRecords() {
2103
+ return projectGraphDevtoolsEntityRecords(
2104
+ store.getState(),
2105
+ storeId,
2106
+ entityRevisions,
2107
+ valuePolicy,
2108
+ viewRegistry.getViewIdsByEntity()
2109
+ );
2110
+ },
2111
+ getViews() {
2112
+ return viewRegistry.getSnapshot();
2113
+ },
2114
+ getRelationships() {
2115
+ return projectGraphDevtoolsRelationships(store.getState(), storeId);
2116
+ },
2117
+ registerView(definition) {
2118
+ return viewRegistry.register(definition);
2119
+ },
2120
+ rewind(cursor) {
2121
+ if (!timeTravelEnabled) return null;
2122
+ const attempt = snapshotHistory.rewind(cursor, store.getState(), restoreGraphData);
2123
+ if (attempt === null || attempt.status === "expired-history") return attempt;
2124
+ projectionFailureRevision += 1;
2125
+ const event = publishTimeTravel(
2126
+ "rewound",
2127
+ cursor,
2128
+ attempt.previousCursor,
2129
+ "retained",
2130
+ attempt.previousSource,
2131
+ "command"
2132
+ );
2133
+ return {
2134
+ status: "rewound",
2135
+ cursor,
2136
+ source: "retained",
2137
+ previousCursor: attempt.previousCursor,
2138
+ previousSource: attempt.previousSource,
2139
+ changedAt: event.observedAt
2140
+ };
2141
+ },
2142
+ returnToLive() {
2143
+ if (!timeTravelEnabled) return null;
2144
+ const previous = snapshotHistory.returnToLive(restoreGraphData);
2145
+ if (previous === null) return null;
2146
+ projectionFailureRevision += 1;
2147
+ const event = publishTimeTravel(
2148
+ "live",
2149
+ null,
2150
+ previous.previousCursor,
2151
+ null,
2152
+ previous.previousSource,
2153
+ "command"
2154
+ );
2155
+ return {
2156
+ status: "live",
2157
+ cursor: null,
2158
+ previousCursor: previous.previousCursor,
2159
+ previousSource: previous.previousSource,
2160
+ reason: "command",
2161
+ changedAt: event.observedAt
2162
+ };
2163
+ },
2164
+ inspectHistoryImport(candidate) {
2165
+ if (disposed) {
2166
+ return { status: "rejected", reason: "disposed", message: "DevTools controller is disposed" };
2167
+ }
2168
+ if (!timeTravelEnabled) {
2169
+ return {
2170
+ status: "rejected",
2171
+ reason: "time-travel-unavailable",
2172
+ message: "Time travel is disabled for this controller"
2173
+ };
2174
+ }
2175
+ return snapshotHistory.inspectImport(candidate, storeId);
2176
+ },
2177
+ confirmHistoryImport(candidateId, cursor) {
2178
+ if (disposed) {
2179
+ return { status: "rejected", reason: "disposed", message: "DevTools controller is disposed" };
2180
+ }
2181
+ if (!timeTravelEnabled) {
2182
+ return {
2183
+ status: "rejected",
2184
+ reason: "time-travel-unavailable",
2185
+ message: "Time travel is disabled for this controller"
2186
+ };
2187
+ }
2188
+ const attempt = snapshotHistory.restoreImport(
2189
+ candidateId,
2190
+ cursor,
2191
+ store.getState(),
2192
+ restoreGraphData
2193
+ );
2194
+ if (attempt.status === "candidate-not-found") {
2195
+ return {
2196
+ status: "rejected",
2197
+ reason: "candidate-not-found",
2198
+ message: `Import candidate ${candidateId} is no longer awaiting confirmation`
2199
+ };
2200
+ }
2201
+ if (attempt.status === "snapshot-not-found") {
2202
+ return {
2203
+ status: "rejected",
2204
+ reason: "snapshot-not-found",
2205
+ message: `Import candidate ${candidateId} does not contain cursor ${cursor}`
2206
+ };
2207
+ }
2208
+ if (attempt.status === "restore-failed") {
2209
+ return {
2210
+ status: "rejected",
2211
+ reason: "restore-failed",
2212
+ message: `Import candidate ${candidateId} could not be restored through the graph boundary`
2213
+ };
2214
+ }
2215
+ projectionFailureRevision += 1;
2216
+ const event = publishTimeTravel(
2217
+ "rewound",
2218
+ cursor,
2219
+ attempt.previousCursor,
2220
+ "import",
2221
+ attempt.previousSource,
2222
+ "import"
2223
+ );
2224
+ const receipt = {
2225
+ status: "rewound",
2226
+ source: "import",
2227
+ candidateId,
2228
+ cursor,
2229
+ previousCursor: attempt.previousCursor,
2230
+ previousSource: attempt.previousSource,
2231
+ changedAt: event.observedAt
2232
+ };
2233
+ return receipt;
2234
+ },
2235
+ clearHistory() {
2236
+ history.length = 0;
2237
+ historySizes.length = 0;
2238
+ retainedBytes = 0;
2239
+ snapshotHistory.clear();
2240
+ },
2241
+ subscribe(listener, replay = false) {
2242
+ if (disposed) return () => {
2243
+ };
2244
+ if (replay) {
2245
+ for (const event of [...history]) {
2246
+ try {
2247
+ listener(event);
2248
+ } catch {
2249
+ }
2250
+ }
2251
+ }
2252
+ listeners.add(listener);
2253
+ return () => listeners.delete(listener);
2254
+ },
2255
+ connect(clientId = `client-${nextClientNumber++}`) {
2256
+ if (disposed) return createDisposedTransport(storeId, clientId);
2257
+ activeClients += 1;
2258
+ publishLifecycle("client-connected", clientId);
2259
+ let closed = false;
2260
+ const connectionSubscriptions = /* @__PURE__ */ new Set();
2261
+ return {
2262
+ request(command) {
2263
+ if (closed) {
2264
+ const parsed = parseCommand(command);
2265
+ return Promise.resolve(resultError(
2266
+ storeId,
2267
+ parsed?.requestId ?? "closed",
2268
+ "disposed",
2269
+ "DevTools client is disconnected"
2270
+ ));
2271
+ }
2272
+ return Promise.resolve(controller.handleCommand(command));
2273
+ },
2274
+ subscribe(listener) {
2275
+ if (closed) return () => {
2276
+ };
2277
+ const unsubscribe = controller.subscribe(listener);
2278
+ connectionSubscriptions.add(unsubscribe);
2279
+ return () => {
2280
+ connectionSubscriptions.delete(unsubscribe);
2281
+ unsubscribe();
2282
+ };
2283
+ },
2284
+ close() {
2285
+ if (closed) return;
2286
+ closed = true;
2287
+ for (const unsubscribe of connectionSubscriptions) unsubscribe();
2288
+ connectionSubscriptions.clear();
2289
+ activeClients = Math.max(0, activeClients - 1);
2290
+ publishLifecycle("client-disconnected", clientId);
2291
+ }
2292
+ };
2293
+ },
2294
+ handleCommand(command) {
2295
+ const parsed = parseCommand(command);
2296
+ const requestId = parsed?.requestId ?? "invalid";
2297
+ if (disposed) return resultError(storeId, requestId, "disposed", "DevTools controller is disposed");
2298
+ if (!parsed) return resultError(storeId, requestId, "invalid-envelope", "Invalid DevTools command envelope");
2299
+ if (parsed.version !== GRAPH_DEVTOOLS_PROTOCOL_VERSION) {
2300
+ return resultError(storeId, parsed.requestId, "unsupported-version", `Unsupported DevTools protocol version ${parsed.version}`);
2301
+ }
2302
+ if (parsed.storeId !== storeId) {
2303
+ return resultError(storeId, parsed.requestId, "wrong-store", `Command targets ${parsed.storeId}, not ${storeId}`);
2304
+ }
2305
+ let result;
2306
+ switch (parsed.command) {
2307
+ case "get-capabilities":
2308
+ result = capabilities;
2309
+ break;
2310
+ case "get-snapshot":
2311
+ result = controller.getSnapshot();
2312
+ break;
2313
+ case "get-history":
2314
+ result = controller.getHistory();
2315
+ break;
2316
+ case "get-history-status":
2317
+ result = controller.getHistoryStatus();
2318
+ break;
2319
+ case "get-entity-records":
2320
+ result = controller.getEntityRecords();
2321
+ break;
2322
+ case "get-views":
2323
+ result = controller.getViews();
2324
+ break;
2325
+ case "get-relationships":
2326
+ result = controller.getRelationships();
2327
+ break;
2328
+ case "get-time-travel-status": {
2329
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
2330
+ result = controller.getSnapshotHistoryStatus();
2331
+ break;
2332
+ }
2333
+ case "rewind": {
2334
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
2335
+ const payload = parseRewindPayload(parsed.payload);
2336
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid rewind payload");
2337
+ const receipt = controller.rewind(payload.cursor);
2338
+ if (!receipt) return resultError(storeId, parsed.requestId, "snapshot-not-found", `Snapshot cursor ${payload.cursor} is not retained`);
2339
+ result = receipt;
2340
+ break;
2341
+ }
2342
+ case "return-to-live": {
2343
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
2344
+ const receipt = controller.returnToLive();
2345
+ if (!receipt) return resultError(storeId, parsed.requestId, "not-rewound", "The graph is already live");
2346
+ result = receipt;
2347
+ break;
2348
+ }
2349
+ case "inspect-history-import": {
2350
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
2351
+ const payload = parseInspectHistoryImportPayload(parsed.payload);
2352
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid history import inspection payload");
2353
+ result = controller.inspectHistoryImport(payload.candidate);
2354
+ break;
2355
+ }
2356
+ case "confirm-history-import": {
2357
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
2358
+ const payload = parseConfirmHistoryImportPayload(parsed.payload);
2359
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid history import confirmation payload");
2360
+ if (!payload.confirm) return resultError(storeId, parsed.requestId, "confirmation-required", "History import restore requires confirm: true");
2361
+ result = controller.confirmHistoryImport(payload.candidateId, payload.cursor);
2362
+ break;
2363
+ }
2364
+ case "preview-entity-patch": {
2365
+ const payload = parsePreviewPayload(parsed.payload);
2366
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid entity preview payload");
2367
+ const receipt = previewController.apply(payload.type, payload.id, payload.patch);
2368
+ if (!receipt) return resultError(storeId, parsed.requestId, "entity-not-found", `Entity ${payload.type}:${payload.id} was not found`);
2369
+ result = receipt;
2370
+ break;
2371
+ }
2372
+ case "restore-entity-preview": {
2373
+ const payload = parseRestorePreviewPayload(parsed.payload);
2374
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid preview restore payload");
2375
+ const receipt = previewController.restore(payload.previewId);
2376
+ if (!receipt) return resultError(storeId, parsed.requestId, "preview-not-found", `Preview ${payload.previewId} was not found`);
2377
+ result = receipt;
2378
+ break;
2379
+ }
2380
+ case "clear-history":
2381
+ controller.clearHistory();
2382
+ result = { cleared: true };
2383
+ break;
2384
+ default:
2385
+ return resultError(storeId, parsed.requestId, "unsupported-command", `Unsupported DevTools command ${parsed.command}`);
2386
+ }
2387
+ return {
2388
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
2389
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
2390
+ requestId: parsed.requestId,
2391
+ storeId,
2392
+ ok: true,
2393
+ result
2394
+ };
2395
+ },
2396
+ isDisposed() {
2397
+ return disposed;
2398
+ }
2399
+ };
2400
+ const unregisterCompatibility = registerGraphDevtoolsCompatibilityDelegate(store, {
2401
+ configure(capacity) {
2402
+ controller.configureSnapshotLimit(capacity);
2403
+ },
2404
+ capture(label) {
2405
+ return controller.captureCompatibilitySnapshot(label);
2406
+ },
2407
+ restoreByIndex(index) {
2408
+ if (!Number.isInteger(index)) return false;
2409
+ const reference = controller.getCompatibilitySnapshotReferences()[index];
2410
+ if (!reference) return false;
2411
+ return controller.rewind(reference.cursor)?.status === "rewound";
2412
+ },
2413
+ restoreByCursor(cursor) {
2414
+ return controller.rewind(cursor)?.status === "rewound";
2415
+ },
2416
+ step(delta) {
2417
+ if (!Number.isInteger(delta)) return false;
2418
+ const references = controller.getCompatibilitySnapshotReferences();
2419
+ if (references.length === 0) return false;
2420
+ const status = controller.getSnapshotHistoryStatus();
2421
+ const activeIndex = status.source === "retained" && status.cursor !== null ? references.findIndex((reference) => reference.cursor === status.cursor) : -1;
2422
+ const base = activeIndex === -1 ? references.length - 1 : activeIndex;
2423
+ const target = Math.max(0, Math.min(references.length - 1, base + delta));
2424
+ return controller.rewind(references[target].cursor)?.status === "rewound";
2425
+ },
2426
+ getState() {
2427
+ const references = controller.getCompatibilitySnapshotReferences();
2428
+ const status = controller.getSnapshotHistoryStatus();
2429
+ const cursor = status.source === "retained" && status.cursor !== null ? references.findIndex((reference) => reference.cursor === status.cursor) : -1;
2430
+ return {
2431
+ snapshots: references.map((reference) => ({
2432
+ seq: reference.cursor,
2433
+ at: Date.parse(reference.capturedAt),
2434
+ ...reference.label !== void 0 ? { label: reference.label } : {}
2435
+ })),
2436
+ cursor: cursor === -1 ? null : cursor,
2437
+ capacity: status.snapshotLimit
2438
+ };
2439
+ },
2440
+ subscribe(listener) {
2441
+ return controller.subscribeSnapshotHistory(listener);
2442
+ },
2443
+ clear() {
2444
+ controller.clearSnapshotHistory();
2445
+ }
2446
+ });
2447
+ const dispose = () => {
2448
+ if (disposed) return;
2449
+ activeClients = 0;
2450
+ publishLifecycle("disposed");
2451
+ disposed = true;
2452
+ unregisterCompatibility();
2453
+ unsubscribeStore();
2454
+ listeners.clear();
2455
+ history.length = 0;
2456
+ historySizes.length = 0;
2457
+ retainedBytes = 0;
2458
+ entityRevisions.clear();
2459
+ entityValueRevisions.clear();
2460
+ viewRegistry.dispose();
2461
+ previewController.dispose();
2462
+ snapshotHistory.dispose();
2463
+ };
2464
+ publishLifecycle("attached");
2465
+ return { controller, dispose };
2466
+ }
2467
+ function createDisposedTransport(storeId, clientId) {
2468
+ return {
2469
+ request(command) {
2470
+ const parsed = parseCommand(command);
2471
+ return Promise.resolve(resultError(
2472
+ storeId,
2473
+ parsed?.requestId ?? clientId,
2474
+ "disposed",
2475
+ "DevTools controller is disposed"
2476
+ ));
2477
+ },
2478
+ subscribe() {
2479
+ return () => {
2480
+ };
2481
+ },
2482
+ close() {
2483
+ }
2484
+ };
2485
+ }
2486
+ function attachGraphDevtools(store, options = {}) {
2487
+ if (options.enabled === false) {
2488
+ return { enabled: false, controller: null, detach() {
2489
+ } };
2490
+ }
2491
+ let entry = controllers.get(store);
2492
+ if (!entry || entry.controller.isDisposed()) {
2493
+ entry = { ...createController(store, options), references: 0 };
2494
+ controllers.set(store, entry);
2495
+ }
2496
+ entry.references += 1;
2497
+ let detached = false;
2498
+ return {
2499
+ enabled: true,
2500
+ controller: entry.controller,
2501
+ detach() {
2502
+ if (detached) return;
2503
+ detached = true;
2504
+ const current = controllers.get(store);
2505
+ if (!current || current.controller !== entry?.controller) return;
2506
+ current.references = Math.max(0, current.references - 1);
2507
+ if (current.references === 0) {
2508
+ current.dispose();
2509
+ controllers.delete(store);
2510
+ }
2511
+ }
2512
+ };
2513
+ }
2514
+ function getGraphDevtoolsController(store) {
2515
+ const entry = controllers.get(store);
2516
+ return entry && !entry.controller.isDisposed() ? entry.controller : null;
2517
+ }
2518
+ registerGraphDevtoolsCompatibilityFactory((store) => {
2519
+ const attachment = attachGraphDevtools(store);
2520
+ return () => attachment.detach();
2521
+ });
2522
+
2523
+ // src/devtools/client.ts
2524
+ var nextRequest = 1;
2525
+ function createGraphDevtoolsClient(storeId, transport) {
2526
+ let disconnected = false;
2527
+ return {
2528
+ storeId,
2529
+ request(command, payload) {
2530
+ const requestId = `request-${nextRequest++}`;
2531
+ return transport.request({
2532
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
2533
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
2534
+ requestId,
2535
+ storeId,
2536
+ command,
2537
+ ...payload !== void 0 ? { payload } : {}
2538
+ });
2539
+ },
2540
+ subscribe(listener) {
2541
+ if (disconnected) return () => {
2542
+ };
2543
+ return transport.subscribe(listener);
2544
+ },
2545
+ disconnect() {
2546
+ if (disconnected) return;
2547
+ disconnected = true;
2548
+ transport.close();
2549
+ }
2550
+ };
2551
+ }
2552
+
2553
+ exports.GRAPH_DEVTOOLS_PROTOCOL = GRAPH_DEVTOOLS_PROTOCOL;
2554
+ exports.GRAPH_DEVTOOLS_PROTOCOL_VERSION = GRAPH_DEVTOOLS_PROTOCOL_VERSION;
2555
+ exports.attachGraphDevtools = attachGraphDevtools;
2556
+ exports.createGraphDevtoolsClient = createGraphDevtoolsClient;
2557
+ exports.getGraphDevtoolsController = getGraphDevtoolsController;
2558
+ exports.projectGraphDevtoolsEntityRecords = projectGraphDevtoolsEntityRecords;
2559
+ exports.projectGraphDevtoolsRelationships = projectGraphDevtoolsRelationships;
2560
+ //# sourceMappingURL=devtools.cjs.map
2561
+ //# sourceMappingURL=devtools.cjs.map