@dxos/index-core 0.8.4-main.fcfe5033a5 → 0.9.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.
Files changed (40) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/index.mjs +190 -101
  4. package/dist/lib/neutral/index.mjs.map +4 -4
  5. package/dist/lib/neutral/meta.json +1 -1
  6. package/dist/types/src/index-engine.d.ts +31 -17
  7. package/dist/types/src/index-engine.d.ts.map +1 -1
  8. package/dist/types/src/index-tracker.d.ts +3 -3
  9. package/dist/types/src/index.d.ts +3 -3
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/indexes/entity-meta-index.d.ts +113 -0
  12. package/dist/types/src/indexes/entity-meta-index.d.ts.map +1 -0
  13. package/dist/types/src/indexes/entity-meta-index.test.d.ts +2 -0
  14. package/dist/types/src/indexes/entity-meta-index.test.d.ts.map +1 -0
  15. package/dist/types/src/indexes/fts-index.d.ts +7 -6
  16. package/dist/types/src/indexes/fts-index.d.ts.map +1 -1
  17. package/dist/types/src/indexes/index.d.ts +1 -1
  18. package/dist/types/src/indexes/interface.d.ts +15 -4
  19. package/dist/types/src/indexes/interface.d.ts.map +1 -1
  20. package/dist/types/src/indexes/reverse-ref-index.d.ts +3 -2
  21. package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -1
  22. package/dist/types/src/utils.d.ts +3 -3
  23. package/dist/types/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +13 -18
  25. package/src/index-engine.test.ts +138 -16
  26. package/src/index-engine.ts +123 -58
  27. package/src/index.ts +9 -3
  28. package/src/indexes/{object-meta-index.test.ts → entity-meta-index.test.ts} +114 -53
  29. package/src/indexes/{object-meta-index.ts → entity-meta-index.ts} +140 -71
  30. package/src/indexes/fts-index.test.ts +188 -34
  31. package/src/indexes/fts-index.ts +32 -15
  32. package/src/indexes/index.ts +1 -1
  33. package/src/indexes/interface.ts +16 -4
  34. package/src/indexes/reverse-ref-index.test.ts +57 -43
  35. package/src/indexes/reverse-ref-index.ts +22 -14
  36. package/src/utils.ts +5 -5
  37. package/dist/types/src/indexes/object-meta-index.d.ts +0 -88
  38. package/dist/types/src/indexes/object-meta-index.d.ts.map +0 -1
  39. package/dist/types/src/indexes/object-meta-index.test.d.ts +0 -2
  40. package/dist/types/src/indexes/object-meta-index.test.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/index-core",
3
- "version": "0.8.4-main.fcfe5033a5",
3
+ "version": "0.9.0",
4
4
  "description": "Indexing core.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
12
12
  "author": "info@dxos.org",
13
13
  "sideEffects": false,
14
14
  "type": "module",
@@ -24,24 +24,19 @@
24
24
  "src"
25
25
  ],
26
26
  "dependencies": {
27
- "@effect/cli": "0.73.2",
28
- "@effect/experimental": "0.58.0",
29
- "@effect/platform": "0.94.4",
30
- "@effect/sql": "0.49.0",
31
- "effect": "3.20.0",
32
- "@dxos/async": "0.8.4-main.fcfe5033a5",
33
- "@dxos/context": "0.8.4-main.fcfe5033a5",
34
- "@dxos/debug": "0.8.4-main.fcfe5033a5",
35
- "@dxos/echo-protocol": "0.8.4-main.fcfe5033a5",
36
- "@dxos/invariant": "0.8.4-main.fcfe5033a5",
37
- "@dxos/keys": "0.8.4-main.fcfe5033a5",
38
- "@dxos/echo": "0.8.4-main.fcfe5033a5",
39
- "@dxos/effect": "0.8.4-main.fcfe5033a5",
40
- "@dxos/sql-sqlite": "0.8.4-main.fcfe5033a5",
41
- "@dxos/log": "0.8.4-main.fcfe5033a5"
27
+ "@effect/experimental": "0.60.0",
28
+ "@effect/platform": "0.96.1",
29
+ "@effect/sql": "0.51.1",
30
+ "effect": "3.21.3",
31
+ "@dxos/context": "0.9.0",
32
+ "@dxos/echo-protocol": "0.9.0",
33
+ "@dxos/echo": "0.9.0",
34
+ "@dxos/sql-sqlite": "0.9.0",
35
+ "@dxos/invariant": "0.9.0",
36
+ "@dxos/keys": "0.9.0"
42
37
  },
43
38
  "devDependencies": {
44
- "@effect/sql-sqlite-node": "0.50.1"
39
+ "@effect/sql-sqlite-node": "0.52.0"
45
40
  },
46
41
  "publishConfig": {
47
42
  "access": "public"
@@ -11,16 +11,16 @@ import * as Layer from 'effect/Layer';
11
11
  import { Context } from '@dxos/context';
12
12
  import { ATTR_TYPE } from '@dxos/echo/internal';
13
13
  import { invariant } from '@dxos/invariant';
14
- import { DXN, ObjectId, SpaceId } from '@dxos/keys';
14
+ import { DXN, EntityId, SpaceId } from '@dxos/keys';
15
15
  import * as SqlTransaction from '@dxos/sql-sqlite/SqlTransaction';
16
16
 
17
- import { type DataSourceCursor, type IndexDataSource, IndexEngine } from './index-engine';
17
+ import { type DataSourceCursor, type IndexDataSource, IndexEngine, type IndexingResult } from './index-engine';
18
18
  import { type IndexCursor, IndexTracker } from './index-tracker';
19
- import { FtsIndex, type IndexerObject, ObjectMetaIndex, ReverseRefIndex } from './indexes';
19
+ import { FtsIndex, type IndexerObject, EntityMetaIndex, ReverseRefIndex } from './indexes';
20
20
 
21
- const TYPE_DEFAULT = DXN.parse('dxn:type:test.com/type/Type:0.1.0').toString();
22
- const TYPE_A = DXN.parse('dxn:type:test.com/type/TypeA:0.1.0').toString();
23
- const TYPE_B = DXN.parse('dxn:type:test.com/type/TypeB:0.1.0').toString();
21
+ const TYPE_DEFAULT = DXN.make('com.example.type.Type', '0.1.0');
22
+ const TYPE_A = DXN.make('com.example.type.TypeA', '0.1.0');
23
+ const TYPE_B = DXN.make('com.example.type.TypeB', '0.1.0');
24
24
 
25
25
  const TestLayer = SqlTransaction.layer.pipe(
26
26
  Layer.provideMerge(
@@ -95,7 +95,7 @@ describe('IndexEngine', () => {
95
95
  const setup = Effect.gen(function* () {
96
96
  const tracker = new IndexTracker();
97
97
  yield* tracker.migrate();
98
- const metaIndex = new ObjectMetaIndex();
98
+ const metaIndex = new EntityMetaIndex();
99
99
  yield* metaIndex.migrate();
100
100
  const ftsIndex = new FtsIndex();
101
101
  yield* ftsIndex.migrate();
@@ -119,10 +119,12 @@ describe('IndexEngine', () => {
119
119
  spaceId,
120
120
  documentId: 'doc-1',
121
121
  queueId: null,
122
+ queueNamespace: null,
122
123
  recordId: null,
124
+ createdAt: null,
123
125
  updatedAt: Date.now(),
124
126
  data: {
125
- id: ObjectId.random(),
127
+ id: EntityId.random(),
126
128
  [ATTR_TYPE]: TYPE_DEFAULT,
127
129
  title: 'Hello',
128
130
  },
@@ -136,7 +138,7 @@ describe('IndexEngine', () => {
136
138
  expect(updated).toBe(2);
137
139
 
138
140
  // Verify using the SAME index instance.
139
- const results1 = yield* metaIndex.query({ spaceId: spaceId.toString(), typeDxn: TYPE_DEFAULT });
141
+ const results1 = yield* metaIndex.query({ spaceId, typeDXN: TYPE_DEFAULT });
140
142
  expect(results1).toHaveLength(1);
141
143
  expect(results1[0].objectId).toBe(obj1.data.id);
142
144
  expect(results1[0].version).toBeGreaterThan(0);
@@ -156,7 +158,9 @@ describe('IndexEngine', () => {
156
158
  spaceId,
157
159
  documentId: obj1.documentId,
158
160
  queueId: null,
161
+ queueNamespace: null,
159
162
  recordId: null,
163
+ createdAt: null,
160
164
  updatedAt: Date.now(),
161
165
  data: { id: obj1.data.id, [ATTR_TYPE]: obj1.data[ATTR_TYPE], title: 'Hello World' },
162
166
  };
@@ -167,7 +171,7 @@ describe('IndexEngine', () => {
167
171
  expect(updated2).toBe(2);
168
172
 
169
173
  // Verify update.
170
- const results2 = yield* metaIndex.query({ spaceId: spaceId.toString(), typeDxn: TYPE_DEFAULT });
174
+ const results2 = yield* metaIndex.query({ spaceId, typeDXN: TYPE_DEFAULT });
171
175
  expect(results2).toHaveLength(1);
172
176
  expect(results2[0].objectId).toBe(obj1Updated.data.id);
173
177
  expect(results2[0].version).toBeGreaterThan(results1[0].version);
@@ -195,11 +199,13 @@ describe('IndexEngine', () => {
195
199
  {
196
200
  spaceId,
197
201
  queueId: null,
202
+ queueNamespace: null,
198
203
  documentId: 'd1',
199
204
  recordId: null,
205
+ createdAt: null,
200
206
  updatedAt: Date.now(),
201
207
  data: {
202
- id: ObjectId.random(),
208
+ id: EntityId.random(),
203
209
  [ATTR_TYPE]: TYPE_A,
204
210
  val: 1,
205
211
  },
@@ -207,11 +213,13 @@ describe('IndexEngine', () => {
207
213
  {
208
214
  spaceId,
209
215
  queueId: null,
216
+ queueNamespace: null,
210
217
  documentId: 'd2',
211
218
  recordId: null,
219
+ createdAt: null,
212
220
  updatedAt: Date.now(),
213
221
  data: {
214
- id: ObjectId.random(),
222
+ id: EntityId.random(),
215
223
  [ATTR_TYPE]: TYPE_A,
216
224
  val: 2,
217
225
  },
@@ -219,11 +227,13 @@ describe('IndexEngine', () => {
219
227
  {
220
228
  spaceId,
221
229
  queueId: null,
230
+ queueNamespace: null,
222
231
  documentId: 'd3',
223
232
  recordId: null,
233
+ createdAt: null,
224
234
  updatedAt: Date.now(),
225
235
  data: {
226
- id: ObjectId.random(),
236
+ id: EntityId.random(),
227
237
  [ATTR_TYPE]: TYPE_B,
228
238
  val: 3,
229
239
  },
@@ -234,10 +244,10 @@ describe('IndexEngine', () => {
234
244
 
235
245
  yield* engine.update(Context.default(), dataSource, { spaceId: null });
236
246
 
237
- const resultsA = yield* metaIndex.query({ spaceId: spaceId.toString(), typeDxn: TYPE_A });
247
+ const resultsA = yield* metaIndex.query({ spaceId, typeDXN: TYPE_A });
238
248
  expect(resultsA).toHaveLength(2);
239
249
 
240
- const resultsB = yield* metaIndex.query({ spaceId: spaceId.toString(), typeDxn: TYPE_B });
250
+ const resultsB = yield* metaIndex.query({ spaceId, typeDXN: TYPE_B });
241
251
  expect(resultsB).toHaveLength(1);
242
252
 
243
253
  const ftsResults = yield* ftsIndex.query({
@@ -269,10 +279,12 @@ describe('IndexEngine', () => {
269
279
  {
270
280
  spaceId,
271
281
  queueId: null,
282
+ queueNamespace: null,
272
283
  documentId: 'doc-done-test',
273
284
  recordId: null,
285
+ createdAt: null,
274
286
  updatedAt: Date.now(),
275
- data: { id: ObjectId.random(), [ATTR_TYPE]: TYPE_DEFAULT, title: 'Done test' },
287
+ data: { id: EntityId.random(), [ATTR_TYPE]: TYPE_DEFAULT, title: 'Done test' },
276
288
  },
277
289
  ]);
278
290
 
@@ -289,4 +301,114 @@ describe('IndexEngine', () => {
289
301
  expect(done2).toBe(true);
290
302
  }, Effect.provide(TestLayer)),
291
303
  );
304
+
305
+ it.effect(
306
+ 'IndexingResult contains correct sets for a batch with multiple objects across spaces',
307
+ Effect.fnUntraced(function* () {
308
+ const { tracker, metaIndex, ftsIndex, reverseRefIndex } = yield* setup;
309
+ const engine = new IndexEngine({ tracker, objectMetaIndex: metaIndex, ftsIndex, reverseRefIndex });
310
+ const dataSource = new MockIndexDataSource();
311
+ const spaceId1 = SpaceId.random();
312
+ const spaceId2 = SpaceId.random();
313
+ const id1 = EntityId.random();
314
+ const id2 = EntityId.random();
315
+
316
+ const obj1: IndexerObject = {
317
+ spaceId: spaceId1,
318
+ documentId: 'doc-result-1',
319
+ queueId: null,
320
+ queueNamespace: null,
321
+ recordId: null,
322
+ createdAt: null,
323
+ updatedAt: Date.now(),
324
+ data: { id: id1, [ATTR_TYPE]: TYPE_A, title: 'Doc in space1' },
325
+ };
326
+ const obj2: IndexerObject = {
327
+ spaceId: spaceId2,
328
+ documentId: 'doc-result-2',
329
+ queueId: null,
330
+ queueNamespace: null,
331
+ recordId: null,
332
+ createdAt: null,
333
+ updatedAt: Date.now(),
334
+ data: { id: id2, [ATTR_TYPE]: TYPE_B, title: 'Doc in space2' },
335
+ };
336
+
337
+ dataSource.push([obj1, obj2]);
338
+
339
+ const result: IndexingResult = yield* engine.update(Context.default(), dataSource, { spaceId: null });
340
+
341
+ expect(result.updated).toBeGreaterThan(0);
342
+ expect(result.done).toBe(false);
343
+
344
+ // Spaces: both spaceIds should be present.
345
+ expect(result.spaces.has(spaceId1)).toBe(true);
346
+ expect(result.spaces.has(spaceId2)).toBe(true);
347
+
348
+ // Documents: both doc IDs should be present.
349
+ expect(result.documents.has('doc-result-1')).toBe(true);
350
+ expect(result.documents.has('doc-result-2')).toBe(true);
351
+
352
+ // Types: both TYPE_A and TYPE_B.
353
+ expect(result.types.has(TYPE_A)).toBe(true);
354
+ expect(result.types.has(TYPE_B)).toBe(true);
355
+
356
+ // Object ids.
357
+ expect(result.objects.has(id1)).toBe(true);
358
+ expect(result.objects.has(id2)).toBe(true);
359
+ }, Effect.provide(TestLayer)),
360
+ );
361
+
362
+ it.effect(
363
+ 'IndexingResult includes typename for deleted objects',
364
+ Effect.fnUntraced(function* () {
365
+ const { tracker, metaIndex, ftsIndex, reverseRefIndex } = yield* setup;
366
+ const engine = new IndexEngine({ tracker, objectMetaIndex: metaIndex, ftsIndex, reverseRefIndex });
367
+ const dataSource = new MockIndexDataSource();
368
+ const spaceId = SpaceId.random();
369
+
370
+ const deletedObj: IndexerObject = {
371
+ spaceId,
372
+ documentId: 'doc-deleted',
373
+ queueId: null,
374
+ queueNamespace: null,
375
+ recordId: null,
376
+ createdAt: null,
377
+ updatedAt: Date.now(),
378
+ data: {
379
+ id: EntityId.random(),
380
+ [ATTR_TYPE]: TYPE_DEFAULT,
381
+ '@deleted': true,
382
+ },
383
+ };
384
+
385
+ dataSource.push([deletedObj]);
386
+
387
+ const result: IndexingResult = yield* engine.update(Context.default(), dataSource, { spaceId: null });
388
+
389
+ expect(result.updated).toBeGreaterThan(0);
390
+ // Deleted objects should still contribute their typename to the hint.
391
+ expect(result.types.has(TYPE_DEFAULT)).toBe(true);
392
+ expect(result.spaces.has(spaceId)).toBe(true);
393
+ }, Effect.provide(TestLayer)),
394
+ );
395
+
396
+ it.effect(
397
+ 'IndexingResult is empty when no objects are indexed',
398
+ Effect.fnUntraced(function* () {
399
+ const { tracker, metaIndex, ftsIndex, reverseRefIndex } = yield* setup;
400
+ const engine = new IndexEngine({ tracker, objectMetaIndex: metaIndex, ftsIndex, reverseRefIndex });
401
+ const dataSource = new MockIndexDataSource();
402
+
403
+ const result: IndexingResult = yield* engine.update(Context.default(), dataSource, { spaceId: null });
404
+
405
+ expect(result.updated).toBe(0);
406
+ expect(result.done).toBe(true);
407
+ expect(result.spaces.size).toBe(0);
408
+ expect(result.queues.size).toBe(0);
409
+ expect(result.documents.size).toBe(0);
410
+ expect(result.types.size).toBe(0);
411
+ expect(result.objects.size).toBe(0);
412
+ }, Effect.provide(TestLayer)),
413
+ );
292
414
  });
@@ -7,7 +7,8 @@ import type * as SqlError from '@effect/sql/SqlError';
7
7
  import * as Effect from 'effect/Effect';
8
8
 
9
9
  import { type Context } from '@dxos/context';
10
- import type { ObjectId, SpaceId } from '@dxos/keys';
10
+ import { ATTR_TYPE } from '@dxos/echo/internal';
11
+ import type { EntityId, SpaceId } from '@dxos/keys';
11
12
  import * as SqlTransaction from '@dxos/sql-sqlite/SqlTransaction';
12
13
 
13
14
  import { type IndexCursor, IndexTracker } from './index-tracker';
@@ -17,12 +18,65 @@ import {
17
18
  type FtsQueryResult,
18
19
  type Index,
19
20
  type IndexerObject,
20
- type ObjectMeta,
21
- ObjectMetaIndex,
21
+ type EntityMeta,
22
+ EntityMetaIndex,
22
23
  ReverseRefIndex,
23
24
  type ReverseRefQuery,
24
25
  } from './indexes';
25
26
 
27
+ /**
28
+ * Result of a single indexing pass over a data source.
29
+ * Carries enough metadata for callers to build targeted invalidation hints.
30
+ */
31
+ export type IndexingResult = {
32
+ updated: number;
33
+ done: boolean;
34
+ spaces: ReadonlySet<SpaceId>;
35
+ queues: ReadonlySet<EntityId>;
36
+ documents: ReadonlySet<string>;
37
+ types: ReadonlySet<string>;
38
+ objects: ReadonlySet<EntityId>;
39
+ };
40
+
41
+ type MutableIndexingResult = {
42
+ updated: number;
43
+ done: boolean;
44
+ spaces: Set<SpaceId>;
45
+ queues: Set<EntityId>;
46
+ documents: Set<string>;
47
+ types: Set<string>;
48
+ objects: Set<EntityId>;
49
+ };
50
+
51
+ const makeEmptyIndexingResult = (): MutableIndexingResult => ({
52
+ updated: 0,
53
+ done: true,
54
+ spaces: new Set(),
55
+ queues: new Set(),
56
+ documents: new Set(),
57
+ types: new Set(),
58
+ objects: new Set(),
59
+ });
60
+
61
+ const accumulateIndexingResult = (acc: MutableIndexingResult, objects: readonly IndexerObject[]) => {
62
+ for (const obj of objects) {
63
+ acc.spaces.add(obj.spaceId);
64
+ if (obj.queueId) {
65
+ acc.queues.add(obj.queueId);
66
+ }
67
+ if (obj.documentId) {
68
+ acc.documents.add(obj.documentId);
69
+ }
70
+ const t = (obj.data as Record<string, unknown>)[ATTR_TYPE];
71
+ if (t) {
72
+ acc.types.add(String(t));
73
+ }
74
+ if (obj.data.id) {
75
+ acc.objects.add(obj.data.id as EntityId);
76
+ }
77
+ }
78
+ };
79
+
26
80
  /**
27
81
  * Cursor into indexable data-source.
28
82
  */
@@ -52,20 +106,20 @@ export interface IndexDataSource {
52
106
 
53
107
  export interface IndexEngineParams {
54
108
  tracker: IndexTracker;
55
- objectMetaIndex: ObjectMetaIndex;
109
+ objectMetaIndex: EntityMetaIndex;
56
110
  ftsIndex: FtsIndex;
57
111
  reverseRefIndex: ReverseRefIndex;
58
112
  }
59
113
 
60
114
  export class IndexEngine {
61
115
  readonly #tracker: IndexTracker;
62
- readonly #objectMetaIndex: ObjectMetaIndex;
116
+ readonly #objectMetaIndex: EntityMetaIndex;
63
117
  readonly #ftsIndex: FtsIndex;
64
118
  readonly #reverseRefIndex: ReverseRefIndex;
65
119
 
66
120
  constructor(params?: IndexEngineParams) {
67
121
  this.#tracker = params?.tracker ?? new IndexTracker();
68
- this.#objectMetaIndex = params?.objectMetaIndex ?? new ObjectMetaIndex();
122
+ this.#objectMetaIndex = params?.objectMetaIndex ?? new EntityMetaIndex();
69
123
  this.#ftsIndex = params?.ftsIndex ?? new FtsIndex();
70
124
  this.#reverseRefIndex = params?.reverseRefIndex ?? new ReverseRefIndex();
71
125
  }
@@ -97,7 +151,7 @@ export class IndexEngine {
97
151
  spaceIds: readonly SpaceId[];
98
152
  includeAllQueues?: boolean;
99
153
  queueIds?: readonly string[] | null;
100
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
154
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
101
155
  return this.#objectMetaIndex.queryAll(query);
102
156
  }
103
157
 
@@ -110,8 +164,8 @@ export class IndexEngine {
110
164
  }
111
165
 
112
166
  queryType(
113
- query: Pick<ObjectMeta, 'spaceId' | 'typeDxn'>,
114
- ): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
167
+ query: Pick<EntityMeta, 'spaceId' | 'typeDXN'>,
168
+ ): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
115
169
  return this.#objectMetaIndex.query(query);
116
170
  }
117
171
 
@@ -120,18 +174,18 @@ export class IndexEngine {
120
174
  */
121
175
  queryChildren(query: {
122
176
  spaceId: SpaceId[];
123
- parentIds: ObjectId[];
124
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
177
+ parentIds: EntityId[];
178
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
125
179
  return this.#objectMetaIndex.queryChildren(query);
126
180
  }
127
181
 
128
182
  queryTypes(query: {
129
183
  spaceIds: readonly SpaceId[];
130
- typeDxns: readonly ObjectMeta['typeDxn'][];
184
+ typeDxns: readonly EntityMeta['typeDXN'][];
131
185
  inverted?: boolean;
132
186
  includeAllQueues?: boolean;
133
187
  queueIds?: readonly string[] | null;
134
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
188
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
135
189
  return this.#objectMetaIndex.queryTypes(query);
136
190
  }
137
191
  queryByTimeRange(query: {
@@ -142,17 +196,17 @@ export class IndexEngine {
142
196
  createdBefore?: number;
143
197
  includeAllQueues?: boolean;
144
198
  queueIds?: readonly string[] | null;
145
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
199
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
146
200
  return this.#objectMetaIndex.queryByTimeRange(query);
147
201
  }
148
202
 
149
203
  queryRelations(query: {
150
204
  endpoint: 'source' | 'target';
151
205
  anchorDxns: readonly string[];
152
- }): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
206
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
153
207
  return this.#objectMetaIndex.queryRelations(query);
154
208
  }
155
- lookupByRecordIds(recordIds: number[]): Effect.Effect<readonly ObjectMeta[], SqlError.SqlError, SqlClient.SqlClient> {
209
+ lookupByRecordIds(recordIds: number[]): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
156
210
  return this.#objectMetaIndex.lookupByRecordIds(recordIds);
157
211
  }
158
212
 
@@ -160,45 +214,52 @@ export class IndexEngine {
160
214
  objectId: string;
161
215
  spaceId: string;
162
216
  queueId: string;
163
- }): Effect.Effect<ObjectMeta | null, SqlError.SqlError, SqlClient.SqlClient> {
217
+ }): Effect.Effect<EntityMeta | null, SqlError.SqlError, SqlClient.SqlClient> {
164
218
  return this.#objectMetaIndex.lookupByObjectId(query);
165
219
  }
166
220
 
221
+ queryObjectIds(query: {
222
+ spaceIds: readonly SpaceId[];
223
+ objectIds: readonly EntityMeta['objectId'][];
224
+ }): Effect.Effect<readonly EntityMeta[], SqlError.SqlError, SqlClient.SqlClient> {
225
+ return this.#objectMetaIndex.queryObjectIds(query);
226
+ }
227
+
167
228
  update(
168
229
  ctx: Context,
169
230
  dataSource: IndexDataSource,
170
231
  opts: { spaceId: SpaceId | null; limit?: number },
171
- ): Effect.Effect<
172
- { updated: number; done: boolean },
173
- SqlError.SqlError,
174
- SqlTransaction.SqlTransaction | SqlClient.SqlClient
175
- > {
232
+ ): Effect.Effect<IndexingResult, SqlError.SqlError, SqlTransaction.SqlTransaction | SqlClient.SqlClient> {
176
233
  return Effect.gen(this, function* () {
177
- let updated = 0;
178
- let done = true;
234
+ const result = makeEmptyIndexingResult();
179
235
 
180
- const { updated: updatedFtsIndex, done: doneFtsIndex } = yield* this.#update(ctx, this.#ftsIndex, dataSource, {
236
+ const {
237
+ updated: updatedFtsIndex,
238
+ done: doneFtsIndex,
239
+ objects: ftsObjects,
240
+ } = yield* this.#update(ctx, this.#ftsIndex, dataSource, {
181
241
  indexName: 'fts5',
182
242
  spaceId: opts.spaceId,
183
243
  limit: opts.limit,
184
244
  });
185
- updated += updatedFtsIndex;
186
- done = done && doneFtsIndex;
187
-
188
- const { updated: updatedReverseRefIndex, done: doneReverseRefIndex } = yield* this.#update(
189
- ctx,
190
- this.#reverseRefIndex,
191
- dataSource,
192
- {
193
- indexName: 'reverseRef',
194
- spaceId: opts.spaceId,
195
- limit: opts.limit,
196
- },
197
- );
198
- updated += updatedReverseRefIndex;
199
- done = done && doneReverseRefIndex;
245
+ result.updated += updatedFtsIndex;
246
+ result.done = result.done && doneFtsIndex;
247
+ accumulateIndexingResult(result, ftsObjects);
200
248
 
201
- return { updated, done };
249
+ const {
250
+ updated: updatedReverseRefIndex,
251
+ done: doneReverseRefIndex,
252
+ objects: reverseRefObjects,
253
+ } = yield* this.#update(ctx, this.#reverseRefIndex, dataSource, {
254
+ indexName: 'reverseRef',
255
+ spaceId: opts.spaceId,
256
+ limit: opts.limit,
257
+ });
258
+ result.updated += updatedReverseRefIndex;
259
+ result.done = result.done && doneReverseRefIndex;
260
+ accumulateIndexingResult(result, reverseRefObjects);
261
+
262
+ return result as IndexingResult;
202
263
  }).pipe(Effect.withSpan('IndexEngine.update'));
203
264
  }
204
265
 
@@ -206,7 +267,7 @@ export class IndexEngine {
206
267
  * Update a dependent index that requires recordId enrichment.
207
268
  * This method:
208
269
  * 1. Gets changed objects from the source.
209
- * 2. Ensures those objects exist in ObjectMetaIndex.
270
+ * 2. Ensures those objects exist in EntityMetaIndex.
210
271
  * 3. Looks up recordIds for those objects.
211
272
  * 4. Enriches objects with recordIds.
212
273
  * 5. Updates the dependent index.
@@ -217,29 +278,33 @@ export class IndexEngine {
217
278
  source: IndexDataSource,
218
279
  opts: { indexName: string; spaceId: SpaceId | null; limit?: number },
219
280
  ): Effect.Effect<
220
- { updated: number; done: boolean },
281
+ { updated: number; done: boolean; objects: readonly IndexerObject[] },
221
282
  SqlError.SqlError,
222
283
  SqlTransaction.SqlTransaction | SqlClient.SqlClient
223
284
  > {
224
285
  return Effect.gen(this, function* () {
225
286
  const sqlTransaction = yield* SqlTransaction.SqlTransaction;
226
287
 
288
+ // Reads run OUTSIDE the transaction: getChangedObjects may call RuntimeProvider.runPromise
289
+ // internally (e.g. listDocumentHeads), which creates a fresh Effect fiber with no
290
+ // TransactionConnection context. If those reads ran inside withTransaction, they would
291
+ // try to acquire the same semaphore that the transaction already holds — causing a deadlock.
292
+ const cursors = yield* this.#tracker.queryCursors({
293
+ indexName: opts.indexName,
294
+ sourceName: source.sourceName,
295
+ // Pass undefined to get all cursors when spaceId is null.
296
+ spaceId: opts.spaceId ?? undefined,
297
+ });
298
+ const { objects, cursors: updatedCursors } = yield* source.getChangedObjects(ctx, cursors, { limit: opts.limit });
299
+
300
+ if (objects.length === 0) {
301
+ return { updated: 0, done: true, objects: [] as readonly IndexerObject[] };
302
+ }
303
+
304
+ // Writes run INSIDE the transaction for atomicity.
227
305
  return yield* sqlTransaction.withTransaction(
228
306
  Effect.gen(this, function* () {
229
- const cursors = yield* this.#tracker.queryCursors({
230
- indexName: opts.indexName,
231
- sourceName: source.sourceName,
232
- // Pass undefined to get all cursors when spaceId is null.
233
- spaceId: opts.spaceId ?? undefined,
234
- });
235
- const { objects, cursors: updatedCursors } = yield* source.getChangedObjects(ctx, cursors, {
236
- limit: opts.limit,
237
- });
238
- if (objects.length === 0) {
239
- return { updated: 0, done: true };
240
- }
241
-
242
- // Ensure objects exist in ObjectMetaIndex.
307
+ // Ensure objects exist in EntityMetaIndex.
243
308
  yield* this.#objectMetaIndex.update(objects);
244
309
 
245
310
  // Look up recordIds for the objects.
@@ -257,7 +322,7 @@ export class IndexEngine {
257
322
  }),
258
323
  ),
259
324
  );
260
- return { updated: objects.length, done: false };
325
+ return { updated: objects.length, done: false, objects };
261
326
  }),
262
327
  );
263
328
  }).pipe(Effect.withSpan('IndexEngine.#update'));
package/src/index.ts CHANGED
@@ -2,10 +2,16 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export { IndexEngine, type IndexDataSource, type DataSourceCursor, type IndexEngineParams } from './index-engine';
5
+ export {
6
+ IndexEngine,
7
+ type IndexDataSource,
8
+ type DataSourceCursor,
9
+ type IndexEngineParams,
10
+ type IndexingResult,
11
+ } from './index-engine';
6
12
  export { IndexTracker, type IndexCursor } from './index-tracker';
7
13
  export { type IndexerObject, type Index } from './indexes/interface';
8
14
  export { FtsIndex, type FtsQuery } from './indexes/fts-index';
9
- export { ObjectMetaIndex, type ObjectMeta } from './indexes/object-meta-index';
15
+ export { EntityMetaIndex, type EntityMeta } from './indexes/entity-meta-index';
10
16
  export { ReverseRefIndex, type ReverseRef, type ReverseRefQuery } from './indexes/reverse-ref-index';
11
- export { EscapedPropPath, type ObjectPropPath } from './utils';
17
+ export { EscapedPropPath, type EntityPropPath } from './utils';