@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
@@ -14,9 +14,13 @@ import { DXN, ObjectId, SpaceId } from '@dxos/keys';
14
14
  import type { IndexerObject } from './interface';
15
15
  import { ObjectMetaIndex } from './object-meta-index';
16
16
 
17
- const TYPE_PERSON = DXN.parse('dxn:type:example.com/type/Person:0.1.0').toString();
18
- const TYPE_RELATION = DXN.parse('dxn:type:example.com/type/Relation:0.1.0').toString();
19
- const TYPE_RELATION_UPDATED = DXN.parse('dxn:type:example.com/type/RelationUpdated:0.1.0').toString();
17
+ const TYPE_PERSON = DXN.parse('dxn:type:com.example.type.person:0.1.0').toString();
18
+ const TYPE_PERSON_VERSIONLESS = DXN.parse('dxn:type:com.example.type.person').toString();
19
+ const TYPE_RELATION = DXN.parse('dxn:type:com.example.type.relation:0.1.0').toString();
20
+ const TYPE_RELATION_UPDATED = DXN.parse('dxn:type:com.example.type.relation-updated:0.1.0').toString();
21
+ const TYPE_WITH_UNDERSCORE = DXN.parse('dxn:type:com.example.type.person-extra:0.1.0').toString();
22
+ const TYPE_WITH_UNDERSCORE_VERSIONLESS = DXN.parse('dxn:type:com.example.type.person-extra').toString();
23
+ const TYPE_UNDERSCORE_FALSE_POSITIVE = DXN.parse('dxn:type:com.example.type.person-a-extra:0.1.0').toString();
20
24
 
21
25
  const TestLayer = Layer.merge(
22
26
  SqliteClient.layer({
@@ -26,6 +30,93 @@ const TestLayer = Layer.merge(
26
30
  );
27
31
 
28
32
  describe('ObjectMetaIndex', () => {
33
+ it.effect('should match versioned types when queried by versionless type', () =>
34
+ Effect.gen(function* () {
35
+ const index = new ObjectMetaIndex();
36
+ yield* index.migrate();
37
+
38
+ const spaceId = SpaceId.random();
39
+ const objectId = ObjectId.random();
40
+
41
+ const item: IndexerObject = {
42
+ spaceId,
43
+ queueId: ObjectId.random(),
44
+ queueNamespace: 'data',
45
+ documentId: null,
46
+ recordId: null,
47
+ updatedAt: Date.now(),
48
+ data: {
49
+ id: objectId,
50
+ [ATTR_TYPE]: TYPE_PERSON,
51
+ [ATTR_DELETED]: false,
52
+ },
53
+ };
54
+
55
+ yield* index.update([item]);
56
+
57
+ const results = yield* index.query({ spaceId, typeDxn: TYPE_PERSON_VERSIONLESS });
58
+ expect(results.map((_) => _.objectId)).toEqual([objectId]);
59
+
60
+ const otherTypeResults = yield* index.query({
61
+ spaceId,
62
+ typeDxn: DXN.parse('dxn:type:com.example.type.other').toString(),
63
+ });
64
+ expect(otherTypeResults).toEqual([]);
65
+ }).pipe(Effect.provide(TestLayer)),
66
+ );
67
+
68
+ it.effect('should not treat LIKE wildcards in versionless type queries', () =>
69
+ Effect.gen(function* () {
70
+ const index = new ObjectMetaIndex();
71
+ yield* index.migrate();
72
+
73
+ const spaceId = SpaceId.random();
74
+ const objectIdMatch = ObjectId.random();
75
+ const objectIdFalsePositive = ObjectId.random();
76
+
77
+ const match: IndexerObject = {
78
+ spaceId,
79
+ queueId: ObjectId.random(),
80
+ queueNamespace: 'data',
81
+ documentId: null,
82
+ recordId: null,
83
+ updatedAt: Date.now(),
84
+ data: {
85
+ id: objectIdMatch,
86
+ [ATTR_TYPE]: TYPE_WITH_UNDERSCORE,
87
+ [ATTR_DELETED]: false,
88
+ },
89
+ };
90
+
91
+ // Would match prior implementation because '_' is a LIKE wildcard.
92
+ const falsePositive: IndexerObject = {
93
+ spaceId,
94
+ queueId: ObjectId.random(),
95
+ queueNamespace: 'data',
96
+ documentId: null,
97
+ recordId: null,
98
+ updatedAt: Date.now(),
99
+ data: {
100
+ id: objectIdFalsePositive,
101
+ [ATTR_TYPE]: TYPE_UNDERSCORE_FALSE_POSITIVE,
102
+ [ATTR_DELETED]: false,
103
+ },
104
+ };
105
+
106
+ yield* index.update([match, falsePositive]);
107
+
108
+ const queryResults = yield* index.query({ spaceId, typeDxn: TYPE_WITH_UNDERSCORE_VERSIONLESS });
109
+ expect(queryResults.map((_) => _.objectId)).toEqual([objectIdMatch]);
110
+
111
+ const queryTypesResults = yield* index.queryTypes({
112
+ spaceIds: [spaceId],
113
+ typeDxns: [TYPE_WITH_UNDERSCORE_VERSIONLESS],
114
+ includeAllQueues: true,
115
+ });
116
+ expect(queryTypesResults.map((_) => _.objectId)).toEqual([objectIdMatch]);
117
+ }).pipe(Effect.provide(TestLayer)),
118
+ );
119
+
29
120
  it.effect('should store and update object metadata', () =>
30
121
  Effect.gen(function* () {
31
122
  const index = new ObjectMetaIndex();
@@ -38,8 +129,10 @@ describe('ObjectMetaIndex', () => {
38
129
  const item1: IndexerObject = {
39
130
  spaceId,
40
131
  queueId: ObjectId.random(),
132
+ queueNamespace: 'data',
41
133
  documentId: null,
42
134
  recordId: null,
135
+ updatedAt: Date.now(),
43
136
  data: {
44
137
  id: objectId1,
45
138
  [ATTR_TYPE]: TYPE_PERSON,
@@ -50,8 +143,10 @@ describe('ObjectMetaIndex', () => {
50
143
  const item2: IndexerObject = {
51
144
  spaceId,
52
145
  queueId: null,
146
+ queueNamespace: null,
53
147
  documentId: 'doc-123',
54
148
  recordId: null,
149
+ updatedAt: Date.now(),
55
150
  data: {
56
151
  id: objectId2,
57
152
  [ATTR_TYPE]: TYPE_RELATION,
@@ -116,4 +211,267 @@ describe('ObjectMetaIndex', () => {
116
211
  expect(oldTypeResults.length).toBe(0);
117
212
  }).pipe(Effect.provide(TestLayer)),
118
213
  );
214
+
215
+ it.effect('should support queryAll/queryTypes/queryRelations', () =>
216
+ Effect.gen(function* () {
217
+ const index = new ObjectMetaIndex();
218
+ yield* index.migrate();
219
+
220
+ const spaceId = SpaceId.random();
221
+ const objectId1 = ObjectId.random();
222
+ const objectId2 = ObjectId.random();
223
+ const relationId = ObjectId.random();
224
+
225
+ const item1: IndexerObject = {
226
+ spaceId,
227
+ queueId: ObjectId.random(),
228
+ queueNamespace: 'data',
229
+ documentId: null,
230
+ recordId: null,
231
+ updatedAt: Date.now(),
232
+ data: {
233
+ id: objectId1,
234
+ [ATTR_TYPE]: TYPE_PERSON,
235
+ [ATTR_DELETED]: false,
236
+ },
237
+ };
238
+
239
+ const item2: IndexerObject = {
240
+ spaceId,
241
+ queueId: ObjectId.random(),
242
+ queueNamespace: 'data',
243
+ documentId: null,
244
+ recordId: null,
245
+ updatedAt: Date.now(),
246
+ data: {
247
+ id: objectId2,
248
+ [ATTR_TYPE]: TYPE_RELATION,
249
+ [ATTR_DELETED]: false,
250
+ },
251
+ };
252
+
253
+ const relation: IndexerObject = {
254
+ spaceId,
255
+ queueId: ObjectId.random(),
256
+ queueNamespace: 'data',
257
+ documentId: null,
258
+ recordId: null,
259
+ updatedAt: Date.now(),
260
+ data: {
261
+ id: relationId,
262
+ [ATTR_TYPE]: TYPE_RELATION,
263
+ [ATTR_RELATION_SOURCE]: DXN.fromLocalObjectId(objectId1).toString(),
264
+ [ATTR_RELATION_TARGET]: DXN.fromLocalObjectId(objectId2).toString(),
265
+ [ATTR_DELETED]: false,
266
+ },
267
+ };
268
+
269
+ yield* index.update([item1, item2, relation]);
270
+
271
+ const all = yield* index.queryAll({ spaceIds: [spaceId], includeAllQueues: true });
272
+ expect(all.map((_) => _.objectId).sort()).toEqual([objectId1, objectId2, relationId].sort());
273
+
274
+ const allNoQueues = yield* index.queryAll({ spaceIds: [spaceId] });
275
+ expect(allNoQueues).toEqual([]);
276
+
277
+ const types = yield* index.queryTypes({
278
+ spaceIds: [spaceId],
279
+ typeDxns: [TYPE_PERSON, TYPE_RELATION],
280
+ includeAllQueues: true,
281
+ });
282
+ expect(types.map((_) => _.objectId).sort()).toEqual([objectId1, objectId2, relationId].sort());
283
+
284
+ const onlyPerson = yield* index.queryTypes({
285
+ spaceIds: [spaceId],
286
+ typeDxns: [TYPE_PERSON],
287
+ includeAllQueues: true,
288
+ });
289
+ expect(onlyPerson.map((_) => _.objectId)).toEqual([objectId1]);
290
+
291
+ const onlyPersonVersionless = yield* index.queryTypes({
292
+ spaceIds: [spaceId],
293
+ typeDxns: [TYPE_PERSON_VERSIONLESS],
294
+ includeAllQueues: true,
295
+ });
296
+ expect(onlyPersonVersionless.map((_) => _.objectId)).toEqual([objectId1]);
297
+
298
+ const notPerson = yield* index.queryTypes({
299
+ spaceIds: [spaceId],
300
+ typeDxns: [TYPE_PERSON],
301
+ inverted: true,
302
+ includeAllQueues: true,
303
+ });
304
+ expect(notPerson.map((_) => _.objectId).sort()).toEqual([objectId2, relationId].sort());
305
+
306
+ const notPersonVersionless = yield* index.queryTypes({
307
+ spaceIds: [spaceId],
308
+ typeDxns: [TYPE_PERSON_VERSIONLESS],
309
+ inverted: true,
310
+ includeAllQueues: true,
311
+ });
312
+ expect(notPersonVersionless.map((_) => _.objectId).sort()).toEqual([objectId2, relationId].sort());
313
+
314
+ const emptyTypes = yield* index.queryTypes({ spaceIds: [spaceId], typeDxns: [] });
315
+ expect(emptyTypes).toEqual([]);
316
+
317
+ const notEmptyTypes = yield* index.queryTypes({
318
+ spaceIds: [spaceId],
319
+ typeDxns: [],
320
+ inverted: true,
321
+ includeAllQueues: true,
322
+ });
323
+ expect(notEmptyTypes.map((_) => _.objectId).sort()).toEqual([objectId1, objectId2, relationId].sort());
324
+
325
+ const bySource = yield* index.queryRelations({
326
+ endpoint: 'source',
327
+ anchorDxns: [DXN.fromLocalObjectId(objectId1).toString()],
328
+ });
329
+ expect(bySource.map((_) => _.objectId)).toEqual([relationId]);
330
+
331
+ const byTarget = yield* index.queryRelations({
332
+ endpoint: 'target',
333
+ anchorDxns: [DXN.fromLocalObjectId(objectId2).toString()],
334
+ });
335
+ expect(byTarget.map((_) => _.objectId)).toEqual([relationId]);
336
+ }).pipe(Effect.provide(TestLayer)),
337
+ );
338
+
339
+ it.effect('should set createdAt and updatedAt from source timestamp on insert and updatedAt on update', () =>
340
+ Effect.gen(function* () {
341
+ const index = new ObjectMetaIndex();
342
+ yield* index.migrate();
343
+
344
+ const spaceId = SpaceId.random();
345
+ const objectId = ObjectId.random();
346
+ const queueId = ObjectId.random();
347
+
348
+ const insertTimestamp = 1700000000000;
349
+ const item: IndexerObject = {
350
+ spaceId,
351
+ queueId,
352
+ queueNamespace: 'data',
353
+ documentId: null,
354
+ recordId: null,
355
+ updatedAt: insertTimestamp,
356
+ data: {
357
+ id: objectId,
358
+ [ATTR_TYPE]: TYPE_PERSON,
359
+ [ATTR_DELETED]: false,
360
+ },
361
+ };
362
+
363
+ yield* index.update([item]);
364
+
365
+ const results = yield* index.query({ spaceId, typeDxn: TYPE_PERSON });
366
+ expect(results.length).toBe(1);
367
+ expect(results[0].createdAt).toBe(insertTimestamp);
368
+ expect(results[0].updatedAt).toBe(insertTimestamp);
369
+
370
+ const updateTimestamp = 1700001000000;
371
+ yield* index.update([{ ...item, updatedAt: updateTimestamp, data: { ...item.data, [ATTR_DELETED]: true } }]);
372
+
373
+ const updated = yield* index.query({ spaceId, typeDxn: TYPE_PERSON });
374
+ expect(updated[0].createdAt).toBe(insertTimestamp);
375
+ expect(updated[0].updatedAt).toBe(updateTimestamp);
376
+ }).pipe(Effect.provide(TestLayer)),
377
+ );
378
+
379
+ it.effect('should query by time range', () =>
380
+ Effect.gen(function* () {
381
+ const index = new ObjectMetaIndex();
382
+ yield* index.migrate();
383
+
384
+ const spaceId = SpaceId.random();
385
+ const queueId1 = ObjectId.random();
386
+ const queueId2 = ObjectId.random();
387
+ const objectId1 = ObjectId.random();
388
+ const objectId2 = ObjectId.random();
389
+
390
+ const earlyTimestamp = 1700000000000;
391
+ const midpoint = 1700000500000;
392
+ const lateTimestamp = 1700001000000;
393
+
394
+ yield* index.update([
395
+ {
396
+ spaceId,
397
+ queueId: queueId1,
398
+ queueNamespace: 'data',
399
+ documentId: null,
400
+ recordId: null,
401
+ updatedAt: earlyTimestamp,
402
+ data: { id: objectId1, [ATTR_TYPE]: TYPE_PERSON, [ATTR_DELETED]: false },
403
+ },
404
+ ]);
405
+
406
+ yield* index.update([
407
+ {
408
+ spaceId,
409
+ queueId: queueId2,
410
+ queueNamespace: 'data',
411
+ documentId: null,
412
+ recordId: null,
413
+ updatedAt: lateTimestamp,
414
+ data: { id: objectId2, [ATTR_TYPE]: TYPE_PERSON, [ATTR_DELETED]: false },
415
+ },
416
+ ]);
417
+
418
+ // Query all -- both should match.
419
+ const all = yield* index.queryByTimeRange({ spaceIds: [spaceId], includeAllQueues: true });
420
+ expect(all.map((_) => _.objectId).sort()).toEqual([objectId1, objectId2].sort());
421
+
422
+ // Query only objects updated after midpoint (object2 has lateTimestamp).
423
+ const afterMid = yield* index.queryByTimeRange({
424
+ spaceIds: [spaceId],
425
+ updatedAfter: midpoint,
426
+ includeAllQueues: true,
427
+ });
428
+ expect(afterMid.map((_) => _.objectId)).toEqual([objectId2]);
429
+
430
+ // Query only objects created before midpoint (object1 has earlyTimestamp).
431
+ const beforeMid = yield* index.queryByTimeRange({
432
+ spaceIds: [spaceId],
433
+ createdBefore: midpoint,
434
+ includeAllQueues: true,
435
+ });
436
+ expect(beforeMid.map((_) => _.objectId)).toEqual([objectId1]);
437
+ }).pipe(Effect.provide(TestLayer)),
438
+ );
439
+
440
+ it.effect('should round-trip queueNamespace and persist it through updates', () =>
441
+ Effect.gen(function* () {
442
+ const index = new ObjectMetaIndex();
443
+ yield* index.migrate();
444
+
445
+ const spaceId = SpaceId.random();
446
+ const traceQueueId = ObjectId.random();
447
+ const traceObjectId = ObjectId.random();
448
+
449
+ // Initial insert with 'trace' namespace.
450
+ const traceItem: IndexerObject = {
451
+ spaceId,
452
+ queueId: traceQueueId,
453
+ queueNamespace: 'trace',
454
+ documentId: null,
455
+ recordId: null,
456
+ updatedAt: Date.now(),
457
+ data: {
458
+ id: traceObjectId,
459
+ [ATTR_TYPE]: TYPE_PERSON,
460
+ [ATTR_DELETED]: false,
461
+ },
462
+ };
463
+ yield* index.update([traceItem]);
464
+
465
+ const initial = yield* index.queryAll({ spaceIds: [spaceId], includeAllQueues: true });
466
+ expect(initial).toHaveLength(1);
467
+ expect(initial[0].queueNamespace).toBe('trace');
468
+
469
+ // Re-index the same object: the UPDATE branch must preserve the namespace.
470
+ yield* index.update([{ ...traceItem, updatedAt: Date.now() + 1 }]);
471
+
472
+ const afterUpdate = yield* index.queryAll({ spaceIds: [spaceId], includeAllQueues: true });
473
+ expect(afterUpdate).toHaveLength(1);
474
+ expect(afterUpdate[0].queueNamespace).toBe('trace');
475
+ }).pipe(Effect.provide(TestLayer)),
476
+ );
119
477
  });