@enbox/dwn-sdk-js 0.4.16 → 0.4.17
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 +734 -747
- 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-count.js +18 -10
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +10 -7
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +10 -7
- 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/store/index-level.js +143 -6
- package/dist/esm/src/store/index-level.js.map +1 -1
- package/dist/esm/src/store/message-store-level.js +158 -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 +70 -202
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +9 -9
- 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 +138 -73
- package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +264 -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-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 +114 -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 +6 -0
- 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-count.d.ts +1 -0
- package/dist/types/src/handlers/records-count.d.ts.map +1 -1
- package/dist/types/src/handlers/records-query.d.ts.map +1 -1
- 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/store/index-level.d.ts +37 -0
- package/dist/types/src/store/index-level.d.ts.map +1 -1
- package/dist/types/src/store/message-store-level.d.ts +23 -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 +23 -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 +2 -1
- 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 +3 -2
- 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-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-count.ts +34 -13
- package/src/handlers/records-query.ts +14 -11
- package/src/handlers/records-subscribe.ts +18 -11
- package/src/handlers/records-write.ts +17 -32
- package/src/index.ts +3 -3
- package/src/interfaces/protocols-configure.ts +4 -13
- package/src/store/index-level.ts +197 -10
- package/src/store/message-store-level.ts +216 -25
- package/src/store/storage-controller.ts +1 -3
- package/src/types/message-store.ts +25 -2
- package/src/types/protocols-types.ts +7 -26
- package/src/types/query-types.ts +10 -2
- package/src/types/records-types.ts +2 -1
- 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 +105 -293
- package/src/utils/records.ts +9 -9
|
@@ -10,12 +10,12 @@ import { EncryptionControl } from '../core/encryption-control.js';
|
|
|
10
10
|
import { Message } from '../core/message.js';
|
|
11
11
|
import { messageReplyFromError } from '../core/message-reply.js';
|
|
12
12
|
import { ProtocolAuthorization } from '../core/protocol-authorization.js';
|
|
13
|
-
import { queryRecordsWithRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
|
|
14
13
|
import { Records } from '../utils/records.js';
|
|
15
14
|
import { RecordsGrantAuthorization } from '../core/records-grant-authorization.js';
|
|
16
15
|
import { RecordsQuery } from '../interfaces/records-query.js';
|
|
17
16
|
import { SortDirection } from '../types/query-types.js';
|
|
18
17
|
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
18
|
+
import { queryRecordsWithRecordLimitOccupancy, resolveRecordLimitOccupancy } from '../utils/record-limit-occupancy.js';
|
|
19
19
|
|
|
20
20
|
type RecordsQueryProjectionInput = {
|
|
21
21
|
tenant: string;
|
|
@@ -254,17 +254,22 @@ export class RecordsQueryHandler implements MethodHandler {
|
|
|
254
254
|
const {
|
|
255
255
|
tenant, recordsQuery, requester, filters, messageSort, pagination
|
|
256
256
|
} = input;
|
|
257
|
+
const recordLimit = await resolveRecordLimitOccupancy({
|
|
258
|
+
validationStateReader : this.deps.validationStateReader,
|
|
259
|
+
tenant,
|
|
260
|
+
recordsFilter : recordsQuery.message.descriptor.filter,
|
|
261
|
+
messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
|
|
262
|
+
});
|
|
257
263
|
const controlFilters = Records.buildControlRecordsFilters(filters);
|
|
258
264
|
const currentAudienceRecordIdCache = new Map<string, string | undefined>();
|
|
259
265
|
if (controlFilters.length === 0) {
|
|
260
266
|
const result = await queryRecordsWithRecordLimitOccupancy({
|
|
261
|
-
messageStore
|
|
262
|
-
validationStateReader : this.deps.validationStateReader,
|
|
267
|
+
messageStore: this.deps.messageStore,
|
|
263
268
|
tenant,
|
|
264
269
|
filters,
|
|
270
|
+
recordLimit,
|
|
265
271
|
messageSort,
|
|
266
272
|
pagination,
|
|
267
|
-
messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
|
|
268
273
|
});
|
|
269
274
|
return EncryptionControl.projectCurrentAudienceRecordPage({
|
|
270
275
|
messageStore : this.deps.messageStore,
|
|
@@ -280,13 +285,12 @@ export class RecordsQueryHandler implements MethodHandler {
|
|
|
280
285
|
|
|
281
286
|
if (pagination?.limit === undefined || pagination.limit <= 0) {
|
|
282
287
|
const result = await queryRecordsWithRecordLimitOccupancy({
|
|
283
|
-
messageStore
|
|
284
|
-
validationStateReader : this.deps.validationStateReader,
|
|
288
|
+
messageStore: this.deps.messageStore,
|
|
285
289
|
tenant,
|
|
286
290
|
filters,
|
|
291
|
+
recordLimit,
|
|
287
292
|
messageSort,
|
|
288
293
|
pagination,
|
|
289
|
-
messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
|
|
290
294
|
});
|
|
291
295
|
const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
|
|
292
296
|
messageStore : this.deps.messageStore,
|
|
@@ -311,13 +315,12 @@ export class RecordsQueryHandler implements MethodHandler {
|
|
|
311
315
|
do {
|
|
312
316
|
const remainingLimit = pagination.limit - visibleMessages.length;
|
|
313
317
|
const result = await queryRecordsWithRecordLimitOccupancy({
|
|
314
|
-
messageStore
|
|
315
|
-
validationStateReader : this.deps.validationStateReader,
|
|
318
|
+
messageStore : this.deps.messageStore,
|
|
316
319
|
tenant,
|
|
317
320
|
filters,
|
|
321
|
+
recordLimit,
|
|
318
322
|
messageSort,
|
|
319
|
-
pagination
|
|
320
|
-
messageTimestamp : recordsQuery.message.descriptor.messageTimestamp,
|
|
323
|
+
pagination : { ...pagination, cursor, limit: remainingLimit },
|
|
321
324
|
});
|
|
322
325
|
const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
|
|
323
326
|
messageStore : this.deps.messageStore,
|
|
@@ -17,7 +17,11 @@ import { RecordsSubscribe } from '../interfaces/records-subscribe.js';
|
|
|
17
17
|
import { SortDirection } from '../types/query-types.js';
|
|
18
18
|
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
19
19
|
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isRecordLimitOccupant,
|
|
22
|
+
queryRecordsWithRecordLimitOccupancy,
|
|
23
|
+
resolveRecordLimitOccupancy,
|
|
24
|
+
} from '../utils/record-limit-occupancy.js';
|
|
21
25
|
|
|
22
26
|
type ProjectedRecordsSubscriptionHandler = {
|
|
23
27
|
listener: SubscriptionListener;
|
|
@@ -604,17 +608,22 @@ export class RecordsSubscribeHandler implements MethodHandler {
|
|
|
604
608
|
messageSort: MessageSort,
|
|
605
609
|
pagination: { cursor?: PaginationCursor; limit?: number } | undefined,
|
|
606
610
|
): Promise<{ messages: RecordsQueryReplyEntry[], cursor?: PaginationCursor }> {
|
|
611
|
+
const recordLimit = await resolveRecordLimitOccupancy({
|
|
612
|
+
validationStateReader : this.deps.validationStateReader,
|
|
613
|
+
tenant,
|
|
614
|
+
recordsFilter : recordsSubscribe.message.descriptor.filter,
|
|
615
|
+
messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
|
|
616
|
+
});
|
|
607
617
|
const controlFilters = Records.buildControlRecordsFilters(filters);
|
|
608
618
|
const currentAudienceRecordIdCache = new Map<string, string | undefined>();
|
|
609
619
|
if (controlFilters.length === 0) {
|
|
610
620
|
const result = await queryRecordsWithRecordLimitOccupancy({
|
|
611
|
-
messageStore
|
|
612
|
-
validationStateReader : this.deps.validationStateReader,
|
|
621
|
+
messageStore: this.deps.messageStore,
|
|
613
622
|
tenant,
|
|
614
623
|
filters,
|
|
624
|
+
recordLimit,
|
|
615
625
|
messageSort,
|
|
616
626
|
pagination,
|
|
617
|
-
messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
|
|
618
627
|
});
|
|
619
628
|
return EncryptionControl.projectCurrentAudienceRecordPage({
|
|
620
629
|
messageStore: this.deps.messageStore,
|
|
@@ -627,13 +636,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
|
|
|
627
636
|
|
|
628
637
|
if (pagination?.limit === undefined || pagination.limit <= 0) {
|
|
629
638
|
const result = await queryRecordsWithRecordLimitOccupancy({
|
|
630
|
-
messageStore
|
|
631
|
-
validationStateReader : this.deps.validationStateReader,
|
|
639
|
+
messageStore: this.deps.messageStore,
|
|
632
640
|
tenant,
|
|
633
641
|
filters,
|
|
642
|
+
recordLimit,
|
|
634
643
|
messageSort,
|
|
635
644
|
pagination,
|
|
636
|
-
messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
|
|
637
645
|
});
|
|
638
646
|
const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
|
|
639
647
|
messageStore: this.deps.messageStore,
|
|
@@ -655,13 +663,12 @@ export class RecordsSubscribeHandler implements MethodHandler {
|
|
|
655
663
|
do {
|
|
656
664
|
const remainingLimit = pagination.limit - visibleMessages.length;
|
|
657
665
|
const result = await queryRecordsWithRecordLimitOccupancy({
|
|
658
|
-
messageStore
|
|
659
|
-
validationStateReader : this.deps.validationStateReader,
|
|
666
|
+
messageStore : this.deps.messageStore,
|
|
660
667
|
tenant,
|
|
661
668
|
filters,
|
|
669
|
+
recordLimit,
|
|
662
670
|
messageSort,
|
|
663
|
-
pagination
|
|
664
|
-
messageTimestamp : recordsSubscribe.message.descriptor.messageTimestamp,
|
|
671
|
+
pagination : { ...pagination, cursor, limit: remainingLimit },
|
|
665
672
|
});
|
|
666
673
|
const projectedResult = await EncryptionControl.projectCurrentAudienceRecordPage({
|
|
667
674
|
messageStore: this.deps.messageStore,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CoreProtocol } from '../core/core-protocol.js';
|
|
2
2
|
import type { ProgressToken } from '../types/subscriptions.js';
|
|
3
|
+
import type { ProtocolRuleSet } from '../types/protocols-types.js';
|
|
3
4
|
import type { GenericMessage, GenericMessageReply } from '../types/message-types.js';
|
|
4
5
|
import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
|
|
5
6
|
import type { RecordsQueryReplyEntry, RecordsWriteMessage } from '../types/records-types.js';
|
|
@@ -64,8 +65,13 @@ export class RecordsWriteHandler implements MethodHandler {
|
|
|
64
65
|
return messageReplyFromError(e, 400);
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
let protocolRuleSet: ProtocolRuleSet;
|
|
67
69
|
try {
|
|
68
|
-
await ProtocolAuthorization.validateReferentialIntegrity(
|
|
70
|
+
protocolRuleSet = await ProtocolAuthorization.validateReferentialIntegrity(
|
|
71
|
+
tenant,
|
|
72
|
+
recordsWrite,
|
|
73
|
+
this.deps.validationStateReader,
|
|
74
|
+
);
|
|
69
75
|
} catch (e) {
|
|
70
76
|
return messageReplyFromError(e, 400);
|
|
71
77
|
}
|
|
@@ -87,7 +93,7 @@ export class RecordsWriteHandler implements MethodHandler {
|
|
|
87
93
|
// messageTimestamp is <= the most recent squash record at the same path and parent context.
|
|
88
94
|
// The squash record acts as a temporal floor — no record older than the latest squash can exist.
|
|
89
95
|
try {
|
|
90
|
-
await this.enforceSquashBackstop(tenant, message);
|
|
96
|
+
await this.enforceSquashBackstop(tenant, message, protocolRuleSet);
|
|
91
97
|
} catch (e) {
|
|
92
98
|
return messageReplyFromError(e, 409);
|
|
93
99
|
}
|
|
@@ -445,38 +451,17 @@ export class RecordsWriteHandler implements MethodHandler {
|
|
|
445
451
|
* and there exists a squash record at the same protocol path and parent context whose
|
|
446
452
|
* `messageTimestamp` is >= the incoming message's `messageTimestamp`, reject with 409.
|
|
447
453
|
*
|
|
454
|
+
* The rule set is the exact policy already resolved by referential-integrity validation. Reusing
|
|
455
|
+
* it prevents a second store lookup from observing different state or failing open.
|
|
456
|
+
*
|
|
448
457
|
* This check only applies to protocol-based records at `$squash: true` paths.
|
|
449
458
|
*/
|
|
450
|
-
private async enforceSquashBackstop(
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// Fetch the protocol definition active at the incoming message timestamp to check if $squash is enabled at this path.
|
|
457
|
-
// The reader resolves core protocols (e.g. permissions) from the registry.
|
|
458
|
-
let protocolDefinition;
|
|
459
|
-
try {
|
|
460
|
-
protocolDefinition = await this.deps.validationStateReader.fetchProtocolDefinition(
|
|
461
|
-
tenant,
|
|
462
|
-
message.descriptor.protocol,
|
|
463
|
-
message.descriptor.messageTimestamp,
|
|
464
|
-
);
|
|
465
|
-
} catch (error) {
|
|
466
|
-
// If the protocol definition can't be found, skip the backstop check.
|
|
467
|
-
// Authorization will handle the missing protocol error later.
|
|
468
|
-
console.warn(`enforceSquashBackstop: failed to fetch protocol definition for '${message.descriptor.protocol}':`, error);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Walk the structure to find the rule set for this protocol path
|
|
473
|
-
const pathSegments = message.descriptor.protocolPath.split('/');
|
|
474
|
-
let ruleSet = protocolDefinition.structure[pathSegments[0]];
|
|
475
|
-
for (let i = 1; i < pathSegments.length && ruleSet !== undefined; i++) {
|
|
476
|
-
ruleSet = ruleSet[pathSegments[i]] as typeof ruleSet;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
if (ruleSet?.$squash !== true) {
|
|
459
|
+
private async enforceSquashBackstop(
|
|
460
|
+
tenant: string,
|
|
461
|
+
message: RecordsWriteMessage,
|
|
462
|
+
ruleSet: ProtocolRuleSet,
|
|
463
|
+
): Promise<void> {
|
|
464
|
+
if (ruleSet.$squash !== true) {
|
|
480
465
|
return;
|
|
481
466
|
}
|
|
482
467
|
|
package/src/index.ts
CHANGED
|
@@ -9,9 +9,8 @@ export { StoreValidationStateReader } from './core/validation-state-reader.js';
|
|
|
9
9
|
export type { RecordedValidationRead } from './core/recording-validation-state-reader.js';
|
|
10
10
|
export { RecordingValidationStateReader } from './core/recording-validation-state-reader.js';
|
|
11
11
|
export type { MessagesFilter, MessagesQueryDescriptor, MessagesQueryMessage, MessagesQueryReply, MessagesQueryReplyEntry, MessagesReadMessage, MessagesReadReply, MessagesReadReplyEntry, MessagesReadDescriptor, MessagesSubscribeDescriptor, MessagesSubscribeMessage, MessagesSubscribeReply, MessagesSubscribeMessageOptions } from './types/messages-types.js';
|
|
12
|
-
export type { GT, LT, Filter, FilterValue, KeyValues, EqualFilter, OneOfFilter, RangeFilter, RangeCriterion, PaginationCursor, QueryOptions, RangeValue, StartsWithFilter } from './types/query-types.js';
|
|
12
|
+
export type { GT, LT, Filter, FilterValue, KeyValues, EqualFilter, OneOfFilter, RangeFilter, RangeCriterion, PaginationCursor, QueryOptions, RangeValue, StartsWithFilter, SubtreeFilter } from './types/query-types.js';
|
|
13
13
|
export type { ProtocolsConfigureDescriptor, ProtocolDefinition, ProtocolTypes, ProtocolRuleSet, ProtocolsQueryFilter, ProtocolsConfigureMessage, ProtocolsQueryMessage, ProtocolsQueryReply, ProtocolActionRule, ProtocolDeliveryStrategy, ProtocolKeyAgreement, ProtocolsQueryDescriptor, ProtocolRecordLimitDefinition, ProtocolSizeDefinition, ProtocolTagsDefinition, ProtocolTagSchema, ProtocolType, ProtocolUses } from './types/protocols-types.js';
|
|
14
|
-
export { ProtocolRecordLimitStrategy } from './types/protocols-types.js';
|
|
15
14
|
export type { DataEncodedRecordsWriteMessage, RecordsCountDescriptor, RecordsCountMessage, RecordsCountReply, RecordsDeleteMessage, RecordsFilter, RecordsQueryMessage, RecordsQueryReply, RecordsQueryReplyEntry, RecordsReadMessage, RecordsReadReply, RecordsSubscribeDescriptor, RecordsSubscribeMessage, RecordsSubscribeReply, RecordsWriteDescriptor, RecordsWriteTags, RecordsWriteTagValue, RecordsWriteMessage, RecordsWriteSignaturePayload, RecordsDeleteDescriptor, RecordsQueryDescriptor, RecordsReadDescriptor, RecordsSubscribeMessageOptions, RecordsWriteMessageOptions, InternalRecordsWriteMessage, RecordEvent, RecordsWriteTagsFilter } from './types/records-types.js';
|
|
16
15
|
export type { GeneralJws, SignatureEntry } from './types/jws-types.js';
|
|
17
16
|
export { authenticate } from './core/auth.js';
|
|
@@ -53,6 +52,7 @@ export { DwnError, DwnErrorCode } from './core/dwn-error.js';
|
|
|
53
52
|
export type { DwnErrorInfo } from './core/dwn-error.js';
|
|
54
53
|
export { DwnInterfaceName, DwnMethodName } from './enums/dwn-interface-method.js';
|
|
55
54
|
export { Encoder } from './utils/encoder.js';
|
|
55
|
+
export { assertValidSubtreeFilters, isSubtreeFilter } from './utils/filter.js';
|
|
56
56
|
export { MessagesSubscribe } from './interfaces/messages-subscribe.js';
|
|
57
57
|
export type { MessagesSubscribeOptions } from './interfaces/messages-subscribe.js';
|
|
58
58
|
export { Encryption, ContentEncryptionAlgorithm, KeyAgreementAlgorithm, ROLE_AUDIENCE_DERIVATION_SCHEME, SEAL_DERIVATION_SCHEME } from './utils/encryption.js';
|
|
@@ -82,7 +82,7 @@ export type { MessagesReadOptions } from './interfaces/messages-read.js';
|
|
|
82
82
|
export { MessagesQuery } from './interfaces/messages-query.js';
|
|
83
83
|
export type { MessagesQueryOptions } from './interfaces/messages-query.js';
|
|
84
84
|
export type { UnionMessageReply } from './core/message-reply.js';
|
|
85
|
-
export type { MessageStore, MessageStoreLatestStateTransition, MessageStoreOptions, MessageStorePutResult } from './types/message-store.js';
|
|
85
|
+
export type { MessageStore, MessageStoreLatestStateTransition, MessageStoreOptions, MessageStorePutResult, MessageStoreQueryOptions, RecordLimitOccupancy } from './types/message-store.js';
|
|
86
86
|
export { Replication } from './utils/replication.js';
|
|
87
87
|
export type { MessageInterface } from './types/message-interface.js';
|
|
88
88
|
export { PermissionGrant } from './protocols/permission-grant.js';
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
import { AbstractMessage } from '../core/abstract-message.js';
|
|
11
11
|
import { DwnConstant } from '../core/dwn-constant.js';
|
|
12
|
+
import { FilterUtility } from '../utils/filter.js';
|
|
12
13
|
import { Message } from '../core/message.js';
|
|
13
14
|
import { PermissionGrant } from '../protocols/permission-grant.js';
|
|
14
15
|
import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js';
|
|
@@ -18,7 +19,7 @@ import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
|
18
19
|
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
19
20
|
import { getRoleAudienceContextId, getRuleSetAtPath, isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
|
|
20
21
|
import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js';
|
|
21
|
-
import { ProtocolAction, ProtocolActor
|
|
22
|
+
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
|
|
22
23
|
|
|
23
24
|
export type ProtocolsConfigureOptions = {
|
|
24
25
|
messageTimestamp?: string;
|
|
@@ -300,7 +301,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
|
|
|
300
301
|
|
|
301
302
|
// Validate $recordLimit
|
|
302
303
|
if (ruleSet.$recordLimit !== undefined) {
|
|
303
|
-
const { max
|
|
304
|
+
const { max } = ruleSet.$recordLimit;
|
|
304
305
|
|
|
305
306
|
if (!Number.isInteger(max) || max < 1) {
|
|
306
307
|
throw new DwnError(
|
|
@@ -315,15 +316,6 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
|
|
|
315
316
|
`Invalid $recordLimit.max value ${max} at protocol path '${ruleSetProtocolPath}': must be <= ${DwnConstant.maxRecordLimit}.`
|
|
316
317
|
);
|
|
317
318
|
}
|
|
318
|
-
|
|
319
|
-
const validStrategies = Object.values(ProtocolRecordLimitStrategy) as string[];
|
|
320
|
-
if (!validStrategies.includes(strategy as string)) {
|
|
321
|
-
throw new DwnError(
|
|
322
|
-
DwnErrorCode.ProtocolsConfigureInvalidRecordLimit,
|
|
323
|
-
`Invalid $recordLimit.strategy '${strategy}' at protocol path '${ruleSetProtocolPath}': ` +
|
|
324
|
-
`must be one of ${validStrategies.join(', ')}.`
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
319
|
}
|
|
328
320
|
|
|
329
321
|
if (ruleSet.$tags !== undefined) {
|
|
@@ -412,8 +404,7 @@ export class ProtocolsConfigure extends AbstractMessage<ProtocolsConfigureMessag
|
|
|
412
404
|
ProtocolsConfigure.validateCrossProtocolAlias(actionRule.of, uses, ruleSetProtocolPath, 'of');
|
|
413
405
|
} else {
|
|
414
406
|
// Local `of`: must be self or ancestor
|
|
415
|
-
const isSelfOrAncestor =
|
|
416
|
-
|| ruleSetProtocolPath.startsWith(actionRule.of + '/');
|
|
407
|
+
const isSelfOrAncestor = FilterUtility.matchesSubtree(actionRule.of, ruleSetProtocolPath);
|
|
417
408
|
if (!isSelfOrAncestor) {
|
|
418
409
|
throw new DwnError(
|
|
419
410
|
DwnErrorCode.ProtocolsConfigureInvalidActionOfNotAnAncestor,
|
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,
|
|
@@ -318,17 +318,12 @@ export class IndexLevel {
|
|
|
318
318
|
* @returns {IndexedItem[]} an array of `IndexedItem` that match the given filters.
|
|
319
319
|
*/
|
|
320
320
|
async query(tenant: string, filters: Filter[], queryOptions: QueryOptions, options?: IndexLevelOptions): Promise<IndexedItem[]> {
|
|
321
|
+
assertValidSubtreeFilters(filters);
|
|
321
322
|
|
|
322
323
|
// 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
|
-
}
|
|
324
|
+
const compoundQuery = this.queryUsingCompoundIndex(tenant, filters, queryOptions, options);
|
|
325
|
+
if (compoundQuery !== undefined) {
|
|
326
|
+
return compoundQuery;
|
|
332
327
|
}
|
|
333
328
|
|
|
334
329
|
// Strategy 2: in-memory paging for concise filters
|
|
@@ -340,6 +335,47 @@ export class IndexLevel {
|
|
|
340
335
|
return this.queryWithIteratorPaging(tenant, filters, queryOptions, options);
|
|
341
336
|
}
|
|
342
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Queries one bounded page without selecting the in-memory strategy.
|
|
340
|
+
*
|
|
341
|
+
* Projection callers use this when they must close the outer iterator before
|
|
342
|
+
* opening related index ranges. A matching compound index is preferred;
|
|
343
|
+
* otherwise the requested-sort index is streamed directly.
|
|
344
|
+
*/
|
|
345
|
+
async queryWithBoundedPaging(
|
|
346
|
+
tenant: string,
|
|
347
|
+
filters: Filter[],
|
|
348
|
+
queryOptions: QueryOptions,
|
|
349
|
+
options?: IndexLevelOptions,
|
|
350
|
+
): Promise<IndexedItem[]> {
|
|
351
|
+
assertValidSubtreeFilters(filters);
|
|
352
|
+
|
|
353
|
+
const compoundQuery = this.queryUsingCompoundIndex(tenant, filters, queryOptions, options);
|
|
354
|
+
return compoundQuery ?? this.queryWithIteratorPaging(tenant, filters, queryOptions, options);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private queryUsingCompoundIndex(
|
|
358
|
+
tenant: string,
|
|
359
|
+
filters: Filter[],
|
|
360
|
+
queryOptions: QueryOptions,
|
|
361
|
+
options?: IndexLevelOptions,
|
|
362
|
+
): Promise<IndexedItem[]> | undefined {
|
|
363
|
+
if (filters.length !== 1 || isEmptyObject(filters[0])) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const compoundIndex = selectCompoundIndex(filters[0], queryOptions, this._compoundIndexes);
|
|
368
|
+
if (compoundIndex === undefined) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return queryWithCompoundIndex(
|
|
373
|
+
this.db, tenant, filters[0], queryOptions, compoundIndex,
|
|
374
|
+
IndexLevel.encodeValue, IndexLevel.delimiter,
|
|
375
|
+
this.queryWithIteratorPaging.bind(this), options
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
343
379
|
/**
|
|
344
380
|
* Counts the number of items that match the given filters without loading full records.
|
|
345
381
|
*
|
|
@@ -348,6 +384,7 @@ export class IndexLevel {
|
|
|
348
384
|
*/
|
|
349
385
|
async count(tenant: string, filters: Filter[], queryOptions: Omit<QueryOptions, 'limit' | 'cursor'>,
|
|
350
386
|
options?: IndexLevelOptions): Promise<number> {
|
|
387
|
+
assertValidSubtreeFilters(filters);
|
|
351
388
|
|
|
352
389
|
// try compound index for single-filter queries
|
|
353
390
|
if (filters.length === 1 && !isEmptyObject(filters[0])) {
|
|
@@ -365,6 +402,117 @@ export class IndexLevel {
|
|
|
365
402
|
return results.length;
|
|
366
403
|
}
|
|
367
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Finds the inclusive rank boundary for one `$recordLimit` parent group.
|
|
407
|
+
*
|
|
408
|
+
* The existing `parentId` index keeps the scan isolated to one nested-record
|
|
409
|
+
* group. Root records have no `parentId`, so they are read from the exact
|
|
410
|
+
* `protocolPath` range and explicitly fenced to rows without a parent. The
|
|
411
|
+
* bounded max-heap retains only the earliest `max` rank keys while the range
|
|
412
|
+
* is streamed; no group-sized array is materialized.
|
|
413
|
+
*/
|
|
414
|
+
public async findRecordLimitRankCutoff(
|
|
415
|
+
tenant: string,
|
|
416
|
+
candidateFilter: Filter,
|
|
417
|
+
parentId: string | undefined,
|
|
418
|
+
max: number,
|
|
419
|
+
options?: IndexLevelOptions,
|
|
420
|
+
): Promise<string | undefined> {
|
|
421
|
+
if (!Number.isInteger(max) || max < 1) {
|
|
422
|
+
throw new TypeError(`IndexLevel: record-limit max must be a positive integer.`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const protocolPath = candidateFilter.protocolPath;
|
|
426
|
+
if (typeof protocolPath !== 'string') {
|
|
427
|
+
throw new TypeError(`IndexLevel: record-limit candidate filter must contain one protocolPath.`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const indexName = parentId === undefined ? 'protocolPath' : 'parentId';
|
|
431
|
+
const indexValue = parentId ?? protocolPath;
|
|
432
|
+
const matchPrefix = IndexLevel.keySegmentJoin(IndexLevel.encodeValue(indexValue), '');
|
|
433
|
+
const partition = await this.getIndexPartition(tenant, indexName);
|
|
434
|
+
const rankHeap: string[] = [];
|
|
435
|
+
|
|
436
|
+
for await (const [key, value] of partition.iterator({ gt: matchPrefix }, options)) {
|
|
437
|
+
if (!key.startsWith(matchPrefix)) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const item = JSON.parse(value) as IndexedItem;
|
|
442
|
+
if ((parentId === undefined && item.indexes.parentId !== undefined) ||
|
|
443
|
+
!FilterUtility.matchFilter(item.indexes, candidateFilter)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
IndexLevel.addToBoundedMaxHeap(rankHeap, IndexLevel.createRecordLimitRankKey(item.indexes), max);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return rankHeap[0];
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Builds the deterministic `$recordLimit` rank key: oldest creation time,
|
|
455
|
+
* then record ID. Message CID deliberately does not participate because
|
|
456
|
+
* occupancy belongs to the logical record, not a particular update.
|
|
457
|
+
*/
|
|
458
|
+
public static createRecordLimitRankKey(indexes: KeyValues): string {
|
|
459
|
+
const { dateCreated, recordId } = indexes;
|
|
460
|
+
if (typeof dateCreated !== 'string' || typeof recordId !== 'string') {
|
|
461
|
+
throw new TypeError(`IndexLevel: record-limit candidates require string dateCreated and recordId indexes.`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return IndexLevel.keySegmentJoin(IndexLevel.encodeValue(dateCreated), IndexLevel.encodeValue(recordId));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private static addToBoundedMaxHeap(heap: string[], value: string, max: number): void {
|
|
468
|
+
if (heap.length < max) {
|
|
469
|
+
heap.push(value);
|
|
470
|
+
IndexLevel.siftRecordLimitRankUp(heap, heap.length - 1);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (lexicographicalCompare(value, heap[0]) >= 0) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
heap[0] = value;
|
|
479
|
+
IndexLevel.siftRecordLimitRankDown(heap, 0);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private static siftRecordLimitRankUp(heap: string[], startIndex: number): void {
|
|
483
|
+
let index = startIndex;
|
|
484
|
+
while (index > 0) {
|
|
485
|
+
const parentIndex = (index - 1) >>> 1;
|
|
486
|
+
if (lexicographicalCompare(heap[parentIndex], heap[index]) >= 0) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
[heap[parentIndex], heap[index]] = [heap[index], heap[parentIndex]];
|
|
491
|
+
index = parentIndex;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private static siftRecordLimitRankDown(heap: string[], startIndex: number): void {
|
|
496
|
+
let index = startIndex;
|
|
497
|
+
while (true) {
|
|
498
|
+
const leftIndex = index * 2 + 1;
|
|
499
|
+
if (leftIndex >= heap.length) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const rightIndex = leftIndex + 1;
|
|
504
|
+
const largerChildIndex = rightIndex < heap.length && lexicographicalCompare(heap[rightIndex], heap[leftIndex]) > 0
|
|
505
|
+
? rightIndex
|
|
506
|
+
: leftIndex;
|
|
507
|
+
if (lexicographicalCompare(heap[index], heap[largerChildIndex]) >= 0) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
[heap[index], heap[largerChildIndex]] = [heap[largerChildIndex], heap[index]];
|
|
512
|
+
index = largerChildIndex;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
368
516
|
/**
|
|
369
517
|
* Queries the sort property index for items that match the filters. If no filters are provided, all items are returned.
|
|
370
518
|
* This query is a linear iterator over the sorted index, checking each item for a match.
|
|
@@ -589,6 +737,17 @@ export class IndexLevel {
|
|
|
589
737
|
const exactMatches = await this.filterExactMatches(tenant, propertyName, propertyValue, levelOptions);
|
|
590
738
|
processResults(exactMatches);
|
|
591
739
|
}
|
|
740
|
+
} else if (isSubtreeFilter(propertyFilter)) {
|
|
741
|
+
const exactMatches = await this.filterExactMatches(tenant, propertyName, propertyFilter.subtree, levelOptions);
|
|
742
|
+
processResults(exactMatches);
|
|
743
|
+
|
|
744
|
+
const descendantMatches = await this.filterSubtreeDescendantMatches(
|
|
745
|
+
tenant,
|
|
746
|
+
propertyName,
|
|
747
|
+
propertyFilter.subtree,
|
|
748
|
+
levelOptions,
|
|
749
|
+
);
|
|
750
|
+
processResults(descendantMatches);
|
|
592
751
|
} else if (FilterUtility.isRangeFilter(propertyFilter)) {
|
|
593
752
|
// `propertyFilter` is a `RangeFilter`
|
|
594
753
|
const rangeMatches = await this.filterRangeMatches(tenant, propertyName, propertyFilter, levelOptions);
|
|
@@ -633,6 +792,34 @@ export class IndexLevel {
|
|
|
633
792
|
return matches;
|
|
634
793
|
}
|
|
635
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Returns every slash-delimited descendant of one hierarchical path.
|
|
797
|
+
*
|
|
798
|
+
* The lower bound removes JSON's closing quote so it is the encoded key
|
|
799
|
+
* prefix itself. ASCII `/` and `0` are adjacent in the store's lexicographic
|
|
800
|
+
* ordering, making `path0` the first value outside that prefix without
|
|
801
|
+
* relying on a finite Unicode sentinel.
|
|
802
|
+
*/
|
|
803
|
+
private async filterSubtreeDescendantMatches(
|
|
804
|
+
tenant: string,
|
|
805
|
+
propertyName: string,
|
|
806
|
+
subtree: string,
|
|
807
|
+
options?: IndexLevelOptions,
|
|
808
|
+
): Promise<IndexedItem[]> {
|
|
809
|
+
const encodedPrefix = IndexLevel.encodeValue(`${subtree}/`).slice(0, -1);
|
|
810
|
+
const iteratorOptions: LevelWrapperIteratorOptions<string> = {
|
|
811
|
+
gte : encodedPrefix,
|
|
812
|
+
lt : IndexLevel.encodeValue(`${subtree}0`),
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const filterPartition = await this.getIndexPartition(tenant, propertyName);
|
|
816
|
+
const matches: IndexedItem[] = [];
|
|
817
|
+
for await (const [ _key, value ] of filterPartition.iterator(iteratorOptions, options)) {
|
|
818
|
+
matches.push(JSON.parse(value) as IndexedItem);
|
|
819
|
+
}
|
|
820
|
+
return matches;
|
|
821
|
+
}
|
|
822
|
+
|
|
636
823
|
/**
|
|
637
824
|
* Returns items that match the range filter.
|
|
638
825
|
*
|