@dxos/index-core 0.0.0 → 0.8.4-main.1068cf700f

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.
Files changed (42) hide show
  1. package/dist/lib/neutral/index.mjs +658 -0
  2. package/dist/lib/neutral/index.mjs.map +7 -0
  3. package/dist/lib/neutral/meta.json +1 -0
  4. package/dist/types/src/index-engine.d.ts +83 -0
  5. package/dist/types/src/index-engine.d.ts.map +1 -0
  6. package/dist/types/src/index-engine.test.d.ts +2 -0
  7. package/dist/types/src/index-engine.test.d.ts.map +1 -0
  8. package/dist/types/src/index-tracker.d.ts +44 -0
  9. package/dist/types/src/index-tracker.d.ts.map +1 -0
  10. package/dist/types/src/index-tracker.test.d.ts +2 -0
  11. package/dist/types/src/index-tracker.test.d.ts.map +1 -0
  12. package/dist/types/src/index.d.ts +8 -0
  13. package/dist/types/src/index.d.ts.map +1 -0
  14. package/dist/types/src/indexes/fts-index.d.ts +63 -0
  15. package/dist/types/src/indexes/fts-index.d.ts.map +1 -0
  16. package/dist/types/src/indexes/fts-index.test.d.ts +2 -0
  17. package/dist/types/src/indexes/fts-index.test.d.ts.map +1 -0
  18. package/dist/types/src/indexes/fts5.test.d.ts +2 -0
  19. package/dist/types/src/indexes/fts5.test.d.ts.map +1 -0
  20. package/dist/types/src/indexes/index.d.ts +5 -0
  21. package/dist/types/src/indexes/index.d.ts.map +1 -0
  22. package/dist/types/src/indexes/interface.d.ts +47 -0
  23. package/dist/types/src/indexes/interface.d.ts.map +1 -0
  24. package/dist/types/src/indexes/object-meta-index.d.ts +60 -0
  25. package/dist/types/src/indexes/object-meta-index.d.ts.map +1 -0
  26. package/dist/types/src/indexes/object-meta-index.test.d.ts +2 -0
  27. package/dist/types/src/indexes/object-meta-index.test.d.ts.map +1 -0
  28. package/dist/types/src/indexes/reverse-ref-index.d.ts +37 -0
  29. package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -0
  30. package/dist/types/src/indexes/reverse-ref-index.test.d.ts +2 -0
  31. package/dist/types/src/indexes/reverse-ref-index.test.d.ts.map +1 -0
  32. package/dist/types/src/utils.d.ts +17 -0
  33. package/dist/types/src/utils.d.ts.map +1 -0
  34. package/dist/types/tsconfig.tsbuildinfo +1 -0
  35. package/package.json +22 -18
  36. package/src/index-engine.test.ts +8 -5
  37. package/src/index-engine.ts +53 -9
  38. package/src/index.ts +3 -2
  39. package/src/indexes/fts-index.ts +41 -2
  40. package/src/indexes/object-meta-index.test.ts +179 -0
  41. package/src/indexes/object-meta-index.ts +148 -13
  42. package/src/utils.ts +1 -1
@@ -0,0 +1,658 @@
1
+ // src/index-engine.ts
2
+ import * as Effect5 from "effect/Effect";
3
+ import * as SqlTransaction from "@dxos/sql-sqlite/SqlTransaction";
4
+
5
+ // src/index-tracker.ts
6
+ import * as SqlClient from "@effect/sql/SqlClient";
7
+ import * as Effect from "effect/Effect";
8
+ import * as Schema from "effect/Schema";
9
+ import { SpaceId } from "@dxos/keys";
10
+ var IndexCursor = Schema.Struct({
11
+ /**
12
+ * Name of the index owning this cursor.
13
+ */
14
+ indexName: Schema.String,
15
+ /**
16
+ * Space id.
17
+ */
18
+ spaceId: Schema.NullOr(SpaceId),
19
+ /**
20
+ * Source name.
21
+ * 'automerge' / 'queue' / 'index' (for secondary indexes)
22
+ */
23
+ sourceName: Schema.String,
24
+ /**
25
+ * Document id or queue id.
26
+ * doc_id, queue_id, '' <empty string> (if indexing entire namespace)
27
+ */
28
+ resourceId: Schema.NullOr(Schema.String),
29
+ /**
30
+ * Heads, queue position, version.
31
+ */
32
+ cursor: Schema.Union(Schema.Number, Schema.String)
33
+ });
34
+ var IndexTracker = class {
35
+ migrate = Effect.fn("IndexTracker.migrate")(function* () {
36
+ const sql = yield* SqlClient.SqlClient;
37
+ yield* sql`CREATE TABLE IF NOT EXISTS indexCursor (
38
+ indexName TEXT NOT NULL,
39
+ spaceId TEXT NOT NULL DEFAULT '',
40
+ sourceName TEXT NOT NULL,
41
+ resourceId TEXT NOT NULL DEFAULT '',
42
+ cursor,
43
+ PRIMARY KEY (indexName, spaceId, sourceName, resourceId)
44
+ )`;
45
+ });
46
+ queryCursors = Effect.fn("IndexTracker.queryCursors")((query) => Effect.gen(function* () {
47
+ const sql = yield* SqlClient.SqlClient;
48
+ const spaceIdParam = query.spaceId === void 0 ? null : query.spaceId ?? "";
49
+ const sourceNameParam = query.sourceName === void 0 ? null : query.sourceName;
50
+ const resourceIdParam = query.resourceId === void 0 ? null : query.resourceId ?? "";
51
+ const rows = yield* sql`
52
+ SELECT * FROM indexCursor
53
+ WHERE indexName = ${query.indexName}
54
+ AND (${spaceIdParam} IS NULL OR spaceId = ${spaceIdParam})
55
+ AND (${sourceNameParam} IS NULL OR sourceName = ${sourceNameParam})
56
+ AND (${resourceIdParam} IS NULL OR resourceId = ${resourceIdParam})
57
+ `;
58
+ return rows.map((row) => ({
59
+ indexName: row.indexName,
60
+ spaceId: row.spaceId === "" ? null : Schema.decodeSync(SpaceId)(row.spaceId),
61
+ sourceName: row.sourceName,
62
+ resourceId: row.resourceId === "" ? null : row.resourceId,
63
+ cursor: row.cursor
64
+ }));
65
+ }));
66
+ updateCursors = Effect.fn("IndexTracker.updateCursors")((cursors) => Effect.gen(function* () {
67
+ const sql = yield* SqlClient.SqlClient;
68
+ yield* Effect.forEach(cursors, (cursor) => {
69
+ const spaceId = cursor.spaceId ?? "";
70
+ const resourceId = cursor.resourceId ?? "";
71
+ return sql`
72
+ INSERT INTO indexCursor (indexName, spaceId, sourceName, resourceId, cursor)
73
+ VALUES (${cursor.indexName}, ${spaceId}, ${cursor.sourceName}, ${resourceId}, ${cursor.cursor})
74
+ ON CONFLICT(indexName, spaceId, sourceName, resourceId) DO UPDATE SET cursor = excluded.cursor
75
+ `;
76
+ }, {
77
+ discard: true
78
+ });
79
+ }));
80
+ };
81
+
82
+ // src/indexes/fts-index.ts
83
+ import * as SqlClient3 from "@effect/sql/SqlClient";
84
+ import * as Effect2 from "effect/Effect";
85
+ var escapeFts5Query = (text) => {
86
+ return text.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, '""')}"`).join(" ");
87
+ };
88
+ var FtsIndex = class {
89
+ migrate = Effect2.fn("FtsIndex.migrate")(function* () {
90
+ const sql = yield* SqlClient3.SqlClient;
91
+ yield* sql`CREATE VIRTUAL TABLE IF NOT EXISTS ftsIndex USING fts5(snapshot, tokenize='trigram')`;
92
+ });
93
+ query({ query, spaceId, includeAllQueues, queueIds }) {
94
+ return Effect2.gen(function* () {
95
+ const trimmed = query.trim();
96
+ if (trimmed.length === 0) {
97
+ return [];
98
+ }
99
+ const sql = yield* SqlClient3.SqlClient;
100
+ const terms = trimmed.split(/\s+/).filter(Boolean);
101
+ const minTermLength = Math.min(...terms.map((t) => t.length));
102
+ const useBm25 = minTermLength >= 3;
103
+ const conditions = minTermLength < 3 ? terms.map((term) => sql`f.snapshot LIKE ${"%" + term + "%"}`) : [
104
+ sql`f.snapshot MATCH ${escapeFts5Query(trimmed)}`
105
+ ];
106
+ const sourceConditions = [];
107
+ if (spaceId && spaceId.length > 0) {
108
+ if (includeAllQueues) {
109
+ sourceConditions.push(sql`m.spaceId IN ${sql.in(spaceId)}`);
110
+ } else {
111
+ sourceConditions.push(sql`(m.spaceId IN ${sql.in(spaceId)} AND m.queueId = '')`);
112
+ }
113
+ }
114
+ if (queueIds && queueIds.length > 0) {
115
+ sourceConditions.push(sql`m.queueId IN ${sql.in(queueIds)}`);
116
+ }
117
+ if (sourceConditions.length > 0) {
118
+ conditions.push(sql`(${sql.or(sourceConditions)})`);
119
+ }
120
+ if (useBm25) {
121
+ const rows = yield* sql`
122
+ SELECT m.*, -bm25(ftsIndex) AS rank
123
+ FROM ftsIndex AS f
124
+ JOIN objectMeta AS m ON f.rowid = m.recordId
125
+ WHERE ${sql.and(conditions)}
126
+ ORDER BY rank DESC
127
+ `;
128
+ return rows;
129
+ } else {
130
+ const rows = yield* sql`
131
+ SELECT m.*
132
+ FROM ftsIndex AS f
133
+ JOIN objectMeta AS m ON f.rowid = m.recordId
134
+ WHERE ${sql.and(conditions)}
135
+ `;
136
+ return rows.map((row) => ({
137
+ ...row,
138
+ rank: 1
139
+ }));
140
+ }
141
+ });
142
+ }
143
+ /**
144
+ * Query snapshots by recordIds.
145
+ * Returns the parsed JSON snapshots for queue objects.
146
+ */
147
+ querySnapshotsJSON(recordIds) {
148
+ return Effect2.gen(function* () {
149
+ if (recordIds.length === 0) {
150
+ return [];
151
+ }
152
+ const sql = yield* SqlClient3.SqlClient;
153
+ const results = yield* sql`SELECT rowid, snapshot FROM ftsIndex WHERE rowid IN ${sql.in(recordIds)}`;
154
+ return results.map((r) => ({
155
+ recordId: r.rowid,
156
+ snapshot: JSON.parse(r.snapshot)
157
+ }));
158
+ });
159
+ }
160
+ update = Effect2.fn("FtsIndex.update")((objects) => Effect2.gen(function* () {
161
+ const sql = yield* SqlClient3.SqlClient;
162
+ yield* Effect2.forEach(objects, (object) => Effect2.gen(function* () {
163
+ const { recordId, data } = object;
164
+ if (recordId === null) {
165
+ return yield* Effect2.die(new Error("FtsIndex.update requires recordId to be set"));
166
+ }
167
+ const snapshot = JSON.stringify(data);
168
+ const existing = yield* sql`SELECT rowid FROM ftsIndex WHERE rowid = ${recordId}`;
169
+ if (existing.length > 0) {
170
+ yield* sql`DELETE FROM ftsIndex WHERE rowid = ${recordId}`;
171
+ }
172
+ yield* sql`INSERT INTO ftsIndex (rowid, snapshot) VALUES (${recordId}, ${snapshot})`;
173
+ }), {
174
+ discard: true
175
+ });
176
+ }));
177
+ };
178
+
179
+ // src/indexes/object-meta-index.ts
180
+ import * as SqlClient5 from "@effect/sql/SqlClient";
181
+ import * as Effect3 from "effect/Effect";
182
+ import * as Schema2 from "effect/Schema";
183
+ import { ATTR_DELETED, ATTR_PARENT, ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET, ATTR_TYPE } from "@dxos/echo/internal";
184
+ import { DXN } from "@dxos/keys";
185
+ var _escapeLikePrefix = (prefix) => {
186
+ const escaped = prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
187
+ return `${escaped}:%`;
188
+ };
189
+ var ObjectMeta = Schema2.Struct({
190
+ recordId: Schema2.Number,
191
+ objectId: Schema2.String,
192
+ queueId: Schema2.String,
193
+ spaceId: Schema2.String,
194
+ documentId: Schema2.String,
195
+ entityKind: Schema2.String,
196
+ /** The versioned DXN of the type of the object. */
197
+ typeDxn: Schema2.String,
198
+ deleted: Schema2.Boolean,
199
+ source: Schema2.NullOr(Schema2.String),
200
+ target: Schema2.NullOr(Schema2.String),
201
+ /** Parent object id (nullable). */
202
+ parent: Schema2.NullOr(Schema2.String),
203
+ /** Monotonically increasing sequence number assigned on insert/update for tracking indexing order. */
204
+ version: Schema2.Number
205
+ });
206
+ var ObjectMetaIndex = class {
207
+ migrate = Effect3.fn("ObjectMetaIndex.runMigrations")(function* () {
208
+ const sql = yield* SqlClient5.SqlClient;
209
+ yield* sql`CREATE TABLE IF NOT EXISTS objectMeta (
210
+ recordId INTEGER PRIMARY KEY AUTOINCREMENT,
211
+ objectId TEXT NOT NULL,
212
+ queueId TEXT NOT NULL DEFAULT '',
213
+ spaceId TEXT NOT NULL,
214
+ documentId TEXT NOT NULL DEFAULT '',
215
+ entityKind TEXT NOT NULL,
216
+ typeDxn TEXT NOT NULL,
217
+ deleted INTEGER NOT NULL,
218
+ source TEXT,
219
+ target TEXT,
220
+ parent TEXT,
221
+ version INTEGER NOT NULL
222
+ )`;
223
+ yield* Effect3.catchAll(sql`ALTER TABLE objectMeta ADD COLUMN parent TEXT`, () => Effect3.void);
224
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_objectId ON objectMeta(spaceId, objectId)`;
225
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_typeDxn ON objectMeta(spaceId, typeDxn)`;
226
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_version ON objectMeta(version)`;
227
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_parent ON objectMeta(spaceId, parent)`;
228
+ });
229
+ query = Effect3.fn("ObjectMetaIndex.query")((query) => Effect3.gen(function* () {
230
+ const sql = yield* SqlClient5.SqlClient;
231
+ const parsedType = DXN.tryParse(query.typeDxn)?.asTypeDXN();
232
+ const rows = parsedType && parsedType.version === void 0 ? yield* sql`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND (typeDxn = ${query.typeDxn} OR typeDxn LIKE ${_escapeLikePrefix(query.typeDxn)} ESCAPE '\\')` : yield* sql`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND typeDxn = ${query.typeDxn}`;
233
+ return rows.map((row) => ({
234
+ ...row,
235
+ deleted: !!row.deleted
236
+ }));
237
+ }));
238
+ queryAll = Effect3.fn("ObjectMetaIndex.queryAll")((query) => Effect3.gen(function* () {
239
+ if (query.spaceIds.length === 0) {
240
+ return [];
241
+ }
242
+ const sql = yield* SqlClient5.SqlClient;
243
+ const rows = yield* sql`SELECT * FROM objectMeta WHERE ${sql.in("spaceId", query.spaceIds)}`;
244
+ return rows.map((row) => ({
245
+ ...row,
246
+ deleted: !!row.deleted
247
+ }));
248
+ }));
249
+ queryTypes = Effect3.fn("ObjectMetaIndex.queryTypes")(({ spaceIds, typeDxns, inverted = false }) => Effect3.gen(function* () {
250
+ if (spaceIds.length === 0) {
251
+ return [];
252
+ }
253
+ if (typeDxns.length === 0) {
254
+ if (!inverted) {
255
+ return [];
256
+ }
257
+ const sql2 = yield* SqlClient5.SqlClient;
258
+ const rows2 = yield* sql2`SELECT * FROM objectMeta WHERE ${sql2.in("spaceId", spaceIds)}`;
259
+ return rows2.map((row) => ({
260
+ ...row,
261
+ deleted: !!row.deleted
262
+ }));
263
+ }
264
+ const sql = yield* SqlClient5.SqlClient;
265
+ const spaceWhere = sql.in("spaceId", spaceIds);
266
+ const typeWhere = sql.or(typeDxns.map((typeDxn) => {
267
+ const parsedType = DXN.tryParse(typeDxn)?.asTypeDXN();
268
+ return parsedType && parsedType.version === void 0 ? sql.or([
269
+ sql`typeDxn = ${typeDxn}`,
270
+ sql`typeDxn LIKE ${_escapeLikePrefix(typeDxn)} ESCAPE '\\'`
271
+ ]) : sql`typeDxn = ${typeDxn}`;
272
+ }));
273
+ const rows = inverted ? yield* sql`SELECT * FROM objectMeta WHERE ${spaceWhere} AND NOT ${typeWhere}` : yield* sql`SELECT * FROM objectMeta WHERE ${spaceWhere} AND ${typeWhere}`;
274
+ return rows.map((row) => ({
275
+ ...row,
276
+ deleted: !!row.deleted
277
+ }));
278
+ }));
279
+ queryRelations = Effect3.fn("ObjectMetaIndex.queryRelations")(({ endpoint, anchorDxns }) => Effect3.gen(function* () {
280
+ if (anchorDxns.length === 0) {
281
+ return [];
282
+ }
283
+ const sql = yield* SqlClient5.SqlClient;
284
+ const column = endpoint === "source" ? "source" : "target";
285
+ const rows = yield* sql`SELECT * FROM objectMeta WHERE entityKind = 'relation' AND ${sql.in(column, anchorDxns)}`;
286
+ return rows.map((row) => ({
287
+ ...row,
288
+ deleted: !!row.deleted
289
+ }));
290
+ }));
291
+ // TODO(dmaretskyi): Update recordId on objects so that we don't need to look it up separately.
292
+ update = Effect3.fn("ObjectMetaIndex.update")((objects) => Effect3.gen(function* () {
293
+ const sql = yield* SqlClient5.SqlClient;
294
+ yield* Effect3.forEach(objects, (object) => Effect3.gen(function* () {
295
+ const { spaceId, queueId, documentId, data } = object;
296
+ const castData = data;
297
+ const objectId = castData.id;
298
+ let existing;
299
+ if (documentId) {
300
+ existing = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND documentId = ${documentId} AND objectId = ${objectId} LIMIT 1`;
301
+ } else if (queueId) {
302
+ existing = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND queueId = ${queueId} AND objectId = ${objectId} LIMIT 1`;
303
+ } else {
304
+ existing = [];
305
+ }
306
+ const result = yield* sql`SELECT MAX(version) as v FROM objectMeta`;
307
+ const [{ v }] = result;
308
+ const version = (v ?? 0) + 1;
309
+ const entityKind = castData[ATTR_RELATION_SOURCE] ? "relation" : "object";
310
+ const typeDxn = castData[ATTR_TYPE] ? String(castData[ATTR_TYPE]) : "type";
311
+ const deleted = castData[ATTR_DELETED] ? 1 : 0;
312
+ const source = entityKind === "relation" ? castData[ATTR_RELATION_SOURCE] ?? null : null;
313
+ const target = entityKind === "relation" ? castData[ATTR_RELATION_TARGET] ?? null : null;
314
+ const parent = castData[ATTR_PARENT] ?? null;
315
+ if (existing.length > 0) {
316
+ yield* sql`
317
+ UPDATE objectMeta SET
318
+ version = ${version},
319
+ entityKind = ${entityKind},
320
+ typeDxn = ${typeDxn},
321
+ deleted = ${deleted},
322
+ source = ${source},
323
+ target = ${target},
324
+ parent = ${parent}
325
+ WHERE recordId = ${existing[0].recordId}
326
+ `;
327
+ } else {
328
+ yield* sql`
329
+ INSERT INTO objectMeta (
330
+ objectId, queueId, spaceId, documentId,
331
+ entityKind, typeDxn, deleted, source, target, parent, version
332
+ ) VALUES (
333
+ ${objectId}, ${queueId ?? ""}, ${spaceId}, ${documentId ?? ""},
334
+ ${entityKind}, ${typeDxn}, ${deleted},
335
+ ${source}, ${target}, ${parent}, ${version}
336
+ )
337
+ `;
338
+ }
339
+ }), {
340
+ discard: true
341
+ });
342
+ }));
343
+ /**
344
+ * Look up `recordIds` for objects that are already stored in the ObjectMetaIndex.
345
+ * Mutates the objects in place.
346
+ */
347
+ lookupRecordIds = Effect3.fn("ObjectMetaIndex.lookupRecordIds")((objects) => Effect3.gen(function* () {
348
+ const sql = yield* SqlClient5.SqlClient;
349
+ for (const object of objects) {
350
+ const { spaceId, queueId, documentId, data } = object;
351
+ const objectId = data.id;
352
+ let result;
353
+ if (documentId) {
354
+ result = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND documentId = ${documentId} AND objectId = ${objectId} LIMIT 1`;
355
+ } else if (queueId) {
356
+ result = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND queueId = ${queueId} AND objectId = ${objectId} LIMIT 1`;
357
+ } else {
358
+ result = [];
359
+ }
360
+ if (result.length === 0) {
361
+ return yield* Effect3.die(new Error(`Object not found in ObjectMetaIndex: ${spaceId}/${documentId ?? queueId}/${objectId}`));
362
+ }
363
+ object.recordId = result[0].recordId;
364
+ }
365
+ }));
366
+ /**
367
+ * Look up object metadata by recordIds.
368
+ */
369
+ lookupByRecordIds = Effect3.fn("ObjectMetaIndex.lookupByRecordIds")((recordIds) => Effect3.gen(function* () {
370
+ if (recordIds.length === 0) {
371
+ return [];
372
+ }
373
+ const sql = yield* SqlClient5.SqlClient;
374
+ const rows = yield* sql`SELECT * FROM objectMeta WHERE ${sql.in("recordId", recordIds)}`;
375
+ return rows.map((row) => ({
376
+ ...row,
377
+ deleted: !!row.deleted
378
+ }));
379
+ }));
380
+ /**
381
+ * Query children by parent object ids.
382
+ */
383
+ queryChildren = Effect3.fn("ObjectMetaIndex.queryChildren")((query) => Effect3.gen(function* () {
384
+ if (query.parentIds.length === 0) {
385
+ return [];
386
+ }
387
+ const sql = yield* SqlClient5.SqlClient;
388
+ const rows = yield* sql`SELECT * FROM objectMeta WHERE spaceId IN ${sql.in(query.spaceId)} AND parent IN ${sql.in(query.parentIds.map((id) => DXN.fromLocalObjectId(id).toString()))}`;
389
+ return rows.map((row) => ({
390
+ ...row,
391
+ deleted: !!row.deleted
392
+ }));
393
+ }));
394
+ };
395
+
396
+ // src/indexes/reverse-ref-index.ts
397
+ import * as SqlClient7 from "@effect/sql/SqlClient";
398
+ import * as Effect4 from "effect/Effect";
399
+ import * as Schema4 from "effect/Schema";
400
+ import { EncodedReference, isEncodedReference } from "@dxos/echo-protocol";
401
+
402
+ // src/utils.ts
403
+ import * as Schema3 from "effect/Schema";
404
+ import { invariant } from "@dxos/invariant";
405
+ var __dxlog_file = "/__w/dxos/dxos/packages/core/echo/index-core/src/utils.ts";
406
+ var EscapedPropPath = class extends Schema3.String.annotations({
407
+ title: "EscapedPropPath"
408
+ }) {
409
+ static escape(path) {
410
+ return path.map((p) => p.toString().replaceAll("\\", "\\\\").replaceAll(".", "\\.")).join(".");
411
+ }
412
+ static unescape(path) {
413
+ const parts = [];
414
+ let current = "";
415
+ for (let i = 0; i < path.length; i++) {
416
+ if (path[i] === "\\") {
417
+ invariant(i + 1 < path.length && (path[i + 1] === "." || path[i + 1] === "\\"), "Malformed escaping.", {
418
+ F: __dxlog_file,
419
+ L: 34,
420
+ S: this,
421
+ A: [
422
+ "i + 1 < path.length && (path[i + 1] === '.' || path[i + 1] === '\\\\')",
423
+ "'Malformed escaping.'"
424
+ ]
425
+ });
426
+ current = current + path[i + 1];
427
+ i++;
428
+ } else if (path[i] === ".") {
429
+ parts.push(current);
430
+ current = "";
431
+ } else {
432
+ current += path[i];
433
+ }
434
+ }
435
+ parts.push(current);
436
+ return parts;
437
+ }
438
+ };
439
+
440
+ // src/indexes/reverse-ref-index.ts
441
+ var extractReferences = (data) => {
442
+ const refs = [];
443
+ const visit = (path, value) => {
444
+ if (isEncodedReference(value)) {
445
+ const dxn = EncodedReference.toDXN(value);
446
+ const echoId = dxn.asEchoDXN()?.echoId;
447
+ if (!echoId) {
448
+ return;
449
+ }
450
+ refs.push({
451
+ path,
452
+ targetDxn: dxn.toString()
453
+ });
454
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
455
+ for (const [key, v] of Object.entries(value)) {
456
+ visit([
457
+ ...path,
458
+ key
459
+ ], v);
460
+ }
461
+ } else if (Array.isArray(value)) {
462
+ for (let i = 0; i < value.length; i++) {
463
+ visit([
464
+ ...path,
465
+ String(i)
466
+ ], value[i]);
467
+ }
468
+ }
469
+ };
470
+ visit([], data);
471
+ return refs;
472
+ };
473
+ var ReverseRef = Schema4.Struct({
474
+ recordId: Schema4.Number,
475
+ targetDxn: Schema4.String,
476
+ /**
477
+ * Escaped property path within an object.
478
+ *
479
+ * Escaping rules:
480
+ *
481
+ * - '.' -> '\.'
482
+ * - '\' -> '\\'
483
+ * - contact with .
484
+ */
485
+ propPath: Schema4.String
486
+ });
487
+ var ReverseRefIndex = class {
488
+ migrate = Effect4.fn("ReverseRefIndex.migrate")(function* () {
489
+ const sql = yield* SqlClient7.SqlClient;
490
+ yield* sql`CREATE TABLE IF NOT EXISTS reverseRef (
491
+ recordId INTEGER NOT NULL,
492
+ targetDxn TEXT NOT NULL,
493
+ propPath TEXT NOT NULL,
494
+ PRIMARY KEY (recordId, targetDxn, propPath)
495
+ )`;
496
+ yield* sql`CREATE INDEX IF NOT EXISTS idx_reverse_ref_target ON reverseRef(targetDxn)`;
497
+ });
498
+ /**
499
+ * Query all references pointing to a target DXN.
500
+ */
501
+ query = Effect4.fn("ReverseRefIndex.query")(({ targetDxn }) => Effect4.gen(function* () {
502
+ const sql = yield* SqlClient7.SqlClient;
503
+ const rows = yield* sql`SELECT * FROM reverseRef WHERE targetDxn = ${targetDxn}`;
504
+ return rows;
505
+ }));
506
+ update = Effect4.fn("ReverseRefIndex.update")((objects) => Effect4.gen(function* () {
507
+ const sql = yield* SqlClient7.SqlClient;
508
+ yield* Effect4.forEach(objects, (object) => Effect4.gen(function* () {
509
+ const { recordId, data } = object;
510
+ if (recordId === null) {
511
+ yield* Effect4.die(new Error("ReverseRefIndex.update requires recordId to be set"));
512
+ }
513
+ yield* sql`DELETE FROM reverseRef WHERE recordId = ${recordId}`;
514
+ const refs = extractReferences(data);
515
+ yield* Effect4.forEach(refs, (ref) => sql`INSERT INTO reverseRef (recordId, targetDxn, propPath) VALUES (${recordId}, ${ref.targetDxn}, ${EscapedPropPath.escape(ref.path)})`, {
516
+ discard: true
517
+ });
518
+ }), {
519
+ discard: true
520
+ });
521
+ }));
522
+ };
523
+
524
+ // src/index-engine.ts
525
+ var IndexEngine = class {
526
+ #tracker;
527
+ #objectMetaIndex;
528
+ #ftsIndex;
529
+ #reverseRefIndex;
530
+ constructor(params) {
531
+ this.#tracker = params?.tracker ?? new IndexTracker();
532
+ this.#objectMetaIndex = params?.objectMetaIndex ?? new ObjectMetaIndex();
533
+ this.#ftsIndex = params?.ftsIndex ?? new FtsIndex();
534
+ this.#reverseRefIndex = params?.reverseRefIndex ?? new ReverseRefIndex();
535
+ }
536
+ migrate() {
537
+ return Effect5.gen(this, function* () {
538
+ yield* this.#tracker.migrate();
539
+ yield* this.#objectMetaIndex.migrate();
540
+ yield* this.#ftsIndex.migrate();
541
+ yield* this.#reverseRefIndex.migrate();
542
+ });
543
+ }
544
+ /**
545
+ * Query text index and return full object metadata with rank.
546
+ */
547
+ queryText(query) {
548
+ return Effect5.gen(this, function* () {
549
+ return yield* this.#ftsIndex.query(query);
550
+ });
551
+ }
552
+ queryReverseRef(query) {
553
+ return this.#reverseRefIndex.query(query);
554
+ }
555
+ queryAll(query) {
556
+ return this.#objectMetaIndex.queryAll(query);
557
+ }
558
+ /**
559
+ * Query snapshots by recordIds.
560
+ * Used to load queue objects from indexed snapshots.
561
+ */
562
+ querySnapshotsJSON(recordIds) {
563
+ return this.#ftsIndex.querySnapshotsJSON(recordIds);
564
+ }
565
+ queryType(query) {
566
+ return this.#objectMetaIndex.query(query);
567
+ }
568
+ /**
569
+ * Query children by parent object ids.
570
+ */
571
+ queryChildren(query) {
572
+ return this.#objectMetaIndex.queryChildren(query);
573
+ }
574
+ queryTypes(query) {
575
+ return this.#objectMetaIndex.queryTypes(query);
576
+ }
577
+ queryRelations(query) {
578
+ return this.#objectMetaIndex.queryRelations(query);
579
+ }
580
+ lookupByRecordIds(recordIds) {
581
+ return this.#objectMetaIndex.lookupByRecordIds(recordIds);
582
+ }
583
+ update(dataSource, opts) {
584
+ return Effect5.gen(this, function* () {
585
+ let updated = 0;
586
+ const { updated: updatedFtsIndex, done: doneFtsIndex } = yield* this.#update(this.#ftsIndex, dataSource, {
587
+ indexName: "fts",
588
+ spaceId: opts.spaceId,
589
+ limit: opts.limit
590
+ });
591
+ updated += updatedFtsIndex;
592
+ const { updated: updatedReverseRefIndex, done: doneReverseRefIndex } = yield* this.#update(this.#reverseRefIndex, dataSource, {
593
+ indexName: "reverseRef",
594
+ spaceId: opts.spaceId,
595
+ limit: opts.limit
596
+ });
597
+ updated += updatedReverseRefIndex;
598
+ return {
599
+ updated,
600
+ done: doneFtsIndex && doneReverseRefIndex
601
+ };
602
+ }).pipe(Effect5.withSpan("IndexEngine.update"));
603
+ }
604
+ /**
605
+ * Update a dependent index that requires recordId enrichment.
606
+ * This method:
607
+ * 1. Gets changed objects from the source.
608
+ * 2. Ensures those objects exist in ObjectMetaIndex.
609
+ * 3. Looks up recordIds for those objects.
610
+ * 4. Enriches objects with recordIds.
611
+ * 5. Updates the dependent index.
612
+ */
613
+ #update(index, source, opts) {
614
+ return Effect5.gen(this, function* () {
615
+ const sqlTransaction = yield* SqlTransaction.SqlTransaction;
616
+ return yield* sqlTransaction.withTransaction(Effect5.gen(this, function* () {
617
+ const cursors = yield* this.#tracker.queryCursors({
618
+ indexName: opts.indexName,
619
+ sourceName: source.sourceName,
620
+ // Pass undefined to get all cursors when spaceId is null.
621
+ spaceId: opts.spaceId ?? void 0
622
+ });
623
+ const { objects, cursors: updatedCursors } = yield* source.getChangedObjects(cursors, {
624
+ limit: opts.limit
625
+ });
626
+ if (objects.length === 0) {
627
+ return {
628
+ updated: 0,
629
+ done: true
630
+ };
631
+ }
632
+ yield* this.#objectMetaIndex.update(objects);
633
+ yield* this.#objectMetaIndex.lookupRecordIds(objects);
634
+ yield* index.update(objects);
635
+ yield* this.#tracker.updateCursors(updatedCursors.map((_) => ({
636
+ indexName: opts.indexName,
637
+ spaceId: _.spaceId,
638
+ sourceName: source.sourceName,
639
+ resourceId: _.resourceId,
640
+ cursor: _.cursor
641
+ })));
642
+ return {
643
+ updated: objects.length,
644
+ done: false
645
+ };
646
+ }));
647
+ }).pipe(Effect5.withSpan("IndexEngine.#update"));
648
+ }
649
+ };
650
+ export {
651
+ EscapedPropPath,
652
+ FtsIndex,
653
+ IndexEngine,
654
+ IndexTracker,
655
+ ObjectMetaIndex,
656
+ ReverseRefIndex
657
+ };
658
+ //# sourceMappingURL=index.mjs.map