@enbox/dwn-sdk-js 0.4.16 → 0.4.18
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/dist/browser.mjs +1 -1
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/generated/precompiled-validators.js +978 -840
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/json-schemas/definitions.json +5 -0
- package/dist/esm/src/core/dwn-constant.js +2 -2
- package/dist/esm/src/core/dwn-error.js +0 -1
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js +1 -24
- package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization.js +1 -5
- package/dist/esm/src/core/protocol-authorization.js.map +1 -1
- package/dist/esm/src/core/validation-state-reader.js +2 -3
- package/dist/esm/src/core/validation-state-reader.js.map +1 -1
- package/dist/esm/src/handlers/records-collection.js +200 -0
- package/dist/esm/src/handlers/records-collection.js.map +1 -0
- package/dist/esm/src/handlers/records-count.js +26 -195
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +19 -373
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +21 -338
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +8 -26
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/protocols-configure.js +4 -9
- package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
- package/dist/esm/src/interfaces/records-count.js +1 -1
- package/dist/esm/src/interfaces/records-count.js.map +1 -1
- package/dist/esm/src/interfaces/records-query.js +1 -1
- package/dist/esm/src/interfaces/records-query.js.map +1 -1
- package/dist/esm/src/interfaces/records-subscribe.js +3 -1
- package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
- package/dist/esm/src/store/index-level.js +210 -21
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +192 -21
- package/dist/esm/src/store/message-store-level.js.map +1 -1
- package/dist/esm/src/store/storage-controller.js +1 -3
- package/dist/esm/src/store/storage-controller.js.map +1 -1
- package/dist/esm/src/types/protocols-types.js +0 -10
- package/dist/esm/src/types/protocols-types.js.map +1 -1
- package/dist/esm/src/utils/filter.js +35 -1
- package/dist/esm/src/utils/filter.js.map +1 -1
- package/dist/esm/src/utils/grant-key-coverage.js +4 -6
- package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
- package/dist/esm/src/utils/messages.js +4 -14
- package/dist/esm/src/utils/messages.js.map +1 -1
- package/dist/esm/src/utils/permission-scope.js +3 -9
- package/dist/esm/src/utils/permission-scope.js.map +1 -1
- package/dist/esm/src/utils/record-limit-occupancy.js +73 -202
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +19 -52
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
- package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
- package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
- package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
- package/dist/esm/tests/dwn.spec.js +2 -2
- package/dist/esm/tests/dwn.spec.js.map +1 -1
- package/dist/esm/tests/features/records-delivery.spec.js +2 -5
- package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
- package/dist/esm/tests/features/records-nested-query-scope.spec.js +149 -70
- package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +380 -95
- package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
- package/dist/esm/tests/fuzz/filter.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-count.spec.js +52 -1
- package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +57 -3
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/store/index-level.spec.js +208 -1
- package/dist/esm/tests/store/index-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
- package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
- package/dist/esm/tests/store/message-store-level.spec.js +76 -1
- package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
- package/dist/esm/tests/store/message-store.spec.js +81 -0
- package/dist/esm/tests/store/message-store.spec.js.map +1 -1
- package/dist/esm/tests/utils/filters.spec.js +25 -4
- package/dist/esm/tests/utils/filters.spec.js.map +1 -1
- package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
- package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
- package/dist/esm/tests/utils/records.spec.js +12 -0
- package/dist/esm/tests/utils/records.spec.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/dwn-constant.d.ts +2 -2
- package/dist/types/src/core/dwn-error.d.ts +0 -1
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
- package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
- package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
- package/dist/types/src/handlers/records-collection.d.ts +32 -0
- package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
- package/dist/types/src/handlers/records-count.d.ts +0 -34
- package/dist/types/src/handlers/records-count.d.ts.map +1 -1
- package/dist/types/src/handlers/records-query.d.ts +0 -61
- package/dist/types/src/handlers/records-query.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/records-write.d.ts +3 -0
- package/dist/types/src/handlers/records-write.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +3 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/store/index-level.d.ts +61 -0
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/message-store-level.d.ts +24 -6
- package/dist/types/src/store/message-store-level.d.ts.map +1 -1
- package/dist/types/src/store/storage-controller.d.ts.map +1 -1
- package/dist/types/src/types/message-store.d.ts +28 -2
- package/dist/types/src/types/message-store.d.ts.map +1 -1
- package/dist/types/src/types/protocols-types.d.ts +7 -24
- package/dist/types/src/types/protocols-types.d.ts.map +1 -1
- package/dist/types/src/types/query-types.d.ts +8 -1
- package/dist/types/src/types/query-types.d.ts.map +1 -1
- package/dist/types/src/types/records-types.d.ts +3 -2
- package/dist/types/src/types/records-types.d.ts.map +1 -1
- package/dist/types/src/utils/filter.d.ts +14 -1
- package/dist/types/src/utils/filter.d.ts.map +1 -1
- package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
- package/dist/types/src/utils/messages.d.ts.map +1 -1
- package/dist/types/src/utils/permission-scope.d.ts +0 -2
- package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
- package/dist/types/src/utils/record-limit-occupancy.d.ts +25 -12
- package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +6 -24
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/tests/features/records-delivery.spec.d.ts.map +1 -1
- package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
- package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
- package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
- package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/core/dwn-constant.ts +2 -2
- package/src/core/dwn-error.ts +0 -2
- package/src/core/protocol-authorization-validation.ts +1 -33
- package/src/core/protocol-authorization.ts +0 -7
- package/src/core/validation-state-reader.ts +2 -3
- package/src/handlers/records-collection.ts +269 -0
- package/src/handlers/records-count.ts +38 -232
- package/src/handlers/records-query.ts +22 -444
- package/src/handlers/records-subscribe.ts +26 -397
- package/src/handlers/records-write.ts +17 -32
- package/src/index.ts +3 -3
- package/src/interfaces/protocols-configure.ts +4 -13
- package/src/interfaces/records-count.ts +1 -1
- package/src/interfaces/records-query.ts +1 -1
- package/src/interfaces/records-subscribe.ts +5 -2
- package/src/store/index-level.ts +323 -28
- package/src/store/message-store-level.ts +251 -25
- package/src/store/storage-controller.ts +1 -3
- package/src/types/message-store.ts +30 -2
- package/src/types/protocols-types.ts +7 -26
- package/src/types/query-types.ts +10 -2
- package/src/types/records-types.ts +3 -2
- package/src/utils/filter.ts +40 -3
- package/src/utils/grant-key-coverage.ts +4 -7
- package/src/utils/messages.ts +4 -14
- package/src/utils/permission-scope.ts +4 -11
- package/src/utils/record-limit-occupancy.ts +108 -293
- package/src/utils/records.ts +22 -57
|
@@ -52,7 +52,7 @@ export class RecordsQuery extends AbstractMessage<RecordsQueryMessage> {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
|
|
55
|
-
Records.
|
|
55
|
+
Records.validateNestedProtocolPathScope(
|
|
56
56
|
message.descriptor.filter,
|
|
57
57
|
DwnErrorCode.RecordsQueryNestedProtocolPathContextIdInvalid,
|
|
58
58
|
'RecordsQuery'
|
|
@@ -48,10 +48,13 @@ export class RecordsSubscribe extends AbstractMessage<RecordsSubscribeMessage> {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
|
|
51
|
-
Records.
|
|
51
|
+
Records.validateNestedProtocolPathScope(
|
|
52
52
|
message.descriptor.filter,
|
|
53
53
|
DwnErrorCode.RecordsSubscribeNestedProtocolPathContextIdInvalid,
|
|
54
|
-
'RecordsSubscribe'
|
|
54
|
+
'RecordsSubscribe',
|
|
55
|
+
message.descriptor.cursor === undefined
|
|
56
|
+
&& message.descriptor.pagination?.limit !== undefined
|
|
57
|
+
&& !signaturePayload?.protocolRole?.includes('/'),
|
|
55
58
|
);
|
|
56
59
|
|
|
57
60
|
if (signaturePayload?.protocolRole !== undefined) {
|
package/src/store/index-level.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { LevelWrapperBatchOperation, LevelWrapperIteratorOptions, } from '.
|
|
|
4
4
|
import { isEmptyObject } from '@enbox/common';
|
|
5
5
|
import { lexicographicalCompare } from '../utils/string.js';
|
|
6
6
|
import { SortDirection } from '../types/query-types.js';
|
|
7
|
+
import { assertValidSubtreeFilters, FilterSelector, FilterUtility, isSubtreeFilter } from '../utils/filter.js';
|
|
7
8
|
import {
|
|
8
9
|
countWithCompoundIndex,
|
|
9
10
|
createCompoundIndexDeleteOperation,
|
|
@@ -13,7 +14,6 @@ import {
|
|
|
13
14
|
} from './index-level-compound.js';
|
|
14
15
|
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';
|
|
15
16
|
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
16
|
-
import { FilterSelector, FilterUtility } from '../utils/filter.js';
|
|
17
17
|
|
|
18
18
|
export type IndexLevelConfig = {
|
|
19
19
|
location: string,
|
|
@@ -23,6 +23,13 @@ export type IndexLevelConfig = {
|
|
|
23
23
|
|
|
24
24
|
export type IndexedItem = { messageCid: string, indexes: KeyValues };
|
|
25
25
|
|
|
26
|
+
type RecordLimitParentGroupsInput = {
|
|
27
|
+
candidateFilter: Filter;
|
|
28
|
+
filters: Filter[];
|
|
29
|
+
max: number;
|
|
30
|
+
parentIds: readonly string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
26
33
|
/** A value that can be encoded into a lexicographically sortable index key. */
|
|
27
34
|
type IndexableValue = string | number | boolean;
|
|
28
35
|
|
|
@@ -318,17 +325,12 @@ export class IndexLevel {
|
|
|
318
325
|
* @returns {IndexedItem[]} an array of `IndexedItem` that match the given filters.
|
|
319
326
|
*/
|
|
320
327
|
async query(tenant: string, filters: Filter[], queryOptions: QueryOptions, options?: IndexLevelOptions): Promise<IndexedItem[]> {
|
|
328
|
+
assertValidSubtreeFilters(filters);
|
|
321
329
|
|
|
322
330
|
// Strategy 1: try compound index for single-filter queries
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return queryWithCompoundIndex(
|
|
327
|
-
this.db, tenant, filters[0], queryOptions, compoundResult,
|
|
328
|
-
IndexLevel.encodeValue, IndexLevel.delimiter,
|
|
329
|
-
this.queryWithIteratorPaging.bind(this), options
|
|
330
|
-
);
|
|
331
|
-
}
|
|
331
|
+
const compoundQuery = this.queryUsingCompoundIndex(tenant, filters, queryOptions, options);
|
|
332
|
+
if (compoundQuery !== undefined) {
|
|
333
|
+
return compoundQuery;
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
// Strategy 2: in-memory paging for concise filters
|
|
@@ -340,6 +342,47 @@ export class IndexLevel {
|
|
|
340
342
|
return this.queryWithIteratorPaging(tenant, filters, queryOptions, options);
|
|
341
343
|
}
|
|
342
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Queries one bounded page without selecting the in-memory strategy.
|
|
347
|
+
*
|
|
348
|
+
* Projection callers use this when they must close the outer iterator before
|
|
349
|
+
* opening related index ranges. A matching compound index is preferred;
|
|
350
|
+
* otherwise the requested-sort index is streamed directly.
|
|
351
|
+
*/
|
|
352
|
+
async queryWithBoundedPaging(
|
|
353
|
+
tenant: string,
|
|
354
|
+
filters: Filter[],
|
|
355
|
+
queryOptions: QueryOptions,
|
|
356
|
+
options?: IndexLevelOptions,
|
|
357
|
+
): Promise<IndexedItem[]> {
|
|
358
|
+
assertValidSubtreeFilters(filters);
|
|
359
|
+
|
|
360
|
+
const compoundQuery = this.queryUsingCompoundIndex(tenant, filters, queryOptions, options);
|
|
361
|
+
return compoundQuery ?? this.queryWithIteratorPaging(tenant, filters, queryOptions, options);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private queryUsingCompoundIndex(
|
|
365
|
+
tenant: string,
|
|
366
|
+
filters: Filter[],
|
|
367
|
+
queryOptions: QueryOptions,
|
|
368
|
+
options?: IndexLevelOptions,
|
|
369
|
+
): Promise<IndexedItem[]> | undefined {
|
|
370
|
+
if (filters.length !== 1 || isEmptyObject(filters[0])) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const compoundIndex = selectCompoundIndex(filters[0], queryOptions, this._compoundIndexes);
|
|
375
|
+
if (compoundIndex === undefined) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return queryWithCompoundIndex(
|
|
380
|
+
this.db, tenant, filters[0], queryOptions, compoundIndex,
|
|
381
|
+
IndexLevel.encodeValue, IndexLevel.delimiter,
|
|
382
|
+
this.queryWithIteratorPaging.bind(this), options
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
343
386
|
/**
|
|
344
387
|
* Counts the number of items that match the given filters without loading full records.
|
|
345
388
|
*
|
|
@@ -348,6 +391,7 @@ export class IndexLevel {
|
|
|
348
391
|
*/
|
|
349
392
|
async count(tenant: string, filters: Filter[], queryOptions: Omit<QueryOptions, 'limit' | 'cursor'>,
|
|
350
393
|
options?: IndexLevelOptions): Promise<number> {
|
|
394
|
+
assertValidSubtreeFilters(filters);
|
|
351
395
|
|
|
352
396
|
// try compound index for single-filter queries
|
|
353
397
|
if (filters.length === 1 && !isEmptyObject(filters[0])) {
|
|
@@ -365,6 +409,207 @@ export class IndexLevel {
|
|
|
365
409
|
return results.length;
|
|
366
410
|
}
|
|
367
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Finds the inclusive rank boundary for one `$recordLimit` parent group.
|
|
414
|
+
*
|
|
415
|
+
* The existing `parentId` index keeps the scan isolated to one nested-record
|
|
416
|
+
* group. Root records have no `parentId`, so they are read from the exact
|
|
417
|
+
* `protocolPath` range and explicitly fenced to rows without a parent. The
|
|
418
|
+
* bounded max-heap retains only the earliest `max` rank keys while the range
|
|
419
|
+
* is streamed; no group-sized array is materialized.
|
|
420
|
+
*/
|
|
421
|
+
public async findRecordLimitRankCutoff(
|
|
422
|
+
tenant: string,
|
|
423
|
+
candidateFilter: Filter,
|
|
424
|
+
parentId: string | undefined,
|
|
425
|
+
max: number,
|
|
426
|
+
options?: IndexLevelOptions,
|
|
427
|
+
): Promise<string | undefined> {
|
|
428
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
429
|
+
throw new TypeError(`IndexLevel: record-limit max must be a positive integer.`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const protocolPath = candidateFilter.protocolPath;
|
|
433
|
+
if (typeof protocolPath !== 'string') {
|
|
434
|
+
throw new TypeError(`IndexLevel: record-limit candidate filter must contain one protocolPath.`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const indexName = parentId === undefined ? 'protocolPath' : 'parentId';
|
|
438
|
+
const indexValue = parentId ?? protocolPath;
|
|
439
|
+
const rankHeap: string[] = [];
|
|
440
|
+
|
|
441
|
+
for await (const item of this.iterateExactMatches(tenant, indexName, indexValue, options)) {
|
|
442
|
+
if ((parentId === undefined && item.indexes.parentId !== undefined) ||
|
|
443
|
+
!FilterUtility.matchFilter(item.indexes, candidateFilter)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
IndexLevel.addToRecordLimitRankHeap(
|
|
448
|
+
rankHeap,
|
|
449
|
+
IndexLevel.createRecordLimitRankKey(item.indexes),
|
|
450
|
+
max,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return rankHeap[0];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Queries the bounded occupants of explicitly selected direct-parent groups.
|
|
459
|
+
* Occupancy is resolved before caller filters, then the survivors are globally
|
|
460
|
+
* sorted and paginated using ordinary IndexLevel cursor semantics.
|
|
461
|
+
*/
|
|
462
|
+
public async queryRecordLimitParentGroups(
|
|
463
|
+
tenant: string,
|
|
464
|
+
input: RecordLimitParentGroupsInput & { queryOptions: QueryOptions },
|
|
465
|
+
options?: IndexLevelOptions,
|
|
466
|
+
): Promise<IndexedItem[]> {
|
|
467
|
+
assertValidSubtreeFilters([input.candidateFilter, ...input.filters]);
|
|
468
|
+
const { filters, queryOptions } = input;
|
|
469
|
+
const { cursor, limit, sortDirection = SortDirection.Ascending, sortProperty } = queryOptions;
|
|
470
|
+
if (limit !== undefined && limit <= 0) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const cursorStartingKey = cursor === undefined ? undefined : this.createStartingKeyFromCursor(cursor);
|
|
475
|
+
const compareItems = (left: IndexedItem, right: IndexedItem): number =>
|
|
476
|
+
this.sortItems(left, right, sortProperty, sortDirection);
|
|
477
|
+
const matches: IndexedItem[] = [];
|
|
478
|
+
|
|
479
|
+
for await (const item of this.iterateRecordLimitParentGroupOccupants(tenant, input, options)) {
|
|
480
|
+
if (item.indexes[sortProperty] === undefined ||
|
|
481
|
+
!FilterUtility.matchAnyFilter(item.indexes, filters) ||
|
|
482
|
+
(cursorStartingKey !== undefined &&
|
|
483
|
+
!this.isItemAfterCursor(item, sortDirection, sortProperty, cursorStartingKey))) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
matches.push(item);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
matches.sort(compareItems);
|
|
491
|
+
return limit === undefined ? matches : matches.slice(0, limit);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Counts the bounded occupants of explicitly selected direct-parent groups. */
|
|
495
|
+
public async countRecordLimitParentGroups(
|
|
496
|
+
tenant: string,
|
|
497
|
+
input: RecordLimitParentGroupsInput & { sortProperty: string },
|
|
498
|
+
options?: IndexLevelOptions,
|
|
499
|
+
): Promise<number> {
|
|
500
|
+
assertValidSubtreeFilters([input.candidateFilter, ...input.filters]);
|
|
501
|
+
let count = 0;
|
|
502
|
+
for await (const item of this.iterateRecordLimitParentGroupOccupants(tenant, input, options)) {
|
|
503
|
+
if (item.indexes[input.sortProperty] !== undefined &&
|
|
504
|
+
FilterUtility.matchAnyFilter(item.indexes, input.filters)) {
|
|
505
|
+
count++;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return count;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Streams occupants from exact parentId ranges without opening the requested-sort index. */
|
|
512
|
+
private async * iterateRecordLimitParentGroupOccupants(
|
|
513
|
+
tenant: string,
|
|
514
|
+
input: RecordLimitParentGroupsInput,
|
|
515
|
+
options?: IndexLevelOptions,
|
|
516
|
+
): AsyncGenerator<IndexedItem> {
|
|
517
|
+
for (const parentId of new Set(input.parentIds)) {
|
|
518
|
+
const candidateFilter: Filter = { ...input.candidateFilter, parentId };
|
|
519
|
+
const cutoff = await this.findRecordLimitRankCutoff(
|
|
520
|
+
tenant,
|
|
521
|
+
candidateFilter,
|
|
522
|
+
parentId,
|
|
523
|
+
input.max,
|
|
524
|
+
options,
|
|
525
|
+
);
|
|
526
|
+
if (cutoff === undefined) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
for await (const item of this.iterateExactMatches(tenant, 'parentId', parentId, options)) {
|
|
531
|
+
if (FilterUtility.matchFilter(item.indexes, candidateFilter) &&
|
|
532
|
+
lexicographicalCompare(IndexLevel.createRecordLimitRankKey(item.indexes), cutoff) <= 0) {
|
|
533
|
+
yield item;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Builds the deterministic `$recordLimit` rank key: oldest creation time,
|
|
541
|
+
* then record ID. Message CID deliberately does not participate because
|
|
542
|
+
* occupancy belongs to the logical record, not a particular update.
|
|
543
|
+
*/
|
|
544
|
+
public static createRecordLimitRankKey(indexes: KeyValues): string {
|
|
545
|
+
const { dateCreated, recordId } = indexes;
|
|
546
|
+
if (typeof dateCreated !== 'string' || typeof recordId !== 'string') {
|
|
547
|
+
throw new TypeError(`IndexLevel: record-limit candidates require string dateCreated and recordId indexes.`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return IndexLevel.keySegmentJoin(IndexLevel.encodeValue(dateCreated), IndexLevel.encodeValue(recordId));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private static addToRecordLimitRankHeap(
|
|
554
|
+
heap: string[],
|
|
555
|
+
value: string,
|
|
556
|
+
max: number,
|
|
557
|
+
): void {
|
|
558
|
+
if (heap.length < max) {
|
|
559
|
+
heap.push(value);
|
|
560
|
+
IndexLevel.siftRecordLimitRankHeapUp(heap, heap.length - 1);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (lexicographicalCompare(value, heap[0]) >= 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
heap[0] = value;
|
|
569
|
+
IndexLevel.siftRecordLimitRankHeapDown(heap, 0);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private static siftRecordLimitRankHeapUp(
|
|
573
|
+
heap: string[],
|
|
574
|
+
startIndex: number,
|
|
575
|
+
): void {
|
|
576
|
+
let index = startIndex;
|
|
577
|
+
while (index > 0) {
|
|
578
|
+
const parentIndex = (index - 1) >>> 1;
|
|
579
|
+
if (lexicographicalCompare(heap[parentIndex], heap[index]) >= 0) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
[heap[parentIndex], heap[index]] = [heap[index], heap[parentIndex]];
|
|
584
|
+
index = parentIndex;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private static siftRecordLimitRankHeapDown(
|
|
589
|
+
heap: string[],
|
|
590
|
+
startIndex: number,
|
|
591
|
+
): void {
|
|
592
|
+
let index = startIndex;
|
|
593
|
+
while (true) {
|
|
594
|
+
const leftIndex = index * 2 + 1;
|
|
595
|
+
if (leftIndex >= heap.length) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const rightIndex = leftIndex + 1;
|
|
600
|
+
const largerChildIndex = rightIndex < heap.length &&
|
|
601
|
+
lexicographicalCompare(heap[rightIndex], heap[leftIndex]) > 0
|
|
602
|
+
? rightIndex
|
|
603
|
+
: leftIndex;
|
|
604
|
+
if (lexicographicalCompare(heap[index], heap[largerChildIndex]) >= 0) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
[heap[index], heap[largerChildIndex]] = [heap[largerChildIndex], heap[index]];
|
|
609
|
+
index = largerChildIndex;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
368
613
|
/**
|
|
369
614
|
* Queries the sort property index for items that match the filters. If no filters are provided, all items are returned.
|
|
370
615
|
* This query is a linear iterator over the sorted index, checking each item for a match.
|
|
@@ -589,6 +834,17 @@ export class IndexLevel {
|
|
|
589
834
|
const exactMatches = await this.filterExactMatches(tenant, propertyName, propertyValue, levelOptions);
|
|
590
835
|
processResults(exactMatches);
|
|
591
836
|
}
|
|
837
|
+
} else if (isSubtreeFilter(propertyFilter)) {
|
|
838
|
+
const exactMatches = await this.filterExactMatches(tenant, propertyName, propertyFilter.subtree, levelOptions);
|
|
839
|
+
processResults(exactMatches);
|
|
840
|
+
|
|
841
|
+
const descendantMatches = await this.filterSubtreeDescendantMatches(
|
|
842
|
+
tenant,
|
|
843
|
+
propertyName,
|
|
844
|
+
propertyFilter.subtree,
|
|
845
|
+
levelOptions,
|
|
846
|
+
);
|
|
847
|
+
processResults(descendantMatches);
|
|
592
848
|
} else if (FilterUtility.isRangeFilter(propertyFilter)) {
|
|
593
849
|
// `propertyFilter` is a `RangeFilter`
|
|
594
850
|
const rangeMatches = await this.filterRangeMatches(tenant, propertyName, propertyFilter, levelOptions);
|
|
@@ -615,19 +871,54 @@ export class IndexLevel {
|
|
|
615
871
|
propertyValue: EqualFilter,
|
|
616
872
|
options?: IndexLevelOptions
|
|
617
873
|
): Promise<IndexedItem[]> {
|
|
874
|
+
const matches: IndexedItem[] = [];
|
|
875
|
+
for await (const item of this.iterateExactMatches(tenant, propertyName, propertyValue, options)) {
|
|
876
|
+
matches.push(item);
|
|
877
|
+
}
|
|
878
|
+
return matches;
|
|
879
|
+
}
|
|
618
880
|
|
|
881
|
+
/** Streams one exact property-value range without materializing the range. */
|
|
882
|
+
private async * iterateExactMatches(
|
|
883
|
+
tenant: string,
|
|
884
|
+
propertyName: string,
|
|
885
|
+
propertyValue: EqualFilter,
|
|
886
|
+
options?: IndexLevelOptions,
|
|
887
|
+
): AsyncGenerator<IndexedItem> {
|
|
619
888
|
const matchPrefix = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(propertyValue));
|
|
889
|
+
const filterPartition = await this.getIndexPartition(tenant, propertyName);
|
|
890
|
+
|
|
891
|
+
for await (const [key, value] of filterPartition.iterator({ gt: matchPrefix }, options)) {
|
|
892
|
+
if (!key.startsWith(matchPrefix)) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
yield JSON.parse(value) as IndexedItem;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Returns every slash-delimited descendant of one hierarchical path.
|
|
901
|
+
*
|
|
902
|
+
* The lower bound removes JSON's closing quote so it is the encoded key
|
|
903
|
+
* prefix itself. ASCII `/` and `0` are adjacent in the store's lexicographic
|
|
904
|
+
* ordering, making `path0` the first value outside that prefix without
|
|
905
|
+
* relying on a finite Unicode sentinel.
|
|
906
|
+
*/
|
|
907
|
+
private async filterSubtreeDescendantMatches(
|
|
908
|
+
tenant: string,
|
|
909
|
+
propertyName: string,
|
|
910
|
+
subtree: string,
|
|
911
|
+
options?: IndexLevelOptions,
|
|
912
|
+
): Promise<IndexedItem[]> {
|
|
913
|
+
const encodedPrefix = IndexLevel.encodeValue(`${subtree}/`).slice(0, -1);
|
|
620
914
|
const iteratorOptions: LevelWrapperIteratorOptions<string> = {
|
|
621
|
-
|
|
915
|
+
gte : encodedPrefix,
|
|
916
|
+
lt : IndexLevel.encodeValue(`${subtree}0`),
|
|
622
917
|
};
|
|
623
918
|
|
|
624
919
|
const filterPartition = await this.getIndexPartition(tenant, propertyName);
|
|
625
920
|
const matches: IndexedItem[] = [];
|
|
626
|
-
for await (const [
|
|
627
|
-
// immediately stop if we arrive at an index that contains a different property value
|
|
628
|
-
if (!key.startsWith(matchPrefix)) {
|
|
629
|
-
break;
|
|
630
|
-
}
|
|
921
|
+
for await (const [ _key, value ] of filterPartition.iterator(iteratorOptions, options)) {
|
|
631
922
|
matches.push(JSON.parse(value) as IndexedItem);
|
|
632
923
|
}
|
|
633
924
|
return matches;
|
|
@@ -704,24 +995,13 @@ export class IndexLevel {
|
|
|
704
995
|
* Since the array is already sorted, binary search provides O(log n) performance instead of O(n).
|
|
705
996
|
*/
|
|
706
997
|
private findCursorStartingIndex(items: IndexedItem[], sortDirection: SortDirection, sortProperty: string, cursorStartingKey: string): number {
|
|
707
|
-
|
|
708
|
-
const isAfterCursor = (item: IndexedItem): boolean => {
|
|
709
|
-
const { messageCid, indexes } = item;
|
|
710
|
-
const sortValue = indexes[sortProperty] as string | number;
|
|
711
|
-
const itemCompareValue = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(sortValue), messageCid);
|
|
712
|
-
|
|
713
|
-
return sortDirection === SortDirection.Ascending ?
|
|
714
|
-
itemCompareValue > cursorStartingKey :
|
|
715
|
-
itemCompareValue < cursorStartingKey;
|
|
716
|
-
};
|
|
717
|
-
|
|
718
998
|
// binary search for the first item after the cursor
|
|
719
999
|
let low = 0;
|
|
720
1000
|
let high = items.length;
|
|
721
1001
|
|
|
722
1002
|
while (low < high) {
|
|
723
1003
|
const mid = (low + high) >>> 1;
|
|
724
|
-
if (
|
|
1004
|
+
if (this.isItemAfterCursor(items[mid], sortDirection, sortProperty, cursorStartingKey)) {
|
|
725
1005
|
high = mid;
|
|
726
1006
|
} else {
|
|
727
1007
|
low = mid + 1;
|
|
@@ -731,6 +1011,21 @@ export class IndexLevel {
|
|
|
731
1011
|
return low < items.length ? low : -1;
|
|
732
1012
|
}
|
|
733
1013
|
|
|
1014
|
+
private isItemAfterCursor(
|
|
1015
|
+
item: IndexedItem,
|
|
1016
|
+
sortDirection: SortDirection,
|
|
1017
|
+
sortProperty: string,
|
|
1018
|
+
cursorStartingKey: string,
|
|
1019
|
+
): boolean {
|
|
1020
|
+
const { messageCid, indexes } = item;
|
|
1021
|
+
const sortValue = indexes[sortProperty] as string | number;
|
|
1022
|
+
const itemCompareValue = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(sortValue), messageCid);
|
|
1023
|
+
|
|
1024
|
+
return sortDirection === SortDirection.Ascending ?
|
|
1025
|
+
itemCompareValue > cursorStartingKey :
|
|
1026
|
+
itemCompareValue < cursorStartingKey;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
734
1029
|
/**
|
|
735
1030
|
* Gets the indexes given an messageCid. This is a reverse lookup to construct starting keys, as well as deleting indexed items.
|
|
736
1031
|
*/
|