@prometheus-ags/entity-graph-core 3.0.2 → 3.0.3

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @prometheus-ags/entity-graph-core
2
2
 
3
+ ## 3.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Ingest fetched entity lists, lifecycle metadata, and list projections in one
8
+ atomic graph publication. Core and React list paths no longer publish once per
9
+ returned row.
10
+
3
11
  ## 3.0.0
4
12
 
5
13
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -428,6 +428,135 @@ function hasMergeStrategy(type) {
428
428
  return byType.has(type) || globalDefault !== null;
429
429
  }
430
430
 
431
+ // src/view/evaluator.ts
432
+ function matchesFilter(entity, filter) {
433
+ if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
434
+ return matchesGroup(entity, filter);
435
+ }
436
+ function matchesGroup(entity, group) {
437
+ const { logic, clauses } = group;
438
+ if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
439
+ return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
440
+ }
441
+ function matchesClause(entity, clause) {
442
+ const { field, op, value, predicate } = clause;
443
+ const fv = getNestedValue(entity, field);
444
+ switch (op) {
445
+ case "eq":
446
+ return fv === value;
447
+ case "neq":
448
+ return fv !== value;
449
+ case "gt":
450
+ return fv > value;
451
+ case "gte":
452
+ return fv >= value;
453
+ case "lt":
454
+ return fv < value;
455
+ case "lte":
456
+ return fv <= value;
457
+ case "in":
458
+ return Array.isArray(value) && value.includes(fv);
459
+ case "nin":
460
+ return Array.isArray(value) && !value.includes(fv);
461
+ case "isNull":
462
+ return fv == null;
463
+ case "isNotNull":
464
+ return fv != null;
465
+ case "contains":
466
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
467
+ case "startsWith":
468
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
469
+ case "endsWith":
470
+ return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
471
+ case "between": {
472
+ const [lo, hi] = value;
473
+ return fv >= lo && fv <= hi;
474
+ }
475
+ case "arrayContains":
476
+ return Array.isArray(fv) && fv.includes(value);
477
+ case "arrayOverlaps":
478
+ return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
479
+ case "matches":
480
+ return typeof fv === "string" && new RegExp(value).test(fv);
481
+ case "custom":
482
+ return predicate ? predicate(fv, entity) : true;
483
+ default:
484
+ return true;
485
+ }
486
+ }
487
+ function matchesSearch(entity, query, fields) {
488
+ if (!query.trim()) return true;
489
+ const lq = query.toLowerCase();
490
+ return fields.some((field) => {
491
+ const v = getNestedValue(entity, field);
492
+ return typeof v === "string" && v.toLowerCase().includes(lq);
493
+ });
494
+ }
495
+ function compareEntities(a, b, sort) {
496
+ for (const clause of sort) {
497
+ const r = compareByClause(a, b, clause);
498
+ if (r !== 0) return r;
499
+ }
500
+ return 0;
501
+ }
502
+ function compareByClause(a, b, clause) {
503
+ const { field, direction, nulls = "last", comparator } = clause;
504
+ const av = getNestedValue(a, field);
505
+ const bv = getNestedValue(b, field);
506
+ const aNull = av == null;
507
+ const bNull = bv == null;
508
+ if (aNull && bNull) return 0;
509
+ if (aNull) return nulls === "first" ? -1 : 1;
510
+ if (bNull) return nulls === "first" ? 1 : -1;
511
+ let cmp;
512
+ if (comparator) cmp = comparator(av, bv);
513
+ else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
514
+ else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
515
+ else cmp = String(av).localeCompare(String(bv));
516
+ return direction === "desc" ? -cmp : cmp;
517
+ }
518
+ function findInsertionIndex(entity, sortedIds, getEntity, sort) {
519
+ let lo = 0;
520
+ let hi = sortedIds.length;
521
+ while (lo < hi) {
522
+ const mid = lo + hi >>> 1;
523
+ const me = getEntity(sortedIds[mid]);
524
+ if (!me) {
525
+ lo = mid + 1;
526
+ continue;
527
+ }
528
+ if (compareEntities(entity, me, sort) <= 0) hi = mid;
529
+ else lo = mid + 1;
530
+ }
531
+ return lo;
532
+ }
533
+ function applyView(ids, getEntity, filter, sort, search) {
534
+ let entries = [];
535
+ for (const id of ids) {
536
+ const entity = getEntity(id);
537
+ if (!entity) continue;
538
+ entries.push({ id, entity });
539
+ }
540
+ if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
541
+ if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
542
+ if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
543
+ return entries.map((e) => e.id);
544
+ }
545
+ function checkCompleteness(loadedCount, total, hasNextPage) {
546
+ if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
547
+ if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
548
+ return { isComplete: true, reason: "no-more-pages" };
549
+ }
550
+ function getNestedValue(obj, path) {
551
+ const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
552
+ let current = obj;
553
+ for (const part of parts) {
554
+ if (current == null || typeof current !== "object") return void 0;
555
+ current = current[part];
556
+ }
557
+ return current;
558
+ }
559
+
431
560
  // src/graph.ts
432
561
  var EMPTY_IDS = [];
433
562
  var EMPTY_ENTITY_STATE = {
@@ -512,6 +641,117 @@ function createGraphStore() {
512
641
  if (!s.syncMetadata[key]) s.syncMetadata[key] = defaultSyncMetadata();
513
642
  }
514
643
  }),
644
+ ingestFetchedList: (type, entries, options = {}) => set((s) => {
645
+ const fetchedAt = Date.now();
646
+ const ids = entries.map(({ id }) => id);
647
+ const batches = [
648
+ { type, entries },
649
+ ...options.sideBatches ?? []
650
+ ];
651
+ const origins = /* @__PURE__ */ new Map();
652
+ for (const batch of batches) {
653
+ if (!s.entities[batch.type]) s.entities[batch.type] = {};
654
+ for (const { id } of batch.entries) {
655
+ const key = ek(batch.type, id);
656
+ if (!origins.has(key)) {
657
+ origins.set(key, s.syncMetadata[key]?.origin ?? "server");
658
+ }
659
+ }
660
+ }
661
+ for (const batch of batches) {
662
+ const strategy = getMergeStrategy(batch.type);
663
+ for (const { id, data } of batch.entries) {
664
+ const key = ek(batch.type, id);
665
+ const ctx = {
666
+ type: batch.type,
667
+ id,
668
+ origin: origins.get(key) ?? "server",
669
+ updatedAt: fetchedAt
670
+ };
671
+ s.entities[batch.type][id] = strategy(s.entities[batch.type][id], data, ctx);
672
+ }
673
+ }
674
+ const completed = /* @__PURE__ */ new Set();
675
+ for (const batch of batches) {
676
+ for (const { id } of batch.entries) {
677
+ const key = ek(batch.type, id);
678
+ if (completed.has(key)) continue;
679
+ completed.add(key);
680
+ if (!s.entityStates[key]) s.entityStates[key] = defaultEntityState();
681
+ s.entityStates[key].lastFetched = fetchedAt;
682
+ s.entityStates[key].isFetching = false;
683
+ s.entityStates[key].error = null;
684
+ s.entityStates[key].stale = false;
685
+ s.syncMetadata[key] = {
686
+ ...s.syncMetadata[key] ?? defaultSyncMetadata(),
687
+ synced: true,
688
+ origin: "server",
689
+ updatedAt: fetchedAt
690
+ };
691
+ }
692
+ }
693
+ const readDraftEntity = (id) => {
694
+ const base = s.entities[type]?.[id];
695
+ if (!base) return null;
696
+ const patch = s.patches[type]?.[id];
697
+ const metadata = s.syncMetadata[ek(type, id)] ?? defaultSyncMetadata();
698
+ return {
699
+ ...base,
700
+ ...patch ?? {},
701
+ $synced: metadata.synced,
702
+ $origin: metadata.origin,
703
+ $updatedAt: metadata.updatedAt
704
+ };
705
+ };
706
+ for (const target of options.lists ?? []) {
707
+ const existing = s.lists[target.key] ?? defaultListState();
708
+ const targetIds = target.ids ?? ids;
709
+ const nextIds = target.mode === "append" ? Array.from(/* @__PURE__ */ new Set([...existing.ids, ...targetIds])) : targetIds;
710
+ s.lists[target.key] = {
711
+ ...existing,
712
+ ...target.meta ?? {},
713
+ ids: nextIds,
714
+ isFetching: false,
715
+ isFetchingMore: false,
716
+ error: null,
717
+ lastError: null,
718
+ stale: false,
719
+ lastFetched: fetchedAt
720
+ };
721
+ }
722
+ for (const projection of options.projections ?? []) {
723
+ const existing = s.lists[projection.key] ?? defaultListState();
724
+ const nextIds = [...existing.ids];
725
+ const { filter, sort, search } = projection.view;
726
+ for (const id of new Set(ids)) {
727
+ if (nextIds.includes(id)) continue;
728
+ const entity = readDraftEntity(id);
729
+ if (!entity) continue;
730
+ const matches = (!filter || matchesFilter(entity, filter)) && (!search?.query || matchesSearch(entity, search.query, search.fields));
731
+ if (!matches) continue;
732
+ if (sort && sort.length > 0) {
733
+ const index = findInsertionIndex(entity, nextIds, readDraftEntity, sort);
734
+ nextIds.splice(index, 0, id);
735
+ } else {
736
+ nextIds.unshift(id);
737
+ }
738
+ }
739
+ s.lists[projection.key] = projection.completeFetch ? {
740
+ ...existing,
741
+ ids: nextIds,
742
+ isFetching: false,
743
+ isFetchingMore: false,
744
+ error: null,
745
+ lastError: null,
746
+ stale: false,
747
+ lastFetched: fetchedAt
748
+ } : { ...existing, ids: nextIds };
749
+ }
750
+ for (const key of options.finishListFetches ?? []) {
751
+ if (!s.lists[key]) s.lists[key] = defaultListState();
752
+ s.lists[key].isFetching = false;
753
+ }
754
+ }),
515
755
  replaceEntity: (type, id, data) => set((s) => {
516
756
  if (!s.entities[type]) s.entities[type] = {};
517
757
  s.entities[type][id] = data;
@@ -1006,12 +1246,12 @@ async function fetchList(opts, params, engineOpts, isLoadMore = false, storeApi
1006
1246
  try {
1007
1247
  const response = await fetch(params);
1008
1248
  const normalized = response.items.map(normalize);
1009
- storeApi.getState().upsertEntities(type, normalized.map(({ id, data }) => ({ id, data })));
1010
- for (const { id } of normalized) storeApi.getState().setEntityFetched(type, id);
1011
- const ids = normalized.map(({ id }) => id);
1012
1249
  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 };
1013
- if (mode === "append" && isLoadMore) storeApi.getState().appendListResult(key, ids, meta);
1014
- else storeApi.getState().setListResult(key, ids, meta);
1250
+ storeApi.getState().ingestFetchedList(
1251
+ type,
1252
+ normalized.map(({ id, data }) => ({ id, data })),
1253
+ { lists: [{ key, mode: mode === "append" && isLoadMore ? "append" : "replace", meta }] }
1254
+ );
1015
1255
  if (sideEffects) sideEffects(response.items, storeApi);
1016
1256
  opts.onSuccess?.(response);
1017
1257
  } catch (err) {
@@ -1872,135 +2112,6 @@ function subscribeTimeTravel(cb) {
1872
2112
  return () => listeners.delete(cb);
1873
2113
  }
1874
2114
 
1875
- // src/view/evaluator.ts
1876
- function matchesFilter(entity, filter) {
1877
- if (Array.isArray(filter)) return filter.every((clause) => matchesClause(entity, clause));
1878
- return matchesGroup(entity, filter);
1879
- }
1880
- function matchesGroup(entity, group) {
1881
- const { logic, clauses } = group;
1882
- if (logic === "and") return clauses.every((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
1883
- return clauses.some((c) => "logic" in c ? matchesGroup(entity, c) : matchesClause(entity, c));
1884
- }
1885
- function matchesClause(entity, clause) {
1886
- const { field, op, value, predicate } = clause;
1887
- const fv = getNestedValue(entity, field);
1888
- switch (op) {
1889
- case "eq":
1890
- return fv === value;
1891
- case "neq":
1892
- return fv !== value;
1893
- case "gt":
1894
- return fv > value;
1895
- case "gte":
1896
- return fv >= value;
1897
- case "lt":
1898
- return fv < value;
1899
- case "lte":
1900
- return fv <= value;
1901
- case "in":
1902
- return Array.isArray(value) && value.includes(fv);
1903
- case "nin":
1904
- return Array.isArray(value) && !value.includes(fv);
1905
- case "isNull":
1906
- return fv == null;
1907
- case "isNotNull":
1908
- return fv != null;
1909
- case "contains":
1910
- return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().includes(value.toLowerCase());
1911
- case "startsWith":
1912
- return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().startsWith(value.toLowerCase());
1913
- case "endsWith":
1914
- return typeof fv === "string" && typeof value === "string" && fv.toLowerCase().endsWith(value.toLowerCase());
1915
- case "between": {
1916
- const [lo, hi] = value;
1917
- return fv >= lo && fv <= hi;
1918
- }
1919
- case "arrayContains":
1920
- return Array.isArray(fv) && fv.includes(value);
1921
- case "arrayOverlaps":
1922
- return Array.isArray(fv) && Array.isArray(value) && value.some((v) => fv.includes(v));
1923
- case "matches":
1924
- return typeof fv === "string" && new RegExp(value).test(fv);
1925
- case "custom":
1926
- return predicate ? predicate(fv, entity) : true;
1927
- default:
1928
- return true;
1929
- }
1930
- }
1931
- function matchesSearch(entity, query, fields) {
1932
- if (!query.trim()) return true;
1933
- const lq = query.toLowerCase();
1934
- return fields.some((field) => {
1935
- const v = getNestedValue(entity, field);
1936
- return typeof v === "string" && v.toLowerCase().includes(lq);
1937
- });
1938
- }
1939
- function compareEntities(a, b, sort) {
1940
- for (const clause of sort) {
1941
- const r = compareByClause(a, b, clause);
1942
- if (r !== 0) return r;
1943
- }
1944
- return 0;
1945
- }
1946
- function compareByClause(a, b, clause) {
1947
- const { field, direction, nulls = "last", comparator } = clause;
1948
- const av = getNestedValue(a, field);
1949
- const bv = getNestedValue(b, field);
1950
- const aNull = av == null;
1951
- const bNull = bv == null;
1952
- if (aNull && bNull) return 0;
1953
- if (aNull) return nulls === "first" ? -1 : 1;
1954
- if (bNull) return nulls === "first" ? 1 : -1;
1955
- let cmp;
1956
- if (comparator) cmp = comparator(av, bv);
1957
- else if (typeof av === "string" && typeof bv === "string") cmp = av.localeCompare(bv, void 0, { sensitivity: "base", numeric: true });
1958
- else if (typeof av === "number" && typeof bv === "number") cmp = av - bv;
1959
- else cmp = String(av).localeCompare(String(bv));
1960
- return direction === "desc" ? -cmp : cmp;
1961
- }
1962
- function findInsertionIndex(entity, sortedIds, getEntity, sort) {
1963
- let lo = 0;
1964
- let hi = sortedIds.length;
1965
- while (lo < hi) {
1966
- const mid = lo + hi >>> 1;
1967
- const me = getEntity(sortedIds[mid]);
1968
- if (!me) {
1969
- lo = mid + 1;
1970
- continue;
1971
- }
1972
- if (compareEntities(entity, me, sort) <= 0) hi = mid;
1973
- else lo = mid + 1;
1974
- }
1975
- return lo;
1976
- }
1977
- function applyView(ids, getEntity, filter, sort, search) {
1978
- let entries = [];
1979
- for (const id of ids) {
1980
- const entity = getEntity(id);
1981
- if (!entity) continue;
1982
- entries.push({ id, entity });
1983
- }
1984
- if (filter && entries.length > 0) entries = entries.filter(({ entity }) => matchesFilter(entity, filter));
1985
- if (search?.query) entries = entries.filter(({ entity }) => matchesSearch(entity, search.query, search.fields));
1986
- if (sort && sort.length > 0) entries.sort((a, b) => compareEntities(a.entity, b.entity, sort));
1987
- return entries.map((e) => e.id);
1988
- }
1989
- function checkCompleteness(loadedCount, total, hasNextPage) {
1990
- if (!hasNextPage && total !== null && loadedCount >= total) return { isComplete: true, reason: "all-loaded" };
1991
- if (hasNextPage) return { isComplete: false, reason: "has-more-pages" };
1992
- return { isComplete: true, reason: "no-more-pages" };
1993
- }
1994
- function getNestedValue(obj, path) {
1995
- const parts = path.replace(/\[(\d+)\]/g, ".$1").split(".");
1996
- let current = obj;
1997
- for (const part of parts) {
1998
- if (current == null || typeof current !== "object") return void 0;
1999
- current = current[part];
2000
- }
2001
- return current;
2002
- }
2003
-
2004
2115
  // src/view/incremental.ts
2005
2116
  var IncrementalView = class {
2006
2117
  opts;