@prometheus-ags/entity-graph-core 3.0.4 → 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,2019 @@
1
+ import { registerGraphDevtoolsCompatibilityFactory, registerGraphDevtoolsCompatibilityDelegate, getRegisteredSchemas, serializeKey, EMPTY_ENTITY_STATE, EMPTY_SYNC_METADATA, EMPTY_LIST_STATE } from './chunk-BE3BMRKF.mjs';
2
+
3
+ // src/devtools/protocol.ts
4
+ var GRAPH_DEVTOOLS_PROTOCOL = "prometheus.entity-graph.devtools";
5
+ var GRAPH_DEVTOOLS_PROTOCOL_VERSION = 1;
6
+
7
+ // src/devtools/projection.ts
8
+ function countRecordEntries(record) {
9
+ return Object.keys(record).length;
10
+ }
11
+ function collectGraphDevtoolsCounts(state) {
12
+ let entities = 0;
13
+ let entityTypes = 0;
14
+ for (const bucket of Object.values(state.entities)) {
15
+ const count = Object.keys(bucket).length;
16
+ if (count > 0) entityTypes += 1;
17
+ entities += count;
18
+ }
19
+ let patchedEntities = 0;
20
+ for (const bucket of Object.values(state.patches)) {
21
+ for (const patch of Object.values(bucket)) {
22
+ if (Object.keys(patch).length > 0) patchedEntities += 1;
23
+ }
24
+ }
25
+ let listMemberships = 0;
26
+ let fetching = 0;
27
+ let stale = 0;
28
+ let errors = 0;
29
+ for (const list of Object.values(state.lists)) {
30
+ listMemberships += list.ids.length;
31
+ if (list.isFetching || list.isFetchingMore) fetching += 1;
32
+ if (list.stale) stale += 1;
33
+ if (list.error !== null || list.lastError !== null) errors += 1;
34
+ }
35
+ for (const entityState of Object.values(state.entityStates)) {
36
+ if (entityState.isFetching) fetching += 1;
37
+ if (entityState.stale) stale += 1;
38
+ if (entityState.error !== null) errors += 1;
39
+ }
40
+ return {
41
+ entityTypes,
42
+ entities,
43
+ patchedEntities,
44
+ entityStates: countRecordEntries(state.entityStates),
45
+ syncMetadata: countRecordEntries(state.syncMetadata),
46
+ lists: countRecordEntries(state.lists),
47
+ listMemberships,
48
+ fetching,
49
+ stale,
50
+ errors
51
+ };
52
+ }
53
+ function withValues(change, before, after, policy, storeId) {
54
+ if (policy.mode !== "include") return { ...change, valueState: "hidden-by-policy" };
55
+ const redact = policy.redact ?? ((value) => value);
56
+ const baseContext = {
57
+ storeId,
58
+ category: change.category,
59
+ key: change.key,
60
+ ...change.id !== void 0 ? { id: change.id } : {},
61
+ fieldPath: [],
62
+ destination: "history"
63
+ };
64
+ const projectValue = (value, side) => {
65
+ try {
66
+ return { value: toGraphDevtoolsTransportValue(redact(value, { ...baseContext, side })), failed: false };
67
+ } catch {
68
+ return { value: { $type: "redaction-error" }, failed: true };
69
+ }
70
+ };
71
+ const projectedBefore = before !== void 0 ? projectValue(before, "before") : void 0;
72
+ const projectedAfter = after !== void 0 ? projectValue(after, "after") : void 0;
73
+ return {
74
+ ...change,
75
+ valueState: projectedBefore?.failed || projectedAfter?.failed ? "redaction-error" : policy.redact ? "redacted" : "included",
76
+ ...projectedBefore ? { before: projectedBefore.value } : {},
77
+ ...projectedAfter ? { after: projectedAfter.value } : {}
78
+ };
79
+ }
80
+ function toGraphDevtoolsTransportValue(value, seen = /* @__PURE__ */ new WeakSet()) {
81
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
82
+ if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
83
+ if (typeof value === "bigint") return { $type: "bigint", value: value.toString() };
84
+ if (typeof value === "undefined") return { $type: "undefined" };
85
+ if (typeof value === "function" || typeof value === "symbol") return { $type: typeof value };
86
+ if (value instanceof Date) return { $type: "date", value: value.toISOString() };
87
+ if (value instanceof Error) return { $type: "error", name: value.name, message: value.message };
88
+ if (typeof value !== "object") return String(value);
89
+ if (seen.has(value)) return { $type: "circular" };
90
+ seen.add(value);
91
+ try {
92
+ if (Array.isArray(value)) return value.map((item) => toGraphDevtoolsTransportValue(item, seen));
93
+ const normalized = {};
94
+ for (const [key, item] of Object.entries(value)) {
95
+ normalized[key] = toGraphDevtoolsTransportValue(item, seen);
96
+ }
97
+ return normalized;
98
+ } finally {
99
+ seen.delete(value);
100
+ }
101
+ }
102
+ function actionFor(before, after) {
103
+ if (before === void 0) return "added";
104
+ if (after === void 0) return "removed";
105
+ return "updated";
106
+ }
107
+ function diffNestedRecords(category, before, after, policy, storeId) {
108
+ const changes = [];
109
+ for (const type of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
110
+ const beforeBucket = before[type] ?? {};
111
+ const afterBucket = after[type] ?? {};
112
+ if (beforeBucket === afterBucket) continue;
113
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(beforeBucket), ...Object.keys(afterBucket)])) {
114
+ const previous = beforeBucket[id];
115
+ const current = afterBucket[id];
116
+ if (previous === current) continue;
117
+ changes.push(withValues({
118
+ category,
119
+ action: actionFor(previous, current),
120
+ key: type,
121
+ id
122
+ }, previous, current, policy, storeId));
123
+ }
124
+ }
125
+ return changes;
126
+ }
127
+ function diffFlatRecords(category, before, after, policy, storeId) {
128
+ const changes = [];
129
+ if (before === after) return changes;
130
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
131
+ const previous = before[key];
132
+ const current = after[key];
133
+ if (previous === current) continue;
134
+ changes.push(withValues({
135
+ category,
136
+ action: actionFor(previous, current),
137
+ key
138
+ }, previous, current, policy, storeId));
139
+ }
140
+ return changes;
141
+ }
142
+ function diffLists(before, after, policy, storeId) {
143
+ const changes = [];
144
+ if (before === after) return changes;
145
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)])) {
146
+ const previous = before[key];
147
+ const current = after[key];
148
+ if (previous === current) continue;
149
+ changes.push(withValues({
150
+ category: "list",
151
+ action: actionFor(previous, current),
152
+ key,
153
+ beforeCount: previous?.ids.length ?? 0,
154
+ afterCount: current?.ids.length ?? 0
155
+ }, previous, current, policy, storeId));
156
+ }
157
+ return changes;
158
+ }
159
+ function projectGraphDevtoolsChanges(before, after, policy, storeId) {
160
+ return [
161
+ ...diffNestedRecords("entity", before.entities, after.entities, policy, storeId),
162
+ ...diffNestedRecords("patch", before.patches, after.patches, policy, storeId),
163
+ ...diffFlatRecords("entity-state", before.entityStates, after.entityStates, policy, storeId),
164
+ ...diffFlatRecords("sync", before.syncMetadata, after.syncMetadata, policy, storeId),
165
+ ...diffLists(before.lists, after.lists, policy, storeId)
166
+ ];
167
+ }
168
+
169
+ // src/devtools/inspection.ts
170
+ function wireEntityKey(type, id) {
171
+ return `${type}:${id}`;
172
+ }
173
+ function graphDevtoolsEntityIdentityKey(type, id) {
174
+ return JSON.stringify([type, id]);
175
+ }
176
+ function parseGraphDevtoolsEntityIdentityKey(key) {
177
+ try {
178
+ const value = JSON.parse(key);
179
+ if (Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string") return { type: value[0], id: value[1] };
180
+ } catch {
181
+ }
182
+ return null;
183
+ }
184
+ function splitEntityKey(key) {
185
+ const separator = key.indexOf(":");
186
+ if (separator < 1 || separator === key.length - 1) return null;
187
+ return { type: key.slice(0, separator), id: key.slice(separator + 1) };
188
+ }
189
+ function graphDevtoolsEntityIdentitiesForStateKey(state, key) {
190
+ const matches = [];
191
+ const types = /* @__PURE__ */ new Set([...Object.keys(state.entities), ...Object.keys(state.patches)]);
192
+ for (const type of types) {
193
+ const ids = /* @__PURE__ */ new Set([
194
+ ...Object.keys(state.entities[type] ?? {}),
195
+ ...Object.keys(state.patches[type] ?? {})
196
+ ]);
197
+ for (const id of ids) {
198
+ if (wireEntityKey(type, id) === key) matches.push({ type, id });
199
+ }
200
+ }
201
+ if (matches.length > 0) return matches;
202
+ const fallback = splitEntityKey(key);
203
+ return fallback ? [fallback] : [];
204
+ }
205
+ function toIsoTimestamp(value) {
206
+ return value === null ? null : new Date(value).toISOString();
207
+ }
208
+ function compareEntityIdentity(left, right) {
209
+ if (left.type !== right.type) return left.type < right.type ? -1 : 1;
210
+ if (left.id === right.id) return 0;
211
+ return left.id < right.id ? -1 : 1;
212
+ }
213
+ function collectEntityIdentities(state) {
214
+ const identities = /* @__PURE__ */ new Map();
215
+ for (const [type, bucket] of Object.entries(state.entities)) {
216
+ for (const id of Object.keys(bucket)) {
217
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
218
+ }
219
+ }
220
+ for (const [type, bucket] of Object.entries(state.patches)) {
221
+ for (const id of Object.keys(bucket)) {
222
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
223
+ }
224
+ }
225
+ for (const key of [...Object.keys(state.entityStates), ...Object.keys(state.syncMetadata)]) {
226
+ for (const identity of graphDevtoolsEntityIdentitiesForStateKey(state, key)) {
227
+ identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
228
+ }
229
+ }
230
+ return [...identities.values()].sort(compareEntityIdentity);
231
+ }
232
+ function dirtyReasonsFor(canonical, patch, synced) {
233
+ const reasons = [];
234
+ for (const field of Object.keys(patch ?? {}).sort()) {
235
+ reasons.push({
236
+ kind: "local-patch",
237
+ field,
238
+ change: canonical && Object.hasOwn(canonical, field) ? "changed" : "added"
239
+ });
240
+ }
241
+ if (!synced) {
242
+ reasons.push({ kind: "sync-state", field: null, change: "unsynced" });
243
+ }
244
+ return reasons;
245
+ }
246
+ function projectGraphDevtoolsInspectionValue(value, policy, context) {
247
+ if (policy.mode !== "include") return { $type: "hidden-by-policy" };
248
+ try {
249
+ const projected = policy.redact ? policy.redact(value, {
250
+ ...context,
251
+ fieldPath: [],
252
+ side: "after",
253
+ destination: "inspection"
254
+ }) : value;
255
+ return toGraphDevtoolsTransportValue(projected);
256
+ } catch {
257
+ return { $type: "redaction-error" };
258
+ }
259
+ }
260
+ function recordFor(state, storeId, type, id, revisions, valuePolicy) {
261
+ const key = wireEntityKey(type, id);
262
+ const identityKey = graphDevtoolsEntityIdentityKey(type, id);
263
+ const canonical = state.entities[type]?.[id];
264
+ const storedPatch = state.patches[type]?.[id];
265
+ const patch = storedPatch && Object.keys(storedPatch).length > 0 ? storedPatch : void 0;
266
+ const entityState = state.entityStates[key] ?? EMPTY_ENTITY_STATE;
267
+ const sync = state.syncMetadata[key] ?? EMPTY_SYNC_METADATA;
268
+ const dirtyReasons = dirtyReasonsFor(canonical, patch, sync.synced);
269
+ return {
270
+ key,
271
+ type,
272
+ id,
273
+ presence: canonical ? "present" : "missing-canonical",
274
+ canonical: canonical ? projectGraphDevtoolsInspectionValue(canonical, valuePolicy, { storeId, category: "entity", key: type, id }) : null,
275
+ patch: patch ? projectGraphDevtoolsInspectionValue(patch, valuePolicy, { storeId, category: "patch", key: type, id }) : null,
276
+ merged: canonical ? projectGraphDevtoolsInspectionValue(
277
+ { ...canonical, ...patch ?? {} },
278
+ valuePolicy,
279
+ { storeId, category: "entity", key: type, id }
280
+ ) : null,
281
+ dirty: dirtyReasons.length > 0,
282
+ dirtyReasons,
283
+ entityState: {
284
+ isFetching: entityState.isFetching,
285
+ lastFetchedAt: toIsoTimestamp(entityState.lastFetched),
286
+ stale: entityState.stale,
287
+ error: entityState.error === null ? null : {
288
+ kind: "entity-fetch",
289
+ message: entityState.error,
290
+ retryable: null
291
+ }
292
+ },
293
+ sync: {
294
+ synced: sync.synced,
295
+ origin: sync.origin,
296
+ updatedAt: toIsoTimestamp(sync.updatedAt)
297
+ },
298
+ revision: revisions.get(identityKey) ?? 0,
299
+ viewIds: []
300
+ };
301
+ }
302
+ function projectGraphDevtoolsEntityRecords(state, storeId, revisions, valuePolicy = { mode: "metadata-only" }, viewIdsByEntity = /* @__PURE__ */ new Map()) {
303
+ return {
304
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
305
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
306
+ storeId,
307
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
308
+ entityRecords: collectEntityIdentities(state).map(({ type, id }) => {
309
+ const record = recordFor(state, storeId, type, id, revisions, valuePolicy);
310
+ return {
311
+ ...record,
312
+ viewIds: [...viewIdsByEntity.get(graphDevtoolsEntityIdentityKey(type, id)) ?? []]
313
+ };
314
+ })
315
+ };
316
+ }
317
+ function advanceGraphDevtoolsEntityRevisions(changes, revisions, state, valueRevisions) {
318
+ const touched = /* @__PURE__ */ new Set();
319
+ const valueTouched = /* @__PURE__ */ new Set();
320
+ for (const change of changes) {
321
+ if ((change.category === "entity" || change.category === "patch") && change.id !== void 0) {
322
+ const key = graphDevtoolsEntityIdentityKey(change.key, change.id);
323
+ touched.add(key);
324
+ valueTouched.add(key);
325
+ continue;
326
+ }
327
+ if (change.category === "entity-state" || change.category === "sync") {
328
+ for (const identity of graphDevtoolsEntityIdentitiesForStateKey(state, change.key)) {
329
+ touched.add(graphDevtoolsEntityIdentityKey(identity.type, identity.id));
330
+ }
331
+ }
332
+ }
333
+ for (const key of touched) revisions.set(key, (revisions.get(key) ?? 0) + 1);
334
+ if (valueRevisions) {
335
+ for (const key of valueTouched) valueRevisions.set(key, (valueRevisions.get(key) ?? 0) + 1);
336
+ }
337
+ }
338
+
339
+ // src/devtools/relationships.ts
340
+ function mergedEntity(state, type, id) {
341
+ const canonical = state.entities[type]?.[id];
342
+ if (!canonical) return null;
343
+ return { ...canonical, ...state.patches[type]?.[id] ?? {} };
344
+ }
345
+ function statusFor(state, type, id) {
346
+ return state.entities[type]?.[id] ? "resolved" : "missing-target";
347
+ }
348
+ function edge(relation, descriptor, direction, sourceType, sourceId, sourceField, targetType, targetId, state) {
349
+ return {
350
+ relation,
351
+ cardinality: descriptor.cardinality,
352
+ direction,
353
+ source: { type: sourceType, id: sourceId, field: sourceField },
354
+ target: { type: targetType, id: targetId },
355
+ status: statusFor(state, targetType, targetId)
356
+ };
357
+ }
358
+ function belongsToEdges(state, schemaType, relation, descriptor) {
359
+ const edges = [];
360
+ for (const id of Object.keys(state.entities[schemaType] ?? {})) {
361
+ const entity = mergedEntity(state, schemaType, id);
362
+ const targetId = entity?.[descriptor.foreignKey];
363
+ if (typeof targetId !== "string" || targetId.length === 0) continue;
364
+ edges.push(edge(
365
+ relation,
366
+ descriptor,
367
+ "outgoing",
368
+ schemaType,
369
+ id,
370
+ descriptor.foreignKey,
371
+ descriptor.targetType,
372
+ targetId,
373
+ state
374
+ ));
375
+ }
376
+ return edges;
377
+ }
378
+ function hasManyEdges(state, schemaType, relation, descriptor) {
379
+ const edges = [];
380
+ for (const parentId of Object.keys(state.entities[schemaType] ?? {})) {
381
+ const childIds = /* @__PURE__ */ new Set();
382
+ for (const childId of Object.keys(state.entities[descriptor.targetType] ?? {})) {
383
+ const child = mergedEntity(state, descriptor.targetType, childId);
384
+ if (child?.[descriptor.foreignKey] === parentId) childIds.add(childId);
385
+ }
386
+ const listKey = serializeKey(descriptor.listKeyPrefix(parentId));
387
+ for (const childId of state.lists[listKey]?.ids ?? []) {
388
+ if (!state.entities[descriptor.targetType]?.[childId]) childIds.add(childId);
389
+ }
390
+ for (const childId of childIds) {
391
+ edges.push(edge(
392
+ relation,
393
+ descriptor,
394
+ "reverse",
395
+ schemaType,
396
+ parentId,
397
+ null,
398
+ descriptor.targetType,
399
+ childId,
400
+ state
401
+ ));
402
+ }
403
+ }
404
+ return edges;
405
+ }
406
+ function manyToManyEdges(state, schemaType, relation, descriptor) {
407
+ const field = descriptor.localArrayField;
408
+ if (!field) return [];
409
+ const edges = [];
410
+ for (const id of Object.keys(state.entities[schemaType] ?? {})) {
411
+ const entity = mergedEntity(state, schemaType, id);
412
+ const targetIds = entity?.[field];
413
+ if (!Array.isArray(targetIds)) continue;
414
+ for (const targetId of new Set(targetIds)) {
415
+ if (typeof targetId !== "string" || targetId.length === 0) continue;
416
+ edges.push(edge(
417
+ relation,
418
+ descriptor,
419
+ "outgoing",
420
+ schemaType,
421
+ id,
422
+ field,
423
+ descriptor.targetType,
424
+ targetId,
425
+ state
426
+ ));
427
+ }
428
+ }
429
+ return edges;
430
+ }
431
+ function compareRelationships(left, right) {
432
+ const leftKey = [
433
+ left.source.type,
434
+ left.source.id,
435
+ left.relation,
436
+ left.direction,
437
+ left.target.type,
438
+ left.target.id
439
+ ].join("\0");
440
+ const rightKey = [
441
+ right.source.type,
442
+ right.source.id,
443
+ right.relation,
444
+ right.direction,
445
+ right.target.type,
446
+ right.target.id
447
+ ].join("\0");
448
+ if (leftKey === rightKey) return 0;
449
+ return leftKey < rightKey ? -1 : 1;
450
+ }
451
+ function projectGraphDevtoolsRelationships(state, storeId) {
452
+ const relationships = [];
453
+ for (const schema of getRegisteredSchemas()) {
454
+ for (const [name, descriptor] of Object.entries(schema.relations ?? {})) {
455
+ switch (descriptor.cardinality) {
456
+ case "belongsTo":
457
+ relationships.push(...belongsToEdges(state, schema.type, name, descriptor));
458
+ break;
459
+ case "hasMany":
460
+ relationships.push(...hasManyEdges(state, schema.type, name, descriptor));
461
+ break;
462
+ case "manyToMany":
463
+ relationships.push(...manyToManyEdges(state, schema.type, name, descriptor));
464
+ break;
465
+ }
466
+ }
467
+ }
468
+ relationships.sort(compareRelationships);
469
+ return {
470
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
471
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
472
+ storeId,
473
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
474
+ relationships
475
+ };
476
+ }
477
+
478
+ // src/devtools/time-travel.ts
479
+ function cloneGraphData(data) {
480
+ return structuredClone({
481
+ entities: data.entities,
482
+ patches: data.patches,
483
+ entityStates: data.entityStates,
484
+ syncMetadata: data.syncMetadata,
485
+ lists: data.lists
486
+ });
487
+ }
488
+ function encodedBytes(value) {
489
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
490
+ }
491
+ function isRecord(value) {
492
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
493
+ const prototype = Object.getPrototypeOf(value);
494
+ return prototype === Object.prototype || prototype === null;
495
+ }
496
+ function isJsonValue(value, ancestors = /* @__PURE__ */ new Set()) {
497
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
498
+ if (typeof value === "number") return Number.isFinite(value);
499
+ if (typeof value !== "object") return false;
500
+ if (ancestors.has(value)) return false;
501
+ ancestors.add(value);
502
+ const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : isRecord(value) && Object.values(value).every((item) => isJsonValue(item, ancestors));
503
+ ancestors.delete(value);
504
+ return valid;
505
+ }
506
+ function isNullableString(value) {
507
+ return value === null || typeof value === "string";
508
+ }
509
+ function isNullableFiniteNumber(value) {
510
+ return value === null || typeof value === "number" && Number.isFinite(value);
511
+ }
512
+ function isTimestamp(value) {
513
+ return typeof value === "string" && value.length > 0 && Number.isFinite(Date.parse(value));
514
+ }
515
+ function isEntityTable(value) {
516
+ return isRecord(value) && Object.values(value).every(
517
+ (entities) => isRecord(entities) && Object.values(entities).every(isRecord)
518
+ );
519
+ }
520
+ function isEntityStates(value) {
521
+ return isRecord(value) && Object.values(value).every((state) => isRecord(state) && typeof state.isFetching === "boolean" && isNullableFiniteNumber(state.lastFetched) && isNullableString(state.error) && typeof state.stale === "boolean");
522
+ }
523
+ function isSyncMetadata(value) {
524
+ 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));
525
+ }
526
+ function isLists(value) {
527
+ 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));
528
+ }
529
+ function isGraphData(value) {
530
+ return isRecord(value) && isEntityTable(value.entities) && isEntityTable(value.patches) && isEntityStates(value.entityStates) && isSyncMetadata(value.syncMetadata) && isLists(value.lists);
531
+ }
532
+ function createGraphDevtoolsSnapshotHistory(options) {
533
+ const retained = [];
534
+ const compatibilityLabels = /* @__PURE__ */ new Map();
535
+ const listeners = /* @__PURE__ */ new Set();
536
+ let snapshotLimit = options.snapshotLimit;
537
+ let retainedBytes = 0;
538
+ let latestCursor = 0;
539
+ let baselineCursor = null;
540
+ let lastUnavailable = null;
541
+ const unavailableReferences = /* @__PURE__ */ new Map();
542
+ let clearedThroughCursor = 0;
543
+ let mode = "live";
544
+ let activeCursor = null;
545
+ let activeSource = null;
546
+ let protectedLiveHead = null;
547
+ let importedCandidate = null;
548
+ let nextImportCandidate = 1;
549
+ let disposed = false;
550
+ const notify = () => {
551
+ for (const listener of [...listeners]) {
552
+ try {
553
+ listener();
554
+ } catch {
555
+ }
556
+ }
557
+ };
558
+ const unavailable = (cursor, capturedAt, eventSequence, reason, label) => {
559
+ const reference = {
560
+ cursor,
561
+ capturedAt,
562
+ eventSequence,
563
+ ...label !== void 0 ? { label } : {},
564
+ status: "unavailable",
565
+ reason
566
+ };
567
+ lastUnavailable = reference;
568
+ unavailableReferences.set(cursor, reference);
569
+ while (unavailableReferences.size > Math.max(1, snapshotLimit)) {
570
+ const oldest = unavailableReferences.keys().next().value;
571
+ if (oldest === void 0) break;
572
+ unavailableReferences.delete(oldest);
573
+ }
574
+ return reference;
575
+ };
576
+ const expired = (cursor) => {
577
+ if (cursor > latestCursor) return null;
578
+ const status = history.getStatus();
579
+ const unavailableReference = unavailableReferences.get(cursor);
580
+ if (unavailableReference) {
581
+ return {
582
+ status: "expired-history",
583
+ cursor,
584
+ reason: "unavailable",
585
+ unavailableReason: unavailableReference.reason,
586
+ oldestCursor: status.oldestCursor,
587
+ newestCursor: status.newestCursor,
588
+ latestCursor: status.latestCursor
589
+ };
590
+ }
591
+ return {
592
+ status: "expired-history",
593
+ cursor,
594
+ reason: cursor <= clearedThroughCursor ? "cleared" : "evicted",
595
+ oldestCursor: status.oldestCursor,
596
+ newestCursor: status.newestCursor,
597
+ latestCursor: status.latestCursor
598
+ };
599
+ };
600
+ const enterRewind = (cursor, source, data, liveState, restore) => {
601
+ if (disposed || mode === "rewound" && protectedLiveHead === null) return null;
602
+ const previousCursor = activeCursor;
603
+ const previousSource = activeSource;
604
+ let target;
605
+ let nextLiveHead = protectedLiveHead;
606
+ try {
607
+ target = cloneGraphData(data);
608
+ if (mode === "live") nextLiveHead = cloneGraphData(liveState);
609
+ restore(target);
610
+ } catch {
611
+ return null;
612
+ }
613
+ protectedLiveHead = nextLiveHead;
614
+ mode = "rewound";
615
+ activeCursor = cursor;
616
+ activeSource = source;
617
+ return { status: "rewound", previousCursor, previousSource };
618
+ };
619
+ const history = {
620
+ capture(state, eventSequence, label) {
621
+ importedCandidate = null;
622
+ const cursor = ++latestCursor;
623
+ const capturedAt = (/* @__PURE__ */ new Date()).toISOString();
624
+ if (baselineCursor === null) baselineCursor = cursor;
625
+ if (disposed || snapshotLimit === 0 || options.snapshotBytesLimit === 0) {
626
+ const reference2 = unavailable(cursor, capturedAt, eventSequence, "retention-disabled", label);
627
+ notify();
628
+ return reference2;
629
+ }
630
+ let data;
631
+ let bytes;
632
+ try {
633
+ data = cloneGraphData(state);
634
+ bytes = encodedBytes(data);
635
+ } catch {
636
+ const reference2 = unavailable(cursor, capturedAt, eventSequence, "capture-failed", label);
637
+ notify();
638
+ return reference2;
639
+ }
640
+ if (bytes > options.snapshotBytesLimit) {
641
+ const unavailableReference = unavailable(cursor, capturedAt, eventSequence, "oversize", label);
642
+ notify();
643
+ return unavailableReference;
644
+ }
645
+ const reference = {
646
+ cursor,
647
+ capturedAt,
648
+ eventSequence,
649
+ ...label !== void 0 ? { label } : {},
650
+ status: "retained",
651
+ bytes
652
+ };
653
+ retained.push({ reference, data });
654
+ retainedBytes += bytes;
655
+ while (retained.length > snapshotLimit || retainedBytes > options.snapshotBytesLimit) {
656
+ const removed = retained.shift();
657
+ if (removed) compatibilityLabels.delete(removed.reference.cursor);
658
+ retainedBytes -= removed?.reference.bytes ?? 0;
659
+ }
660
+ notify();
661
+ return reference;
662
+ },
663
+ captureCompatibility(state, label) {
664
+ const candidateCursor = mode === "live" ? latestCursor : activeSource === "retained" ? activeCursor : null;
665
+ const current = candidateCursor === null ? void 0 : retained.find(({ reference: reference2 }) => reference2.cursor === candidateCursor);
666
+ if (current && !compatibilityLabels.has(current.reference.cursor)) {
667
+ compatibilityLabels.set(current.reference.cursor, label);
668
+ notify();
669
+ return {
670
+ ...current.reference,
671
+ ...label !== void 0 ? { label } : {}
672
+ };
673
+ }
674
+ const reference = history.capture(state, null, label);
675
+ if (reference.status === "retained") {
676
+ compatibilityLabels.set(reference.cursor, label);
677
+ }
678
+ return reference;
679
+ },
680
+ getStatus() {
681
+ return {
682
+ mode,
683
+ cursor: activeCursor,
684
+ source: activeSource,
685
+ retainedSnapshots: retained.length,
686
+ retainedBytes,
687
+ snapshotLimit,
688
+ byteLimit: options.snapshotBytesLimit,
689
+ baselineCursor,
690
+ oldestCursor: retained[0]?.reference.cursor ?? null,
691
+ newestCursor: retained.length > 0 ? retained[retained.length - 1].reference.cursor : null,
692
+ latestCursor: latestCursor === 0 ? null : latestCursor,
693
+ lastUnavailable,
694
+ importCandidate: importedCandidate ? {
695
+ candidateId: importedCandidate.candidateId,
696
+ snapshots: importedCandidate.snapshots.length,
697
+ bytes: importedCandidate.bytes
698
+ } : null
699
+ };
700
+ },
701
+ getReferences() {
702
+ return retained.map(({ reference }) => ({ ...reference }));
703
+ },
704
+ getCompatibilityReferences() {
705
+ return retained.filter(({ reference }) => compatibilityLabels.has(reference.cursor)).map(({ reference }) => {
706
+ const label = compatibilityLabels.get(reference.cursor);
707
+ return { ...reference, ...label !== void 0 ? { label } : {} };
708
+ });
709
+ },
710
+ read(cursor) {
711
+ const snapshot = retained.find((candidate) => candidate.reference.cursor === cursor);
712
+ return snapshot ? cloneGraphData(snapshot.data) : null;
713
+ },
714
+ rewind(cursor, liveState, restore) {
715
+ if (disposed) return null;
716
+ const snapshot = retained.find((candidate) => candidate.reference.cursor === cursor);
717
+ if (!snapshot) return expired(cursor);
718
+ const result = enterRewind(cursor, "retained", snapshot.data, liveState, restore);
719
+ if (result) notify();
720
+ return result;
721
+ },
722
+ inspectImport(candidate, storeId) {
723
+ importedCandidate = null;
724
+ const rejected = (reason, message) => {
725
+ notify();
726
+ return { status: "rejected", reason, message };
727
+ };
728
+ try {
729
+ if (!isRecord(candidate) || !isJsonValue(candidate) || candidate.protocol !== GRAPH_DEVTOOLS_PROTOCOL) {
730
+ return rejected("invalid-envelope", "Import must use the Prometheus entity-graph DevTools protocol");
731
+ }
732
+ if (candidate.version !== GRAPH_DEVTOOLS_PROTOCOL_VERSION) {
733
+ return rejected("unsupported-version", `Import protocol version ${String(candidate.version)} is unsupported`);
734
+ }
735
+ if (candidate.storeId !== storeId) {
736
+ return rejected("wrong-store", `Import targets ${String(candidate.storeId)}, not ${storeId}`);
737
+ }
738
+ if (!isTimestamp(candidate.exportedAt) || !Array.isArray(candidate.snapshots) || candidate.snapshots.length === 0) {
739
+ return rejected("invalid-envelope", "Import metadata and at least one snapshot are required");
740
+ }
741
+ if (candidate.snapshots.length > snapshotLimit) {
742
+ return rejected("snapshot-limit-exceeded", `Import contains ${candidate.snapshots.length} snapshots; limit is ${snapshotLimit}`);
743
+ }
744
+ const importBytes = encodedBytes(candidate);
745
+ if (importBytes > options.snapshotBytesLimit) {
746
+ return rejected("byte-limit-exceeded", `Import contains ${importBytes} bytes; limit is ${options.snapshotBytesLimit}`);
747
+ }
748
+ const snapshots = [];
749
+ let previousCursor = 0;
750
+ for (const value of candidate.snapshots) {
751
+ if (!isRecord(value)) return rejected("invalid-envelope", "Every imported snapshot must be an object");
752
+ const cursor = value.cursor;
753
+ const eventSequence = value.eventSequence;
754
+ 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)) {
755
+ return rejected("invalid-envelope", "Imported snapshots must have ordered stable cursors and valid graph data");
756
+ }
757
+ const data = cloneGraphData(value.data);
758
+ snapshots.push({
759
+ cursor,
760
+ capturedAt: value.capturedAt,
761
+ eventSequence,
762
+ bytes: encodedBytes(data),
763
+ data
764
+ });
765
+ previousCursor = cursor;
766
+ }
767
+ const candidateBytes = snapshots.reduce((total, snapshot) => total + snapshot.bytes, 0);
768
+ while (retained.length + snapshots.length > snapshotLimit || retainedBytes + candidateBytes > options.snapshotBytesLimit) {
769
+ const removed = retained.shift();
770
+ if (!removed) break;
771
+ compatibilityLabels.delete(removed.reference.cursor);
772
+ retainedBytes -= removed.reference.bytes;
773
+ }
774
+ const candidateId = `import-${nextImportCandidate++}`;
775
+ importedCandidate = { candidateId, bytes: candidateBytes, snapshots };
776
+ notify();
777
+ const receipt = {
778
+ status: "awaiting-confirmation",
779
+ candidateId,
780
+ storeId,
781
+ protocolVersion: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
782
+ inspectedAt: (/* @__PURE__ */ new Date()).toISOString(),
783
+ bytes: importBytes,
784
+ snapshots: snapshots.map(({ data: _data, ...metadata }) => metadata)
785
+ };
786
+ return receipt;
787
+ } catch {
788
+ return rejected("invalid-envelope", "Import could not be safely inspected or cloned");
789
+ }
790
+ },
791
+ restoreImport(candidateId, cursor, liveState, restore) {
792
+ if (!importedCandidate || importedCandidate.candidateId !== candidateId) {
793
+ return { status: "candidate-not-found" };
794
+ }
795
+ const snapshot = importedCandidate.snapshots.find((candidate) => candidate.cursor === cursor);
796
+ if (!snapshot) return { status: "snapshot-not-found" };
797
+ const restored = enterRewind(cursor, "import", snapshot.data, liveState, restore);
798
+ if (!restored) return { status: "restore-failed" };
799
+ importedCandidate = null;
800
+ notify();
801
+ return restored;
802
+ },
803
+ returnToLive(restore) {
804
+ if (disposed || mode !== "rewound" || protectedLiveHead === null) return null;
805
+ const previousCursor = activeCursor;
806
+ const previousSource = activeSource;
807
+ if (previousCursor === null || previousSource === null) return null;
808
+ try {
809
+ restore(cloneGraphData(protectedLiveHead));
810
+ } catch {
811
+ return null;
812
+ }
813
+ mode = "live";
814
+ activeCursor = null;
815
+ activeSource = null;
816
+ protectedLiveHead = null;
817
+ notify();
818
+ return { previousCursor, previousSource };
819
+ },
820
+ leaveRewindForMutation() {
821
+ if (mode !== "rewound") return null;
822
+ const previousCursor = activeCursor;
823
+ const previousSource = activeSource;
824
+ if (previousCursor === null || previousSource === null) return null;
825
+ mode = "live";
826
+ activeCursor = null;
827
+ activeSource = null;
828
+ protectedLiveHead = null;
829
+ notify();
830
+ return { previousCursor, previousSource };
831
+ },
832
+ configureLimit(nextLimit) {
833
+ if (!Number.isFinite(nextLimit) || nextLimit <= 0) return;
834
+ snapshotLimit = Math.max(1, Math.floor(nextLimit));
835
+ while (retained.length > snapshotLimit) {
836
+ const removed = retained.shift();
837
+ if (removed) compatibilityLabels.delete(removed.reference.cursor);
838
+ retainedBytes -= removed?.reference.bytes ?? 0;
839
+ }
840
+ while (unavailableReferences.size > Math.max(1, snapshotLimit)) {
841
+ const oldest = unavailableReferences.keys().next().value;
842
+ if (oldest === void 0) break;
843
+ unavailableReferences.delete(oldest);
844
+ }
845
+ if (importedCandidate && retained.length + importedCandidate.snapshots.length > snapshotLimit) {
846
+ importedCandidate = null;
847
+ }
848
+ notify();
849
+ },
850
+ subscribe(listener) {
851
+ if (disposed) return () => {
852
+ };
853
+ listeners.add(listener);
854
+ return () => listeners.delete(listener);
855
+ },
856
+ clear() {
857
+ retained.length = 0;
858
+ compatibilityLabels.clear();
859
+ retainedBytes = 0;
860
+ clearedThroughCursor = latestCursor;
861
+ importedCandidate = null;
862
+ notify();
863
+ },
864
+ dispose() {
865
+ disposed = true;
866
+ retained.length = 0;
867
+ compatibilityLabels.clear();
868
+ retainedBytes = 0;
869
+ lastUnavailable = null;
870
+ unavailableReferences.clear();
871
+ mode = "live";
872
+ activeCursor = null;
873
+ activeSource = null;
874
+ protectedLiveHead = null;
875
+ importedCandidate = null;
876
+ listeners.clear();
877
+ }
878
+ };
879
+ return history;
880
+ }
881
+
882
+ // src/devtools/views.ts
883
+ function orderedUnique(values) {
884
+ return [...new Set(values)];
885
+ }
886
+ function sameValues(left, right) {
887
+ return left.length === right.length && left.every((value, index) => value === right[index]);
888
+ }
889
+ function membershipFor(entry) {
890
+ const entityIds = /* @__PURE__ */ new Set();
891
+ for (const registration of entry.registrations.values()) {
892
+ for (const id of registration.entityIds) entityIds.add(id);
893
+ }
894
+ return [...entityIds].map((id) => ({ type: entry.definition.entityType, id }));
895
+ }
896
+ function viewRecord(entry, state) {
897
+ const membership = membershipFor(entry);
898
+ const registrations = [...entry.registrations.values()];
899
+ const lastRenderedAt = registrations.map((registration) => registration.lastRenderedAt).filter((value) => value !== null).sort().at(-1) ?? null;
900
+ const queryKey = entry.definition.queryKey ?? null;
901
+ const listState = queryKey === null ? EMPTY_LIST_STATE : state.lists[queryKey] ?? EMPTY_LIST_STATE;
902
+ return {
903
+ ...entry.definition,
904
+ queryKey,
905
+ registeredAt: entry.registeredAt,
906
+ lastRenderedAt,
907
+ renderCount: registrations.reduce((total, registration) => total + registration.renderCount, 0),
908
+ subscriberCount: registrations.length,
909
+ membership,
910
+ list: entry.definition.kind === "list" ? {
911
+ visibleCount: membership.length,
912
+ graphCount: listState.ids.length,
913
+ total: listState.total,
914
+ isFetching: listState.isFetching,
915
+ isFetchingMore: listState.isFetchingMore,
916
+ stale: listState.stale,
917
+ hasNextPage: listState.hasNextPage,
918
+ hasPreviousPage: listState.hasPrevPage
919
+ } : null
920
+ };
921
+ }
922
+ function allEntityIdentities(state, memberships) {
923
+ const identities = /* @__PURE__ */ new Map();
924
+ for (const [type, bucket] of Object.entries(state.entities)) {
925
+ for (const id of Object.keys(bucket)) {
926
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
927
+ }
928
+ }
929
+ for (const [type, bucket] of Object.entries(state.patches)) {
930
+ for (const id of Object.keys(bucket)) {
931
+ identities.set(graphDevtoolsEntityIdentityKey(type, id), { type, id });
932
+ }
933
+ }
934
+ for (const key of [
935
+ ...Object.keys(state.entityStates),
936
+ ...Object.keys(state.syncMetadata)
937
+ ]) {
938
+ for (const identity of graphDevtoolsEntityIdentitiesForStateKey(state, key)) {
939
+ identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
940
+ }
941
+ }
942
+ for (const key of memberships.keys()) {
943
+ const identity = parseGraphDevtoolsEntityIdentityKey(key);
944
+ if (identity) identities.set(key, identity);
945
+ }
946
+ return [...identities.values()].sort((left, right) => {
947
+ if (left.type !== right.type) return left.type < right.type ? -1 : 1;
948
+ if (left.id === right.id) return 0;
949
+ return left.id < right.id ? -1 : 1;
950
+ });
951
+ }
952
+ function createGraphDevtoolsViewRegistry(storeId, getState, onViewChange) {
953
+ const entries = /* @__PURE__ */ new Map();
954
+ let disposed = false;
955
+ const viewIdsByEntity = () => {
956
+ const result = /* @__PURE__ */ new Map();
957
+ for (const [viewId, entry] of entries) {
958
+ for (const membership of membershipFor(entry)) {
959
+ const key = graphDevtoolsEntityIdentityKey(membership.type, membership.id);
960
+ const viewIds = result.get(key) ?? [];
961
+ viewIds.push(viewId);
962
+ result.set(key, viewIds);
963
+ }
964
+ }
965
+ for (const viewIds of result.values()) viewIds.sort();
966
+ return result;
967
+ };
968
+ return {
969
+ register(definition) {
970
+ if (disposed) return { viewId: definition.viewId, updateMembership() {
971
+ }, unregister() {
972
+ } };
973
+ const token = Symbol(definition.viewId);
974
+ let entry = entries.get(definition.viewId);
975
+ const isNew = entry === void 0;
976
+ if (!entry) {
977
+ entry = {
978
+ definition: { ...definition, queryKey: definition.queryKey ?? null },
979
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
980
+ registrations: /* @__PURE__ */ new Map()
981
+ };
982
+ entries.set(definition.viewId, entry);
983
+ }
984
+ entry.registrations.set(token, { entityIds: [], renderCount: 0, lastRenderedAt: null });
985
+ if (isNew) onViewChange({ state: "registered", viewId: definition.viewId, membershipCount: 0 });
986
+ let unregistered = false;
987
+ return {
988
+ viewId: definition.viewId,
989
+ updateMembership(entityIds) {
990
+ if (disposed || unregistered) return;
991
+ const currentEntry = entries.get(definition.viewId);
992
+ const registration = currentEntry?.registrations.get(token);
993
+ if (!currentEntry || !registration) return;
994
+ const before = membershipFor(currentEntry);
995
+ registration.entityIds = orderedUnique(entityIds);
996
+ registration.renderCount += 1;
997
+ registration.lastRenderedAt = (/* @__PURE__ */ new Date()).toISOString();
998
+ const after = membershipFor(currentEntry);
999
+ const beforeKeys = before.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1000
+ const afterKeys = after.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1001
+ if (!sameValues(beforeKeys, afterKeys)) {
1002
+ onViewChange({
1003
+ state: "membership-changed",
1004
+ viewId: definition.viewId,
1005
+ membershipCount: after.length
1006
+ });
1007
+ }
1008
+ },
1009
+ unregister() {
1010
+ if (disposed || unregistered) return;
1011
+ unregistered = true;
1012
+ const currentEntry = entries.get(definition.viewId);
1013
+ if (!currentEntry) return;
1014
+ const before = membershipFor(currentEntry);
1015
+ currentEntry.registrations.delete(token);
1016
+ if (currentEntry.registrations.size === 0) {
1017
+ entries.delete(definition.viewId);
1018
+ onViewChange({ state: "unregistered", viewId: definition.viewId, membershipCount: 0 });
1019
+ return;
1020
+ }
1021
+ const after = membershipFor(currentEntry);
1022
+ const beforeKeys = before.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1023
+ const afterKeys = after.map(({ type, id }) => graphDevtoolsEntityIdentityKey(type, id));
1024
+ if (!sameValues(beforeKeys, afterKeys)) {
1025
+ onViewChange({
1026
+ state: "membership-changed",
1027
+ viewId: definition.viewId,
1028
+ membershipCount: after.length
1029
+ });
1030
+ }
1031
+ }
1032
+ };
1033
+ },
1034
+ getSnapshot() {
1035
+ const state = getState();
1036
+ const memberships = viewIdsByEntity();
1037
+ return {
1038
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1039
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1040
+ storeId,
1041
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1042
+ views: [...entries.values()].map((entry) => viewRecord(entry, state)).sort((left, right) => left.viewId < right.viewId ? -1 : left.viewId > right.viewId ? 1 : 0),
1043
+ entityViewMembership: allEntityIdentities(state, memberships).map(({ type, id }) => ({
1044
+ type,
1045
+ id,
1046
+ viewIds: [...memberships.get(graphDevtoolsEntityIdentityKey(type, id)) ?? []]
1047
+ }))
1048
+ };
1049
+ },
1050
+ getViewIdsByEntity() {
1051
+ return viewIdsByEntity();
1052
+ },
1053
+ dispose() {
1054
+ disposed = true;
1055
+ entries.clear();
1056
+ }
1057
+ };
1058
+ }
1059
+
1060
+ // src/devtools/previews.ts
1061
+ function replaceEntityPatch(store, type, id, patch) {
1062
+ store.setState((state) => {
1063
+ const patches = { ...state.patches };
1064
+ const bucket = { ...patches[type] ?? {} };
1065
+ if (patch === void 0) delete bucket[id];
1066
+ else bucket[id] = { ...patch };
1067
+ if (Object.keys(bucket).length === 0) delete patches[type];
1068
+ else patches[type] = bucket;
1069
+ return { patches };
1070
+ });
1071
+ }
1072
+ function createGraphDevtoolsPreviewController(store, storeId, valuePolicy, valueRevisionFor) {
1073
+ const receipts = /* @__PURE__ */ new Map();
1074
+ const activeByEntity = /* @__PURE__ */ new Map();
1075
+ let nextPreview = 1;
1076
+ let disposed = false;
1077
+ const projectPatch = (type, id, value) => projectGraphDevtoolsInspectionValue(
1078
+ value,
1079
+ valuePolicy,
1080
+ { storeId, category: "patch", key: type, id }
1081
+ );
1082
+ const removeReceipt = (receipt) => {
1083
+ receipts.delete(receipt.previewId);
1084
+ const key = graphDevtoolsEntityIdentityKey(receipt.type, receipt.id);
1085
+ if (activeByEntity.get(key) === receipt.previewId) activeByEntity.delete(key);
1086
+ };
1087
+ return {
1088
+ apply(type, id, patch) {
1089
+ if (disposed || !store.getState().entities[type]?.[id]) return null;
1090
+ const key = graphDevtoolsEntityIdentityKey(type, id);
1091
+ const previousPreviewId = activeByEntity.get(key);
1092
+ if (previousPreviewId) receipts.delete(previousPreviewId);
1093
+ const storedPriorPatch = store.getState().patches[type]?.[id];
1094
+ const priorPatch = storedPriorPatch === void 0 ? void 0 : { ...storedPriorPatch };
1095
+ const previewPatch = { ...patch };
1096
+ const baselineRevision = valueRevisionFor(type, id);
1097
+ store.getState().patchEntity(type, id, previewPatch);
1098
+ const appliedPatch = { ...store.getState().patches[type]?.[id] ?? {} };
1099
+ const previewRevision = valueRevisionFor(type, id);
1100
+ const receipt = {
1101
+ previewId: `preview-${storeId}-${nextPreview++}`,
1102
+ type,
1103
+ id,
1104
+ priorPatch,
1105
+ previewPatch,
1106
+ appliedPatch,
1107
+ baselineRevision,
1108
+ previewRevision,
1109
+ appliedAt: (/* @__PURE__ */ new Date()).toISOString()
1110
+ };
1111
+ receipts.set(receipt.previewId, receipt);
1112
+ activeByEntity.set(key, receipt.previewId);
1113
+ return {
1114
+ previewId: receipt.previewId,
1115
+ entity: { type, id },
1116
+ priorPatch: priorPatch === void 0 ? null : projectPatch(type, id, priorPatch),
1117
+ previewPatch: projectPatch(type, id, previewPatch),
1118
+ appliedPatch: projectPatch(type, id, appliedPatch),
1119
+ baselineRevision,
1120
+ previewRevision,
1121
+ appliedAt: receipt.appliedAt
1122
+ };
1123
+ },
1124
+ restore(previewId) {
1125
+ if (disposed) return null;
1126
+ const receipt = receipts.get(previewId);
1127
+ if (!receipt) return null;
1128
+ const observedRevision = valueRevisionFor(receipt.type, receipt.id);
1129
+ if (observedRevision !== receipt.previewRevision) {
1130
+ const currentPatch = store.getState().patches[receipt.type]?.[receipt.id];
1131
+ const conflict = {
1132
+ previewId,
1133
+ status: "conflict",
1134
+ reason: "entity-changed-since-preview",
1135
+ expectedRevision: receipt.previewRevision,
1136
+ observedRevision,
1137
+ currentPatch: currentPatch === void 0 ? null : projectPatch(receipt.type, receipt.id, currentPatch),
1138
+ priorPatch: receipt.priorPatch === void 0 ? null : projectPatch(receipt.type, receipt.id, receipt.priorPatch)
1139
+ };
1140
+ removeReceipt(receipt);
1141
+ return conflict;
1142
+ }
1143
+ replaceEntityPatch(store, receipt.type, receipt.id, receipt.priorPatch);
1144
+ removeReceipt(receipt);
1145
+ return {
1146
+ previewId,
1147
+ status: "restored",
1148
+ restoredPatch: receipt.priorPatch === void 0 ? null : projectPatch(receipt.type, receipt.id, receipt.priorPatch),
1149
+ observedRevision,
1150
+ restoredAt: (/* @__PURE__ */ new Date()).toISOString()
1151
+ };
1152
+ },
1153
+ dispose() {
1154
+ disposed = true;
1155
+ receipts.clear();
1156
+ activeByEntity.clear();
1157
+ }
1158
+ };
1159
+ }
1160
+
1161
+ // src/devtools/controller.ts
1162
+ var controllers = /* @__PURE__ */ new WeakMap();
1163
+ var storeNumbers = /* @__PURE__ */ new WeakMap();
1164
+ var nextStoreNumber = 1;
1165
+ function nowDuration() {
1166
+ return globalThis.performance?.now() ?? Date.now();
1167
+ }
1168
+ function boundedLimit(value, fallback, minimum = 0) {
1169
+ const candidate = value ?? fallback;
1170
+ return Number.isFinite(candidate) ? Math.max(minimum, Math.floor(candidate)) : fallback;
1171
+ }
1172
+ function storeIdFor(store) {
1173
+ let number = storeNumbers.get(store);
1174
+ if (number === void 0) {
1175
+ number = nextStoreNumber++;
1176
+ storeNumbers.set(store, number);
1177
+ }
1178
+ return `graph-${number}`;
1179
+ }
1180
+ function resultError(storeId, requestId, code, message) {
1181
+ return {
1182
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1183
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1184
+ requestId,
1185
+ storeId,
1186
+ ok: false,
1187
+ error: { code, message }
1188
+ };
1189
+ }
1190
+ function parseCommand(command) {
1191
+ if (typeof command !== "object" || command === null) return null;
1192
+ const candidate = command;
1193
+ if (candidate.protocol !== GRAPH_DEVTOOLS_PROTOCOL || typeof candidate.version !== "number" || typeof candidate.requestId !== "string" || typeof candidate.storeId !== "string" || typeof candidate.command !== "string") return null;
1194
+ return candidate;
1195
+ }
1196
+ function parsePreviewPayload(payload) {
1197
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1198
+ const candidate = payload;
1199
+ 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;
1200
+ return candidate;
1201
+ }
1202
+ function parseRestorePreviewPayload(payload) {
1203
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1204
+ const previewId = payload.previewId;
1205
+ return typeof previewId === "string" && previewId.length > 0 ? { previewId } : null;
1206
+ }
1207
+ function parseRewindPayload(payload) {
1208
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1209
+ const cursor = payload.cursor;
1210
+ return typeof cursor === "number" && Number.isSafeInteger(cursor) && cursor > 0 ? { cursor } : null;
1211
+ }
1212
+ function parseInspectHistoryImportPayload(payload) {
1213
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1214
+ if (!Object.prototype.hasOwnProperty.call(payload, "candidate")) return null;
1215
+ return { candidate: payload.candidate };
1216
+ }
1217
+ function parseConfirmHistoryImportPayload(payload) {
1218
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return null;
1219
+ const candidate = payload;
1220
+ 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;
1221
+ return candidate;
1222
+ }
1223
+ function createController(store, options) {
1224
+ const storeId = options.storeId ?? storeIdFor(store);
1225
+ const historyLimit = boundedLimit(options.historyLimit, 500);
1226
+ const historyBytesLimit = boundedLimit(options.historyBytesLimit, 5 * 1024 * 1024);
1227
+ const eventBytesLimit = boundedLimit(options.eventBytesLimit, 256 * 1024, 1024);
1228
+ const snapshotLimit = boundedLimit(options.snapshotLimit, 50);
1229
+ const snapshotBytesLimit = boundedLimit(options.snapshotBytesLimit, 10 * 1024 * 1024);
1230
+ const timeTravelEnabled = snapshotLimit > 0 && snapshotBytesLimit > 0;
1231
+ const timeTravelCommands = timeTravelEnabled ? [
1232
+ "get-time-travel-status",
1233
+ "rewind",
1234
+ "return-to-live",
1235
+ "inspect-history-import",
1236
+ "confirm-history-import"
1237
+ ] : [];
1238
+ const timeTravelFeatures = timeTravelEnabled ? ["snapshot-history", "time-travel", "history-import"] : [];
1239
+ const valuePolicy = options.values ?? { mode: "metadata-only" };
1240
+ const capabilities = {
1241
+ protocolVersion: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1242
+ metadataOnlyByDefault: true,
1243
+ commands: [
1244
+ "get-capabilities",
1245
+ "get-snapshot",
1246
+ "get-history",
1247
+ "get-history-status",
1248
+ "get-entity-records",
1249
+ "get-views",
1250
+ "get-relationships",
1251
+ "preview-entity-patch",
1252
+ "restore-entity-preview",
1253
+ ...timeTravelCommands,
1254
+ "clear-history"
1255
+ ],
1256
+ features: [
1257
+ "semantic-events",
1258
+ "diagnostic-events",
1259
+ "bounded-history",
1260
+ "multi-client",
1261
+ "multi-store",
1262
+ "entity-inspection",
1263
+ "view-inspection",
1264
+ "relationship-inspection",
1265
+ "local-preview",
1266
+ ...timeTravelFeatures
1267
+ ],
1268
+ limits: {
1269
+ historyEvents: historyLimit,
1270
+ historyBytes: historyBytesLimit,
1271
+ eventBytes: eventBytesLimit,
1272
+ snapshots: snapshotLimit,
1273
+ snapshotBytes: snapshotBytesLimit
1274
+ }
1275
+ };
1276
+ const listeners = /* @__PURE__ */ new Set();
1277
+ const history = [];
1278
+ const historySizes = [];
1279
+ const entityRevisions = /* @__PURE__ */ new Map();
1280
+ const entityValueRevisions = /* @__PURE__ */ new Map();
1281
+ let retainedBytes = 0;
1282
+ let sequence = 0;
1283
+ let activeClients = 0;
1284
+ let nextClientNumber = 1;
1285
+ let disposed = false;
1286
+ let projectionFailureRevision = 0;
1287
+ let internalReplayDepth = 0;
1288
+ const snapshotHistory = createGraphDevtoolsSnapshotHistory({
1289
+ snapshotLimit,
1290
+ snapshotBytesLimit
1291
+ });
1292
+ snapshotHistory.capture(store.getState(), null);
1293
+ const nextBase = () => {
1294
+ const current = ++sequence;
1295
+ const eventId = `${storeId}:${current}`;
1296
+ return {
1297
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1298
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1299
+ storeId,
1300
+ sequence: current,
1301
+ eventId,
1302
+ correlationId: eventId,
1303
+ observedAt: (/* @__PURE__ */ new Date()).toISOString()
1304
+ };
1305
+ };
1306
+ const encodedBytes2 = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
1307
+ const boundEvent = (event) => {
1308
+ if (event.type !== "mutation") return event;
1309
+ if (encodedBytes2(event) <= eventBytesLimit) return event;
1310
+ const valuesTruncated = event.payload.changes.some(
1311
+ (change) => "before" in change || "after" in change
1312
+ );
1313
+ const withoutValues = {
1314
+ ...event,
1315
+ payload: {
1316
+ ...event.payload,
1317
+ valuesTruncated,
1318
+ changes: event.payload.changes.map(({ before: _before, after: _after, ...change }) => ({
1319
+ ...change,
1320
+ valueState: change.valueState === "hidden-by-policy" || change.valueState === "redaction-error" ? change.valueState : "truncated"
1321
+ }))
1322
+ }
1323
+ };
1324
+ if (encodedBytes2(withoutValues) <= eventBytesLimit) return withoutValues;
1325
+ let lower = 0;
1326
+ let upper = withoutValues.payload.changes.length;
1327
+ let bounded = {
1328
+ ...withoutValues,
1329
+ payload: {
1330
+ ...withoutValues.payload,
1331
+ changes: [],
1332
+ changesOmitted: withoutValues.payload.changes.length
1333
+ }
1334
+ };
1335
+ while (lower <= upper) {
1336
+ const count = Math.floor((lower + upper) / 2);
1337
+ const candidate = {
1338
+ ...withoutValues,
1339
+ payload: {
1340
+ ...withoutValues.payload,
1341
+ changes: withoutValues.payload.changes.slice(0, count),
1342
+ changesOmitted: withoutValues.payload.changes.length - count
1343
+ }
1344
+ };
1345
+ if (encodedBytes2(candidate) <= eventBytesLimit) {
1346
+ bounded = candidate;
1347
+ lower = count + 1;
1348
+ } else {
1349
+ upper = count - 1;
1350
+ }
1351
+ }
1352
+ return bounded;
1353
+ };
1354
+ const publish = (candidate) => {
1355
+ if (disposed) return;
1356
+ const event = boundEvent(candidate);
1357
+ if (historyLimit > 0 && historyBytesLimit > 0) {
1358
+ const size = encodedBytes2(event);
1359
+ history.push(event);
1360
+ historySizes.push(size);
1361
+ retainedBytes += size;
1362
+ while (history.length > historyLimit || retainedBytes > historyBytesLimit) {
1363
+ history.shift();
1364
+ retainedBytes -= historySizes.shift() ?? 0;
1365
+ }
1366
+ }
1367
+ for (const listener of [...listeners]) {
1368
+ try {
1369
+ listener(event);
1370
+ } catch {
1371
+ }
1372
+ }
1373
+ };
1374
+ const publishLifecycle = (state, clientId) => {
1375
+ publish({
1376
+ ...nextBase(),
1377
+ type: "lifecycle",
1378
+ payload: {
1379
+ state,
1380
+ ...clientId !== void 0 ? { clientId } : {},
1381
+ activeClients
1382
+ }
1383
+ });
1384
+ };
1385
+ const publishTimeTravel = (state, cursor, previousCursor, source, previousSource, reason) => {
1386
+ const event = {
1387
+ ...nextBase(),
1388
+ type: "time-travel",
1389
+ payload: { state, cursor, previousCursor, source, previousSource, reason }
1390
+ };
1391
+ publish(event);
1392
+ return event;
1393
+ };
1394
+ const restoreGraphData = (data) => {
1395
+ internalReplayDepth += 1;
1396
+ try {
1397
+ store.setState(data);
1398
+ } finally {
1399
+ internalReplayDepth -= 1;
1400
+ }
1401
+ };
1402
+ const leaveRewindForMutation = () => {
1403
+ const previous = snapshotHistory.leaveRewindForMutation();
1404
+ if (previous === null) return;
1405
+ projectionFailureRevision += 1;
1406
+ publishTimeTravel(
1407
+ "live",
1408
+ null,
1409
+ previous.previousCursor,
1410
+ null,
1411
+ previous.previousSource,
1412
+ "mutation"
1413
+ );
1414
+ };
1415
+ const viewRegistry = createGraphDevtoolsViewRegistry(storeId, () => store.getState(), (payload) => {
1416
+ const event = { ...nextBase(), type: "view", payload };
1417
+ publish(event);
1418
+ });
1419
+ const affectedProjection = (changes, previous, current) => {
1420
+ const identities = /* @__PURE__ */ new Map();
1421
+ const changedListKeys = /* @__PURE__ */ new Set();
1422
+ for (const change of changes) {
1423
+ if ((change.category === "entity" || change.category === "patch") && change.id !== void 0) {
1424
+ identities.set(graphDevtoolsEntityIdentityKey(change.key, change.id), {
1425
+ type: change.key,
1426
+ id: change.id
1427
+ });
1428
+ } else if (change.category === "entity-state" || change.category === "sync") {
1429
+ const stateIdentities = [
1430
+ ...graphDevtoolsEntityIdentitiesForStateKey(previous, change.key),
1431
+ ...graphDevtoolsEntityIdentitiesForStateKey(current, change.key)
1432
+ ];
1433
+ for (const identity of stateIdentities) {
1434
+ identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
1435
+ }
1436
+ } else if (change.category === "list") {
1437
+ changedListKeys.add(change.key);
1438
+ }
1439
+ }
1440
+ const views = viewRegistry.getSnapshot().views;
1441
+ const affectedViewIds = /* @__PURE__ */ new Set();
1442
+ for (const view of views) {
1443
+ if (view.queryKey !== null && changedListKeys.has(view.queryKey)) {
1444
+ affectedViewIds.add(view.viewId);
1445
+ const previousIds = previous.lists[view.queryKey]?.ids ?? [];
1446
+ const currentIds = current.lists[view.queryKey]?.ids ?? [];
1447
+ const previousPositions = new Map(previousIds.map((id, index) => [id, index]));
1448
+ const currentPositions = new Map(currentIds.map((id, index) => [id, index]));
1449
+ const ids = /* @__PURE__ */ new Set([...previousIds, ...currentIds]);
1450
+ for (const id of ids) {
1451
+ if (previousPositions.get(id) === currentPositions.get(id)) continue;
1452
+ identities.set(graphDevtoolsEntityIdentityKey(view.entityType, id), {
1453
+ type: view.entityType,
1454
+ id
1455
+ });
1456
+ }
1457
+ }
1458
+ if (view.membership.some((member) => identities.has(graphDevtoolsEntityIdentityKey(member.type, member.id)))) {
1459
+ affectedViewIds.add(view.viewId);
1460
+ }
1461
+ }
1462
+ return {
1463
+ affectedEntities: [...identities.values()],
1464
+ affectedViewIds: [...affectedViewIds].sort()
1465
+ };
1466
+ };
1467
+ const unsubscribeStore = store.subscribe((current, previous) => {
1468
+ if (disposed || internalReplayDepth > 0) return;
1469
+ const startedAt = nowDuration();
1470
+ let changes;
1471
+ try {
1472
+ changes = projectGraphDevtoolsChanges(previous, current, valuePolicy, storeId);
1473
+ } catch {
1474
+ leaveRewindForMutation();
1475
+ projectionFailureRevision += 1;
1476
+ const base2 = nextBase();
1477
+ const event2 = {
1478
+ ...base2,
1479
+ type: "diagnostic",
1480
+ payload: {
1481
+ code: "projection-failed",
1482
+ message: "A graph publication could not be projected for DevTools.",
1483
+ snapshot: snapshotHistory.capture(current, base2.sequence)
1484
+ }
1485
+ };
1486
+ publish(event2);
1487
+ return;
1488
+ }
1489
+ if (changes.length === 0) return;
1490
+ leaveRewindForMutation();
1491
+ advanceGraphDevtoolsEntityRevisions(changes, entityRevisions, current, entityValueRevisions);
1492
+ const affected = affectedProjection(changes, previous, current);
1493
+ const base = nextBase();
1494
+ const event = {
1495
+ ...base,
1496
+ type: "mutation",
1497
+ payload: {
1498
+ snapshot: snapshotHistory.capture(current, base.sequence),
1499
+ valuesTruncated: false,
1500
+ changesOmitted: 0,
1501
+ changes,
1502
+ ...affected,
1503
+ before: collectGraphDevtoolsCounts(previous),
1504
+ after: collectGraphDevtoolsCounts(current),
1505
+ projectionDurationMs: Math.max(0, nowDuration() - startedAt)
1506
+ }
1507
+ };
1508
+ publish(event);
1509
+ });
1510
+ const previewController = createGraphDevtoolsPreviewController(
1511
+ store,
1512
+ storeId,
1513
+ valuePolicy,
1514
+ (type, id) => (entityValueRevisions.get(graphDevtoolsEntityIdentityKey(type, id)) ?? 0) + projectionFailureRevision
1515
+ );
1516
+ const controller = {
1517
+ storeId,
1518
+ capabilities,
1519
+ getSnapshot() {
1520
+ return {
1521
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1522
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1523
+ storeId,
1524
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
1525
+ counts: collectGraphDevtoolsCounts(store.getState()),
1526
+ history: controller.getHistoryStatus(),
1527
+ snapshots: controller.getSnapshotHistoryStatus()
1528
+ };
1529
+ },
1530
+ getHistory() {
1531
+ return [...history];
1532
+ },
1533
+ getHistoryStatus() {
1534
+ return {
1535
+ retainedEvents: history.length,
1536
+ retainedBytes,
1537
+ eventLimit: historyLimit,
1538
+ byteLimit: historyBytesLimit,
1539
+ oldestSequence: history[0]?.sequence ?? null,
1540
+ newestSequence: history.length > 0 ? history[history.length - 1].sequence : null
1541
+ };
1542
+ },
1543
+ getSnapshotHistoryStatus() {
1544
+ return snapshotHistory.getStatus();
1545
+ },
1546
+ getSnapshotReferences() {
1547
+ return snapshotHistory.getReferences();
1548
+ },
1549
+ getCompatibilitySnapshotReferences() {
1550
+ return snapshotHistory.getCompatibilityReferences();
1551
+ },
1552
+ captureCompatibilitySnapshot(label) {
1553
+ const reference = snapshotHistory.captureCompatibility(store.getState(), label);
1554
+ return reference.status === "retained" ? reference.cursor : -1;
1555
+ },
1556
+ configureSnapshotLimit(nextSnapshotLimit) {
1557
+ snapshotHistory.configureLimit(nextSnapshotLimit);
1558
+ capabilities.limits.snapshots = snapshotHistory.getStatus().snapshotLimit;
1559
+ },
1560
+ subscribeSnapshotHistory(listener) {
1561
+ return snapshotHistory.subscribe(listener);
1562
+ },
1563
+ clearSnapshotHistory() {
1564
+ snapshotHistory.clear();
1565
+ },
1566
+ getEntityRecords() {
1567
+ return projectGraphDevtoolsEntityRecords(
1568
+ store.getState(),
1569
+ storeId,
1570
+ entityRevisions,
1571
+ valuePolicy,
1572
+ viewRegistry.getViewIdsByEntity()
1573
+ );
1574
+ },
1575
+ getViews() {
1576
+ return viewRegistry.getSnapshot();
1577
+ },
1578
+ getRelationships() {
1579
+ return projectGraphDevtoolsRelationships(store.getState(), storeId);
1580
+ },
1581
+ registerView(definition) {
1582
+ return viewRegistry.register(definition);
1583
+ },
1584
+ rewind(cursor) {
1585
+ if (!timeTravelEnabled) return null;
1586
+ const attempt = snapshotHistory.rewind(cursor, store.getState(), restoreGraphData);
1587
+ if (attempt === null || attempt.status === "expired-history") return attempt;
1588
+ projectionFailureRevision += 1;
1589
+ const event = publishTimeTravel(
1590
+ "rewound",
1591
+ cursor,
1592
+ attempt.previousCursor,
1593
+ "retained",
1594
+ attempt.previousSource,
1595
+ "command"
1596
+ );
1597
+ return {
1598
+ status: "rewound",
1599
+ cursor,
1600
+ source: "retained",
1601
+ previousCursor: attempt.previousCursor,
1602
+ previousSource: attempt.previousSource,
1603
+ changedAt: event.observedAt
1604
+ };
1605
+ },
1606
+ returnToLive() {
1607
+ if (!timeTravelEnabled) return null;
1608
+ const previous = snapshotHistory.returnToLive(restoreGraphData);
1609
+ if (previous === null) return null;
1610
+ projectionFailureRevision += 1;
1611
+ const event = publishTimeTravel(
1612
+ "live",
1613
+ null,
1614
+ previous.previousCursor,
1615
+ null,
1616
+ previous.previousSource,
1617
+ "command"
1618
+ );
1619
+ return {
1620
+ status: "live",
1621
+ cursor: null,
1622
+ previousCursor: previous.previousCursor,
1623
+ previousSource: previous.previousSource,
1624
+ reason: "command",
1625
+ changedAt: event.observedAt
1626
+ };
1627
+ },
1628
+ inspectHistoryImport(candidate) {
1629
+ if (disposed) {
1630
+ return { status: "rejected", reason: "disposed", message: "DevTools controller is disposed" };
1631
+ }
1632
+ if (!timeTravelEnabled) {
1633
+ return {
1634
+ status: "rejected",
1635
+ reason: "time-travel-unavailable",
1636
+ message: "Time travel is disabled for this controller"
1637
+ };
1638
+ }
1639
+ return snapshotHistory.inspectImport(candidate, storeId);
1640
+ },
1641
+ confirmHistoryImport(candidateId, cursor) {
1642
+ if (disposed) {
1643
+ return { status: "rejected", reason: "disposed", message: "DevTools controller is disposed" };
1644
+ }
1645
+ if (!timeTravelEnabled) {
1646
+ return {
1647
+ status: "rejected",
1648
+ reason: "time-travel-unavailable",
1649
+ message: "Time travel is disabled for this controller"
1650
+ };
1651
+ }
1652
+ const attempt = snapshotHistory.restoreImport(
1653
+ candidateId,
1654
+ cursor,
1655
+ store.getState(),
1656
+ restoreGraphData
1657
+ );
1658
+ if (attempt.status === "candidate-not-found") {
1659
+ return {
1660
+ status: "rejected",
1661
+ reason: "candidate-not-found",
1662
+ message: `Import candidate ${candidateId} is no longer awaiting confirmation`
1663
+ };
1664
+ }
1665
+ if (attempt.status === "snapshot-not-found") {
1666
+ return {
1667
+ status: "rejected",
1668
+ reason: "snapshot-not-found",
1669
+ message: `Import candidate ${candidateId} does not contain cursor ${cursor}`
1670
+ };
1671
+ }
1672
+ if (attempt.status === "restore-failed") {
1673
+ return {
1674
+ status: "rejected",
1675
+ reason: "restore-failed",
1676
+ message: `Import candidate ${candidateId} could not be restored through the graph boundary`
1677
+ };
1678
+ }
1679
+ projectionFailureRevision += 1;
1680
+ const event = publishTimeTravel(
1681
+ "rewound",
1682
+ cursor,
1683
+ attempt.previousCursor,
1684
+ "import",
1685
+ attempt.previousSource,
1686
+ "import"
1687
+ );
1688
+ const receipt = {
1689
+ status: "rewound",
1690
+ source: "import",
1691
+ candidateId,
1692
+ cursor,
1693
+ previousCursor: attempt.previousCursor,
1694
+ previousSource: attempt.previousSource,
1695
+ changedAt: event.observedAt
1696
+ };
1697
+ return receipt;
1698
+ },
1699
+ clearHistory() {
1700
+ history.length = 0;
1701
+ historySizes.length = 0;
1702
+ retainedBytes = 0;
1703
+ snapshotHistory.clear();
1704
+ },
1705
+ subscribe(listener, replay = false) {
1706
+ if (disposed) return () => {
1707
+ };
1708
+ if (replay) {
1709
+ for (const event of [...history]) {
1710
+ try {
1711
+ listener(event);
1712
+ } catch {
1713
+ }
1714
+ }
1715
+ }
1716
+ listeners.add(listener);
1717
+ return () => listeners.delete(listener);
1718
+ },
1719
+ connect(clientId = `client-${nextClientNumber++}`) {
1720
+ if (disposed) return createDisposedTransport(storeId, clientId);
1721
+ activeClients += 1;
1722
+ publishLifecycle("client-connected", clientId);
1723
+ let closed = false;
1724
+ const connectionSubscriptions = /* @__PURE__ */ new Set();
1725
+ return {
1726
+ request(command) {
1727
+ if (closed) {
1728
+ const parsed = parseCommand(command);
1729
+ return Promise.resolve(resultError(
1730
+ storeId,
1731
+ parsed?.requestId ?? "closed",
1732
+ "disposed",
1733
+ "DevTools client is disconnected"
1734
+ ));
1735
+ }
1736
+ return Promise.resolve(controller.handleCommand(command));
1737
+ },
1738
+ subscribe(listener) {
1739
+ if (closed) return () => {
1740
+ };
1741
+ const unsubscribe = controller.subscribe(listener);
1742
+ connectionSubscriptions.add(unsubscribe);
1743
+ return () => {
1744
+ connectionSubscriptions.delete(unsubscribe);
1745
+ unsubscribe();
1746
+ };
1747
+ },
1748
+ close() {
1749
+ if (closed) return;
1750
+ closed = true;
1751
+ for (const unsubscribe of connectionSubscriptions) unsubscribe();
1752
+ connectionSubscriptions.clear();
1753
+ activeClients = Math.max(0, activeClients - 1);
1754
+ publishLifecycle("client-disconnected", clientId);
1755
+ }
1756
+ };
1757
+ },
1758
+ handleCommand(command) {
1759
+ const parsed = parseCommand(command);
1760
+ const requestId = parsed?.requestId ?? "invalid";
1761
+ if (disposed) return resultError(storeId, requestId, "disposed", "DevTools controller is disposed");
1762
+ if (!parsed) return resultError(storeId, requestId, "invalid-envelope", "Invalid DevTools command envelope");
1763
+ if (parsed.version !== GRAPH_DEVTOOLS_PROTOCOL_VERSION) {
1764
+ return resultError(storeId, parsed.requestId, "unsupported-version", `Unsupported DevTools protocol version ${parsed.version}`);
1765
+ }
1766
+ if (parsed.storeId !== storeId) {
1767
+ return resultError(storeId, parsed.requestId, "wrong-store", `Command targets ${parsed.storeId}, not ${storeId}`);
1768
+ }
1769
+ let result;
1770
+ switch (parsed.command) {
1771
+ case "get-capabilities":
1772
+ result = capabilities;
1773
+ break;
1774
+ case "get-snapshot":
1775
+ result = controller.getSnapshot();
1776
+ break;
1777
+ case "get-history":
1778
+ result = controller.getHistory();
1779
+ break;
1780
+ case "get-history-status":
1781
+ result = controller.getHistoryStatus();
1782
+ break;
1783
+ case "get-entity-records":
1784
+ result = controller.getEntityRecords();
1785
+ break;
1786
+ case "get-views":
1787
+ result = controller.getViews();
1788
+ break;
1789
+ case "get-relationships":
1790
+ result = controller.getRelationships();
1791
+ break;
1792
+ case "get-time-travel-status": {
1793
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
1794
+ result = controller.getSnapshotHistoryStatus();
1795
+ break;
1796
+ }
1797
+ case "rewind": {
1798
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
1799
+ const payload = parseRewindPayload(parsed.payload);
1800
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid rewind payload");
1801
+ const receipt = controller.rewind(payload.cursor);
1802
+ if (!receipt) return resultError(storeId, parsed.requestId, "snapshot-not-found", `Snapshot cursor ${payload.cursor} is not retained`);
1803
+ result = receipt;
1804
+ break;
1805
+ }
1806
+ case "return-to-live": {
1807
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
1808
+ const receipt = controller.returnToLive();
1809
+ if (!receipt) return resultError(storeId, parsed.requestId, "not-rewound", "The graph is already live");
1810
+ result = receipt;
1811
+ break;
1812
+ }
1813
+ case "inspect-history-import": {
1814
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
1815
+ const payload = parseInspectHistoryImportPayload(parsed.payload);
1816
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid history import inspection payload");
1817
+ result = controller.inspectHistoryImport(payload.candidate);
1818
+ break;
1819
+ }
1820
+ case "confirm-history-import": {
1821
+ if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
1822
+ const payload = parseConfirmHistoryImportPayload(parsed.payload);
1823
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid history import confirmation payload");
1824
+ if (!payload.confirm) return resultError(storeId, parsed.requestId, "confirmation-required", "History import restore requires confirm: true");
1825
+ result = controller.confirmHistoryImport(payload.candidateId, payload.cursor);
1826
+ break;
1827
+ }
1828
+ case "preview-entity-patch": {
1829
+ const payload = parsePreviewPayload(parsed.payload);
1830
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid entity preview payload");
1831
+ const receipt = previewController.apply(payload.type, payload.id, payload.patch);
1832
+ if (!receipt) return resultError(storeId, parsed.requestId, "entity-not-found", `Entity ${payload.type}:${payload.id} was not found`);
1833
+ result = receipt;
1834
+ break;
1835
+ }
1836
+ case "restore-entity-preview": {
1837
+ const payload = parseRestorePreviewPayload(parsed.payload);
1838
+ if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid preview restore payload");
1839
+ const receipt = previewController.restore(payload.previewId);
1840
+ if (!receipt) return resultError(storeId, parsed.requestId, "preview-not-found", `Preview ${payload.previewId} was not found`);
1841
+ result = receipt;
1842
+ break;
1843
+ }
1844
+ case "clear-history":
1845
+ controller.clearHistory();
1846
+ result = { cleared: true };
1847
+ break;
1848
+ default:
1849
+ return resultError(storeId, parsed.requestId, "unsupported-command", `Unsupported DevTools command ${parsed.command}`);
1850
+ }
1851
+ return {
1852
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1853
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1854
+ requestId: parsed.requestId,
1855
+ storeId,
1856
+ ok: true,
1857
+ result
1858
+ };
1859
+ },
1860
+ isDisposed() {
1861
+ return disposed;
1862
+ }
1863
+ };
1864
+ const unregisterCompatibility = registerGraphDevtoolsCompatibilityDelegate(store, {
1865
+ configure(capacity) {
1866
+ controller.configureSnapshotLimit(capacity);
1867
+ },
1868
+ capture(label) {
1869
+ return controller.captureCompatibilitySnapshot(label);
1870
+ },
1871
+ restoreByIndex(index) {
1872
+ if (!Number.isInteger(index)) return false;
1873
+ const reference = controller.getCompatibilitySnapshotReferences()[index];
1874
+ if (!reference) return false;
1875
+ return controller.rewind(reference.cursor)?.status === "rewound";
1876
+ },
1877
+ restoreByCursor(cursor) {
1878
+ return controller.rewind(cursor)?.status === "rewound";
1879
+ },
1880
+ step(delta) {
1881
+ if (!Number.isInteger(delta)) return false;
1882
+ const references = controller.getCompatibilitySnapshotReferences();
1883
+ if (references.length === 0) return false;
1884
+ const status = controller.getSnapshotHistoryStatus();
1885
+ const activeIndex = status.source === "retained" && status.cursor !== null ? references.findIndex((reference) => reference.cursor === status.cursor) : -1;
1886
+ const base = activeIndex === -1 ? references.length - 1 : activeIndex;
1887
+ const target = Math.max(0, Math.min(references.length - 1, base + delta));
1888
+ return controller.rewind(references[target].cursor)?.status === "rewound";
1889
+ },
1890
+ getState() {
1891
+ const references = controller.getCompatibilitySnapshotReferences();
1892
+ const status = controller.getSnapshotHistoryStatus();
1893
+ const cursor = status.source === "retained" && status.cursor !== null ? references.findIndex((reference) => reference.cursor === status.cursor) : -1;
1894
+ return {
1895
+ snapshots: references.map((reference) => ({
1896
+ seq: reference.cursor,
1897
+ at: Date.parse(reference.capturedAt),
1898
+ ...reference.label !== void 0 ? { label: reference.label } : {}
1899
+ })),
1900
+ cursor: cursor === -1 ? null : cursor,
1901
+ capacity: status.snapshotLimit
1902
+ };
1903
+ },
1904
+ subscribe(listener) {
1905
+ return controller.subscribeSnapshotHistory(listener);
1906
+ },
1907
+ clear() {
1908
+ controller.clearSnapshotHistory();
1909
+ }
1910
+ });
1911
+ const dispose = () => {
1912
+ if (disposed) return;
1913
+ activeClients = 0;
1914
+ publishLifecycle("disposed");
1915
+ disposed = true;
1916
+ unregisterCompatibility();
1917
+ unsubscribeStore();
1918
+ listeners.clear();
1919
+ history.length = 0;
1920
+ historySizes.length = 0;
1921
+ retainedBytes = 0;
1922
+ entityRevisions.clear();
1923
+ entityValueRevisions.clear();
1924
+ viewRegistry.dispose();
1925
+ previewController.dispose();
1926
+ snapshotHistory.dispose();
1927
+ };
1928
+ publishLifecycle("attached");
1929
+ return { controller, dispose };
1930
+ }
1931
+ function createDisposedTransport(storeId, clientId) {
1932
+ return {
1933
+ request(command) {
1934
+ const parsed = parseCommand(command);
1935
+ return Promise.resolve(resultError(
1936
+ storeId,
1937
+ parsed?.requestId ?? clientId,
1938
+ "disposed",
1939
+ "DevTools controller is disposed"
1940
+ ));
1941
+ },
1942
+ subscribe() {
1943
+ return () => {
1944
+ };
1945
+ },
1946
+ close() {
1947
+ }
1948
+ };
1949
+ }
1950
+ function attachGraphDevtools(store, options = {}) {
1951
+ if (options.enabled === false) {
1952
+ return { enabled: false, controller: null, detach() {
1953
+ } };
1954
+ }
1955
+ let entry = controllers.get(store);
1956
+ if (!entry || entry.controller.isDisposed()) {
1957
+ entry = { ...createController(store, options), references: 0 };
1958
+ controllers.set(store, entry);
1959
+ }
1960
+ entry.references += 1;
1961
+ let detached = false;
1962
+ return {
1963
+ enabled: true,
1964
+ controller: entry.controller,
1965
+ detach() {
1966
+ if (detached) return;
1967
+ detached = true;
1968
+ const current = controllers.get(store);
1969
+ if (!current || current.controller !== entry?.controller) return;
1970
+ current.references = Math.max(0, current.references - 1);
1971
+ if (current.references === 0) {
1972
+ current.dispose();
1973
+ controllers.delete(store);
1974
+ }
1975
+ }
1976
+ };
1977
+ }
1978
+ function getGraphDevtoolsController(store) {
1979
+ const entry = controllers.get(store);
1980
+ return entry && !entry.controller.isDisposed() ? entry.controller : null;
1981
+ }
1982
+ registerGraphDevtoolsCompatibilityFactory((store) => {
1983
+ const attachment = attachGraphDevtools(store);
1984
+ return () => attachment.detach();
1985
+ });
1986
+
1987
+ // src/devtools/client.ts
1988
+ var nextRequest = 1;
1989
+ function createGraphDevtoolsClient(storeId, transport) {
1990
+ let disconnected = false;
1991
+ return {
1992
+ storeId,
1993
+ request(command, payload) {
1994
+ const requestId = `request-${nextRequest++}`;
1995
+ return transport.request({
1996
+ protocol: GRAPH_DEVTOOLS_PROTOCOL,
1997
+ version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
1998
+ requestId,
1999
+ storeId,
2000
+ command,
2001
+ ...payload !== void 0 ? { payload } : {}
2002
+ });
2003
+ },
2004
+ subscribe(listener) {
2005
+ if (disconnected) return () => {
2006
+ };
2007
+ return transport.subscribe(listener);
2008
+ },
2009
+ disconnect() {
2010
+ if (disconnected) return;
2011
+ disconnected = true;
2012
+ transport.close();
2013
+ }
2014
+ };
2015
+ }
2016
+
2017
+ export { GRAPH_DEVTOOLS_PROTOCOL, GRAPH_DEVTOOLS_PROTOCOL_VERSION, attachGraphDevtools, createGraphDevtoolsClient, getGraphDevtoolsController, projectGraphDevtoolsEntityRecords, projectGraphDevtoolsRelationships };
2018
+ //# sourceMappingURL=devtools.mjs.map
2019
+ //# sourceMappingURL=devtools.mjs.map