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

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