@prometheus-ags/entity-graph-core 3.0.5 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +248 -0
- package/dist/chunk-BE3BMRKF.mjs +938 -0
- package/dist/chunk-BE3BMRKF.mjs.map +1 -0
- package/dist/devtools.cjs +2571 -0
- package/dist/devtools.cjs.map +1 -0
- package/dist/devtools.d.cts +531 -0
- package/dist/devtools.d.ts +531 -0
- package/dist/devtools.mjs +2029 -0
- package/dist/devtools.mjs.map +1 -0
- package/dist/graph-ClW3HD8B.d.cts +990 -0
- package/dist/graph-ClW3HD8B.d.ts +990 -0
- package/dist/index.cjs +352 -322
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -1059
- package/dist/index.d.ts +60 -1059
- package/dist/index.mjs +45 -917
- package/dist/index.mjs.map +1 -1
- package/fixtures/devtools/README.md +37 -0
- package/fixtures/devtools/entity-inspection-v1.json +304 -0
- package/fixtures/devtools/time-travel-v1.json +110 -0
- package/package.json +25 -1
|
@@ -0,0 +1,2571 @@
|
|
|
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
|
+
const emptyWithAffectedMetadata = {
|
|
1862
|
+
...withoutValues,
|
|
1863
|
+
payload: {
|
|
1864
|
+
...withoutValues.payload,
|
|
1865
|
+
changes: [],
|
|
1866
|
+
changesOmitted: withoutValues.payload.changes.length
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
const { affectedEntities: _affectedEntities, affectedViewIds: _affectedViewIds, ...compactPayload } = withoutValues.payload;
|
|
1870
|
+
const boundedBase = encodedBytes2(emptyWithAffectedMetadata) <= eventBytesLimit ? withoutValues : { ...withoutValues, payload: compactPayload };
|
|
1871
|
+
let lower = 0;
|
|
1872
|
+
let upper = withoutValues.payload.changes.length;
|
|
1873
|
+
let bounded = {
|
|
1874
|
+
...boundedBase,
|
|
1875
|
+
payload: {
|
|
1876
|
+
...boundedBase.payload,
|
|
1877
|
+
changes: [],
|
|
1878
|
+
changesOmitted: withoutValues.payload.changes.length
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
while (lower <= upper) {
|
|
1882
|
+
const count = Math.floor((lower + upper) / 2);
|
|
1883
|
+
const candidate = {
|
|
1884
|
+
...boundedBase,
|
|
1885
|
+
payload: {
|
|
1886
|
+
...boundedBase.payload,
|
|
1887
|
+
changes: withoutValues.payload.changes.slice(0, count),
|
|
1888
|
+
changesOmitted: withoutValues.payload.changes.length - count
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
if (encodedBytes2(candidate) <= eventBytesLimit) {
|
|
1892
|
+
bounded = candidate;
|
|
1893
|
+
lower = count + 1;
|
|
1894
|
+
} else {
|
|
1895
|
+
upper = count - 1;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
return bounded;
|
|
1899
|
+
};
|
|
1900
|
+
const publish = (candidate) => {
|
|
1901
|
+
if (disposed) return;
|
|
1902
|
+
const event = boundEvent(candidate);
|
|
1903
|
+
if (historyLimit > 0 && historyBytesLimit > 0) {
|
|
1904
|
+
const size = encodedBytes2(event);
|
|
1905
|
+
history.push(event);
|
|
1906
|
+
historySizes.push(size);
|
|
1907
|
+
retainedBytes += size;
|
|
1908
|
+
while (history.length > historyLimit || retainedBytes > historyBytesLimit) {
|
|
1909
|
+
history.shift();
|
|
1910
|
+
retainedBytes -= historySizes.shift() ?? 0;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (const listener of [...listeners]) {
|
|
1914
|
+
try {
|
|
1915
|
+
listener(event);
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
const publishLifecycle = (state, clientId) => {
|
|
1921
|
+
publish({
|
|
1922
|
+
...nextBase(),
|
|
1923
|
+
type: "lifecycle",
|
|
1924
|
+
payload: {
|
|
1925
|
+
state,
|
|
1926
|
+
...clientId !== void 0 ? { clientId } : {},
|
|
1927
|
+
activeClients
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
};
|
|
1931
|
+
const publishTimeTravel = (state, cursor, previousCursor, source, previousSource, reason) => {
|
|
1932
|
+
const event = {
|
|
1933
|
+
...nextBase(),
|
|
1934
|
+
type: "time-travel",
|
|
1935
|
+
payload: { state, cursor, previousCursor, source, previousSource, reason }
|
|
1936
|
+
};
|
|
1937
|
+
publish(event);
|
|
1938
|
+
return event;
|
|
1939
|
+
};
|
|
1940
|
+
const restoreGraphData = (data) => {
|
|
1941
|
+
internalReplayDepth += 1;
|
|
1942
|
+
try {
|
|
1943
|
+
store.setState(data);
|
|
1944
|
+
} finally {
|
|
1945
|
+
internalReplayDepth -= 1;
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
const leaveRewindForMutation = () => {
|
|
1949
|
+
const previous = snapshotHistory.leaveRewindForMutation();
|
|
1950
|
+
if (previous === null) return;
|
|
1951
|
+
projectionFailureRevision += 1;
|
|
1952
|
+
publishTimeTravel(
|
|
1953
|
+
"live",
|
|
1954
|
+
null,
|
|
1955
|
+
previous.previousCursor,
|
|
1956
|
+
null,
|
|
1957
|
+
previous.previousSource,
|
|
1958
|
+
"mutation"
|
|
1959
|
+
);
|
|
1960
|
+
};
|
|
1961
|
+
const viewRegistry = createGraphDevtoolsViewRegistry(storeId, () => store.getState(), (payload) => {
|
|
1962
|
+
const event = { ...nextBase(), type: "view", payload };
|
|
1963
|
+
publish(event);
|
|
1964
|
+
});
|
|
1965
|
+
const affectedProjection = (changes, previous, current) => {
|
|
1966
|
+
const identities = /* @__PURE__ */ new Map();
|
|
1967
|
+
const changedListKeys = /* @__PURE__ */ new Set();
|
|
1968
|
+
for (const change of changes) {
|
|
1969
|
+
if ((change.category === "entity" || change.category === "patch") && change.id !== void 0) {
|
|
1970
|
+
identities.set(graphDevtoolsEntityIdentityKey(change.key, change.id), {
|
|
1971
|
+
type: change.key,
|
|
1972
|
+
id: change.id
|
|
1973
|
+
});
|
|
1974
|
+
} else if (change.category === "entity-state" || change.category === "sync") {
|
|
1975
|
+
const stateIdentities = [
|
|
1976
|
+
...graphDevtoolsEntityIdentitiesForStateKey(previous, change.key),
|
|
1977
|
+
...graphDevtoolsEntityIdentitiesForStateKey(current, change.key)
|
|
1978
|
+
];
|
|
1979
|
+
for (const identity of stateIdentities) {
|
|
1980
|
+
identities.set(graphDevtoolsEntityIdentityKey(identity.type, identity.id), identity);
|
|
1981
|
+
}
|
|
1982
|
+
} else if (change.category === "list") {
|
|
1983
|
+
changedListKeys.add(change.key);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
const views = viewRegistry.getSnapshot().views;
|
|
1987
|
+
const affectedViewIds = /* @__PURE__ */ new Set();
|
|
1988
|
+
for (const view of views) {
|
|
1989
|
+
if (view.queryKey !== null && changedListKeys.has(view.queryKey)) {
|
|
1990
|
+
affectedViewIds.add(view.viewId);
|
|
1991
|
+
const previousIds = previous.lists[view.queryKey]?.ids ?? [];
|
|
1992
|
+
const currentIds = current.lists[view.queryKey]?.ids ?? [];
|
|
1993
|
+
const previousPositions = new Map(previousIds.map((id, index) => [id, index]));
|
|
1994
|
+
const currentPositions = new Map(currentIds.map((id, index) => [id, index]));
|
|
1995
|
+
const ids = /* @__PURE__ */ new Set([...previousIds, ...currentIds]);
|
|
1996
|
+
for (const id of ids) {
|
|
1997
|
+
if (previousPositions.get(id) === currentPositions.get(id)) continue;
|
|
1998
|
+
identities.set(graphDevtoolsEntityIdentityKey(view.entityType, id), {
|
|
1999
|
+
type: view.entityType,
|
|
2000
|
+
id
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
if (view.membership.some((member) => identities.has(graphDevtoolsEntityIdentityKey(member.type, member.id)))) {
|
|
2005
|
+
affectedViewIds.add(view.viewId);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return {
|
|
2009
|
+
affectedEntities: [...identities.values()],
|
|
2010
|
+
affectedViewIds: [...affectedViewIds].sort()
|
|
2011
|
+
};
|
|
2012
|
+
};
|
|
2013
|
+
const unsubscribeStore = store.subscribe((current, previous) => {
|
|
2014
|
+
if (disposed || internalReplayDepth > 0) return;
|
|
2015
|
+
const startedAt = nowDuration();
|
|
2016
|
+
let changes;
|
|
2017
|
+
try {
|
|
2018
|
+
changes = projectGraphDevtoolsChanges(previous, current, valuePolicy, storeId);
|
|
2019
|
+
} catch {
|
|
2020
|
+
leaveRewindForMutation();
|
|
2021
|
+
projectionFailureRevision += 1;
|
|
2022
|
+
const base2 = nextBase();
|
|
2023
|
+
const event2 = {
|
|
2024
|
+
...base2,
|
|
2025
|
+
type: "diagnostic",
|
|
2026
|
+
payload: {
|
|
2027
|
+
code: "projection-failed",
|
|
2028
|
+
message: "A graph publication could not be projected for DevTools.",
|
|
2029
|
+
snapshot: snapshotHistory.capture(current, base2.sequence)
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
publish(event2);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (changes.length === 0) return;
|
|
2036
|
+
leaveRewindForMutation();
|
|
2037
|
+
advanceGraphDevtoolsEntityRevisions(changes, entityRevisions, current, entityValueRevisions);
|
|
2038
|
+
const affected = affectedProjection(changes, previous, current);
|
|
2039
|
+
const base = nextBase();
|
|
2040
|
+
const event = {
|
|
2041
|
+
...base,
|
|
2042
|
+
type: "mutation",
|
|
2043
|
+
payload: {
|
|
2044
|
+
snapshot: snapshotHistory.capture(current, base.sequence),
|
|
2045
|
+
valuesTruncated: false,
|
|
2046
|
+
changesOmitted: 0,
|
|
2047
|
+
changes,
|
|
2048
|
+
...affected,
|
|
2049
|
+
before: collectGraphDevtoolsCounts(previous),
|
|
2050
|
+
after: collectGraphDevtoolsCounts(current),
|
|
2051
|
+
projectionDurationMs: Math.max(0, nowDuration() - startedAt)
|
|
2052
|
+
}
|
|
2053
|
+
};
|
|
2054
|
+
publish(event);
|
|
2055
|
+
});
|
|
2056
|
+
const previewController = createGraphDevtoolsPreviewController(
|
|
2057
|
+
store,
|
|
2058
|
+
storeId,
|
|
2059
|
+
valuePolicy,
|
|
2060
|
+
(type, id) => (entityValueRevisions.get(graphDevtoolsEntityIdentityKey(type, id)) ?? 0) + projectionFailureRevision
|
|
2061
|
+
);
|
|
2062
|
+
const controller = {
|
|
2063
|
+
storeId,
|
|
2064
|
+
capabilities,
|
|
2065
|
+
getSnapshot() {
|
|
2066
|
+
return {
|
|
2067
|
+
protocol: GRAPH_DEVTOOLS_PROTOCOL,
|
|
2068
|
+
version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
|
|
2069
|
+
storeId,
|
|
2070
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2071
|
+
counts: collectGraphDevtoolsCounts(store.getState()),
|
|
2072
|
+
history: controller.getHistoryStatus(),
|
|
2073
|
+
snapshots: controller.getSnapshotHistoryStatus()
|
|
2074
|
+
};
|
|
2075
|
+
},
|
|
2076
|
+
getHistory() {
|
|
2077
|
+
return [...history];
|
|
2078
|
+
},
|
|
2079
|
+
getHistoryStatus() {
|
|
2080
|
+
return {
|
|
2081
|
+
retainedEvents: history.length,
|
|
2082
|
+
retainedBytes,
|
|
2083
|
+
eventLimit: historyLimit,
|
|
2084
|
+
byteLimit: historyBytesLimit,
|
|
2085
|
+
oldestSequence: history[0]?.sequence ?? null,
|
|
2086
|
+
newestSequence: history.length > 0 ? history[history.length - 1].sequence : null
|
|
2087
|
+
};
|
|
2088
|
+
},
|
|
2089
|
+
getSnapshotHistoryStatus() {
|
|
2090
|
+
return snapshotHistory.getStatus();
|
|
2091
|
+
},
|
|
2092
|
+
getSnapshotReferences() {
|
|
2093
|
+
return snapshotHistory.getReferences();
|
|
2094
|
+
},
|
|
2095
|
+
getCompatibilitySnapshotReferences() {
|
|
2096
|
+
return snapshotHistory.getCompatibilityReferences();
|
|
2097
|
+
},
|
|
2098
|
+
captureCompatibilitySnapshot(label) {
|
|
2099
|
+
const reference = snapshotHistory.captureCompatibility(store.getState(), label);
|
|
2100
|
+
return reference.status === "retained" ? reference.cursor : -1;
|
|
2101
|
+
},
|
|
2102
|
+
configureSnapshotLimit(nextSnapshotLimit) {
|
|
2103
|
+
snapshotHistory.configureLimit(nextSnapshotLimit);
|
|
2104
|
+
capabilities.limits.snapshots = snapshotHistory.getStatus().snapshotLimit;
|
|
2105
|
+
},
|
|
2106
|
+
subscribeSnapshotHistory(listener) {
|
|
2107
|
+
return snapshotHistory.subscribe(listener);
|
|
2108
|
+
},
|
|
2109
|
+
clearSnapshotHistory() {
|
|
2110
|
+
snapshotHistory.clear();
|
|
2111
|
+
},
|
|
2112
|
+
getEntityRecords() {
|
|
2113
|
+
return projectGraphDevtoolsEntityRecords(
|
|
2114
|
+
store.getState(),
|
|
2115
|
+
storeId,
|
|
2116
|
+
entityRevisions,
|
|
2117
|
+
valuePolicy,
|
|
2118
|
+
viewRegistry.getViewIdsByEntity()
|
|
2119
|
+
);
|
|
2120
|
+
},
|
|
2121
|
+
getViews() {
|
|
2122
|
+
return viewRegistry.getSnapshot();
|
|
2123
|
+
},
|
|
2124
|
+
getRelationships() {
|
|
2125
|
+
return projectGraphDevtoolsRelationships(store.getState(), storeId);
|
|
2126
|
+
},
|
|
2127
|
+
registerView(definition) {
|
|
2128
|
+
return viewRegistry.register(definition);
|
|
2129
|
+
},
|
|
2130
|
+
rewind(cursor) {
|
|
2131
|
+
if (!timeTravelEnabled) return null;
|
|
2132
|
+
const attempt = snapshotHistory.rewind(cursor, store.getState(), restoreGraphData);
|
|
2133
|
+
if (attempt === null || attempt.status === "expired-history") return attempt;
|
|
2134
|
+
projectionFailureRevision += 1;
|
|
2135
|
+
const event = publishTimeTravel(
|
|
2136
|
+
"rewound",
|
|
2137
|
+
cursor,
|
|
2138
|
+
attempt.previousCursor,
|
|
2139
|
+
"retained",
|
|
2140
|
+
attempt.previousSource,
|
|
2141
|
+
"command"
|
|
2142
|
+
);
|
|
2143
|
+
return {
|
|
2144
|
+
status: "rewound",
|
|
2145
|
+
cursor,
|
|
2146
|
+
source: "retained",
|
|
2147
|
+
previousCursor: attempt.previousCursor,
|
|
2148
|
+
previousSource: attempt.previousSource,
|
|
2149
|
+
changedAt: event.observedAt
|
|
2150
|
+
};
|
|
2151
|
+
},
|
|
2152
|
+
returnToLive() {
|
|
2153
|
+
if (!timeTravelEnabled) return null;
|
|
2154
|
+
const previous = snapshotHistory.returnToLive(restoreGraphData);
|
|
2155
|
+
if (previous === null) return null;
|
|
2156
|
+
projectionFailureRevision += 1;
|
|
2157
|
+
const event = publishTimeTravel(
|
|
2158
|
+
"live",
|
|
2159
|
+
null,
|
|
2160
|
+
previous.previousCursor,
|
|
2161
|
+
null,
|
|
2162
|
+
previous.previousSource,
|
|
2163
|
+
"command"
|
|
2164
|
+
);
|
|
2165
|
+
return {
|
|
2166
|
+
status: "live",
|
|
2167
|
+
cursor: null,
|
|
2168
|
+
previousCursor: previous.previousCursor,
|
|
2169
|
+
previousSource: previous.previousSource,
|
|
2170
|
+
reason: "command",
|
|
2171
|
+
changedAt: event.observedAt
|
|
2172
|
+
};
|
|
2173
|
+
},
|
|
2174
|
+
inspectHistoryImport(candidate) {
|
|
2175
|
+
if (disposed) {
|
|
2176
|
+
return { status: "rejected", reason: "disposed", message: "DevTools controller is disposed" };
|
|
2177
|
+
}
|
|
2178
|
+
if (!timeTravelEnabled) {
|
|
2179
|
+
return {
|
|
2180
|
+
status: "rejected",
|
|
2181
|
+
reason: "time-travel-unavailable",
|
|
2182
|
+
message: "Time travel is disabled for this controller"
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
return snapshotHistory.inspectImport(candidate, storeId);
|
|
2186
|
+
},
|
|
2187
|
+
confirmHistoryImport(candidateId, cursor) {
|
|
2188
|
+
if (disposed) {
|
|
2189
|
+
return { status: "rejected", reason: "disposed", message: "DevTools controller is disposed" };
|
|
2190
|
+
}
|
|
2191
|
+
if (!timeTravelEnabled) {
|
|
2192
|
+
return {
|
|
2193
|
+
status: "rejected",
|
|
2194
|
+
reason: "time-travel-unavailable",
|
|
2195
|
+
message: "Time travel is disabled for this controller"
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
const attempt = snapshotHistory.restoreImport(
|
|
2199
|
+
candidateId,
|
|
2200
|
+
cursor,
|
|
2201
|
+
store.getState(),
|
|
2202
|
+
restoreGraphData
|
|
2203
|
+
);
|
|
2204
|
+
if (attempt.status === "candidate-not-found") {
|
|
2205
|
+
return {
|
|
2206
|
+
status: "rejected",
|
|
2207
|
+
reason: "candidate-not-found",
|
|
2208
|
+
message: `Import candidate ${candidateId} is no longer awaiting confirmation`
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
if (attempt.status === "snapshot-not-found") {
|
|
2212
|
+
return {
|
|
2213
|
+
status: "rejected",
|
|
2214
|
+
reason: "snapshot-not-found",
|
|
2215
|
+
message: `Import candidate ${candidateId} does not contain cursor ${cursor}`
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
if (attempt.status === "restore-failed") {
|
|
2219
|
+
return {
|
|
2220
|
+
status: "rejected",
|
|
2221
|
+
reason: "restore-failed",
|
|
2222
|
+
message: `Import candidate ${candidateId} could not be restored through the graph boundary`
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
projectionFailureRevision += 1;
|
|
2226
|
+
const event = publishTimeTravel(
|
|
2227
|
+
"rewound",
|
|
2228
|
+
cursor,
|
|
2229
|
+
attempt.previousCursor,
|
|
2230
|
+
"import",
|
|
2231
|
+
attempt.previousSource,
|
|
2232
|
+
"import"
|
|
2233
|
+
);
|
|
2234
|
+
const receipt = {
|
|
2235
|
+
status: "rewound",
|
|
2236
|
+
source: "import",
|
|
2237
|
+
candidateId,
|
|
2238
|
+
cursor,
|
|
2239
|
+
previousCursor: attempt.previousCursor,
|
|
2240
|
+
previousSource: attempt.previousSource,
|
|
2241
|
+
changedAt: event.observedAt
|
|
2242
|
+
};
|
|
2243
|
+
return receipt;
|
|
2244
|
+
},
|
|
2245
|
+
clearHistory() {
|
|
2246
|
+
history.length = 0;
|
|
2247
|
+
historySizes.length = 0;
|
|
2248
|
+
retainedBytes = 0;
|
|
2249
|
+
snapshotHistory.clear();
|
|
2250
|
+
},
|
|
2251
|
+
subscribe(listener, replay = false) {
|
|
2252
|
+
if (disposed) return () => {
|
|
2253
|
+
};
|
|
2254
|
+
if (replay) {
|
|
2255
|
+
for (const event of [...history]) {
|
|
2256
|
+
try {
|
|
2257
|
+
listener(event);
|
|
2258
|
+
} catch {
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
listeners.add(listener);
|
|
2263
|
+
return () => listeners.delete(listener);
|
|
2264
|
+
},
|
|
2265
|
+
connect(clientId = `client-${nextClientNumber++}`) {
|
|
2266
|
+
if (disposed) return createDisposedTransport(storeId, clientId);
|
|
2267
|
+
activeClients += 1;
|
|
2268
|
+
publishLifecycle("client-connected", clientId);
|
|
2269
|
+
let closed = false;
|
|
2270
|
+
const connectionSubscriptions = /* @__PURE__ */ new Set();
|
|
2271
|
+
return {
|
|
2272
|
+
request(command) {
|
|
2273
|
+
if (closed) {
|
|
2274
|
+
const parsed = parseCommand(command);
|
|
2275
|
+
return Promise.resolve(resultError(
|
|
2276
|
+
storeId,
|
|
2277
|
+
parsed?.requestId ?? "closed",
|
|
2278
|
+
"disposed",
|
|
2279
|
+
"DevTools client is disconnected"
|
|
2280
|
+
));
|
|
2281
|
+
}
|
|
2282
|
+
return Promise.resolve(controller.handleCommand(command));
|
|
2283
|
+
},
|
|
2284
|
+
subscribe(listener) {
|
|
2285
|
+
if (closed) return () => {
|
|
2286
|
+
};
|
|
2287
|
+
const unsubscribe = controller.subscribe(listener);
|
|
2288
|
+
connectionSubscriptions.add(unsubscribe);
|
|
2289
|
+
return () => {
|
|
2290
|
+
connectionSubscriptions.delete(unsubscribe);
|
|
2291
|
+
unsubscribe();
|
|
2292
|
+
};
|
|
2293
|
+
},
|
|
2294
|
+
close() {
|
|
2295
|
+
if (closed) return;
|
|
2296
|
+
closed = true;
|
|
2297
|
+
for (const unsubscribe of connectionSubscriptions) unsubscribe();
|
|
2298
|
+
connectionSubscriptions.clear();
|
|
2299
|
+
activeClients = Math.max(0, activeClients - 1);
|
|
2300
|
+
publishLifecycle("client-disconnected", clientId);
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
},
|
|
2304
|
+
handleCommand(command) {
|
|
2305
|
+
const parsed = parseCommand(command);
|
|
2306
|
+
const requestId = parsed?.requestId ?? "invalid";
|
|
2307
|
+
if (disposed) return resultError(storeId, requestId, "disposed", "DevTools controller is disposed");
|
|
2308
|
+
if (!parsed) return resultError(storeId, requestId, "invalid-envelope", "Invalid DevTools command envelope");
|
|
2309
|
+
if (parsed.version !== GRAPH_DEVTOOLS_PROTOCOL_VERSION) {
|
|
2310
|
+
return resultError(storeId, parsed.requestId, "unsupported-version", `Unsupported DevTools protocol version ${parsed.version}`);
|
|
2311
|
+
}
|
|
2312
|
+
if (parsed.storeId !== storeId) {
|
|
2313
|
+
return resultError(storeId, parsed.requestId, "wrong-store", `Command targets ${parsed.storeId}, not ${storeId}`);
|
|
2314
|
+
}
|
|
2315
|
+
let result;
|
|
2316
|
+
switch (parsed.command) {
|
|
2317
|
+
case "get-capabilities":
|
|
2318
|
+
result = capabilities;
|
|
2319
|
+
break;
|
|
2320
|
+
case "get-snapshot":
|
|
2321
|
+
result = controller.getSnapshot();
|
|
2322
|
+
break;
|
|
2323
|
+
case "get-history":
|
|
2324
|
+
result = controller.getHistory();
|
|
2325
|
+
break;
|
|
2326
|
+
case "get-history-status":
|
|
2327
|
+
result = controller.getHistoryStatus();
|
|
2328
|
+
break;
|
|
2329
|
+
case "get-entity-records":
|
|
2330
|
+
result = controller.getEntityRecords();
|
|
2331
|
+
break;
|
|
2332
|
+
case "get-views":
|
|
2333
|
+
result = controller.getViews();
|
|
2334
|
+
break;
|
|
2335
|
+
case "get-relationships":
|
|
2336
|
+
result = controller.getRelationships();
|
|
2337
|
+
break;
|
|
2338
|
+
case "get-time-travel-status": {
|
|
2339
|
+
if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
|
|
2340
|
+
result = controller.getSnapshotHistoryStatus();
|
|
2341
|
+
break;
|
|
2342
|
+
}
|
|
2343
|
+
case "rewind": {
|
|
2344
|
+
if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
|
|
2345
|
+
const payload = parseRewindPayload(parsed.payload);
|
|
2346
|
+
if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid rewind payload");
|
|
2347
|
+
const receipt = controller.rewind(payload.cursor);
|
|
2348
|
+
if (!receipt) return resultError(storeId, parsed.requestId, "snapshot-not-found", `Snapshot cursor ${payload.cursor} is not retained`);
|
|
2349
|
+
result = receipt;
|
|
2350
|
+
break;
|
|
2351
|
+
}
|
|
2352
|
+
case "return-to-live": {
|
|
2353
|
+
if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
|
|
2354
|
+
const receipt = controller.returnToLive();
|
|
2355
|
+
if (!receipt) return resultError(storeId, parsed.requestId, "not-rewound", "The graph is already live");
|
|
2356
|
+
result = receipt;
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
case "inspect-history-import": {
|
|
2360
|
+
if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
|
|
2361
|
+
const payload = parseInspectHistoryImportPayload(parsed.payload);
|
|
2362
|
+
if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid history import inspection payload");
|
|
2363
|
+
result = controller.inspectHistoryImport(payload.candidate);
|
|
2364
|
+
break;
|
|
2365
|
+
}
|
|
2366
|
+
case "confirm-history-import": {
|
|
2367
|
+
if (!timeTravelEnabled) return resultError(storeId, parsed.requestId, "time-travel-unavailable", "Time travel is disabled for this controller");
|
|
2368
|
+
const payload = parseConfirmHistoryImportPayload(parsed.payload);
|
|
2369
|
+
if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid history import confirmation payload");
|
|
2370
|
+
if (!payload.confirm) return resultError(storeId, parsed.requestId, "confirmation-required", "History import restore requires confirm: true");
|
|
2371
|
+
result = controller.confirmHistoryImport(payload.candidateId, payload.cursor);
|
|
2372
|
+
break;
|
|
2373
|
+
}
|
|
2374
|
+
case "preview-entity-patch": {
|
|
2375
|
+
const payload = parsePreviewPayload(parsed.payload);
|
|
2376
|
+
if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid entity preview payload");
|
|
2377
|
+
const receipt = previewController.apply(payload.type, payload.id, payload.patch);
|
|
2378
|
+
if (!receipt) return resultError(storeId, parsed.requestId, "entity-not-found", `Entity ${payload.type}:${payload.id} was not found`);
|
|
2379
|
+
result = receipt;
|
|
2380
|
+
break;
|
|
2381
|
+
}
|
|
2382
|
+
case "restore-entity-preview": {
|
|
2383
|
+
const payload = parseRestorePreviewPayload(parsed.payload);
|
|
2384
|
+
if (!payload) return resultError(storeId, parsed.requestId, "invalid-payload", "Invalid preview restore payload");
|
|
2385
|
+
const receipt = previewController.restore(payload.previewId);
|
|
2386
|
+
if (!receipt) return resultError(storeId, parsed.requestId, "preview-not-found", `Preview ${payload.previewId} was not found`);
|
|
2387
|
+
result = receipt;
|
|
2388
|
+
break;
|
|
2389
|
+
}
|
|
2390
|
+
case "clear-history":
|
|
2391
|
+
controller.clearHistory();
|
|
2392
|
+
result = { cleared: true };
|
|
2393
|
+
break;
|
|
2394
|
+
default:
|
|
2395
|
+
return resultError(storeId, parsed.requestId, "unsupported-command", `Unsupported DevTools command ${parsed.command}`);
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
protocol: GRAPH_DEVTOOLS_PROTOCOL,
|
|
2399
|
+
version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
|
|
2400
|
+
requestId: parsed.requestId,
|
|
2401
|
+
storeId,
|
|
2402
|
+
ok: true,
|
|
2403
|
+
result
|
|
2404
|
+
};
|
|
2405
|
+
},
|
|
2406
|
+
isDisposed() {
|
|
2407
|
+
return disposed;
|
|
2408
|
+
}
|
|
2409
|
+
};
|
|
2410
|
+
const unregisterCompatibility = registerGraphDevtoolsCompatibilityDelegate(store, {
|
|
2411
|
+
configure(capacity) {
|
|
2412
|
+
controller.configureSnapshotLimit(capacity);
|
|
2413
|
+
},
|
|
2414
|
+
capture(label) {
|
|
2415
|
+
return controller.captureCompatibilitySnapshot(label);
|
|
2416
|
+
},
|
|
2417
|
+
restoreByIndex(index) {
|
|
2418
|
+
if (!Number.isInteger(index)) return false;
|
|
2419
|
+
const reference = controller.getCompatibilitySnapshotReferences()[index];
|
|
2420
|
+
if (!reference) return false;
|
|
2421
|
+
return controller.rewind(reference.cursor)?.status === "rewound";
|
|
2422
|
+
},
|
|
2423
|
+
restoreByCursor(cursor) {
|
|
2424
|
+
return controller.rewind(cursor)?.status === "rewound";
|
|
2425
|
+
},
|
|
2426
|
+
step(delta) {
|
|
2427
|
+
if (!Number.isInteger(delta)) return false;
|
|
2428
|
+
const references = controller.getCompatibilitySnapshotReferences();
|
|
2429
|
+
if (references.length === 0) return false;
|
|
2430
|
+
const status = controller.getSnapshotHistoryStatus();
|
|
2431
|
+
const activeIndex = status.source === "retained" && status.cursor !== null ? references.findIndex((reference) => reference.cursor === status.cursor) : -1;
|
|
2432
|
+
const base = activeIndex === -1 ? references.length - 1 : activeIndex;
|
|
2433
|
+
const target = Math.max(0, Math.min(references.length - 1, base + delta));
|
|
2434
|
+
return controller.rewind(references[target].cursor)?.status === "rewound";
|
|
2435
|
+
},
|
|
2436
|
+
getState() {
|
|
2437
|
+
const references = controller.getCompatibilitySnapshotReferences();
|
|
2438
|
+
const status = controller.getSnapshotHistoryStatus();
|
|
2439
|
+
const cursor = status.source === "retained" && status.cursor !== null ? references.findIndex((reference) => reference.cursor === status.cursor) : -1;
|
|
2440
|
+
return {
|
|
2441
|
+
snapshots: references.map((reference) => ({
|
|
2442
|
+
seq: reference.cursor,
|
|
2443
|
+
at: Date.parse(reference.capturedAt),
|
|
2444
|
+
...reference.label !== void 0 ? { label: reference.label } : {}
|
|
2445
|
+
})),
|
|
2446
|
+
cursor: cursor === -1 ? null : cursor,
|
|
2447
|
+
capacity: status.snapshotLimit
|
|
2448
|
+
};
|
|
2449
|
+
},
|
|
2450
|
+
subscribe(listener) {
|
|
2451
|
+
return controller.subscribeSnapshotHistory(listener);
|
|
2452
|
+
},
|
|
2453
|
+
clear() {
|
|
2454
|
+
controller.clearSnapshotHistory();
|
|
2455
|
+
}
|
|
2456
|
+
});
|
|
2457
|
+
const dispose = () => {
|
|
2458
|
+
if (disposed) return;
|
|
2459
|
+
activeClients = 0;
|
|
2460
|
+
publishLifecycle("disposed");
|
|
2461
|
+
disposed = true;
|
|
2462
|
+
unregisterCompatibility();
|
|
2463
|
+
unsubscribeStore();
|
|
2464
|
+
listeners.clear();
|
|
2465
|
+
history.length = 0;
|
|
2466
|
+
historySizes.length = 0;
|
|
2467
|
+
retainedBytes = 0;
|
|
2468
|
+
entityRevisions.clear();
|
|
2469
|
+
entityValueRevisions.clear();
|
|
2470
|
+
viewRegistry.dispose();
|
|
2471
|
+
previewController.dispose();
|
|
2472
|
+
snapshotHistory.dispose();
|
|
2473
|
+
};
|
|
2474
|
+
publishLifecycle("attached");
|
|
2475
|
+
return { controller, dispose };
|
|
2476
|
+
}
|
|
2477
|
+
function createDisposedTransport(storeId, clientId) {
|
|
2478
|
+
return {
|
|
2479
|
+
request(command) {
|
|
2480
|
+
const parsed = parseCommand(command);
|
|
2481
|
+
return Promise.resolve(resultError(
|
|
2482
|
+
storeId,
|
|
2483
|
+
parsed?.requestId ?? clientId,
|
|
2484
|
+
"disposed",
|
|
2485
|
+
"DevTools controller is disposed"
|
|
2486
|
+
));
|
|
2487
|
+
},
|
|
2488
|
+
subscribe() {
|
|
2489
|
+
return () => {
|
|
2490
|
+
};
|
|
2491
|
+
},
|
|
2492
|
+
close() {
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
function attachGraphDevtools(store, options = {}) {
|
|
2497
|
+
if (options.enabled === false) {
|
|
2498
|
+
return { enabled: false, controller: null, detach() {
|
|
2499
|
+
} };
|
|
2500
|
+
}
|
|
2501
|
+
let entry = controllers.get(store);
|
|
2502
|
+
if (!entry || entry.controller.isDisposed()) {
|
|
2503
|
+
entry = { ...createController(store, options), references: 0 };
|
|
2504
|
+
controllers.set(store, entry);
|
|
2505
|
+
}
|
|
2506
|
+
entry.references += 1;
|
|
2507
|
+
let detached = false;
|
|
2508
|
+
return {
|
|
2509
|
+
enabled: true,
|
|
2510
|
+
controller: entry.controller,
|
|
2511
|
+
detach() {
|
|
2512
|
+
if (detached) return;
|
|
2513
|
+
detached = true;
|
|
2514
|
+
const current = controllers.get(store);
|
|
2515
|
+
if (!current || current.controller !== entry?.controller) return;
|
|
2516
|
+
current.references = Math.max(0, current.references - 1);
|
|
2517
|
+
if (current.references === 0) {
|
|
2518
|
+
current.dispose();
|
|
2519
|
+
controllers.delete(store);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
function getGraphDevtoolsController(store) {
|
|
2525
|
+
const entry = controllers.get(store);
|
|
2526
|
+
return entry && !entry.controller.isDisposed() ? entry.controller : null;
|
|
2527
|
+
}
|
|
2528
|
+
registerGraphDevtoolsCompatibilityFactory((store) => {
|
|
2529
|
+
const attachment = attachGraphDevtools(store);
|
|
2530
|
+
return () => attachment.detach();
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
// src/devtools/client.ts
|
|
2534
|
+
var nextRequest = 1;
|
|
2535
|
+
function createGraphDevtoolsClient(storeId, transport) {
|
|
2536
|
+
let disconnected = false;
|
|
2537
|
+
return {
|
|
2538
|
+
storeId,
|
|
2539
|
+
request(command, payload) {
|
|
2540
|
+
const requestId = `request-${nextRequest++}`;
|
|
2541
|
+
return transport.request({
|
|
2542
|
+
protocol: GRAPH_DEVTOOLS_PROTOCOL,
|
|
2543
|
+
version: GRAPH_DEVTOOLS_PROTOCOL_VERSION,
|
|
2544
|
+
requestId,
|
|
2545
|
+
storeId,
|
|
2546
|
+
command,
|
|
2547
|
+
...payload !== void 0 ? { payload } : {}
|
|
2548
|
+
});
|
|
2549
|
+
},
|
|
2550
|
+
subscribe(listener) {
|
|
2551
|
+
if (disconnected) return () => {
|
|
2552
|
+
};
|
|
2553
|
+
return transport.subscribe(listener);
|
|
2554
|
+
},
|
|
2555
|
+
disconnect() {
|
|
2556
|
+
if (disconnected) return;
|
|
2557
|
+
disconnected = true;
|
|
2558
|
+
transport.close();
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
exports.GRAPH_DEVTOOLS_PROTOCOL = GRAPH_DEVTOOLS_PROTOCOL;
|
|
2564
|
+
exports.GRAPH_DEVTOOLS_PROTOCOL_VERSION = GRAPH_DEVTOOLS_PROTOCOL_VERSION;
|
|
2565
|
+
exports.attachGraphDevtools = attachGraphDevtools;
|
|
2566
|
+
exports.createGraphDevtoolsClient = createGraphDevtoolsClient;
|
|
2567
|
+
exports.getGraphDevtoolsController = getGraphDevtoolsController;
|
|
2568
|
+
exports.projectGraphDevtoolsEntityRecords = projectGraphDevtoolsEntityRecords;
|
|
2569
|
+
exports.projectGraphDevtoolsRelationships = projectGraphDevtoolsRelationships;
|
|
2570
|
+
//# sourceMappingURL=devtools.cjs.map
|
|
2571
|
+
//# sourceMappingURL=devtools.cjs.map
|