@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.
@@ -0,0 +1,938 @@
1
+ import { createStore } from 'zustand/vanilla';
2
+ import { subscribeWithSelector } from 'zustand/middleware';
3
+ import { immer } from 'zustand/middleware/immer';
4
+
5
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
+ }) : x)(function(x) {
8
+ if (typeof require !== "undefined") return require.apply(this, arguments);
9
+ throw Error('Dynamic require of "' + x + '" is not supported');
10
+ });
11
+
12
+ // src/merge/lww.ts
13
+ var lwwStrategy = (prev, next) => {
14
+ return prev ? { ...prev, ...next } : { ...next };
15
+ };
16
+
17
+ // src/merge/registry.ts
18
+ var byType = /* @__PURE__ */ new Map();
19
+ var globalDefault = null;
20
+ function registerMergeStrategy(type, strategy) {
21
+ byType.set(type, strategy);
22
+ }
23
+ function setDefaultMergeStrategy(strategy) {
24
+ globalDefault = strategy;
25
+ }
26
+ function getMergeStrategy(type) {
27
+ return byType.get(type) ?? globalDefault ?? lwwStrategy;
28
+ }
29
+ function hasMergeStrategy(type) {
30
+ return byType.has(type) || globalDefault !== null;
31
+ }
32
+
33
+ // src/view/evaluator.ts
34
+ function matchesFilter(entity, filter) {
35
+ if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
36
+ return matchesGroup(entity, filter);
37
+ }
38
+ function matchesGroup(entity, group) {
39
+ const { logic, clauses } = group;
40
+ if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
41
+ return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
42
+ }
43
+ function matchesClause(entity, clause) {
44
+ const { field, op, value, predicate } = clause;
45
+ const fv = getNestedValue(entity, field);
46
+ switch (op) {
47
+ case "eq":
48
+ return fv === value;
49
+ case "neq":
50
+ return fv !== value;
51
+ case "gt":
52
+ return fv > value;
53
+ case "gte":
54
+ return fv >= value;
55
+ case "lt":
56
+ return fv < value;
57
+ case "lte":
58
+ return fv <= value;
59
+ case "in":
60
+ return Array.isArray(value) && value.includes(fv);
61
+ case "nin":
62
+ return Array.isArray(value) && !value.includes(fv);
63
+ case "isNull":
64
+ return fv == null;
65
+ case "isNotNull":
66
+ return fv != null;
67
+ case "contains":
68
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
69
+ case "startsWith":
70
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
71
+ case "endsWith":
72
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
73
+ case "between": {
74
+ const [lo, hi] = value;
75
+ return fv >= lo && fv <= hi;
76
+ }
77
+ case "arrayContains":
78
+ return Array.isArray(fv) && fv.includes(value);
79
+ case "arrayOverlaps":
80
+ return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
81
+ case "matches":
82
+ return typeof fv === "string" && new RegExp(value).test(fv);
83
+ case "custom":
84
+ return predicate ? predicate(fv, entity) : true;
85
+ default:
86
+ return true;
87
+ }
88
+ }
89
+ function matchesSearch(entity, query, fields) {
90
+ if (!query.trim()) return true;
91
+ const lq = query.toLowerCase();
92
+ return fields.some((field) => {
93
+ const v = getNestedValue(entity, field);
94
+ return typeof v === "string" && v.toLowerCase().includes(lq);
95
+ });
96
+ }
97
+ function compareEntities(a, b, sort) {
98
+ for (const clause of sort) {
99
+ const r = compareByClause(a, b, clause);
100
+ if (r !== 0) return r;
101
+ }
102
+ return 0;
103
+ }
104
+ function compareByClause(a, b, clause) {
105
+ const { field, direction, nulls = "last", comparator } = clause;
106
+ const av = getNestedValue(a, field);
107
+ const bv = getNestedValue(b, field);
108
+ const aNull = av == null;
109
+ const bNull = bv == null;
110
+ if (aNull && bNull) return 0;
111
+ if (aNull) return nulls === "first" ? -1 : 1;
112
+ if (bNull) return nulls === "first" ? 1 : -1;
113
+ let cmp;
114
+ if (comparator) cmp = comparator(av, bv);
115
+ else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
116
+ else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
117
+ else cmp = String(av).localeCompare(String(bv));
118
+ return direction === "desc" ? -cmp : cmp;
119
+ }
120
+ function findInsertionIndex(entity, sortedIds, getEntity, sort) {
121
+ let lo = 0;
122
+ let hi = sortedIds.length;
123
+ while (lo < hi) {
124
+ const mid = lo + hi >>> 1;
125
+ const me = getEntity(sortedIds[mid]);
126
+ if (!me) {
127
+ lo = mid + 1;
128
+ continue;
129
+ }
130
+ if (compareEntities(entity, me, sort) <= 0) hi = mid;
131
+ else lo = mid + 1;
132
+ }
133
+ return lo;
134
+ }
135
+ function applyView(ids, getEntity, filter, sort, search) {
136
+ let entries = [];
137
+ for (const id of ids) {
138
+ const entity = getEntity(id);
139
+ if (!entity) continue;
140
+ entries.push({ id, entity });
141
+ }
142
+ if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
143
+ if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
144
+ if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
145
+ return entries.map((e) => e.id);
146
+ }
147
+ function checkCompleteness(loadedCount, total, hasNextPage) {
148
+ if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
149
+ if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
150
+ return { isComplete: true, reason: "no-more-pages" };
151
+ }
152
+ function getNestedValue(obj, path) {
153
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
154
+ let current = obj;
155
+ for (const part of parts) {
156
+ if (current == null || typeof current !== "object") return void 0;
157
+ current = current[part];
158
+ }
159
+ return current;
160
+ }
161
+ var EMPTY_IDS = [];
162
+ var EMPTY_ENTITY_STATE = {
163
+ isFetching: false,
164
+ lastFetched: null,
165
+ error: null,
166
+ stale: false
167
+ };
168
+ var EMPTY_SYNC_METADATA = {
169
+ synced: true,
170
+ origin: "server",
171
+ updatedAt: null
172
+ };
173
+ var EMPTY_LIST_STATE = {
174
+ ids: EMPTY_IDS,
175
+ total: null,
176
+ nextCursor: null,
177
+ prevCursor: null,
178
+ hasNextPage: false,
179
+ hasPrevPage: false,
180
+ isFetching: false,
181
+ isFetchingMore: false,
182
+ error: null,
183
+ lastError: null,
184
+ lastFetched: null,
185
+ stale: false,
186
+ currentPage: null,
187
+ pageSize: null
188
+ };
189
+ function defaultEntityState() {
190
+ return { ...EMPTY_ENTITY_STATE };
191
+ }
192
+ function defaultSyncMetadata() {
193
+ return { ...EMPTY_SYNC_METADATA };
194
+ }
195
+ function defaultListState() {
196
+ return { ...EMPTY_LIST_STATE, ids: [] };
197
+ }
198
+ function ek(type, id) {
199
+ return `${type}:${id}`;
200
+ }
201
+ var snapshotCache = /* @__PURE__ */ new Map();
202
+ function readCachedEntitySnapshot(type, id, base, patch, metadata) {
203
+ const key = ek(type, id);
204
+ const cached = snapshotCache.get(key);
205
+ if (cached && cached.base === base && cached.patch === patch && cached.metadata === metadata) {
206
+ return cached.snapshot;
207
+ }
208
+ const snapshot = {
209
+ ...patch ? { ...base, ...patch } : base,
210
+ $synced: metadata.synced,
211
+ $origin: metadata.origin,
212
+ $updatedAt: metadata.updatedAt
213
+ };
214
+ snapshotCache.set(key, { base, patch, metadata, snapshot });
215
+ return snapshot;
216
+ }
217
+ function createGraphStore() {
218
+ return createStore()(
219
+ subscribeWithSelector(
220
+ immer((set, get) => ({
221
+ entities: {},
222
+ patches: {},
223
+ entityStates: {},
224
+ syncMetadata: {},
225
+ lists: {},
226
+ upsertEntity: (type, id, data) => set((s) => {
227
+ if (!s.entities[type]) s.entities[type] = {};
228
+ const key = ek(type, id);
229
+ const strategy = getMergeStrategy(type);
230
+ const ctx = { type, id, origin: s.syncMetadata[key]?.origin ?? "server", updatedAt: Date.now() };
231
+ s.entities[type][id] = strategy(s.entities[type][id], data, ctx);
232
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
233
+ }),
234
+ upsertEntities: (type, entries) => set((s) => {
235
+ if (!s.entities[type]) s.entities[type] = {};
236
+ const strategy = getMergeStrategy(type);
237
+ for (const { id, data } of entries) {
238
+ const key = ek(type, id);
239
+ const ctx = { type, id, origin: s.syncMetadata[key]?.origin ?? "server", updatedAt: Date.now() };
240
+ s.entities[type][id] = strategy(s.entities[type][id], data, ctx);
241
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
242
+ }
243
+ }),
244
+ ingestFetchedList: (type, entries, options = {}) => set((s) => {
245
+ const fetchedAt = Date.now();
246
+ const ids = entries.map(({ id }) => id);
247
+ const batches = [
248
+ { type, entries },
249
+ ...options.sideBatches ?? []
250
+ ];
251
+ const origins = /* @__PURE__ */ new Map();
252
+ for (const batch of batches) {
253
+ if (!s.entities[batch.type]) s.entities[batch.type] = {};
254
+ for (const { id } of batch.entries) {
255
+ const key = ek(batch.type, id);
256
+ if (!origins.has(key)) {
257
+ origins.set(key, s.syncMetadata[key]?.origin ?? "server");
258
+ }
259
+ }
260
+ }
261
+ for (const batch of batches) {
262
+ const strategy = getMergeStrategy(batch.type);
263
+ for (const { id, data } of batch.entries) {
264
+ const key = ek(batch.type, id);
265
+ const ctx = {
266
+ type: batch.type,
267
+ id,
268
+ origin: origins.get(key) ?? "server",
269
+ updatedAt: fetchedAt
270
+ };
271
+ s.entities[batch.type][id] = strategy(s.entities[batch.type][id], data, ctx);
272
+ }
273
+ }
274
+ const completed = /* @__PURE__ */ new Set();
275
+ for (const batch of batches) {
276
+ for (const { id } of batch.entries) {
277
+ const key = ek(batch.type, id);
278
+ if (completed.has(key)) continue;
279
+ completed.add(key);
280
+ if (!s.entityStates[key]) s.entityStates[key] = defaultEntityState();
281
+ s.entityStates[key].lastFetched = fetchedAt;
282
+ s.entityStates[key].isFetching = false;
283
+ s.entityStates[key].error = null;
284
+ s.entityStates[key].stale = false;
285
+ s.syncMetadata[key] = {
286
+ ...s.syncMetadata[key] ?? defaultSyncMetadata(),
287
+ synced: true,
288
+ origin: "server",
289
+ updatedAt: fetchedAt
290
+ };
291
+ }
292
+ }
293
+ const readDraftEntity = (id) => {
294
+ const base = s.entities[type]?.[id];
295
+ if (!base) return null;
296
+ const patch = s.patches[type]?.[id];
297
+ const metadata = s.syncMetadata[ek(type, id)] ?? defaultSyncMetadata();
298
+ return {
299
+ ...base,
300
+ ...patch ?? {},
301
+ $synced: metadata.synced,
302
+ $origin: metadata.origin,
303
+ $updatedAt: metadata.updatedAt
304
+ };
305
+ };
306
+ for (const target of options.lists ?? []) {
307
+ const existing = s.lists[target.key] ?? defaultListState();
308
+ const targetIds = target.ids ?? ids;
309
+ const nextIds = target.mode === "append" ? Array.from(/* @__PURE__ */ new Set([...existing.ids, ...targetIds])) : targetIds;
310
+ s.lists[target.key] = {
311
+ ...existing,
312
+ ...target.meta ?? {},
313
+ ids: nextIds,
314
+ isFetching: false,
315
+ isFetchingMore: false,
316
+ error: null,
317
+ lastError: null,
318
+ stale: false,
319
+ lastFetched: fetchedAt
320
+ };
321
+ }
322
+ for (const projection of options.projections ?? []) {
323
+ const existing = s.lists[projection.key] ?? defaultListState();
324
+ const nextIds = [...existing.ids];
325
+ const { filter, sort, search } = projection.view;
326
+ for (const id of new Set(ids)) {
327
+ if (nextIds.includes(id)) continue;
328
+ const entity = readDraftEntity(id);
329
+ if (!entity) continue;
330
+ const matches = (!filter || matchesFilter(entity, filter)) && (!search?.query || matchesSearch(entity, search.query, search.fields));
331
+ if (!matches) continue;
332
+ if (sort && sort.length > 0) {
333
+ const index = findInsertionIndex(entity, nextIds, readDraftEntity, sort);
334
+ nextIds.splice(index, 0, id);
335
+ } else {
336
+ nextIds.unshift(id);
337
+ }
338
+ }
339
+ s.lists[projection.key] = projection.completeFetch ? {
340
+ ...existing,
341
+ ids: nextIds,
342
+ isFetching: false,
343
+ isFetchingMore: false,
344
+ error: null,
345
+ lastError: null,
346
+ stale: false,
347
+ lastFetched: fetchedAt
348
+ } : { ...existing, ids: nextIds };
349
+ }
350
+ for (const key of options.finishListFetches ?? []) {
351
+ if (!s.lists[key]) s.lists[key] = defaultListState();
352
+ s.lists[key].isFetching = false;
353
+ }
354
+ }),
355
+ replaceEntity: (type, id, data) => set((s) => {
356
+ if (!s.entities[type]) s.entities[type] = {};
357
+ s.entities[type][id] = data;
358
+ const key = ek(type, id);
359
+ if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
360
+ }),
361
+ removeEntity: (type, id) => set((s) => {
362
+ delete s.entities[type]?.[id];
363
+ delete s.patches[type]?.[id];
364
+ delete s.entityStates[ek(type, id)];
365
+ delete s.syncMetadata[ek(type, id)];
366
+ }),
367
+ patchEntity: (type, id, patch) => set((s) => {
368
+ if (!s.patches[type]) s.patches[type] = {};
369
+ s.patches[type][id] = { ...s.patches[type][id] ?? {}, ...patch };
370
+ }),
371
+ unpatchEntity: (type, id, keys) => set((s) => {
372
+ const p = s.patches[type]?.[id];
373
+ if (!p) return;
374
+ for (const k of keys) delete p[k];
375
+ }),
376
+ clearPatch: (type, id) => set((s) => {
377
+ delete s.patches[type]?.[id];
378
+ }),
379
+ setEntityFetching: (type, id, fetching) => set((s) => {
380
+ const k = ek(type, id);
381
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
382
+ s.entityStates[k].isFetching = fetching;
383
+ }),
384
+ setEntityError: (type, id, error) => set((s) => {
385
+ const k = ek(type, id);
386
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
387
+ s.entityStates[k].error = error;
388
+ s.entityStates[k].isFetching = false;
389
+ }),
390
+ setEntityFetched: (type, id) => set((s) => {
391
+ const k = ek(type, id);
392
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
393
+ s.entityStates[k].lastFetched = Date.now();
394
+ s.entityStates[k].isFetching = false;
395
+ s.entityStates[k].error = null;
396
+ s.entityStates[k].stale = false;
397
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), synced: true, origin: "server", updatedAt: Date.now() };
398
+ }),
399
+ setEntityStale: (type, id, stale) => set((s) => {
400
+ const k = ek(type, id);
401
+ if (!s.entityStates[k]) s.entityStates[k] = defaultEntityState();
402
+ s.entityStates[k].stale = stale;
403
+ }),
404
+ setEntitySyncMetadata: (type, id, metadata) => set((s) => {
405
+ const k = ek(type, id);
406
+ s.syncMetadata[k] = { ...s.syncMetadata[k] ?? defaultSyncMetadata(), ...metadata };
407
+ }),
408
+ clearEntitySyncMetadata: (type, id) => set((s) => {
409
+ delete s.syncMetadata[ek(type, id)];
410
+ }),
411
+ setListResult: (key, ids, meta) => set((s) => {
412
+ const ex = s.lists[key] ?? defaultListState();
413
+ s.lists[key] = { ...ex, ...meta, ids, isFetching: false, isFetchingMore: false, error: null, lastError: null, stale: false, lastFetched: Date.now() };
414
+ }),
415
+ appendListResult: (key, ids, meta) => set((s) => {
416
+ const ex = s.lists[key] ?? defaultListState();
417
+ 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() };
418
+ }),
419
+ prependListResult: (key, ids, meta) => set((s) => {
420
+ const ex = s.lists[key] ?? defaultListState();
421
+ 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() };
422
+ }),
423
+ removeIdFromAllLists: (_type, id) => set((s) => {
424
+ for (const key of Object.keys(s.lists)) {
425
+ const list = s.lists[key];
426
+ const idx = list.ids.indexOf(id);
427
+ if (idx !== -1) {
428
+ list.ids.splice(idx, 1);
429
+ if (list.total !== null) list.total -= 1;
430
+ }
431
+ }
432
+ }),
433
+ insertIdInList: (key, id, position) => set((s) => {
434
+ if (!s.lists[key]) s.lists[key] = defaultListState();
435
+ const ids = s.lists[key].ids;
436
+ const ex = ids.indexOf(id);
437
+ if (ex !== -1) ids.splice(ex, 1);
438
+ if (position === "start") ids.unshift(id);
439
+ else if (position === "end") ids.push(id);
440
+ else ids.splice(position, 0, id);
441
+ }),
442
+ setListFetching: (key, fetching) => set((s) => {
443
+ if (!s.lists[key]) s.lists[key] = defaultListState();
444
+ s.lists[key].isFetching = fetching;
445
+ }),
446
+ setListFetchingMore: (key, fetchingMore) => set((s) => {
447
+ if (!s.lists[key]) s.lists[key] = defaultListState();
448
+ s.lists[key].isFetchingMore = fetchingMore;
449
+ }),
450
+ setListError: (key, error, typed) => set((s) => {
451
+ if (!s.lists[key]) s.lists[key] = defaultListState();
452
+ s.lists[key].error = error;
453
+ s.lists[key].lastError = typed ?? (error === null ? null : s.lists[key].lastError);
454
+ s.lists[key].isFetching = false;
455
+ s.lists[key].isFetchingMore = false;
456
+ s.lists[key].lastFetched = Date.now();
457
+ s.lists[key].stale = false;
458
+ }),
459
+ setListStale: (key, stale) => set((s) => {
460
+ if (!s.lists[key]) s.lists[key] = defaultListState();
461
+ s.lists[key].stale = stale;
462
+ }),
463
+ invalidateEntity: (type, id) => set((s) => {
464
+ if (id) {
465
+ const k = ek(type, id);
466
+ if (s.entityStates[k]) s.entityStates[k].stale = true;
467
+ } else {
468
+ for (const k of Object.keys(s.entityStates)) if (k.startsWith(`${type}:`)) s.entityStates[k].stale = true;
469
+ }
470
+ }),
471
+ invalidateLists: (matcher) => set((s) => {
472
+ const pred = typeof matcher === "string" ? (k) => k.startsWith(matcher) : matcher;
473
+ for (const key of Object.keys(s.lists)) if (pred(key)) s.lists[key].stale = true;
474
+ }),
475
+ invalidateType: (type) => {
476
+ get().invalidateEntity(type);
477
+ get().invalidateLists(type);
478
+ },
479
+ readEntity: (type, id) => {
480
+ const s = get();
481
+ const base = s.entities[type]?.[id];
482
+ if (!base) return null;
483
+ const patch = s.patches[type]?.[id];
484
+ return patch ? { ...base, ...patch } : base;
485
+ },
486
+ readEntitySnapshot: (type, id) => {
487
+ const s = get();
488
+ const base = s.entities[type]?.[id];
489
+ if (!base) {
490
+ snapshotCache.delete(ek(type, id));
491
+ return null;
492
+ }
493
+ const patch = s.patches[type]?.[id];
494
+ const metadata = s.syncMetadata[ek(type, id)] ?? EMPTY_SYNC_METADATA;
495
+ return readCachedEntitySnapshot(
496
+ type,
497
+ id,
498
+ base,
499
+ patch,
500
+ metadata
501
+ );
502
+ }
503
+ }))
504
+ )
505
+ );
506
+ }
507
+ var graphStore = createGraphStore();
508
+ var useGraphStore = graphStore;
509
+
510
+ // src/legacy-devtools.ts
511
+ var listeners = /* @__PURE__ */ new Set();
512
+ function publish(event) {
513
+ if (listeners.size === 0) return;
514
+ for (const listener of [...listeners]) {
515
+ try {
516
+ listener(event);
517
+ } catch {
518
+ console.warn("[engine] devtools subscriber threw");
519
+ }
520
+ }
521
+ }
522
+ function subscribeLegacyDevtoolsEvent(listener) {
523
+ listeners.add(listener);
524
+ return () => listeners.delete(listener);
525
+ }
526
+ function emitLegacyDevtoolsEvent(event) {
527
+ publish(event);
528
+ }
529
+
530
+ // src/engine.ts
531
+ function serializeKey(key) {
532
+ return JSON.stringify(key, (_, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).sort()) : v);
533
+ }
534
+ function sleep(ms) {
535
+ return new Promise((r) => setTimeout(r, ms));
536
+ }
537
+ var inflightByStore = /* @__PURE__ */ new WeakMap();
538
+ function inflightFor(store) {
539
+ let inflight = inflightByStore.get(store);
540
+ if (!inflight) {
541
+ inflight = /* @__PURE__ */ new Map();
542
+ inflightByStore.set(store, inflight);
543
+ }
544
+ return inflight;
545
+ }
546
+ function dedupe(key, fn, store = graphStore) {
547
+ const inflight = inflightFor(store);
548
+ if (inflight.has(key)) return inflight.get(key);
549
+ const p = fn().finally(() => inflight.delete(key));
550
+ inflight.set(key, p);
551
+ return p;
552
+ }
553
+ var globalSubscriberStatsListeners = /* @__PURE__ */ new Set();
554
+ var subscriberStatsListenersByStore = /* @__PURE__ */ new WeakMap();
555
+ function subscriberStatsListenersFor(store) {
556
+ let listeners2 = subscriberStatsListenersByStore.get(store);
557
+ if (!listeners2) {
558
+ listeners2 = /* @__PURE__ */ new Set();
559
+ subscriberStatsListenersByStore.set(store, listeners2);
560
+ }
561
+ return listeners2;
562
+ }
563
+ function emitSubscriberStatsChange(store) {
564
+ for (const cb of /* @__PURE__ */ new Set([
565
+ ...globalSubscriberStatsListeners,
566
+ ...subscriberStatsListenersFor(store)
567
+ ])) cb();
568
+ }
569
+ var subscribersByStore = /* @__PURE__ */ new WeakMap();
570
+ function subscribersFor(store) {
571
+ let subscribers = subscribersByStore.get(store);
572
+ if (!subscribers) {
573
+ subscribers = /* @__PURE__ */ new Map();
574
+ subscribersByStore.set(store, subscribers);
575
+ }
576
+ return subscribers;
577
+ }
578
+ function registerSubscriber(key, store = graphStore) {
579
+ const subscribers = subscribersFor(store);
580
+ const token = Symbol(key);
581
+ if (!subscribers.has(key)) subscribers.set(key, /* @__PURE__ */ new Set());
582
+ subscribers.get(key).add(token);
583
+ emitSubscriberStatsChange(store);
584
+ return token;
585
+ }
586
+ function unregisterSubscriber(key, token, store = graphStore) {
587
+ const subscribers = subscribersFor(store);
588
+ const set = subscribers.get(key);
589
+ if (!set) return;
590
+ set.delete(token);
591
+ if (set.size === 0) subscribers.delete(key);
592
+ emitSubscriberStatsChange(store);
593
+ }
594
+ function hasSubscribers(key, store = graphStore) {
595
+ return (subscribersFor(store).get(key)?.size ?? 0) > 0;
596
+ }
597
+ var DEFAULT_OPTIONS = {
598
+ defaultStaleTime: 3e4,
599
+ defaultGcTime: 3e5,
600
+ gcInterval: 6e4,
601
+ maxRetries: 3,
602
+ retryBaseDelay: 1e3,
603
+ revalidateOnFocus: true,
604
+ revalidateOnReconnect: true
605
+ };
606
+ var engineOptions = { ...DEFAULT_OPTIONS };
607
+ function subscribeSubscriberStats(onChange, store) {
608
+ const listeners2 = store ? subscriberStatsListenersFor(store) : globalSubscriberStatsListeners;
609
+ listeners2.add(onChange);
610
+ return () => listeners2.delete(onChange);
611
+ }
612
+ function getActiveSubscriberCount(store = graphStore) {
613
+ let n = 0;
614
+ for (const set of subscribersFor(store).values()) n += set.size;
615
+ return n;
616
+ }
617
+ function subscribeDevtoolsEvent(cb) {
618
+ return subscribeLegacyDevtoolsEvent(cb);
619
+ }
620
+ var gcIntervalsByStore = /* @__PURE__ */ new Map();
621
+ function runGarbageCollection(storeApi = graphStore) {
622
+ const store = storeApi.getState();
623
+ const { defaultGcTime: gcTime } = getEngineOptions();
624
+ const now = Date.now();
625
+ const toRemove = [];
626
+ for (const type of Object.keys(store.entities)) {
627
+ const bucket = store.entities[type];
628
+ if (!bucket) continue;
629
+ for (const id of Object.keys(bucket)) {
630
+ const key = `${type}:${id}`;
631
+ if (hasSubscribers(key, storeApi)) continue;
632
+ const patch = store.patches[type]?.[id];
633
+ if (patch !== void 0 && Object.keys(patch).length > 0) continue;
634
+ const entityState = store.entityStates[key];
635
+ if (entityState?.isFetching) continue;
636
+ const lastFetched = entityState?.lastFetched;
637
+ if (lastFetched == null) continue;
638
+ if (now - lastFetched <= gcTime) continue;
639
+ toRemove.push({ type, id });
640
+ }
641
+ }
642
+ for (const { type, id } of toRemove) {
643
+ store.removeEntity(type, id);
644
+ store.removeIdFromAllLists(type, id);
645
+ }
646
+ }
647
+ function stopGarbageCollector(storeApi = graphStore) {
648
+ const intervalId = gcIntervalsByStore.get(storeApi);
649
+ if (intervalId != null && typeof clearInterval !== "undefined") {
650
+ clearInterval(intervalId);
651
+ gcIntervalsByStore.delete(storeApi);
652
+ }
653
+ }
654
+ function startGarbageCollector(storeApi = graphStore) {
655
+ stopGarbageCollector(storeApi);
656
+ if (typeof window === "undefined" || typeof setInterval === "undefined") return () => {
657
+ };
658
+ const intervalId = setInterval(
659
+ () => runGarbageCollection(storeApi),
660
+ getEngineOptions().gcInterval
661
+ );
662
+ gcIntervalsByStore.set(storeApi, intervalId);
663
+ return () => stopGarbageCollector(storeApi);
664
+ }
665
+ function restartGarbageCollector(storeApi = graphStore) {
666
+ startGarbageCollector(storeApi);
667
+ }
668
+ function configureEngine(opts) {
669
+ engineOptions = { ...DEFAULT_OPTIONS, ...opts };
670
+ const activeStores = [...gcIntervalsByStore.keys()];
671
+ if (activeStores.length === 0) restartGarbageCollector();
672
+ else for (const storeApi of activeStores) restartGarbageCollector(storeApi);
673
+ }
674
+ function getEngineOptions() {
675
+ return engineOptions;
676
+ }
677
+ async function fetchEntity(opts, engineOpts, storeApi = graphStore) {
678
+ const { type, id, fetch, normalize, sideEffects, idField = "id" } = opts;
679
+ if (!id) return;
680
+ storeApi.getState().setEntityFetching(type, id, true);
681
+ const attempt = async (retries) => {
682
+ try {
683
+ const raw = await fetch(id);
684
+ const normalized = normalize(raw);
685
+ const resolvedId = normalized[idField] ?? id;
686
+ storeApi.getState().upsertEntity(type, resolvedId, normalized);
687
+ storeApi.getState().setEntityFetched(type, resolvedId);
688
+ if (sideEffects) sideEffects(raw, storeApi);
689
+ opts.onSuccess?.(normalized);
690
+ } catch (err) {
691
+ const error = err instanceof Error ? err : new Error(String(err));
692
+ if (retries < engineOpts.maxRetries) {
693
+ await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
694
+ return attempt(retries + 1);
695
+ }
696
+ storeApi.getState().setEntityError(type, id, error.message);
697
+ opts.onError?.(error);
698
+ }
699
+ };
700
+ await dedupe(`${type}:${id}`, () => attempt(0), storeApi);
701
+ }
702
+ async function fetchList(opts, params, engineOpts, isLoadMore = false, storeApi = graphStore) {
703
+ const { type, queryKey, fetch, normalize, sideEffects, mode = "replace" } = opts;
704
+ const key = serializeKey(queryKey);
705
+ const store = storeApi.getState();
706
+ if (!fetch || !normalize) return;
707
+ if (isLoadMore) store.setListFetchingMore(key, true);
708
+ else store.setListFetching(key, true);
709
+ const attempt = async (retries) => {
710
+ try {
711
+ const response = await fetch(params);
712
+ const normalized = response.items.map(normalize);
713
+ const meta = { total: response.total ?? null, nextCursor: response.nextCursor ?? null, prevCursor: response.prevCursor ?? null, hasNextPage: response.hasNextPage ?? !!response.nextCursor, hasPrevPage: response.hasPrevPage ?? !!response.prevCursor, currentPage: response.page ?? null, pageSize: response.pageSize ?? null };
714
+ storeApi.getState().ingestFetchedList(
715
+ type,
716
+ normalized.map(({ id, data }) => ({ id, data })),
717
+ { lists: [{ key, mode: mode === "append" && isLoadMore ? "append" : "replace", meta }] }
718
+ );
719
+ if (sideEffects) sideEffects(response.items, storeApi);
720
+ opts.onSuccess?.(response);
721
+ } catch (err) {
722
+ const error = err instanceof Error ? err : new Error(String(err));
723
+ if (retries < engineOpts.maxRetries) {
724
+ await sleep(engineOpts.retryBaseDelay * Math.pow(2, retries));
725
+ return attempt(retries + 1);
726
+ }
727
+ storeApi.getState().setListError(key, error.message);
728
+ opts.onError?.(error);
729
+ }
730
+ };
731
+ await dedupe(isLoadMore ? `${key}:more` : key, () => attempt(0), storeApi);
732
+ }
733
+ var globalListenerAttachments = /* @__PURE__ */ new WeakMap();
734
+ function releaseGlobalListenerAttachment(storeApi, attachment) {
735
+ let released = false;
736
+ return () => {
737
+ if (released) return;
738
+ released = true;
739
+ const current = globalListenerAttachments.get(storeApi);
740
+ if (current !== attachment) return;
741
+ current.references -= 1;
742
+ if (current.references > 0) return;
743
+ current.dispose();
744
+ globalListenerAttachments.delete(storeApi);
745
+ };
746
+ }
747
+ function attachGlobalListeners(storeApi = graphStore) {
748
+ if (typeof window === "undefined") return () => {
749
+ };
750
+ const existing = globalListenerAttachments.get(storeApi);
751
+ if (existing) {
752
+ existing.references += 1;
753
+ return releaseGlobalListenerAttachment(storeApi, existing);
754
+ }
755
+ const stopGc = startGarbageCollector(storeApi);
756
+ const revalidateAll = () => {
757
+ const state = storeApi.getState();
758
+ for (const key of subscribersFor(storeApi).keys()) {
759
+ if (!hasSubscribers(key, storeApi)) continue;
760
+ const colonIdx = key.indexOf(":");
761
+ if (colonIdx === -1) continue;
762
+ const type = key.slice(0, colonIdx);
763
+ const id = key.slice(colonIdx + 1);
764
+ state.setEntityStale(type, id, true);
765
+ }
766
+ };
767
+ const revalidateWhenVisible = () => {
768
+ if (document.visibilityState === "visible") revalidateAll();
769
+ };
770
+ const revalidateOnFocus = engineOptions.revalidateOnFocus;
771
+ const revalidateOnReconnect = engineOptions.revalidateOnReconnect;
772
+ if (revalidateOnFocus) {
773
+ window.addEventListener("visibilitychange", revalidateWhenVisible);
774
+ window.addEventListener("focus", revalidateAll);
775
+ }
776
+ if (revalidateOnReconnect) window.addEventListener("online", revalidateAll);
777
+ const attachment = {
778
+ references: 1,
779
+ dispose: () => {
780
+ if (revalidateOnFocus) {
781
+ window.removeEventListener("visibilitychange", revalidateWhenVisible);
782
+ window.removeEventListener("focus", revalidateAll);
783
+ }
784
+ if (revalidateOnReconnect) {
785
+ window.removeEventListener("online", revalidateAll);
786
+ }
787
+ stopGc();
788
+ }
789
+ };
790
+ globalListenerAttachments.set(storeApi, attachment);
791
+ return releaseGlobalListenerAttachment(storeApi, attachment);
792
+ }
793
+
794
+ // src/crud/relations.ts
795
+ var schemaRegistry = /* @__PURE__ */ new Map();
796
+ function registerSchema(schema) {
797
+ schemaRegistry.set(schema.type, schema);
798
+ }
799
+ function getSchema(type) {
800
+ return schemaRegistry.get(type) ?? null;
801
+ }
802
+ function getRegisteredSchemas() {
803
+ return [...schemaRegistry.values()];
804
+ }
805
+ function cascadeInvalidation(ctx, storeApi = graphStore) {
806
+ const schema = schemaRegistry.get(ctx.type);
807
+ if (!schema) return;
808
+ const store = storeApi.getState();
809
+ if (schema.globalListKeys) for (const key of schema.globalListKeys) store.invalidateLists(key);
810
+ if (!schema.relations) return;
811
+ for (const [, relation] of Object.entries(schema.relations)) {
812
+ switch (relation.cardinality) {
813
+ case "belongsTo": {
814
+ const prevFk = ctx.previous?.[relation.foreignKey];
815
+ const nextFk = ctx.next?.[relation.foreignKey];
816
+ if (prevFk && prevFk !== nextFk && relation.invalidateTargetLists) for (const kp of relation.invalidateTargetLists) store.invalidateLists((k) => k.startsWith(kp) && k.includes(prevFk));
817
+ if (nextFk && relation.invalidateTargetLists) for (const kp of relation.invalidateTargetLists) store.invalidateLists((k) => k.startsWith(kp) && k.includes(nextFk));
818
+ if (prevFk) store.invalidateEntity(relation.targetType, prevFk);
819
+ if (nextFk) store.invalidateEntity(relation.targetType, nextFk);
820
+ break;
821
+ }
822
+ case "hasMany":
823
+ store.invalidateLists(serializeKey(relation.listKeyPrefix(ctx.id)));
824
+ break;
825
+ case "manyToMany": {
826
+ const prevIds = ctx.previous?.[relation.localArrayField ?? ""];
827
+ const nextIds = ctx.next?.[relation.localArrayField ?? ""];
828
+ for (const relatedId of /* @__PURE__ */ new Set([...prevIds ?? [], ...nextIds ?? []])) store.invalidateLists(serializeKey(relation.listKeyPrefix(relatedId)));
829
+ break;
830
+ }
831
+ }
832
+ }
833
+ for (const [, otherSchema] of schemaRegistry) {
834
+ if (!otherSchema.relations) continue;
835
+ for (const [, rel] of Object.entries(otherSchema.relations)) {
836
+ if (rel.targetType !== ctx.type) continue;
837
+ if (rel.cardinality === "hasMany") store.invalidateLists(serializeKey(rel.listKeyPrefix(ctx.id)));
838
+ }
839
+ }
840
+ }
841
+ function readRelations(type, entity, storeApi = graphStore) {
842
+ const schema = schemaRegistry.get(type);
843
+ if (!schema?.relations) return {};
844
+ const store = storeApi.getState();
845
+ const result = {};
846
+ for (const [name, relation] of Object.entries(schema.relations)) {
847
+ switch (relation.cardinality) {
848
+ case "belongsTo": {
849
+ const fkValue = entity[relation.foreignKey];
850
+ result[name] = fkValue ? store.readEntity(relation.targetType, fkValue) : null;
851
+ break;
852
+ }
853
+ case "hasMany": {
854
+ const listKey = serializeKey(relation.listKeyPrefix(entity.id));
855
+ const listState = store.lists[listKey];
856
+ result[name] = listState ? listState.ids.map((id) => store.readEntity(relation.targetType, id)).filter(Boolean) : [];
857
+ break;
858
+ }
859
+ case "manyToMany": {
860
+ const ids = entity[relation.localArrayField ?? ""];
861
+ result[name] = ids ? ids.map((id) => store.readEntity(relation.targetType, id)).filter(Boolean) : [];
862
+ break;
863
+ }
864
+ }
865
+ }
866
+ return result;
867
+ }
868
+
869
+ // src/devtools-compatibility-bridge.ts
870
+ var BRIDGE_KEY = /* @__PURE__ */ Symbol.for("prometheus.entity-graph.devtools.compatibility.v1");
871
+ var bridgeHost = globalThis;
872
+ var existingBridge = bridgeHost[BRIDGE_KEY];
873
+ var bridgeState = existingBridge?.version === 1 ? existingBridge : { version: 1, entries: /* @__PURE__ */ new WeakMap(), factory: null };
874
+ bridgeHost[BRIDGE_KEY] = bridgeState;
875
+ function getOrCreateEntry(store) {
876
+ let entry = bridgeState.entries.get(store);
877
+ if (!entry) {
878
+ entry = {
879
+ delegate: null,
880
+ listeners: /* @__PURE__ */ new Set(),
881
+ unsubscribeDelegate: null,
882
+ compatibilityDetach: null
883
+ };
884
+ bridgeState.entries.set(store, entry);
885
+ }
886
+ return entry;
887
+ }
888
+ function ensureDelegate(store, entry) {
889
+ const factory = bridgeState.factory;
890
+ if (entry.delegate || entry.compatibilityDetach || !factory) return;
891
+ entry.compatibilityDetach = factory(store);
892
+ }
893
+ function registerGraphDevtoolsCompatibilityFactory(factory) {
894
+ bridgeState.factory = factory;
895
+ }
896
+ function notify(entry) {
897
+ for (const listener of [...entry.listeners]) {
898
+ try {
899
+ listener();
900
+ } catch {
901
+ }
902
+ }
903
+ }
904
+ function registerGraphDevtoolsCompatibilityDelegate(store, delegate) {
905
+ const entry = getOrCreateEntry(store);
906
+ entry.unsubscribeDelegate?.();
907
+ entry.delegate = delegate;
908
+ entry.unsubscribeDelegate = delegate.subscribe(() => notify(entry));
909
+ notify(entry);
910
+ let unregistered = false;
911
+ return () => {
912
+ if (unregistered || entry.delegate !== delegate) return;
913
+ unregistered = true;
914
+ entry.unsubscribeDelegate?.();
915
+ entry.unsubscribeDelegate = null;
916
+ entry.delegate = null;
917
+ notify(entry);
918
+ if (entry.listeners.size === 0) bridgeState.entries.delete(store);
919
+ };
920
+ }
921
+ function getGraphDevtoolsCompatibilityDelegate(store) {
922
+ const entry = getOrCreateEntry(store);
923
+ ensureDelegate(store, entry);
924
+ return entry.delegate;
925
+ }
926
+ function subscribeGraphDevtoolsCompatibility(store, listener) {
927
+ const entry = getOrCreateEntry(store);
928
+ entry.listeners.add(listener);
929
+ ensureDelegate(store, entry);
930
+ return () => {
931
+ entry.listeners.delete(listener);
932
+ if (entry.listeners.size === 0 && entry.delegate === null) bridgeState.entries.delete(store);
933
+ };
934
+ }
935
+
936
+ export { EMPTY_ENTITY_STATE, EMPTY_IDS, EMPTY_LIST_STATE, EMPTY_SYNC_METADATA, __require, applyView, attachGlobalListeners, cascadeInvalidation, checkCompleteness, compareEntities, configureEngine, createGraphStore, dedupe, emitLegacyDevtoolsEvent, fetchEntity, fetchList, findInsertionIndex, getActiveSubscriberCount, getEngineOptions, getGraphDevtoolsCompatibilityDelegate, getMergeStrategy, getRegisteredSchemas, getSchema, graphStore, hasMergeStrategy, lwwStrategy, matchesFilter, matchesSearch, readRelations, registerGraphDevtoolsCompatibilityDelegate, registerGraphDevtoolsCompatibilityFactory, registerMergeStrategy, registerSchema, registerSubscriber, serializeKey, setDefaultMergeStrategy, startGarbageCollector, stopGarbageCollector, subscribeDevtoolsEvent, subscribeGraphDevtoolsCompatibility, subscribeSubscriberStats, unregisterSubscriber, useGraphStore };
937
+ //# sourceMappingURL=chunk-BE3BMRKF.mjs.map
938
+ //# sourceMappingURL=chunk-BE3BMRKF.mjs.map