@dxos/index-core 0.0.0 → 0.8.4-main.03d5cd7b56

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