@absolutejs/sync 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -18
- package/dist/engine/index.d.ts +6 -0
- package/dist/engine/index.js +266 -4
- package/dist/engine/index.js.map +7 -4
- package/dist/engine/search.d.ts +61 -0
- package/dist/engine/syncEngine.d.ts +7 -0
- package/dist/engine/textIndex.d.ts +33 -0
- package/dist/engine/vectorIndex.d.ts +27 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,9 +33,9 @@ top-N ordering are maintained incrementally through a composable operator graph
|
|
|
33
33
|
> write-behind cache), Tier 2 (Drizzle + Prisma topic adapters, `createLiveQuery`),
|
|
34
34
|
> and Tier 3 (sync engine: collections, WebSocket diff transport, optimistic
|
|
35
35
|
> mutations + offline queue, a local-first client cache, declarative row-level
|
|
36
|
-
> permissions, CDC for Postgres/MySQL/SQLite,
|
|
37
|
-
> and a declarative operator graph) are in
|
|
38
|
-
> Everything ships as subpaths of this one package.
|
|
36
|
+
> permissions, live full-text + vector search, CDC for Postgres/MySQL/SQLite,
|
|
37
|
+
> incremental aggregations + joins, and a declarative operator graph) are in
|
|
38
|
+
> place. Everything ships as subpaths of this one package.
|
|
39
39
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
@@ -249,6 +249,37 @@ await orders.mutate({
|
|
|
249
249
|
});
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
+
- **Live search.** A `defineSearchCollection` is a full-text or vector index kept
|
|
253
|
+
live from a table's change feed. The subscription's `params` are the query (a
|
|
254
|
+
string for keyword search, an embedding for similarity); the ranked top-K stream
|
|
255
|
+
back as an ordinary collection and re-rank as rows change. Read permissions on
|
|
256
|
+
the source table still scope a caller's hits. Standalone, `createTextIndex` and
|
|
257
|
+
`createVectorIndex` are reusable (e.g. RAG retrieval with `@absolutejs/rag`).
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// server
|
|
261
|
+
engine.registerSearch(
|
|
262
|
+
defineSearchCollection<Doc>({
|
|
263
|
+
name: 'docSearch',
|
|
264
|
+
table: 'docs',
|
|
265
|
+
index: () =>
|
|
266
|
+
createTextIndex({
|
|
267
|
+
key: (d) => d.id,
|
|
268
|
+
fields: ['title', 'body']
|
|
269
|
+
}),
|
|
270
|
+
source: () => db.select().from(docs), // the corpus to index
|
|
271
|
+
key: (d) => d.id
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// client — params are the query; each result row carries `_score`
|
|
276
|
+
const results = createSyncCollection<Doc>({
|
|
277
|
+
url,
|
|
278
|
+
collection: 'docSearch',
|
|
279
|
+
params: 'quick brown fox' // a vector for createVectorIndex
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
252
283
|
## Write-behind cache — keep a remote store off your hot path
|
|
253
284
|
|
|
254
285
|
```ts
|
|
@@ -328,21 +359,24 @@ mutate({
|
|
|
328
359
|
|
|
329
360
|
### `@absolutejs/sync/engine`
|
|
330
361
|
|
|
331
|
-
| Export
|
|
332
|
-
|
|
|
333
|
-
| `createSyncEngine()`
|
|
334
|
-
| `defineCollection({ name, hydrate, key?, match?, authorize?, tables? })`
|
|
335
|
-
| `defineMutation({ name, handler, authorize? })`
|
|
336
|
-
| `registerWriter(table, { insert, update, delete })`
|
|
337
|
-
| `createAggregate({ key, groupBy?, value? })`
|
|
338
|
-
| `createMaterializedView({ key, match, equals? })`
|
|
339
|
-
| `createPollingChangeSource({ poll, intervalMs?, startSeq?, onProcessed? })`
|
|
340
|
-
| `engine.connectCluster(bus)` + `createInMemoryClusterBus()`
|
|
341
|
-
| `createPresenceHub()` + `syncSocket({ engine, presence })`
|
|
342
|
-
| `query(source).filter().map().join().leftJoin().groupBy().orderBy()`
|
|
343
|
-
| `defineGraphCollection({ name, query, key, authorize? })`
|
|
344
|
-
| `defineReactiveQuery({ name, run, key })` + `registerReactive` / `registerReader`
|
|
345
|
-
| `definePermissions({ [table]: { read?, insert?, update?, delete?, write? } })`
|
|
362
|
+
| Export | What it is |
|
|
363
|
+
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
364
|
+
| `createSyncEngine()` | Registry + view syncer: `register`, `subscribe`, `applyChange`, `connectSource`, `registerMutation`, `registerWriter`, `runMutation`. |
|
|
365
|
+
| `defineCollection({ name, hydrate, key?, match?, authorize?, tables? })` | Define a syncable collection. |
|
|
366
|
+
| `defineMutation({ name, handler, authorize? })` | Define a server mutation. Its `handler` gets `actions.insert/update/delete` (write through a registered `TableWriter` → persists + emits in one step) plus `actions.change` (escape hatch). Changes commit atomically. |
|
|
367
|
+
| `registerWriter(table, { insert, update, delete })` | Teach the engine how to persist a table (any ORM), so writes auto-emit — you can't write without going live. |
|
|
368
|
+
| `createAggregate({ key, groupBy?, value? })` | Incremental count/sum/avg/min/max by group. |
|
|
369
|
+
| `createMaterializedView({ key, match, equals? })` | The predicate-matching IVM primitive (`apply`/`reset` → diffs). |
|
|
370
|
+
| `createPollingChangeSource({ poll, intervalMs?, startSeq?, onProcessed? })` | DB-agnostic CDC `ChangeSource` that tails a changelog (outbox) table. |
|
|
371
|
+
| `engine.connectCluster(bus)` + `createInMemoryClusterBus()` | Horizontal scale: fan changes across server instances over a `ClusterBus` (BYO Redis/Postgres; in-memory bus for dev). |
|
|
372
|
+
| `createPresenceHub()` + `syncSocket({ engine, presence })` | Ephemeral room-scoped presence (online / typing / cursors) over the same socket — not persisted, auto-cleaned on disconnect. |
|
|
373
|
+
| `query(source).filter().map().join().leftJoin().groupBy().orderBy()` | Declarative incremental query builder (the operator graph). |
|
|
374
|
+
| `defineGraphCollection({ name, query, key, authorize? })` | Run a `query` as a live collection. |
|
|
375
|
+
| `defineReactiveQuery({ name, run, key })` + `registerReactive` / `registerReader` | Read-set-tracked query: `run(ctx)` reads via `ctx.db` (`all`/`get`/`where`) and re-runs only when the rows/ranges it read change — no `match`, no manual emit. |
|
|
376
|
+
| `definePermissions({ [table]: { read?, insert?, update?, delete?, write? } })` | Declarative row-level access control. Pass as `createSyncEngine({ permissions })` or `registerPermissions(table, rules)`. Read rules filter every row emitted; write rules gate `actions.insert/update/delete`. |
|
|
377
|
+
| `defineSearchCollection({ name, table, index, source, key, limit? })` + `registerSearch` | Live search collection: the subscription's `params` are the query (string/vector), the ranked top-K stream back as a normal collection, re-ranked as rows change. Each row carries its score under `_score`. |
|
|
378
|
+
| `createTextIndex({ key, fields, tokenize?, stopwords?, k1?, b? })` | Incremental BM25 full-text index (keyword search). Implements `SearchIndex`; usable standalone or inside a search collection. |
|
|
379
|
+
| `createVectorIndex({ key, embedding, metric? })` | Incremental vector index (cosine/dot/euclidean exact k-NN) for semantic search — pairs with `@absolutejs/ai` / `@absolutejs/rag` for RAG retrieval on your own data. |
|
|
346
380
|
|
|
347
381
|
### `@absolutejs/sync/postgres`
|
|
348
382
|
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export { defineGraphCollection, query } from './graph';
|
|
|
32
32
|
export type { GraphCollectionDefinition, GraphInstance, GraphSource, GroupByOptions, JoinOptions, OrderByQueryOptions, Query } from './graph';
|
|
33
33
|
export { definePermissions } from './permissions';
|
|
34
34
|
export type { PermissionsDefinition, ReadRule, TablePermissions, WriteRule } from './permissions';
|
|
35
|
+
export { defineSearchCollection, SEARCH_SCORE_FIELD } from './search';
|
|
36
|
+
export type { SearchCollectionDefinition, SearchHit, SearchIndex } from './search';
|
|
37
|
+
export { createTextIndex } from './textIndex';
|
|
38
|
+
export type { TextIndexOptions } from './textIndex';
|
|
39
|
+
export { createVectorIndex } from './vectorIndex';
|
|
40
|
+
export type { VectorIndexOptions, VectorMetric } from './vectorIndex';
|
|
35
41
|
export { defineMutation } from './mutation';
|
|
36
42
|
export type { MutationActions, MutationDefinition, MutationHandler, TableWriter, TransactionRunner } from './mutation';
|
|
37
43
|
export { createSyncEngine, UnauthorizedError } from './syncEngine';
|
package/dist/engine/index.js
CHANGED
|
@@ -885,6 +885,156 @@ 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
|
+
};
|
|
888
1038
|
// src/engine/mutation.ts
|
|
889
1039
|
var defineMutation = (definition) => definition;
|
|
890
1040
|
// src/engine/syncEngine.ts
|
|
@@ -906,6 +1056,15 @@ var shallowEqual4 = (a, b) => {
|
|
|
906
1056
|
const bKeys = Object.keys(b);
|
|
907
1057
|
return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
|
|
908
1058
|
};
|
|
1059
|
+
var equalsIgnoringScore = (a, b) => {
|
|
1060
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
1061
|
+
return a === b;
|
|
1062
|
+
}
|
|
1063
|
+
const strip = (value) => Object.keys(value).filter((k) => k !== SEARCH_SCORE_FIELD);
|
|
1064
|
+
const aKeys = strip(a);
|
|
1065
|
+
const bKeys = strip(b);
|
|
1066
|
+
return aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]);
|
|
1067
|
+
};
|
|
909
1068
|
var createSyncEngine = (options = {}) => {
|
|
910
1069
|
const registry = new Map;
|
|
911
1070
|
const mutations = new Map;
|
|
@@ -921,6 +1080,8 @@ var createSyncEngine = (options = {}) => {
|
|
|
921
1080
|
return rules?.[op] ?? rules?.write;
|
|
922
1081
|
};
|
|
923
1082
|
const reactiveSubs = new Set;
|
|
1083
|
+
const searchSubs = new Set;
|
|
1084
|
+
const searchIndexes = new Map;
|
|
924
1085
|
const active = new Map;
|
|
925
1086
|
const tableIndex = new Map;
|
|
926
1087
|
const changeLogSize = options.changeLogSize ?? 1024;
|
|
@@ -973,6 +1134,9 @@ var createSyncEngine = (options = {}) => {
|
|
|
973
1134
|
if (subscription.kind === "reactive") {
|
|
974
1135
|
return EMPTY_DIFF;
|
|
975
1136
|
}
|
|
1137
|
+
if (subscription.kind === "search") {
|
|
1138
|
+
return EMPTY_DIFF;
|
|
1139
|
+
}
|
|
976
1140
|
if (subscription.incremental) {
|
|
977
1141
|
try {
|
|
978
1142
|
return subscription.view.apply(change);
|
|
@@ -1085,7 +1249,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1085
1249
|
}
|
|
1086
1250
|
};
|
|
1087
1251
|
};
|
|
1088
|
-
const diffRerun = (sub, rows) => {
|
|
1252
|
+
const diffRerun = (sub, rows, equals = shallowEqual4) => {
|
|
1089
1253
|
const next = new Map;
|
|
1090
1254
|
for (const row of rows) {
|
|
1091
1255
|
next.set(sub.key(row), row);
|
|
@@ -1097,7 +1261,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1097
1261
|
const previous = sub.current.get(rowKey);
|
|
1098
1262
|
if (previous === undefined) {
|
|
1099
1263
|
added.push(row);
|
|
1100
|
-
} else if (!
|
|
1264
|
+
} else if (!equals(previous, row)) {
|
|
1101
1265
|
changed.push(row);
|
|
1102
1266
|
}
|
|
1103
1267
|
}
|
|
@@ -1128,6 +1292,47 @@ var createSyncEngine = (options = {}) => {
|
|
|
1128
1292
|
}
|
|
1129
1293
|
return pairs;
|
|
1130
1294
|
};
|
|
1295
|
+
const ensureSearchIndex = async (definition) => {
|
|
1296
|
+
let entry = searchIndexes.get(definition.name);
|
|
1297
|
+
if (entry === undefined) {
|
|
1298
|
+
entry = { index: definition.index(), definition, hydrated: false };
|
|
1299
|
+
searchIndexes.set(definition.name, entry);
|
|
1300
|
+
}
|
|
1301
|
+
if (!entry.hydrated) {
|
|
1302
|
+
for (const row of await definition.source()) {
|
|
1303
|
+
entry.index.add(row);
|
|
1304
|
+
}
|
|
1305
|
+
entry.hydrated = true;
|
|
1306
|
+
}
|
|
1307
|
+
return entry;
|
|
1308
|
+
};
|
|
1309
|
+
const searchPairs = (changes) => {
|
|
1310
|
+
const touched = new Set;
|
|
1311
|
+
for (const { table, change } of changes) {
|
|
1312
|
+
for (const entry of searchIndexes.values()) {
|
|
1313
|
+
if (!entry.hydrated || entry.definition.table !== table) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (change.op === "delete") {
|
|
1317
|
+
entry.index.remove(entry.definition.key(change.row));
|
|
1318
|
+
} else {
|
|
1319
|
+
entry.index.add(change.row);
|
|
1320
|
+
}
|
|
1321
|
+
touched.add(entry.definition.name);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const pairs = [];
|
|
1325
|
+
for (const sub of searchSubs) {
|
|
1326
|
+
if (!touched.has(sub.collection)) {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const diff = diffRerun(sub, sub.rerun(), equalsIgnoringScore);
|
|
1330
|
+
if (!isEmptyViewDiff(diff)) {
|
|
1331
|
+
pairs.push([sub, diff]);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return pairs;
|
|
1335
|
+
};
|
|
1131
1336
|
const logChange = (changeVersion, entry) => {
|
|
1132
1337
|
changeLog.push(entry);
|
|
1133
1338
|
if (changeLog.length > changeLogSize) {
|
|
@@ -1148,6 +1353,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1148
1353
|
emissions.push(...await reactivePairs([
|
|
1149
1354
|
{ table, key: changedKeyFor(table, change), row: change.row }
|
|
1150
1355
|
]));
|
|
1356
|
+
emissions.push(...searchPairs([{ table, change }]));
|
|
1151
1357
|
for (const [subscription, diff] of emissions) {
|
|
1152
1358
|
subscription.onDiff(diff, changeVersion);
|
|
1153
1359
|
}
|
|
@@ -1188,6 +1394,7 @@ var createSyncEngine = (options = {}) => {
|
|
|
1188
1394
|
}
|
|
1189
1395
|
}
|
|
1190
1396
|
emissions.push(...await reactivePairs(reactiveChanges));
|
|
1397
|
+
emissions.push(...searchPairs(changes));
|
|
1191
1398
|
for (const [subscription, diff] of emissions) {
|
|
1192
1399
|
subscription.onDiff(diff, batchVersion);
|
|
1193
1400
|
}
|
|
@@ -1330,6 +1537,50 @@ var createSyncEngine = (options = {}) => {
|
|
|
1330
1537
|
}
|
|
1331
1538
|
};
|
|
1332
1539
|
};
|
|
1540
|
+
const subscribeSearch = async (collection, definition, params, ctx, onDiff, set) => {
|
|
1541
|
+
const query2 = params;
|
|
1542
|
+
if (definition.authorize !== undefined) {
|
|
1543
|
+
const allowed = await definition.authorize(query2, ctx);
|
|
1544
|
+
if (!allowed) {
|
|
1545
|
+
throw new UnauthorizedError(`subscribe to collection "${collection}"`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
const entry = await ensureSearchIndex(definition);
|
|
1549
|
+
const limit = definition.limit ?? 20;
|
|
1550
|
+
const readRule = readRuleFor(definition.table);
|
|
1551
|
+
const rerun = () => {
|
|
1552
|
+
const candidates = entry.index.search(query2, readRule ? limit * 5 : limit);
|
|
1553
|
+
const visible = readRule ? candidates.filter((hit) => readRule(ctx, hit.row)) : candidates;
|
|
1554
|
+
return visible.slice(0, limit).map((hit) => ({
|
|
1555
|
+
...hit.row,
|
|
1556
|
+
[SEARCH_SCORE_FIELD]: hit.score
|
|
1557
|
+
}));
|
|
1558
|
+
};
|
|
1559
|
+
const initial = rerun();
|
|
1560
|
+
const current = new Map;
|
|
1561
|
+
for (const row of initial) {
|
|
1562
|
+
current.set(definition.key(row), row);
|
|
1563
|
+
}
|
|
1564
|
+
const atVersion = version;
|
|
1565
|
+
const subscription = {
|
|
1566
|
+
kind: "search",
|
|
1567
|
+
collection,
|
|
1568
|
+
key: definition.key,
|
|
1569
|
+
rerun,
|
|
1570
|
+
current,
|
|
1571
|
+
onDiff
|
|
1572
|
+
};
|
|
1573
|
+
set.add(subscription);
|
|
1574
|
+
searchSubs.add(subscription);
|
|
1575
|
+
return {
|
|
1576
|
+
initial,
|
|
1577
|
+
version: atVersion,
|
|
1578
|
+
unsubscribe: () => {
|
|
1579
|
+
set.delete(subscription);
|
|
1580
|
+
searchSubs.delete(subscription);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
};
|
|
1333
1584
|
return {
|
|
1334
1585
|
register: (collection) => {
|
|
1335
1586
|
registry.set(collection.name, collection);
|
|
@@ -1348,6 +1599,9 @@ var createSyncEngine = (options = {}) => {
|
|
|
1348
1599
|
addTableIndex(table, collection.name);
|
|
1349
1600
|
}
|
|
1350
1601
|
},
|
|
1602
|
+
registerSearch: (collection) => {
|
|
1603
|
+
registry.set(collection.name, collection);
|
|
1604
|
+
},
|
|
1351
1605
|
subscribe: async ({ collection, params, ctx, onDiff, since }) => {
|
|
1352
1606
|
const registered = registry.get(collection);
|
|
1353
1607
|
if (registered === undefined) {
|
|
@@ -1368,6 +1622,10 @@ var createSyncEngine = (options = {}) => {
|
|
|
1368
1622
|
const reactived = await subscribeReactive(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
1369
1623
|
return reactived;
|
|
1370
1624
|
}
|
|
1625
|
+
if (registeredKind === "search") {
|
|
1626
|
+
const searched = await subscribeSearch(collection, registered, params, ctx, typedOnDiff, subscribeSet);
|
|
1627
|
+
return searched;
|
|
1628
|
+
}
|
|
1371
1629
|
const definition = registered;
|
|
1372
1630
|
if (definition.authorize !== undefined) {
|
|
1373
1631
|
const allowed = await definition.authorize(params, ctx);
|
|
@@ -1801,12 +2059,15 @@ export {
|
|
|
1801
2059
|
hydrateRoute,
|
|
1802
2060
|
fromRowChange,
|
|
1803
2061
|
filterOp,
|
|
2062
|
+
defineSearchCollection,
|
|
1804
2063
|
defineReactiveQuery,
|
|
1805
2064
|
definePermissions,
|
|
1806
2065
|
defineMutation,
|
|
1807
2066
|
defineJoinCollection,
|
|
1808
2067
|
defineGraphCollection,
|
|
1809
2068
|
defineCollection,
|
|
2069
|
+
createVectorIndex,
|
|
2070
|
+
createTextIndex,
|
|
1810
2071
|
createSyncEngine,
|
|
1811
2072
|
createSyncConnection,
|
|
1812
2073
|
createPresenceHub,
|
|
@@ -1817,8 +2078,9 @@ export {
|
|
|
1817
2078
|
createAggregate,
|
|
1818
2079
|
chain,
|
|
1819
2080
|
aggregateOp,
|
|
1820
|
-
UnauthorizedError
|
|
2081
|
+
UnauthorizedError,
|
|
2082
|
+
SEARCH_SCORE_FIELD
|
|
1821
2083
|
};
|
|
1822
2084
|
|
|
1823
|
-
//# debugId=
|
|
2085
|
+
//# debugId=28E08370F43FEBAE64756E2164756E21
|
|
1824
2086
|
//# sourceMappingURL=index.js.map
|