@dxos/index-core 0.0.0 → 0.8.4-main.21d9917

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