@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import type { CompoundIndexDefinition } from './index-level.js';
|
|
2
|
+
import type { CompoundIndexDefinition, IndexedItem } from './index-level.js';
|
|
3
3
|
import type {
|
|
4
4
|
EventLogEntry,
|
|
5
5
|
EventLogReadOptions,
|
|
@@ -13,22 +13,32 @@ import type {
|
|
|
13
13
|
import type { Filter, KeyValues, PaginationCursor, QueryOptions } from '../types/query-types.js';
|
|
14
14
|
import type { GenericMessage, MessageSort, Pagination } from '../types/message-types.js';
|
|
15
15
|
import type { LevelDatabase, LevelWrapperBatchOperation } from './level-wrapper.js';
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
MessageStore,
|
|
18
|
+
MessageStoreLatestStateTransition,
|
|
19
|
+
MessageStoreOptions,
|
|
20
|
+
MessageStorePutResult,
|
|
21
|
+
MessageStoreQueryOptions,
|
|
22
|
+
RecordLimitOccupancy,
|
|
23
|
+
} from '../types/message-store.js';
|
|
17
24
|
|
|
18
25
|
import * as block from 'multiformats/block';
|
|
19
26
|
import * as cbor from '@ipld/dag-cbor';
|
|
20
27
|
|
|
28
|
+
import { runWithCrossContextLock } from '@enbox/common';
|
|
29
|
+
|
|
21
30
|
import { Cid } from '../utils/cid.js';
|
|
22
31
|
import { CID } from 'multiformats/cid';
|
|
23
32
|
import { executeUnlessAborted } from '../utils/abort.js';
|
|
24
|
-
import { FilterUtility } from '../utils/filter.js';
|
|
25
33
|
import { IndexLevel } from './index-level.js';
|
|
26
34
|
import { Message } from '../core/message.js';
|
|
27
35
|
import { Replication } from '../utils/replication.js';
|
|
28
36
|
import { sha256 } from 'multiformats/hashes/sha2';
|
|
29
37
|
import { SortDirection } from '../types/query-types.js';
|
|
38
|
+
import { assertValidSubtreeFilters, FilterUtility } from '../utils/filter.js';
|
|
30
39
|
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';
|
|
31
40
|
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
41
|
+
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -89,6 +99,8 @@ const HEADS_PARTITION = 'heads';
|
|
|
89
99
|
const META_PARTITION = 'meta';
|
|
90
100
|
|
|
91
101
|
const EPOCH_KEY = 'epoch';
|
|
102
|
+
const RECORD_LIMIT_RAW_PAGE_SIZE = 128;
|
|
103
|
+
const RECORD_LIMIT_GROUP_CACHE_SIZE = 128;
|
|
92
104
|
const CURRENT_PARTITIONS = new Set([
|
|
93
105
|
BLOCKS_PARTITION,
|
|
94
106
|
INDEX_PARTITION,
|
|
@@ -136,6 +148,29 @@ type StorePartitions = {
|
|
|
136
148
|
meta: LevelWrapper<string>;
|
|
137
149
|
};
|
|
138
150
|
|
|
151
|
+
type RecordLimitPopulationInput = {
|
|
152
|
+
tenant: string;
|
|
153
|
+
filters: Filter[];
|
|
154
|
+
queryOptions: QueryOptions;
|
|
155
|
+
recordLimit: RecordLimitOccupancy;
|
|
156
|
+
options?: MessageStoreQueryOptions;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type RecordLimitOccupantInput = {
|
|
160
|
+
tenant: string;
|
|
161
|
+
index: IndexLevel;
|
|
162
|
+
item: IndexedItem;
|
|
163
|
+
candidateFilter: Filter;
|
|
164
|
+
recordLimit: RecordLimitOccupancy;
|
|
165
|
+
groupCutoffs: Map<string, string>;
|
|
166
|
+
options?: MessageStoreQueryOptions;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
type RecordLimitPageInput = Omit<RecordLimitOccupantInput, 'item'> & {
|
|
170
|
+
items: IndexedItem[];
|
|
171
|
+
limit?: number;
|
|
172
|
+
};
|
|
173
|
+
|
|
139
174
|
/**
|
|
140
175
|
* A {@link MessageStore} and {@link ReplicationFeedReader} implementation that works in both the
|
|
141
176
|
* browser and server-side, backed by a SINGLE LevelDB root: message blocks, query indexes, the
|
|
@@ -143,8 +178,9 @@ type StorePartitions = {
|
|
|
143
178
|
* the store epoch are all sublevels of one Level instance, so every mutation commits as one fully
|
|
144
179
|
* atomic batch.
|
|
145
180
|
*
|
|
146
|
-
* A per-tenant
|
|
147
|
-
* seq order by construction
|
|
181
|
+
* A per-tenant cross-context write lock spans seq assignment through batch write, so commit order
|
|
182
|
+
* equals seq order by construction even when browser tabs, workers, or service workers share the
|
|
183
|
+
* same IndexedDB database. Seq assignment is gap-free (a failed batch never persists the head),
|
|
148
184
|
* while the readable log stays sparse after compaction and under filters — readers never assume
|
|
149
185
|
* contiguity.
|
|
150
186
|
*/
|
|
@@ -154,7 +190,6 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
|
|
|
154
190
|
private readonly wakePublisher?: WakePublisher;
|
|
155
191
|
private partitionsPromise?: Promise<StorePartitions>;
|
|
156
192
|
private epochPromise?: Promise<string>;
|
|
157
|
-
private readonly writeLocks: Map<string, Promise<void>> = new Map();
|
|
158
193
|
|
|
159
194
|
/**
|
|
160
195
|
* @param {MessageStoreLevelConfig} config
|
|
@@ -304,7 +339,7 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
|
|
|
304
339
|
filters: Filter[],
|
|
305
340
|
messageSort?: MessageSort,
|
|
306
341
|
pagination?: Pagination,
|
|
307
|
-
options?:
|
|
342
|
+
options?: MessageStoreQueryOptions
|
|
308
343
|
): Promise<{ messages: GenericMessage[], cursor?: PaginationCursor}> {
|
|
309
344
|
options?.signal?.throwIfAborted();
|
|
310
345
|
|
|
@@ -312,7 +347,28 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
|
|
|
312
347
|
// this adds 1 to the limit if provided, that way we can check to see if there are additional results and provide a return cursor.
|
|
313
348
|
const queryOptions = MessageStoreLevel.buildQueryOptions(messageSort, pagination);
|
|
314
349
|
const index = await this.index();
|
|
315
|
-
const
|
|
350
|
+
const recordLimit = options?.recordLimit;
|
|
351
|
+
let results: IndexedItem[];
|
|
352
|
+
if (recordLimit === undefined) {
|
|
353
|
+
results = await index.query(tenant, filters, queryOptions, options);
|
|
354
|
+
} else {
|
|
355
|
+
const parentIds = MessageStoreLevel.getRecordLimitParentIds(recordLimit);
|
|
356
|
+
results = parentIds === undefined
|
|
357
|
+
? await this.collectRecordLimitPopulation({
|
|
358
|
+
tenant,
|
|
359
|
+
filters,
|
|
360
|
+
queryOptions,
|
|
361
|
+
recordLimit,
|
|
362
|
+
options,
|
|
363
|
+
})
|
|
364
|
+
: await index.queryRecordLimitParentGroups(tenant, {
|
|
365
|
+
candidateFilter : MessageStoreLevel.buildRecordLimitCandidateFilter(recordLimit),
|
|
366
|
+
filters,
|
|
367
|
+
max : recordLimit.max,
|
|
368
|
+
parentIds,
|
|
369
|
+
queryOptions,
|
|
370
|
+
}, options);
|
|
371
|
+
}
|
|
316
372
|
|
|
317
373
|
let cursor: PaginationCursor | undefined;
|
|
318
374
|
// checks to see if the returned results are greater than the limit, which would indicate additional results.
|
|
@@ -338,13 +394,192 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
|
|
|
338
394
|
tenant: string,
|
|
339
395
|
filters: Filter[],
|
|
340
396
|
messageSort?: MessageSort,
|
|
341
|
-
options?:
|
|
397
|
+
options?: MessageStoreQueryOptions
|
|
342
398
|
): Promise<number> {
|
|
343
399
|
options?.signal?.throwIfAborted();
|
|
344
400
|
|
|
345
401
|
const queryOptions = MessageStoreLevel.buildQueryOptions(messageSort);
|
|
346
402
|
const index = await this.index();
|
|
347
|
-
|
|
403
|
+
const recordLimit = options?.recordLimit;
|
|
404
|
+
if (recordLimit === undefined) {
|
|
405
|
+
return index.count(tenant, filters, queryOptions, options);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const parentIds = MessageStoreLevel.getRecordLimitParentIds(recordLimit);
|
|
409
|
+
if (parentIds !== undefined) {
|
|
410
|
+
return index.countRecordLimitParentGroups(tenant, {
|
|
411
|
+
candidateFilter : MessageStoreLevel.buildRecordLimitCandidateFilter(recordLimit),
|
|
412
|
+
filters,
|
|
413
|
+
max : recordLimit.max,
|
|
414
|
+
parentIds,
|
|
415
|
+
sortProperty : queryOptions.sortProperty,
|
|
416
|
+
}, options);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
let count = 0;
|
|
420
|
+
for await (const _item of this.iterateRecordLimitPopulation({
|
|
421
|
+
tenant,
|
|
422
|
+
filters,
|
|
423
|
+
queryOptions,
|
|
424
|
+
recordLimit,
|
|
425
|
+
options,
|
|
426
|
+
})) {
|
|
427
|
+
count++;
|
|
428
|
+
}
|
|
429
|
+
return count;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Materializes one bounded page from the shared record-limit population iterator.
|
|
434
|
+
*/
|
|
435
|
+
private async collectRecordLimitPopulation(input: RecordLimitPopulationInput): Promise<IndexedItem[]> {
|
|
436
|
+
const results: IndexedItem[] = [];
|
|
437
|
+
for await (const item of this.iterateRecordLimitPopulation(input)) {
|
|
438
|
+
results.push(item);
|
|
439
|
+
}
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Streams caller-filtered records in requested order and admits only records
|
|
445
|
+
* inside their direct-parent group's deterministic rank boundary.
|
|
446
|
+
*
|
|
447
|
+
* Raw candidates are fetched into a small page before any parent lookup is
|
|
448
|
+
* opened. This is deliberate: browser-level/IndexedDB cursors must not be
|
|
449
|
+
* held open while another index transaction is awaited.
|
|
450
|
+
*/
|
|
451
|
+
private async * iterateRecordLimitPopulation(input: RecordLimitPopulationInput): AsyncGenerator<IndexedItem> {
|
|
452
|
+
const { filters, options, queryOptions, recordLimit, tenant } = input;
|
|
453
|
+
const index = await this.index();
|
|
454
|
+
const candidateFilter = MessageStoreLevel.buildRecordLimitCandidateFilter(recordLimit);
|
|
455
|
+
const groupCutoffs = new Map<string, string>();
|
|
456
|
+
const outputLimit = queryOptions.limit;
|
|
457
|
+
let outputCount = 0;
|
|
458
|
+
let rawCursor = queryOptions.cursor;
|
|
459
|
+
|
|
460
|
+
if (outputLimit !== undefined && outputLimit <= 0) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
while (true) {
|
|
465
|
+
const rawItems = await index.queryWithBoundedPaging(tenant, filters, {
|
|
466
|
+
...queryOptions,
|
|
467
|
+
cursor : rawCursor,
|
|
468
|
+
limit : RECORD_LIMIT_RAW_PAGE_SIZE,
|
|
469
|
+
}, options);
|
|
470
|
+
if (rawItems.length === 0) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const occupants = await this.filterRecordLimitOccupants({
|
|
475
|
+
tenant,
|
|
476
|
+
index,
|
|
477
|
+
items : rawItems,
|
|
478
|
+
candidateFilter,
|
|
479
|
+
recordLimit,
|
|
480
|
+
groupCutoffs,
|
|
481
|
+
options,
|
|
482
|
+
limit : outputLimit === undefined ? undefined : outputLimit - outputCount,
|
|
483
|
+
});
|
|
484
|
+
for (const occupant of occupants) {
|
|
485
|
+
yield occupant;
|
|
486
|
+
outputCount++;
|
|
487
|
+
if (outputLimit !== undefined && outputCount >= outputLimit) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (rawItems.length < RECORD_LIMIT_RAW_PAGE_SIZE) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
rawCursor = IndexLevel.createCursorFromItem(rawItems.at(-1)!, queryOptions.sortProperty);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private async filterRecordLimitOccupants(input: RecordLimitPageInput): Promise<IndexedItem[]> {
|
|
500
|
+
const { items, limit, ...occupantInput } = input;
|
|
501
|
+
const occupants: IndexedItem[] = [];
|
|
502
|
+
for (const item of items) {
|
|
503
|
+
if (await this.isWithinRecordLimit({ ...occupantInput, item })) {
|
|
504
|
+
occupants.push(item);
|
|
505
|
+
if (limit !== undefined && occupants.length >= limit) {
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return occupants;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private async isWithinRecordLimit(input: RecordLimitOccupantInput): Promise<boolean> {
|
|
514
|
+
if (!FilterUtility.matchFilter(input.item.indexes, input.candidateFilter)) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const indexedParentId = input.item.indexes.parentId;
|
|
519
|
+
if (indexedParentId !== undefined && typeof indexedParentId !== 'string') {
|
|
520
|
+
throw new TypeError(`MessageStoreLevel: record-limit candidate parentId must be a string.`);
|
|
521
|
+
}
|
|
522
|
+
const parentId = indexedParentId as string | undefined;
|
|
523
|
+
const groupKey = parentId === undefined ? 'root' : `parent:${parentId}`;
|
|
524
|
+
|
|
525
|
+
let cutoff = input.groupCutoffs.get(groupKey);
|
|
526
|
+
if (cutoff !== undefined) {
|
|
527
|
+
// Refresh insertion order so the fixed-size map acts as a small LRU.
|
|
528
|
+
input.groupCutoffs.delete(groupKey);
|
|
529
|
+
input.groupCutoffs.set(groupKey, cutoff);
|
|
530
|
+
} else {
|
|
531
|
+
cutoff = await input.index.findRecordLimitRankCutoff(
|
|
532
|
+
input.tenant,
|
|
533
|
+
input.candidateFilter,
|
|
534
|
+
parentId,
|
|
535
|
+
input.recordLimit.max,
|
|
536
|
+
input.options,
|
|
537
|
+
);
|
|
538
|
+
if (cutoff === undefined) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
MessageStoreLevel.cacheRecordLimitCutoff(input.groupCutoffs, groupKey, cutoff);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return IndexLevel.createRecordLimitRankKey(input.item.indexes) <= cutoff;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private static cacheRecordLimitCutoff(
|
|
548
|
+
cache: Map<string, string>,
|
|
549
|
+
groupKey: string,
|
|
550
|
+
cutoff: string,
|
|
551
|
+
): void {
|
|
552
|
+
if (cache.size >= RECORD_LIMIT_GROUP_CACHE_SIZE) {
|
|
553
|
+
const oldestKey = cache.keys().next().value as string | undefined;
|
|
554
|
+
if (oldestKey !== undefined) {
|
|
555
|
+
cache.delete(oldestKey);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
cache.set(groupKey, cutoff);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private static buildRecordLimitCandidateFilter(recordLimit: RecordLimitOccupancy): Filter {
|
|
562
|
+
const filter: Filter = {
|
|
563
|
+
interface : DwnInterfaceName.Records,
|
|
564
|
+
method : DwnMethodName.Write,
|
|
565
|
+
isLatestBaseState : true,
|
|
566
|
+
protocol : recordLimit.protocol,
|
|
567
|
+
protocolPath : recordLimit.protocolPath,
|
|
568
|
+
};
|
|
569
|
+
if (recordLimit.contextId !== undefined) {
|
|
570
|
+
filter.contextId = { subtree: recordLimit.contextId };
|
|
571
|
+
}
|
|
572
|
+
if (recordLimit.parentId !== undefined) {
|
|
573
|
+
filter.parentId = recordLimit.parentId;
|
|
574
|
+
}
|
|
575
|
+
return filter;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private static getRecordLimitParentIds(recordLimit: RecordLimitOccupancy): readonly string[] | undefined {
|
|
579
|
+
if (recordLimit.parentId === undefined) {
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
return Array.isArray(recordLimit.parentId) ? recordLimit.parentId : [recordLimit.parentId];
|
|
348
583
|
}
|
|
349
584
|
|
|
350
585
|
/**
|
|
@@ -718,8 +953,11 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
|
|
|
718
953
|
async logRead(tenant: string, options: EventLogReadOptions = {}): Promise<EventLogReadResult> {
|
|
719
954
|
const partitions = await this.partitions();
|
|
720
955
|
const { cursor, limit, filters } = options;
|
|
956
|
+
if (filters !== undefined) {
|
|
957
|
+
assertValidSubtreeFilters(filters);
|
|
958
|
+
}
|
|
721
959
|
|
|
722
|
-
// Head-captured-first: the per-tenant write
|
|
960
|
+
// Head-captured-first: the per-tenant write lock serializes commits in position order, so an
|
|
723
961
|
// observed head H is a visibility barrier — every position <= H is already committed when the
|
|
724
962
|
// range scans below run, and anything committed afterward has a position > H and waits for
|
|
725
963
|
// the next page.
|
|
@@ -865,20 +1103,8 @@ export class MessageStoreLevel implements MessageStore, ReplicationFeedReader {
|
|
|
865
1103
|
* a later batch could land first).
|
|
866
1104
|
*/
|
|
867
1105
|
private async withTenantWriteLock<T>(tenant: string, task: () => Promise<T>): Promise<T> {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
const current = new Promise<void>((resolve) => { release = resolve; });
|
|
871
|
-
this.writeLocks.set(tenant, current);
|
|
872
|
-
|
|
873
|
-
await previous;
|
|
874
|
-
try {
|
|
875
|
-
return await task();
|
|
876
|
-
} finally {
|
|
877
|
-
release();
|
|
878
|
-
if (this.writeLocks.get(tenant) === current) {
|
|
879
|
-
this.writeLocks.delete(tenant);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
1106
|
+
const lockName = `enbox:dwn-message-store:${JSON.stringify([this.config.location!, tenant])}`;
|
|
1107
|
+
return runWithCrossContextLock(lockName, task);
|
|
882
1108
|
}
|
|
883
1109
|
|
|
884
1110
|
/**
|
|
@@ -6,7 +6,6 @@ import type { MessageStore, MessageStorePutResult } from '../types/message-store
|
|
|
6
6
|
import type { RecordsDeleteMessage, RecordsWriteMessage } from '../types/records-types.js';
|
|
7
7
|
|
|
8
8
|
import { DwnConstant } from '../core/dwn-constant.js';
|
|
9
|
-
import { FilterUtility } from '../utils/filter.js';
|
|
10
9
|
import { Message } from '../core/message.js';
|
|
11
10
|
import { Records } from '../utils/records.js';
|
|
12
11
|
import { RecordsDelete } from '../interfaces/records-delete.js';
|
|
@@ -123,8 +122,7 @@ export class StorageController {
|
|
|
123
122
|
// Scope by parent context for nested records
|
|
124
123
|
const parentContextId = Records.getParentContextFromOfContextId(message.contextId);
|
|
125
124
|
if (parentContextId !== undefined && parentContextId !== '') {
|
|
126
|
-
|
|
127
|
-
filter.contextId = prefixFilter;
|
|
125
|
+
filter.contextId = { subtree: parentContextId };
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
// Query for all records at this path and context
|
|
@@ -6,6 +6,34 @@ export interface MessageStoreOptions {
|
|
|
6
6
|
signal?: AbortSignal;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Read-time `$recordLimit` occupancy policy for one concrete protocol path.
|
|
11
|
+
*
|
|
12
|
+
* Stores partition matching latest RecordsWrites by `parentId`, rank each
|
|
13
|
+
* partition by `dateCreated` then `recordId` ascending, and apply `max`
|
|
14
|
+
* before the caller's filters, sort, and pagination.
|
|
15
|
+
*/
|
|
16
|
+
export type RecordLimitOccupancy = {
|
|
17
|
+
protocol: string;
|
|
18
|
+
protocolPath: string;
|
|
19
|
+
/**
|
|
20
|
+
* Root of the candidate context subtree. An exact-record request supplies
|
|
21
|
+
* its direct-parent context here so sibling candidates still consume slots.
|
|
22
|
+
* Omitted for root protocol paths and selections scoped only by direct parent.
|
|
23
|
+
*/
|
|
24
|
+
contextId?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Direct-parent groups selected by the Records filter. Including this in
|
|
27
|
+
* the candidate population keeps each selected group's ranking complete.
|
|
28
|
+
*/
|
|
29
|
+
parentId?: string | string[];
|
|
30
|
+
max: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface MessageStoreQueryOptions extends MessageStoreOptions {
|
|
34
|
+
recordLimit?: RecordLimitOccupancy;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
/**
|
|
10
38
|
* Result of a {@link MessageStore.put} operation.
|
|
11
39
|
*/
|
|
@@ -98,7 +126,7 @@ export interface MessageStore {
|
|
|
98
126
|
filters: Filter[],
|
|
99
127
|
messageSort?: MessageSort,
|
|
100
128
|
pagination?: Pagination,
|
|
101
|
-
options?:
|
|
129
|
+
options?: MessageStoreQueryOptions
|
|
102
130
|
): Promise<{ messages: GenericMessage[], cursor?: PaginationCursor}>;
|
|
103
131
|
|
|
104
132
|
/**
|
|
@@ -109,7 +137,7 @@ export interface MessageStore {
|
|
|
109
137
|
tenant: string,
|
|
110
138
|
filters: Filter[],
|
|
111
139
|
messageSort?: MessageSort,
|
|
112
|
-
options?:
|
|
140
|
+
options?: MessageStoreQueryOptions
|
|
113
141
|
): Promise<number>;
|
|
114
142
|
|
|
115
143
|
/**
|
|
@@ -149,33 +149,15 @@ export type ProtocolSizeDefinition = {
|
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
153
|
-
*/
|
|
154
|
-
export enum ProtocolRecordLimitStrategy {
|
|
155
|
-
/** Reject the incoming write with an error. The author must delete old records first. */
|
|
156
|
-
Reject = 'reject',
|
|
157
|
-
/** Automatically delete the oldest record(s) (by `dateCreated`) to make room for the new one. */
|
|
158
|
-
PurgeOldest = 'purgeOldest',
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Limits the number of records at a given protocol path within the same parent context.
|
|
152
|
+
* Limits the visible records at a given protocol path within the same parent context.
|
|
163
153
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* Only initial writes (new records) count toward the limit; updates to existing records do not.
|
|
154
|
+
* Root-level records form one group for the protocol path. Nested records are grouped by
|
|
155
|
+
* direct parent context. Occupants are selected deterministically at read time; valid writes
|
|
156
|
+
* outside the visible population remain stored and may become visible later.
|
|
168
157
|
*/
|
|
169
158
|
export type ProtocolRecordLimitDefinition = {
|
|
170
|
-
/** Maximum number of records
|
|
159
|
+
/** Maximum number of visible records at this path within the same parent context. Must be >= 1. */
|
|
171
160
|
max: number;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Strategy when the limit is reached.
|
|
175
|
-
* Currently only `'reject'` is implemented. Future strategies (e.g. `'purgeOldest'`)
|
|
176
|
-
* are defined in the wire format but not yet enforced.
|
|
177
|
-
*/
|
|
178
|
-
strategy: ProtocolRecordLimitStrategy | `${ProtocolRecordLimitStrategy}` | (string & {});
|
|
179
161
|
};
|
|
180
162
|
|
|
181
163
|
/**
|
|
@@ -287,9 +269,8 @@ export type ProtocolRuleSet = {
|
|
|
287
269
|
$tags?: ProtocolTagsDefinition;
|
|
288
270
|
|
|
289
271
|
/**
|
|
290
|
-
* If
|
|
291
|
-
* parent context
|
|
292
|
-
* whether the write is rejected or the oldest record is purged.
|
|
272
|
+
* If `$recordLimit` is set, reads expose at most `max` records at this protocol path
|
|
273
|
+
* within each direct parent context.
|
|
293
274
|
*/
|
|
294
275
|
$recordLimit?: ProtocolRecordLimitDefinition;
|
|
295
276
|
|
package/src/types/query-types.ts
CHANGED
|
@@ -37,7 +37,15 @@ export type StartsWithFilter = {
|
|
|
37
37
|
startsWith: string;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Selects one hierarchical path and every proper `/`-delimited descendant.
|
|
42
|
+
* Supported only for the `contextId` and `protocolPath` indexes.
|
|
43
|
+
*/
|
|
44
|
+
export type SubtreeFilter = {
|
|
45
|
+
subtree: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type FilterValue = EqualFilter | OneOfFilter | RangeFilter | SubtreeFilter;
|
|
41
49
|
|
|
42
50
|
export type Filter = {
|
|
43
51
|
[property: string]: FilterValue;
|
|
@@ -58,4 +66,4 @@ export type RangeCriterion = {
|
|
|
58
66
|
export type PaginationCursor = {
|
|
59
67
|
messageCid: string;
|
|
60
68
|
value: string | number;
|
|
61
|
-
};
|
|
69
|
+
};
|
|
@@ -154,13 +154,14 @@ export type RecordsFilter = {
|
|
|
154
154
|
published?: boolean;
|
|
155
155
|
|
|
156
156
|
/**
|
|
157
|
-
*
|
|
157
|
+
* Selects the exact context and its proper `/`-delimited descendants.
|
|
158
|
+
* Prefix-sharing sibling contexts are excluded.
|
|
158
159
|
*/
|
|
159
160
|
contextId?: string;
|
|
160
161
|
schema?: string;
|
|
161
162
|
tags?: { [property:string]: RecordsWriteTagsFilter }
|
|
162
163
|
recordId?: string;
|
|
163
|
-
parentId?: string;
|
|
164
|
+
parentId?: string | string[];
|
|
164
165
|
dataFormat?: string;
|
|
165
166
|
dataSize?: RangeFilter;
|
|
166
167
|
dataCid?: string;
|
package/src/utils/filter.ts
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
|
-
import type { EqualFilter, Filter, FilterValue, KeyValues, OneOfFilter, RangeCriterion, RangeFilter, RangeValue } from '../types/query-types.js';
|
|
1
|
+
import type { EqualFilter, Filter, FilterValue, KeyValues, OneOfFilter, RangeCriterion, RangeFilter, RangeValue, SubtreeFilter } from '../types/query-types.js';
|
|
2
|
+
|
|
3
|
+
const SUBTREE_FILTER_PROPERTIES = new Set(['contextId', 'protocolPath']);
|
|
4
|
+
|
|
5
|
+
/** Returns whether the value is a well-formed hierarchical subtree filter. */
|
|
6
|
+
export function isSubtreeFilter(filter: FilterValue): filter is SubtreeFilter {
|
|
7
|
+
return typeof filter === 'object' &&
|
|
8
|
+
filter !== null &&
|
|
9
|
+
!Array.isArray(filter) &&
|
|
10
|
+
Object.keys(filter).length === 1 &&
|
|
11
|
+
typeof (filter as Partial<SubtreeFilter>).subtree === 'string';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Rejects subtree filters for indexes without hierarchical path semantics.
|
|
16
|
+
*
|
|
17
|
+
* @throws {TypeError} If a subtree filter targets an index other than `contextId` or `protocolPath`.
|
|
18
|
+
*/
|
|
19
|
+
export function assertValidSubtreeFilters(filters: Filter[]): void {
|
|
20
|
+
for (const filter of filters) {
|
|
21
|
+
for (const property in filter) {
|
|
22
|
+
if (isSubtreeFilter(filter[property]) && !SUBTREE_FILTER_PROPERTIES.has(property)) {
|
|
23
|
+
throw new TypeError(`SubtreeFilter is not supported for index '${property}'.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
2
28
|
|
|
3
29
|
/**
|
|
4
30
|
* A Utility class to help match indexes against filters.
|
|
@@ -84,7 +110,9 @@ export class FilterUtility {
|
|
|
84
110
|
if (this.matchOneOf(filterValue, indexValue)) {
|
|
85
111
|
return true;
|
|
86
112
|
}
|
|
87
|
-
} else if (this.
|
|
113
|
+
} else if (isSubtreeFilter(filterValue) && this.matchesSubtree(filterValue.subtree, indexValue)) {
|
|
114
|
+
return true;
|
|
115
|
+
} else if (this.isRangeFilter(filterValue) && this.matchRange(filterValue, indexValue as RangeValue)) {
|
|
88
116
|
// `filterValue` is a `RangeFilter`
|
|
89
117
|
// range filters cannot range over booleans
|
|
90
118
|
return true;
|
|
@@ -181,6 +209,15 @@ export class FilterUtility {
|
|
|
181
209
|
};
|
|
182
210
|
}
|
|
183
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Returns whether a candidate is the selected hierarchical path or one of
|
|
214
|
+
* its proper `/`-delimited descendants.
|
|
215
|
+
*/
|
|
216
|
+
static matchesSubtree(subtree: string, candidate: unknown): candidate is string {
|
|
217
|
+
return typeof candidate === 'string' &&
|
|
218
|
+
(candidate === subtree || candidate.startsWith(`${subtree}/`));
|
|
219
|
+
}
|
|
220
|
+
|
|
184
221
|
}
|
|
185
222
|
|
|
186
223
|
export class FilterSelector {
|
|
@@ -238,4 +275,4 @@ export class FilterSelector {
|
|
|
238
275
|
singlePropertyFilter[firstRemainingProperty] = filter[firstRemainingProperty];
|
|
239
276
|
return singlePropertyFilter;
|
|
240
277
|
}
|
|
241
|
-
}
|
|
278
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PermissionScope, RecordsPermissionScope } from '../types/permission-types.js';
|
|
2
2
|
import type { ProtocolDefinition, ProtocolRuleSet } from '../types/protocols-types.js';
|
|
3
3
|
|
|
4
|
+
import { FilterUtility } from './filter.js';
|
|
4
5
|
import { KeyDerivationScheme } from './hd-key.js';
|
|
5
6
|
import { ProtocolAction } from '../types/protocols-types.js';
|
|
6
7
|
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
@@ -171,7 +172,7 @@ function readGrantKeyScopeCoversDeliveredScope(input: {
|
|
|
171
172
|
return false;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
if (
|
|
175
|
+
if (FilterUtility.matchesSubtree(grantProtocolPath, deliveredProtocolPath)) {
|
|
175
176
|
return true;
|
|
176
177
|
}
|
|
177
178
|
|
|
@@ -206,7 +207,7 @@ function writeGrantKeyScopeCoversDeliveredScope(input: {
|
|
|
206
207
|
return true;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
|
-
return
|
|
210
|
+
return FilterUtility.matchesSubtree(grantProtocolPath, deliveredProtocolPath);
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
function getGrantKeyReadRolePathsForScope(protocolDefinition: ProtocolDefinition, scopeProtocolPath: string): string[] {
|
|
@@ -257,7 +258,7 @@ function collectReadRolePaths(protocolDefinition: ProtocolDefinition, ruleSet: P
|
|
|
257
258
|
|
|
258
259
|
function getGrantKeyRolePathsCoveredByScope(protocolDefinition: ProtocolDefinition, scopeProtocolPath?: string): string[] {
|
|
259
260
|
return collectRolePaths(protocolDefinition, protocolDefinition.structure as ProtocolRuleSet)
|
|
260
|
-
.filter((rolePath) => scopeProtocolPath === undefined ||
|
|
261
|
+
.filter((rolePath) => scopeProtocolPath === undefined || FilterUtility.matchesSubtree(scopeProtocolPath, rolePath));
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
function collectRolePaths(protocolDefinition: ProtocolDefinition, ruleSet: ProtocolRuleSet, parentPath: string[] = []): string[] {
|
|
@@ -285,10 +286,6 @@ function isGrantKeyLocalRolePath(protocolDefinition: ProtocolDefinition, protoco
|
|
|
285
286
|
return ruleSet?.$role === true && ruleSet.$keyAgreement !== undefined;
|
|
286
287
|
}
|
|
287
288
|
|
|
288
|
-
function isBoundaryAwareSubtree(scopePath: string, candidatePath: string): boolean {
|
|
289
|
-
return `${candidatePath}/`.startsWith(`${scopePath}/`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
289
|
function getGrantKeyScopeCacheKey(scope: GrantKeyProtocolPathScope): string {
|
|
293
290
|
return JSON.stringify([scope.protocol, scope.protocolPath]);
|
|
294
291
|
}
|
package/src/utils/messages.ts
CHANGED
|
@@ -196,26 +196,16 @@ export class Messages {
|
|
|
196
196
|
delete filterCopy.dateUpdated;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
// Convert protocolPathPrefix into a protocolPath
|
|
200
|
-
// The range gte: prefix, lt: prefix + '/\uffff' matches:
|
|
201
|
-
// - exact: 'post' (prefix itself)
|
|
202
|
-
// - children: 'post/attachment', 'post/comment', etc.
|
|
203
|
-
// - NOT siblings: 'poster', 'postfix' (excluded because '/' < any alphanumeric)
|
|
199
|
+
// Convert protocolPathPrefix into a segment-aware protocolPath subtree filter.
|
|
204
200
|
if (protocolPathPrefix !== undefined) {
|
|
205
201
|
delete (filterCopy as any).protocolPathPrefix;
|
|
206
|
-
filterCopy.protocolPath = {
|
|
207
|
-
gte : protocolPathPrefix,
|
|
208
|
-
lt : protocolPathPrefix + '/\uffff',
|
|
209
|
-
};
|
|
202
|
+
filterCopy.protocolPath = { subtree: protocolPathPrefix };
|
|
210
203
|
}
|
|
211
204
|
|
|
212
|
-
// Convert contextIdPrefix into
|
|
205
|
+
// Convert contextIdPrefix into the same segment-aware subtree filter.
|
|
213
206
|
if (contextIdPrefix !== undefined) {
|
|
214
207
|
delete (filterCopy as any).contextIdPrefix;
|
|
215
|
-
filterCopy.contextId = {
|
|
216
|
-
gte : contextIdPrefix,
|
|
217
|
-
lt : contextIdPrefix + '/\uffff',
|
|
218
|
-
};
|
|
208
|
+
filterCopy.contextId = { subtree: contextIdPrefix };
|
|
219
209
|
}
|
|
220
210
|
|
|
221
211
|
return filterCopy;
|