@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.
- package/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/index.mjs +190 -101
- package/dist/lib/neutral/index.mjs.map +4 -4
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/types/src/index-engine.d.ts +31 -17
- package/dist/types/src/index-engine.d.ts.map +1 -1
- package/dist/types/src/index-tracker.d.ts +3 -3
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/indexes/entity-meta-index.d.ts +113 -0
- package/dist/types/src/indexes/entity-meta-index.d.ts.map +1 -0
- package/dist/types/src/indexes/entity-meta-index.test.d.ts +2 -0
- package/dist/types/src/indexes/entity-meta-index.test.d.ts.map +1 -0
- package/dist/types/src/indexes/fts-index.d.ts +7 -6
- package/dist/types/src/indexes/fts-index.d.ts.map +1 -1
- package/dist/types/src/indexes/index.d.ts +1 -1
- package/dist/types/src/indexes/interface.d.ts +15 -4
- package/dist/types/src/indexes/interface.d.ts.map +1 -1
- package/dist/types/src/indexes/reverse-ref-index.d.ts +3 -2
- package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -1
- package/dist/types/src/utils.d.ts +3 -3
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -18
- package/src/index-engine.test.ts +138 -16
- package/src/index-engine.ts +123 -58
- package/src/index.ts +9 -3
- package/src/indexes/{object-meta-index.test.ts → entity-meta-index.test.ts} +114 -53
- package/src/indexes/{object-meta-index.ts → entity-meta-index.ts} +140 -71
- package/src/indexes/fts-index.test.ts +188 -34
- package/src/indexes/fts-index.ts +32 -15
- package/src/indexes/index.ts +1 -1
- package/src/indexes/interface.ts +16 -4
- package/src/indexes/reverse-ref-index.test.ts +57 -43
- package/src/indexes/reverse-ref-index.ts +22 -14
- package/src/utils.ts +5 -5
- package/dist/types/src/indexes/object-meta-index.d.ts +0 -88
- package/dist/types/src/indexes/object-meta-index.d.ts.map +0 -1
- package/dist/types/src/indexes/object-meta-index.test.d.ts +0 -2
- 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.
|
|
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": "
|
|
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/
|
|
28
|
-
"@effect/
|
|
29
|
-
"@effect/
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"@dxos/
|
|
33
|
-
"@dxos/
|
|
34
|
-
"@dxos/
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
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.
|
|
39
|
+
"@effect/sql-sqlite-node": "0.52.0"
|
|
45
40
|
},
|
|
46
41
|
"publishConfig": {
|
|
47
42
|
"access": "public"
|
package/src/index-engine.test.ts
CHANGED
|
@@ -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,
|
|
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,
|
|
19
|
+
import { FtsIndex, type IndexerObject, EntityMetaIndex, ReverseRefIndex } from './indexes';
|
|
20
20
|
|
|
21
|
-
const TYPE_DEFAULT = DXN.
|
|
22
|
-
const TYPE_A = DXN.
|
|
23
|
-
const TYPE_B = DXN.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
});
|
package/src/index-engine.ts
CHANGED
|
@@ -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
|
|
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
|
|
21
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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<
|
|
114
|
-
): Effect.Effect<readonly
|
|
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:
|
|
124
|
-
}): Effect.Effect<readonly
|
|
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
|
|
184
|
+
typeDxns: readonly EntityMeta['typeDXN'][];
|
|
131
185
|
inverted?: boolean;
|
|
132
186
|
includeAllQueues?: boolean;
|
|
133
187
|
queueIds?: readonly string[] | null;
|
|
134
|
-
}): Effect.Effect<readonly
|
|
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
|
|
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
|
|
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
|
|
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<
|
|
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
|
-
|
|
178
|
-
let done = true;
|
|
234
|
+
const result = makeEmptyIndexingResult();
|
|
179
235
|
|
|
180
|
-
const {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
17
|
+
export { EscapedPropPath, type EntityPropPath } from './utils';
|