@absolutejs/sync 0.4.0 → 0.6.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.
@@ -885,6 +885,158 @@ var query = (source) => makeQuery(source, []);
885
885
  var defineGraphCollection = (definition) => ({ ...definition, kind: "graph" });
886
886
  // src/engine/permissions.ts
887
887
  var definePermissions = (permissions) => permissions;
888
+ // src/engine/search.ts
889
+ var SEARCH_SCORE_FIELD = "_score";
890
+ var defineSearchCollection = (definition) => ({
891
+ ...definition,
892
+ kind: "search"
893
+ });
894
+ // src/engine/textIndex.ts
895
+ var defaultTokenize = (text) => text.toLowerCase().match(/[a-z0-9]+/g) ?? [];
896
+ var createTextIndex = (options) => {
897
+ const { key, fields } = options;
898
+ const tokenize = options.tokenize ?? defaultTokenize;
899
+ const stopwords = new Set(options.stopwords ?? []);
900
+ const k1 = options.k1 ?? 1.5;
901
+ const b = options.b ?? 0.75;
902
+ const docs = new Map;
903
+ const postings = new Map;
904
+ let totalLen = 0;
905
+ const termsOf = (row) => {
906
+ const text = fields.map((field) => {
907
+ const value = row[field];
908
+ return value === undefined || value === null ? "" : String(value);
909
+ }).join(" ");
910
+ return tokenize(text).filter((term) => !stopwords.has(term));
911
+ };
912
+ const remove = (rowKey) => {
913
+ const doc = docs.get(rowKey);
914
+ if (doc === undefined) {
915
+ return;
916
+ }
917
+ for (const term of doc.tf.keys()) {
918
+ const set = postings.get(term);
919
+ if (set !== undefined) {
920
+ set.delete(rowKey);
921
+ if (set.size === 0) {
922
+ postings.delete(term);
923
+ }
924
+ }
925
+ }
926
+ totalLen -= doc.len;
927
+ docs.delete(rowKey);
928
+ };
929
+ const add = (row) => {
930
+ const rowKey = key(row);
931
+ remove(rowKey);
932
+ const terms = termsOf(row);
933
+ const tf = new Map;
934
+ for (const term of terms) {
935
+ tf.set(term, (tf.get(term) ?? 0) + 1);
936
+ }
937
+ for (const term of tf.keys()) {
938
+ let set = postings.get(term);
939
+ if (set === undefined) {
940
+ set = new Set;
941
+ postings.set(term, set);
942
+ }
943
+ set.add(rowKey);
944
+ }
945
+ docs.set(rowKey, { row, len: terms.length, tf });
946
+ totalLen += terms.length;
947
+ };
948
+ const search = (query2, limit) => {
949
+ const total = docs.size;
950
+ if (total === 0) {
951
+ return [];
952
+ }
953
+ const avgdl = totalLen / total;
954
+ const queryTerms = new Set(tokenize(query2).filter((term) => !stopwords.has(term)));
955
+ const scores = new Map;
956
+ for (const term of queryTerms) {
957
+ const set = postings.get(term);
958
+ if (set === undefined) {
959
+ continue;
960
+ }
961
+ const df = set.size;
962
+ const idf = Math.log(1 + (total - df + 0.5) / (df + 0.5));
963
+ for (const rowKey of set) {
964
+ const doc = docs.get(rowKey);
965
+ const freq = doc.tf.get(term) ?? 0;
966
+ const norm = freq * (k1 + 1) / (freq + k1 * (1 - b + b * doc.len / avgdl));
967
+ scores.set(rowKey, (scores.get(rowKey) ?? 0) + idf * norm);
968
+ }
969
+ }
970
+ return [...scores.entries()].map(([rowKey, score]) => ({ row: docs.get(rowKey).row, score })).sort((first, second) => second.score - first.score).slice(0, limit);
971
+ };
972
+ return {
973
+ add,
974
+ remove,
975
+ search,
976
+ size: () => docs.size,
977
+ clear: () => {
978
+ docs.clear();
979
+ postings.clear();
980
+ totalLen = 0;
981
+ }
982
+ };
983
+ };
984
+ // src/engine/vectorIndex.ts
985
+ var dot = (first, second) => {
986
+ const length = Math.min(first.length, second.length);
987
+ let sum = 0;
988
+ for (let index = 0;index < length; index += 1) {
989
+ sum += first[index] * second[index];
990
+ }
991
+ return sum;
992
+ };
993
+ var normOf = (vec) => Math.sqrt(dot(vec, vec));
994
+ var euclidean = (first, second) => {
995
+ const length = Math.max(first.length, second.length);
996
+ let sum = 0;
997
+ for (let index = 0;index < length; index += 1) {
998
+ const delta = (first[index] ?? 0) - (second[index] ?? 0);
999
+ sum += delta * delta;
1000
+ }
1001
+ return Math.sqrt(sum);
1002
+ };
1003
+ var createVectorIndex = (options) => {
1004
+ const { key, embedding } = options;
1005
+ const metric = options.metric ?? "cosine";
1006
+ const entries = new Map;
1007
+ const score = (query2, queryNorm, entry) => {
1008
+ if (metric === "dot") {
1009
+ return dot(query2, entry.vec);
1010
+ }
1011
+ if (metric === "euclidean") {
1012
+ return -euclidean(query2, entry.vec);
1013
+ }
1014
+ const denominator = queryNorm * entry.norm;
1015
+ return denominator === 0 ? 0 : dot(query2, entry.vec) / denominator;
1016
+ };
1017
+ return {
1018
+ add: (row) => {
1019
+ const vec = embedding(row);
1020
+ entries.set(key(row), { row, vec, norm: normOf(vec) });
1021
+ },
1022
+ remove: (rowKey) => {
1023
+ entries.delete(rowKey);
1024
+ },
1025
+ search: (query2, limit) => {
1026
+ const queryNorm = normOf(query2);
1027
+ return [...entries.values()].map((entry) => ({
1028
+ row: entry.row,
1029
+ score: score(query2, queryNorm, entry)
1030
+ })).sort((first, second) => second.score - first.score).slice(0, limit);
1031
+ },
1032
+ size: () => entries.size,
1033
+ clear: () => {
1034
+ entries.clear();
1035
+ }
1036
+ };
1037
+ };
1038
+ // src/engine/schedule.ts
1039
+ var defineSchedule = (definition) => definition;
888
1040
  // src/engine/mutation.ts
889
1041
  var defineMutation = (definition) => definition;
890
1042
  // src/engine/syncEngine.ts
@@ -906,11 +1058,21 @@ var shallowEqual4 = (a, b) => {
906
1058
  const bKeys = Object.keys(b);
907
1059
  return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
908
1060
  };
1061
+ var equalsIgnoringScore = (a, b) => {
1062
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
1063
+ return a === b;
1064
+ }
1065
+ const strip = (value) => Object.keys(value).filter((k) => k !== SEARCH_SCORE_FIELD);
1066
+ const aKeys = strip(a);
1067
+ const bKeys = strip(b);
1068
+ return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
1069
+ };
909
1070
  var createSyncEngine = (options = {}) => {
910
1071
  const registry = new Map;
911
1072
  const mutations = new Map;
912
1073
  const writers = new Map;
913
1074
  const readers = new Map;
1075
+ const schedules = new Map;
914
1076
  const permissions = new Map;
915
1077
  for (const [table, rules] of Object.entries(options.permissions ?? {})) {
916
1078
  permissions.set(table, rules);
@@ -921,6 +1083,8 @@ var createSyncEngine = (options = {}) => {
921
1083
  return rules?.[op] ?? rules?.write;
922
1084
  };
923
1085
  const reactiveSubs = new Set;
1086
+ const searchSubs = new Set;
1087
+ const searchIndexes = new Map;
924
1088
  const active = new Map;
925
1089
  const tableIndex = new Map;
926
1090
  const changeLogSize = options.changeLogSize ?? 1024;
@@ -973,6 +1137,9 @@ var createSyncEngine = (options = {}) => {
973
1137
  if (subscription.kind === "reactive") {
974
1138
  return EMPTY_DIFF;
975
1139
  }
1140
+ if (subscription.kind === "search") {
1141
+ return EMPTY_DIFF;
1142
+ }
976
1143
  if (subscription.incremental) {
977
1144
  try {
978
1145
  return subscription.view.apply(change);
@@ -1037,7 +1204,7 @@ var createSyncEngine = (options = {}) => {
1037
1204
  };
1038
1205
  const depKey = (table, key) => `${table} ${key}`;
1039
1206
  const changedKeyFor = (table, change) => readers.get(table)?.key?.(change.row);
1040
- const makeReadHandle = (ctx, readTables, readKeys, rangeDeps) => {
1207
+ const makeReadHandle = (ctx, readTables, readKeys, rangeDeps, applyRules = true) => {
1041
1208
  const readerFor = (table) => {
1042
1209
  const reader = readers.get(table);
1043
1210
  if (reader === undefined) {
@@ -1045,11 +1212,12 @@ var createSyncEngine = (options = {}) => {
1045
1212
  }
1046
1213
  return reader;
1047
1214
  };
1215
+ const ruleFor = (table) => applyRules ? readRuleFor(table) : undefined;
1048
1216
  return {
1049
1217
  all: async (table) => {
1050
1218
  readTables.add(table);
1051
1219
  const rows = [...await readerFor(table).all(ctx)];
1052
- const rule = readRuleFor(table);
1220
+ const rule = ruleFor(table);
1053
1221
  return rule ? rows.filter((row) => rule(ctx, row)) : rows;
1054
1222
  },
1055
1223
  get: async (table, key) => {
@@ -1063,12 +1231,12 @@ var createSyncEngine = (options = {}) => {
1063
1231
  readTables.add(table);
1064
1232
  }
1065
1233
  const row = await reader.get(key, ctx);
1066
- const rule = readRuleFor(table);
1234
+ const rule = ruleFor(table);
1067
1235
  return rule && row !== undefined && !rule(ctx, row) ? undefined : row;
1068
1236
  },
1069
1237
  where: async (table, predicate) => {
1070
1238
  const reader = readerFor(table);
1071
- const rule = readRuleFor(table);
1239
+ const rule = ruleFor(table);
1072
1240
  const effective = rule ? (row) => predicate(row) && rule(ctx, row) : predicate;
1073
1241
  const matched = [...await reader.all(ctx)].filter(effective);
1074
1242
  if (reader.key !== undefined) {
@@ -1085,7 +1253,72 @@ var createSyncEngine = (options = {}) => {
1085
1253
  }
1086
1254
  };
1087
1255
  };
1088
- const diffRerun = (sub, rows) => {
1256
+ const writerFor = (table) => {
1257
+ const writer = writers.get(table);
1258
+ if (writer === undefined) {
1259
+ throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
1260
+ }
1261
+ return writer;
1262
+ };
1263
+ const authorizeWrite = async (table, op, value, ctx) => {
1264
+ const rule = writeRuleFor(table, op);
1265
+ if (rule === undefined) {
1266
+ return;
1267
+ }
1268
+ let subject = value;
1269
+ if (op !== "insert") {
1270
+ const reader = readers.get(table);
1271
+ if (reader?.get !== undefined) {
1272
+ const id = reader.key ? reader.key(value) : value.id;
1273
+ if (id !== undefined) {
1274
+ const existing = await reader.get(id, ctx);
1275
+ if (existing !== undefined) {
1276
+ subject = existing;
1277
+ }
1278
+ }
1279
+ }
1280
+ }
1281
+ if (!rule(ctx, subject)) {
1282
+ throw new UnauthorizedError(`${op} on table "${table}"`);
1283
+ }
1284
+ };
1285
+ const makeActions = (tx, ctx, enforce) => {
1286
+ const buffered = [];
1287
+ const actions = {
1288
+ change: (collection, change) => {
1289
+ buffered.push({
1290
+ table: collection,
1291
+ change
1292
+ });
1293
+ return Promise.resolve();
1294
+ },
1295
+ insert: async (table, data) => {
1296
+ if (enforce) {
1297
+ await authorizeWrite(table, "insert", data, ctx);
1298
+ }
1299
+ const row = await writerFor(table).insert(data, ctx, tx);
1300
+ buffered.push({ table, change: { op: "insert", row } });
1301
+ return row;
1302
+ },
1303
+ update: async (table, data) => {
1304
+ if (enforce) {
1305
+ await authorizeWrite(table, "update", data, ctx);
1306
+ }
1307
+ const row = await writerFor(table).update(data, ctx, tx);
1308
+ buffered.push({ table, change: { op: "update", row } });
1309
+ return row;
1310
+ },
1311
+ delete: async (table, row) => {
1312
+ if (enforce) {
1313
+ await authorizeWrite(table, "delete", row, ctx);
1314
+ }
1315
+ await writerFor(table).delete(row, ctx, tx);
1316
+ buffered.push({ table, change: { op: "delete", row } });
1317
+ }
1318
+ };
1319
+ return { actions, buffered };
1320
+ };
1321
+ const diffRerun = (sub, rows, equals = shallowEqual4) => {
1089
1322
  const next = new Map;
1090
1323
  for (const row of rows) {
1091
1324
  next.set(sub.key(row), row);
@@ -1097,7 +1330,7 @@ var createSyncEngine = (options = {}) => {
1097
1330
  const previous = sub.current.get(rowKey);
1098
1331
  if (previous === undefined) {
1099
1332
  added.push(row);
1100
- } else if (!shallowEqual4(previous, row)) {
1333
+ } else if (!equals(previous, row)) {
1101
1334
  changed.push(row);
1102
1335
  }
1103
1336
  }
@@ -1128,6 +1361,47 @@ var createSyncEngine = (options = {}) => {
1128
1361
  }
1129
1362
  return pairs;
1130
1363
  };
1364
+ const ensureSearchIndex = async (definition) => {
1365
+ let entry = searchIndexes.get(definition.name);
1366
+ if (entry === undefined) {
1367
+ entry = { index: definition.index(), definition, hydrated: false };
1368
+ searchIndexes.set(definition.name, entry);
1369
+ }
1370
+ if (!entry.hydrated) {
1371
+ for (const row of await definition.source()) {
1372
+ entry.index.add(row);
1373
+ }
1374
+ entry.hydrated = true;
1375
+ }
1376
+ return entry;
1377
+ };
1378
+ const searchPairs = (changes) => {
1379
+ const touched = new Set;
1380
+ for (const { table, change } of changes) {
1381
+ for (const entry of searchIndexes.values()) {
1382
+ if (!entry.hydrated || entry.definition.table !== table) {
1383
+ continue;
1384
+ }
1385
+ if (change.op === "delete") {
1386
+ entry.index.remove(entry.definition.key(change.row));
1387
+ } else {
1388
+ entry.index.add(change.row);
1389
+ }
1390
+ touched.add(entry.definition.name);
1391
+ }
1392
+ }
1393
+ const pairs = [];
1394
+ for (const sub of searchSubs) {
1395
+ if (!touched.has(sub.collection)) {
1396
+ continue;
1397
+ }
1398
+ const diff = diffRerun(sub, sub.rerun(), equalsIgnoringScore);
1399
+ if (!isEmptyViewDiff(diff)) {
1400
+ pairs.push([sub, diff]);
1401
+ }
1402
+ }
1403
+ return pairs;
1404
+ };
1131
1405
  const logChange = (changeVersion, entry) => {
1132
1406
  changeLog.push(entry);
1133
1407
  if (changeLog.length > changeLogSize) {
@@ -1148,6 +1422,7 @@ var createSyncEngine = (options = {}) => {
1148
1422
  emissions.push(...await reactivePairs([
1149
1423
  { table, key: changedKeyFor(table, change), row: change.row }
1150
1424
  ]));
1425
+ emissions.push(...searchPairs([{ table, change }]));
1151
1426
  for (const [subscription, diff] of emissions) {
1152
1427
  subscription.onDiff(diff, changeVersion);
1153
1428
  }
@@ -1188,6 +1463,7 @@ var createSyncEngine = (options = {}) => {
1188
1463
  }
1189
1464
  }
1190
1465
  emissions.push(...await reactivePairs(reactiveChanges));
1466
+ emissions.push(...searchPairs(changes));
1191
1467
  for (const [subscription, diff] of emissions) {
1192
1468
  subscription.onDiff(diff, batchVersion);
1193
1469
  }
@@ -1330,6 +1606,50 @@ var createSyncEngine = (options = {}) => {
1330
1606
  }
1331
1607
  };
1332
1608
  };
1609
+ const subscribeSearch = async (collection, definition, params, ctx, onDiff, set) => {
1610
+ const query2 = params;
1611
+ if (definition.authorize !== undefined) {
1612
+ const allowed = await definition.authorize(query2, ctx);
1613
+ if (!allowed) {
1614
+ throw new UnauthorizedError(`subscribe to collection "${collection}"`);
1615
+ }
1616
+ }
1617
+ const entry = await ensureSearchIndex(definition);
1618
+ const limit = definition.limit ?? 20;
1619
+ const readRule = readRuleFor(definition.table);
1620
+ const rerun = () => {
1621
+ const candidates = entry.index.search(query2, readRule ? limit * 5 : limit);
1622
+ const visible = readRule ? candidates.filter((hit) => readRule(ctx, hit.row)) : candidates;
1623
+ return visible.slice(0, limit).map((hit) => ({
1624
+ ...hit.row,
1625
+ [SEARCH_SCORE_FIELD]: hit.score
1626
+ }));
1627
+ };
1628
+ const initial = rerun();
1629
+ const current = new Map;
1630
+ for (const row of initial) {
1631
+ current.set(definition.key(row), row);
1632
+ }
1633
+ const atVersion = version;
1634
+ const subscription = {
1635
+ kind: "search",
1636
+ collection,
1637
+ key: definition.key,
1638
+ rerun,
1639
+ current,
1640
+ onDiff
1641
+ };
1642
+ set.add(subscription);
1643
+ searchSubs.add(subscription);
1644
+ return {
1645
+ initial,
1646
+ version: atVersion,
1647
+ unsubscribe: () => {
1648
+ set.delete(subscription);
1649
+ searchSubs.delete(subscription);
1650
+ }
1651
+ };
1652
+ };
1333
1653
  return {
1334
1654
  register: (collection) => {
1335
1655
  registry.set(collection.name, collection);
@@ -1348,6 +1668,9 @@ var createSyncEngine = (options = {}) => {
1348
1668
  addTableIndex(table, collection.name);
1349
1669
  }
1350
1670
  },
1671
+ registerSearch: (collection) => {
1672
+ registry.set(collection.name, collection);
1673
+ },
1351
1674
  subscribe: async ({ collection, params, ctx, onDiff, since }) => {
1352
1675
  const registered = registry.get(collection);
1353
1676
  if (registered === undefined) {
@@ -1368,6 +1691,10 @@ var createSyncEngine = (options = {}) => {
1368
1691
  const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1369
1692
  return reactived;
1370
1693
  }
1694
+ if (registeredKind === "search") {
1695
+ const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
1696
+ return searched;
1697
+ }
1371
1698
  const definition = registered;
1372
1699
  if (definition.authorize !== undefined) {
1373
1700
  const allowed = await definition.authorize(params, ctx);
@@ -1488,69 +1815,32 @@ var createSyncEngine = (options = {}) => {
1488
1815
  throw new UnauthorizedError(`run mutation "${name}"`);
1489
1816
  }
1490
1817
  }
1491
- const writerFor = (table) => {
1492
- const writer = writers.get(table);
1493
- if (writer === undefined) {
1494
- throw new Error(`No writer registered for table "${table}" \u2014 register one with engine.registerWriter, or use actions.change`);
1495
- }
1496
- return writer;
1497
- };
1498
- const authorizeWrite = async (table, op, value) => {
1499
- const rule = writeRuleFor(table, op);
1500
- if (rule === undefined) {
1501
- return;
1502
- }
1503
- let subject = value;
1504
- if (op !== "insert") {
1505
- const reader = readers.get(table);
1506
- if (reader?.get !== undefined) {
1507
- const id = reader.key ? reader.key(value) : value.id;
1508
- if (id !== undefined) {
1509
- const existing = await reader.get(id, ctx);
1510
- if (existing !== undefined) {
1511
- subject = existing;
1512
- }
1513
- }
1514
- }
1515
- }
1516
- if (!rule(ctx, subject)) {
1517
- throw new UnauthorizedError(`${op} on table "${table}"`);
1518
- }
1519
- };
1520
1818
  const runHandler = async (tx) => {
1521
- const buffered2 = [];
1522
- const actions = {
1523
- change: (collection, change) => {
1524
- buffered2.push({
1525
- table: collection,
1526
- change
1527
- });
1528
- return Promise.resolve();
1529
- },
1530
- insert: async (table, data) => {
1531
- await authorizeWrite(table, "insert", data);
1532
- const row = await writerFor(table).insert(data, ctx, tx);
1533
- buffered2.push({ table, change: { op: "insert", row } });
1534
- return row;
1535
- },
1536
- update: async (table, data) => {
1537
- await authorizeWrite(table, "update", data);
1538
- const row = await writerFor(table).update(data, ctx, tx);
1539
- buffered2.push({ table, change: { op: "update", row } });
1540
- return row;
1541
- },
1542
- delete: async (table, row) => {
1543
- await authorizeWrite(table, "delete", row);
1544
- await writerFor(table).delete(row, ctx, tx);
1545
- buffered2.push({ table, change: { op: "delete", row } });
1546
- }
1547
- };
1548
- const handlerResult = await mutation.handler(args, ctx, actions);
1549
- return { buffered: buffered2, result: handlerResult };
1819
+ const { actions, buffered: buffered2 } = makeActions(tx, ctx, true);
1820
+ const result2 = await mutation.handler(args, ctx, actions);
1821
+ return { buffered: buffered2, result: result2 };
1550
1822
  };
1551
1823
  const { buffered, result } = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1552
1824
  await applyChangeBatch(buffered);
1553
1825
  return result;
1826
+ },
1827
+ registerSchedule: (schedule) => {
1828
+ schedules.set(schedule.name, schedule);
1829
+ },
1830
+ listSchedules: () => [...schedules.values()],
1831
+ runSchedule: async (name) => {
1832
+ const schedule = schedules.get(name);
1833
+ if (schedule === undefined) {
1834
+ throw new Error(`Unknown schedule "${name}"`);
1835
+ }
1836
+ const runHandler = async (tx) => {
1837
+ const { actions, buffered: buffered2 } = makeActions(tx, {}, false);
1838
+ const db = makeReadHandle({}, new Set, new Set, [], false);
1839
+ await schedule.run({ actions, db });
1840
+ return buffered2;
1841
+ };
1842
+ const buffered = runInTransaction !== undefined ? await runInTransaction((tx) => runHandler(tx)) : await runHandler(undefined);
1843
+ await applyChangeBatch(buffered);
1554
1844
  }
1555
1845
  };
1556
1846
  };
@@ -1801,12 +2091,16 @@ export {
1801
2091
  hydrateRoute,
1802
2092
  fromRowChange,
1803
2093
  filterOp,
2094
+ defineSearchCollection,
2095
+ defineSchedule,
1804
2096
  defineReactiveQuery,
1805
2097
  definePermissions,
1806
2098
  defineMutation,
1807
2099
  defineJoinCollection,
1808
2100
  defineGraphCollection,
1809
2101
  defineCollection,
2102
+ createVectorIndex,
2103
+ createTextIndex,
1810
2104
  createSyncEngine,
1811
2105
  createSyncConnection,
1812
2106
  createPresenceHub,
@@ -1817,8 +2111,9 @@ export {
1817
2111
  createAggregate,
1818
2112
  chain,
1819
2113
  aggregateOp,
1820
- UnauthorizedError
2114
+ UnauthorizedError,
2115
+ SEARCH_SCORE_FIELD
1821
2116
  };
1822
2117
 
1823
- //# debugId=04C93A0AB37B69AD64756E2164756E21
2118
+ //# debugId=54AD7964887E323764756E2164756E21
1824
2119
  //# sourceMappingURL=index.js.map