@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.
Files changed (152) hide show
  1. package/dist/browser.mjs +1 -1
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/generated/precompiled-validators.js +734 -747
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/json-schemas/definitions.json +5 -0
  6. package/dist/esm/src/core/dwn-constant.js +2 -2
  7. package/dist/esm/src/core/dwn-error.js +0 -1
  8. package/dist/esm/src/core/dwn-error.js.map +1 -1
  9. package/dist/esm/src/core/protocol-authorization-validation.js +1 -24
  10. package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
  11. package/dist/esm/src/core/protocol-authorization.js +1 -5
  12. package/dist/esm/src/core/protocol-authorization.js.map +1 -1
  13. package/dist/esm/src/core/validation-state-reader.js +2 -3
  14. package/dist/esm/src/core/validation-state-reader.js.map +1 -1
  15. package/dist/esm/src/handlers/records-count.js +18 -10
  16. package/dist/esm/src/handlers/records-count.js.map +1 -1
  17. package/dist/esm/src/handlers/records-query.js +10 -7
  18. package/dist/esm/src/handlers/records-query.js.map +1 -1
  19. package/dist/esm/src/handlers/records-subscribe.js +10 -7
  20. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  21. package/dist/esm/src/handlers/records-write.js +8 -26
  22. package/dist/esm/src/handlers/records-write.js.map +1 -1
  23. package/dist/esm/src/index.js +1 -1
  24. package/dist/esm/src/index.js.map +1 -1
  25. package/dist/esm/src/interfaces/protocols-configure.js +4 -9
  26. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  27. package/dist/esm/src/store/index-level.js +143 -6
  28. package/dist/esm/src/store/index-level.js.map +1 -1
  29. package/dist/esm/src/store/message-store-level.js +158 -21
  30. package/dist/esm/src/store/message-store-level.js.map +1 -1
  31. package/dist/esm/src/store/storage-controller.js +1 -3
  32. package/dist/esm/src/store/storage-controller.js.map +1 -1
  33. package/dist/esm/src/types/protocols-types.js +0 -10
  34. package/dist/esm/src/types/protocols-types.js.map +1 -1
  35. package/dist/esm/src/utils/filter.js +35 -1
  36. package/dist/esm/src/utils/filter.js.map +1 -1
  37. package/dist/esm/src/utils/grant-key-coverage.js +4 -6
  38. package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
  39. package/dist/esm/src/utils/messages.js +4 -14
  40. package/dist/esm/src/utils/messages.js.map +1 -1
  41. package/dist/esm/src/utils/permission-scope.js +3 -9
  42. package/dist/esm/src/utils/permission-scope.js.map +1 -1
  43. package/dist/esm/src/utils/record-limit-occupancy.js +70 -202
  44. package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
  45. package/dist/esm/src/utils/records.js +9 -9
  46. package/dist/esm/src/utils/records.js.map +1 -1
  47. package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
  48. package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
  49. package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
  50. package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
  51. package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
  52. package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
  53. package/dist/esm/tests/dwn.spec.js +2 -2
  54. package/dist/esm/tests/dwn.spec.js.map +1 -1
  55. package/dist/esm/tests/features/records-delivery.spec.js +2 -5
  56. package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
  57. package/dist/esm/tests/features/records-nested-query-scope.spec.js +138 -73
  58. package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
  59. package/dist/esm/tests/features/records-record-limit.spec.js +264 -95
  60. package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
  61. package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
  62. package/dist/esm/tests/fuzz/filter.fuzz.spec.js.map +1 -1
  63. package/dist/esm/tests/handlers/records-write.spec.js +57 -3
  64. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  65. package/dist/esm/tests/store/index-level.spec.js +114 -1
  66. package/dist/esm/tests/store/index-level.spec.js.map +1 -1
  67. package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
  68. package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
  69. package/dist/esm/tests/store/message-store-level.spec.js +6 -0
  70. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  71. package/dist/esm/tests/store/message-store.spec.js +81 -0
  72. package/dist/esm/tests/store/message-store.spec.js.map +1 -1
  73. package/dist/esm/tests/utils/filters.spec.js +25 -4
  74. package/dist/esm/tests/utils/filters.spec.js.map +1 -1
  75. package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
  76. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
  77. package/dist/esm/tests/utils/records.spec.js +12 -0
  78. package/dist/esm/tests/utils/records.spec.js.map +1 -1
  79. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
  80. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
  81. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  82. package/dist/types/src/core/dwn-constant.d.ts +2 -2
  83. package/dist/types/src/core/dwn-error.d.ts +0 -1
  84. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  85. package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
  86. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  87. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  88. package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
  89. package/dist/types/src/handlers/records-count.d.ts +1 -0
  90. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  91. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  92. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  93. package/dist/types/src/handlers/records-write.d.ts +3 -0
  94. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  95. package/dist/types/src/index.d.ts +3 -3
  96. package/dist/types/src/index.d.ts.map +1 -1
  97. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  98. package/dist/types/src/store/index-level.d.ts +37 -0
  99. package/dist/types/src/store/index-level.d.ts.map +1 -1
  100. package/dist/types/src/store/message-store-level.d.ts +23 -6
  101. package/dist/types/src/store/message-store-level.d.ts.map +1 -1
  102. package/dist/types/src/store/storage-controller.d.ts.map +1 -1
  103. package/dist/types/src/types/message-store.d.ts +23 -2
  104. package/dist/types/src/types/message-store.d.ts.map +1 -1
  105. package/dist/types/src/types/protocols-types.d.ts +7 -24
  106. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  107. package/dist/types/src/types/query-types.d.ts +8 -1
  108. package/dist/types/src/types/query-types.d.ts.map +1 -1
  109. package/dist/types/src/types/records-types.d.ts +2 -1
  110. package/dist/types/src/types/records-types.d.ts.map +1 -1
  111. package/dist/types/src/utils/filter.d.ts +14 -1
  112. package/dist/types/src/utils/filter.d.ts.map +1 -1
  113. package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
  114. package/dist/types/src/utils/messages.d.ts.map +1 -1
  115. package/dist/types/src/utils/permission-scope.d.ts +0 -2
  116. package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
  117. package/dist/types/src/utils/record-limit-occupancy.d.ts +25 -12
  118. package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
  119. package/dist/types/src/utils/records.d.ts +3 -2
  120. package/dist/types/src/utils/records.d.ts.map +1 -1
  121. package/dist/types/tests/features/records-delivery.spec.d.ts.map +1 -1
  122. package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
  123. package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
  124. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  125. package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
  126. package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
  127. package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
  128. package/package.json +4 -4
  129. package/src/core/dwn-constant.ts +2 -2
  130. package/src/core/dwn-error.ts +0 -2
  131. package/src/core/protocol-authorization-validation.ts +1 -33
  132. package/src/core/protocol-authorization.ts +0 -7
  133. package/src/core/validation-state-reader.ts +2 -3
  134. package/src/handlers/records-count.ts +34 -13
  135. package/src/handlers/records-query.ts +14 -11
  136. package/src/handlers/records-subscribe.ts +18 -11
  137. package/src/handlers/records-write.ts +17 -32
  138. package/src/index.ts +3 -3
  139. package/src/interfaces/protocols-configure.ts +4 -13
  140. package/src/store/index-level.ts +197 -10
  141. package/src/store/message-store-level.ts +216 -25
  142. package/src/store/storage-controller.ts +1 -3
  143. package/src/types/message-store.ts +25 -2
  144. package/src/types/protocols-types.ts +7 -26
  145. package/src/types/query-types.ts +10 -2
  146. package/src/types/records-types.ts +2 -1
  147. package/src/utils/filter.ts +40 -3
  148. package/src/utils/grant-key-coverage.ts +4 -7
  149. package/src/utils/messages.ts +4 -14
  150. package/src/utils/permission-scope.ts +4 -11
  151. package/src/utils/record-limit-occupancy.ts +105 -293
  152. package/src/utils/records.ts +9 -9
@@ -1,42 +1,32 @@
1
- import type { MessageStore } from '../types/message-store.js';
2
1
  import type { ProtocolRecordLimitDefinition } from '../types/protocols-types.js';
3
- import type { RecordsWriteMessage } from '../types/records-types.js';
4
2
  import type { ValidationStateReader } from '../types/validation-state-reader.js';
5
3
  import type { Filter, PaginationCursor } from '../types/query-types.js';
6
4
  import type { MessageSort, Pagination } from '../types/message-types.js';
5
+ import type { MessageStore, RecordLimitOccupancy } from '../types/message-store.js';
6
+ import type { RecordsFilter, RecordsWriteMessage } from '../types/records-types.js';
7
7
 
8
- import { FilterUtility } from './filter.js';
9
8
  import { getRuleSetAtPath } from './protocols.js';
10
- import { lexicographicalCompare } from './string.js';
11
- import { ProtocolRecordLimitStrategy } from '../types/protocols-types.js';
12
9
  import { Records } from './records.js';
13
- import { SortDirection } from '../types/query-types.js';
14
10
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
15
11
  import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
16
12
 
17
- type RecordLimitOccupancyDependencies = {
18
- messageStore: MessageStore;
13
+ type RecordLimitPolicyDependencies = {
19
14
  validationStateReader: ValidationStateReader;
20
15
  };
21
16
 
22
- type RecordLimitOccupancyQueryInput = RecordLimitOccupancyDependencies & {
17
+ type RecordLimitOccupancyDependencies = RecordLimitPolicyDependencies & {
18
+ messageStore: MessageStore;
19
+ };
20
+
21
+ type RecordLimitOccupancyQueryInput = {
22
+ messageStore: MessageStore;
23
23
  tenant: string;
24
24
  filters: Filter[];
25
- messageTimestamp: string;
25
+ recordLimit?: RecordLimitOccupancy;
26
26
  messageSort?: MessageSort;
27
27
  pagination?: Pagination;
28
28
  };
29
29
 
30
- type RecordLimitScope = {
31
- protocol: string;
32
- protocolPath: string;
33
- parentContextId: string;
34
- };
35
-
36
- type RecordLimitFilterResolution = {
37
- projectedFilters: Filter[];
38
- };
39
-
40
30
  type OccupancyProjectionInput<T extends RecordsWriteMessage> = {
41
31
  records: T[];
42
32
  max: number;
@@ -45,171 +35,134 @@ type OccupancyProjectionInput<T extends RecordsWriteMessage> = {
45
35
  };
46
36
 
47
37
  /**
48
- * Queries records with bounded `$recordLimit` occupancy projection when every limited filter targets one concrete scope.
49
- *
50
- * Broad filters keep the store's native query path. Projecting those without a store-level grouping primitive would require
51
- * scanning the full matching set, which is worse than leaving them unprojected until a bounded broad-query strategy exists.
38
+ * Queries the caller-visible portion of the deterministic `$recordLimit`
39
+ * occupant population for one concrete protocol path.
52
40
  */
53
41
  export async function queryRecordsWithRecordLimitOccupancy(
54
42
  input: RecordLimitOccupancyQueryInput
55
43
  ): Promise<{ messages: RecordsWriteMessage[]; cursor?: PaginationCursor }> {
56
- const filterResolution = await resolveRecordLimitFilters(input);
57
- if (filterResolution === undefined) {
58
- const { messages, cursor } = await input.messageStore.query(input.tenant, input.filters, input.messageSort, input.pagination);
59
- return { messages: messages.filter(Records.isRecordsWrite), cursor };
60
- }
61
-
62
- if (filterResolution.projectedFilters.length === 0) {
63
- return { messages: [] };
64
- }
65
-
66
44
  const { messages, cursor } = await input.messageStore.query(
67
45
  input.tenant,
68
- filterResolution.projectedFilters,
46
+ input.filters,
69
47
  input.messageSort,
70
- input.pagination
48
+ input.pagination,
49
+ input.recordLimit === undefined ? undefined : { recordLimit: input.recordLimit },
71
50
  );
72
51
 
73
52
  return { messages: messages.filter(Records.isRecordsWrite), cursor };
74
53
  }
75
54
 
76
55
  /**
77
- * Counts records with bounded `$recordLimit` occupancy projection when every limited filter targets one concrete scope.
56
+ * Counts the same deterministic `$recordLimit` occupant population returned
57
+ * by {@link queryRecordsWithRecordLimitOccupancy}.
78
58
  */
79
- export async function countRecordsWithRecordLimitOccupancy(input: Omit<RecordLimitOccupancyQueryInput, 'pagination'>): Promise<number> {
80
- const filterResolution = await resolveRecordLimitFilters(input);
81
- if (filterResolution === undefined) {
82
- return input.messageStore.count(input.tenant, input.filters, input.messageSort);
83
- }
84
-
85
- if (filterResolution.projectedFilters.length === 0) {
86
- return 0;
87
- }
88
-
89
- return input.messageStore.count(input.tenant, filterResolution.projectedFilters, input.messageSort);
59
+ export async function countRecordsWithRecordLimitOccupancy(
60
+ input: Omit<RecordLimitOccupancyQueryInput, 'pagination'>
61
+ ): Promise<number> {
62
+ return input.messageStore.count(
63
+ input.tenant,
64
+ input.filters,
65
+ input.messageSort,
66
+ input.recordLimit === undefined ? undefined : { recordLimit: input.recordLimit },
67
+ );
90
68
  }
91
69
 
92
70
  /**
93
- * Returns true when the latest RecordsWrite is visible under the `$recordLimit` projection.
71
+ * Returns true when the latest RecordsWrite is part of its path's current
72
+ * `$recordLimit` occupant population.
94
73
  */
95
74
  export async function isRecordLimitOccupant(input: RecordLimitOccupancyDependencies & {
96
75
  tenant: string;
97
76
  message: RecordsWriteMessage;
98
77
  messageTimestamp: string;
99
78
  }): Promise<boolean> {
100
- const recordLimit = await getRecordLimitForMessage({
101
- messageStore : input.messageStore,
102
- validationStateReader : input.validationStateReader,
103
- tenant : input.tenant,
104
- message : input.message,
105
- messageTimestamp : input.messageTimestamp,
106
- recordLimitDefinitions : new Map(),
107
- });
108
-
109
- if (recordLimit === undefined) {
79
+ const recordLimitDefinition = await getRecordLimitForMessage(input);
80
+ if (recordLimitDefinition === undefined) {
110
81
  return true;
111
82
  }
112
83
 
113
- const occupantRecordIds = await findOccupantRecordIds({
114
- messageStore : input.messageStore,
115
- tenant : input.tenant,
116
- scope : getRecordLimitScope(input.message),
117
- recordLimit,
118
- });
119
-
120
- return occupantRecordIds.has(input.message.recordId);
121
- }
122
-
123
- async function resolveRecordLimitFilters(
124
- input: Omit<RecordLimitOccupancyQueryInput, 'pagination'>
125
- ): Promise<RecordLimitFilterResolution | undefined> {
126
- const recordLimitDefinitions = new Map<string, ProtocolRecordLimitDefinition | undefined>();
127
- const projectedFilters: Filter[] = [];
128
- let projectionApplied = false;
129
-
130
- for (const filter of input.filters) {
131
- const recordLimit = await getRecordLimitForFilter({
132
- messageStore : input.messageStore,
133
- validationStateReader : input.validationStateReader,
134
- tenant : input.tenant,
135
- filter,
136
- messageTimestamp : input.messageTimestamp,
137
- recordLimitDefinitions,
138
- });
139
-
140
- if (recordLimit === undefined) {
141
- projectedFilters.push(filter);
142
- continue;
143
- }
144
-
145
- const scope = getRecordLimitScopeFromFilter(filter);
146
- if (scope === undefined) {
147
- return undefined;
148
- }
149
-
150
- const occupantRecordIds = await findOccupantRecordIds({
151
- messageStore : input.messageStore,
152
- tenant : input.tenant,
153
- scope,
154
- recordLimit,
155
- });
156
-
157
- const projectedFilter = buildProjectedFilter(filter, occupantRecordIds);
158
- if (projectedFilter !== undefined) {
159
- projectedFilters.push(projectedFilter);
160
- }
161
- projectionApplied = true;
84
+ const { protocol, protocolPath } = input.message.descriptor;
85
+ const recordLimit: RecordLimitOccupancy = {
86
+ protocol,
87
+ protocolPath,
88
+ max: recordLimitDefinition.max,
89
+ };
90
+ const parentContextId = Records.getParentContextFromOfContextId(input.message.contextId);
91
+ if (parentContextId !== undefined && parentContextId !== '') {
92
+ recordLimit.contextId = parentContextId;
162
93
  }
163
94
 
164
- return projectionApplied ? { projectedFilters } : undefined;
95
+ const count = await input.messageStore.count(input.tenant, [{
96
+ interface : DwnInterfaceName.Records,
97
+ method : DwnMethodName.Write,
98
+ isLatestBaseState : true,
99
+ protocol,
100
+ protocolPath,
101
+ recordId : input.message.recordId,
102
+ }], undefined, { recordLimit });
103
+
104
+ return count > 0;
165
105
  }
166
106
 
167
- async function getRecordLimitForMessage(input: RecordLimitOccupancyDependencies & {
107
+ /** Resolves one incoming Records filter to its store-level occupancy policy. */
108
+ export async function resolveRecordLimitOccupancy(input: RecordLimitPolicyDependencies & {
168
109
  tenant: string;
169
- message: RecordsWriteMessage;
110
+ recordsFilter: RecordsFilter;
170
111
  messageTimestamp: string;
171
- recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
172
- }): Promise<ProtocolRecordLimitDefinition | undefined> {
173
- const { protocol, protocolPath } = input.message.descriptor;
112
+ }): Promise<RecordLimitOccupancy | undefined> {
113
+ const { contextId, protocol, protocolPath } = input.recordsFilter;
174
114
  if (protocol === undefined || protocolPath === undefined) {
175
115
  return undefined;
176
116
  }
177
117
 
178
- return getRecordLimit({
179
- validationStateReader : input.validationStateReader,
180
- tenant : input.tenant,
118
+ const recordLimitDefinition = await getRecordLimit({
119
+ validationStateReader : input.validationStateReader,
120
+ tenant : input.tenant,
181
121
  protocol,
182
122
  protocolPath,
183
- messageTimestamp : input.messageTimestamp,
184
- recordLimitDefinitions : input.recordLimitDefinitions,
123
+ messageTimestamp : input.messageTimestamp,
185
124
  });
125
+ if (recordLimitDefinition === undefined) {
126
+ return undefined;
127
+ }
128
+
129
+ const recordLimit: RecordLimitOccupancy = {
130
+ protocol,
131
+ protocolPath,
132
+ max: recordLimitDefinition.max,
133
+ };
134
+ if (!protocolPath.includes('/')) {
135
+ return recordLimit;
136
+ }
137
+ if (contextId === undefined) {
138
+ return undefined;
139
+ }
140
+
141
+ const contextDepth = contextId.split('/').length;
142
+ const protocolPathDepth = protocolPath.split('/').length;
143
+ recordLimit.contextId = contextDepth === protocolPathDepth
144
+ ? Records.getParentContextFromOfContextId(contextId)
145
+ : contextId;
146
+
147
+ return recordLimit;
186
148
  }
187
149
 
188
- async function getRecordLimitForFilter(input: RecordLimitOccupancyDependencies & {
150
+ async function getRecordLimitForMessage(input: RecordLimitOccupancyDependencies & {
189
151
  tenant: string;
190
- filter: Filter;
152
+ message: RecordsWriteMessage;
191
153
  messageTimestamp: string;
192
- recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
193
154
  }): Promise<ProtocolRecordLimitDefinition | undefined> {
194
- if (input.filter.interface !== DwnInterfaceName.Records ||
195
- input.filter.method !== DwnMethodName.Write ||
196
- input.filter.isLatestBaseState !== true
197
- ) {
198
- return undefined;
199
- }
200
-
201
- const { protocol, protocolPath } = input.filter;
202
- if (typeof protocol !== 'string' || typeof protocolPath !== 'string') {
155
+ const { protocol, protocolPath } = input.message.descriptor;
156
+ if (protocol === undefined || protocolPath === undefined) {
203
157
  return undefined;
204
158
  }
205
159
 
206
160
  return getRecordLimit({
207
- validationStateReader : input.validationStateReader,
208
- tenant : input.tenant,
161
+ validationStateReader : input.validationStateReader,
162
+ tenant : input.tenant,
209
163
  protocol,
210
164
  protocolPath,
211
- messageTimestamp : input.messageTimestamp,
212
- recordLimitDefinitions : input.recordLimitDefinitions,
165
+ messageTimestamp : input.messageTimestamp,
213
166
  });
214
167
  }
215
168
 
@@ -219,171 +172,30 @@ async function getRecordLimit(input: {
219
172
  protocol: string;
220
173
  protocolPath: string;
221
174
  messageTimestamp: string;
222
- recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
223
175
  }): Promise<ProtocolRecordLimitDefinition | undefined> {
224
- const key = `${input.protocol}\u0000${input.protocolPath}`;
225
- if (!input.recordLimitDefinitions.has(key)) {
226
- let protocolDefinition;
227
- try {
228
- protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(
229
- input.tenant,
230
- input.protocol,
231
- input.messageTimestamp,
232
- );
233
- } catch (error) {
234
- if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
235
- input.recordLimitDefinitions.set(key, undefined);
236
- return undefined;
237
- }
238
- throw error;
239
- }
240
-
241
- const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
242
- const recordLimit = ruleSet?.$recordLimit;
243
- input.recordLimitDefinitions.set(
244
- key,
245
- recordLimit?.strategy === ProtocolRecordLimitStrategy.Reject ? recordLimit : undefined
246
- );
247
- }
248
-
249
- return input.recordLimitDefinitions.get(key);
250
- }
251
-
252
- async function findOccupantRecordIds(input: {
253
- messageStore: MessageStore;
254
- tenant: string;
255
- scope: RecordLimitScope;
256
- recordLimit: ProtocolRecordLimitDefinition;
257
- }): Promise<Set<string>> {
258
- const scopeFilter = buildRecordLimitScopeFilter(input.scope);
259
- const messageSort = { dateCreated: SortDirection.Ascending };
260
- const { messages: firstPage } = await input.messageStore.query(
261
- input.tenant,
262
- [scopeFilter],
263
- messageSort,
264
- { limit: input.recordLimit.max }
265
- );
266
- const firstPageCandidates = firstPage.filter(Records.isRecordsWrite);
267
- if (firstPageCandidates.length === 0) {
268
- return new Set();
269
- }
270
-
271
- const boundaryDateCreated = firstPageCandidates.at(-1)!.descriptor.dateCreated;
272
- const beforeBoundary = firstPageCandidates.filter(
273
- (message): boolean => message.descriptor.dateCreated < boundaryDateCreated
274
- );
275
- let boundaryCandidates = firstPageCandidates.filter(
276
- (message): boolean => message.descriptor.dateCreated === boundaryDateCreated
277
- );
278
-
279
- if (firstPageCandidates.length === input.recordLimit.max) {
280
- const { messages } = await input.messageStore.query(
176
+ let protocolDefinition;
177
+ try {
178
+ protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(
281
179
  input.tenant,
282
- [{ ...scopeFilter, dateCreated: boundaryDateCreated }],
283
- messageSort
180
+ input.protocol,
181
+ input.messageTimestamp,
284
182
  );
285
- boundaryCandidates = messages.filter(Records.isRecordsWrite);
286
- }
287
-
288
- boundaryCandidates.sort(compareRecordLimitCandidates);
289
-
290
- const rankedCandidates = [
291
- ...beforeBoundary,
292
- ...boundaryCandidates,
293
- ];
294
-
295
- return selectOccupantRecordIds({
296
- records : rankedCandidates,
297
- max : input.recordLimit.max,
298
- getScopeKey : (): string => '',
299
- compareRecords : compareRecordLimitCandidates,
300
- });
301
- }
302
-
303
- function buildRecordLimitScopeFilter(scope: RecordLimitScope): Filter {
304
- const filter: Filter = {
305
- interface : DwnInterfaceName.Records,
306
- method : DwnMethodName.Write,
307
- isLatestBaseState : true,
308
- protocol : scope.protocol,
309
- protocolPath : scope.protocolPath,
310
- };
311
-
312
- if (scope.parentContextId !== '') {
313
- filter.contextId = FilterUtility.constructPrefixFilterAsRangeFilter(scope.parentContextId);
314
- }
315
-
316
- return filter;
317
- }
318
-
319
- function getRecordLimitScopeFromFilter(filter: Filter): RecordLimitScope | undefined {
320
- const { protocol, protocolPath } = filter;
321
- if (typeof protocol !== 'string' || typeof protocolPath !== 'string') {
322
- return undefined;
323
- }
324
-
325
- if (!protocolPath.includes('/')) {
326
- return { protocol, protocolPath, parentContextId: '' };
327
- }
328
-
329
- const parentContextId = getExactParentContextIdFromFilter(filter);
330
- if (parentContextId === undefined) {
331
- return undefined;
332
- }
333
-
334
- return { protocol, protocolPath, parentContextId };
335
- }
336
-
337
- function getRecordLimitScope(message: RecordsWriteMessage): RecordLimitScope {
338
- return {
339
- protocol : message.descriptor.protocol,
340
- protocolPath : message.descriptor.protocolPath,
341
- parentContextId : Records.getParentContextFromOfContextId(message.contextId) ?? '',
342
- };
343
- }
344
-
345
- function getExactParentContextIdFromFilter(filter: Filter): string | undefined {
346
- const { contextId } = filter;
347
- if (contextId === undefined || !FilterUtility.isRangeFilter(contextId)) {
348
- return undefined;
349
- }
350
-
351
- if (typeof contextId.gte !== 'string' || contextId.lt !== `${contextId.gte}\uffff`) {
352
- return undefined;
353
- }
354
-
355
- return contextId.gte;
356
- }
357
-
358
- function buildProjectedFilter(filter: Filter, occupantRecordIds: Set<string>): Filter | undefined {
359
- let projectedRecordIds = Array.from(occupantRecordIds);
360
- const existingRecordIdFilter = filter.recordId;
361
- if (typeof existingRecordIdFilter === 'string') {
362
- projectedRecordIds = occupantRecordIds.has(existingRecordIdFilter) ? [existingRecordIdFilter] : [];
363
- } else if (Array.isArray(existingRecordIdFilter)) {
364
- projectedRecordIds = existingRecordIdFilter
365
- .filter((recordId): recordId is string => typeof recordId === 'string' && occupantRecordIds.has(recordId));
366
- }
367
-
368
- if (projectedRecordIds.length === 0) {
369
- return undefined;
370
- }
371
-
372
- return {
373
- ...filter,
374
- recordId: projectedRecordIds,
375
- };
376
- }
377
-
378
- function compareRecordLimitCandidates(left: RecordsWriteMessage, right: RecordsWriteMessage): number {
379
- const dateComparison = lexicographicalCompare(left.descriptor.dateCreated, right.descriptor.dateCreated);
380
- if (dateComparison !== 0) {
381
- return dateComparison;
183
+ } catch (error) {
184
+ if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
185
+ return undefined;
186
+ }
187
+ throw error;
382
188
  }
383
189
 
384
- return lexicographicalCompare(left.recordId, right.recordId);
190
+ const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
191
+ const recordLimit = ruleSet?.$recordLimit;
192
+ return recordLimit;
385
193
  }
386
194
 
195
+ /**
196
+ * Selects a bounded occupant set for an in-memory projection whose grouping
197
+ * and ranking policy is supplied by the caller.
198
+ */
387
199
  export function selectOccupantRecordIds<T extends RecordsWriteMessage>(input: OccupancyProjectionInput<T>): Set<string> {
388
200
  const occupants = new Set<string>();
389
201
  const groups = new Map<string, T[]>();
@@ -280,8 +280,9 @@ export class Records {
280
280
  }
281
281
 
282
282
  /**
283
- * Nested protocol-path queries must pin one parent context. Otherwise the same
284
- * protocol type is read across every parent instance.
283
+ * Nested protocol-path queries must select the target path itself or one of
284
+ * its ancestor contexts. An omitted, malformed, or deeper context cannot
285
+ * identify a bounded subtree of the selected protocol path.
285
286
  */
286
287
  public static validateNestedProtocolPathQueryScope(
287
288
  filter: RecordsFilter,
@@ -297,10 +298,11 @@ export class Records {
297
298
  return;
298
299
  }
299
300
 
300
- const expectedParentDepth = protocolPath.split('/').length - 1;
301
+ const protocolPathDepth = protocolPath.split('/').length;
301
302
  const contextIdSegments = contextId?.split('/');
302
303
  if (
303
- contextIdSegments?.length === expectedParentDepth &&
304
+ contextIdSegments !== undefined &&
305
+ contextIdSegments.length <= protocolPathDepth &&
304
306
  contextIdSegments.every(segment => segment.length > 0)
305
307
  ) {
306
308
  return;
@@ -308,7 +310,7 @@ export class Records {
308
310
 
309
311
  throw new DwnError(
310
312
  errorCode,
311
- `${operationName} for nested protocol path '${protocolPath}' must include the direct parent contextId in the filter`
313
+ `${operationName} for nested protocol path '${protocolPath}' must include a contextId selecting that path or one of its ancestors`
312
314
  );
313
315
  }
314
316
 
@@ -386,10 +388,8 @@ export class Records {
386
388
  delete filterCopy.dateUpdated;
387
389
  }
388
390
 
389
- // contextId conversion to prefix match
390
- const contextIdPrefixFilter = contextId ? FilterUtility.constructPrefixFilterAsRangeFilter(contextId) : undefined;
391
- if (contextIdPrefixFilter) {
392
- filterCopy.contextId = contextIdPrefixFilter;
391
+ if (contextId !== undefined) {
392
+ filterCopy.contextId = { subtree: contextId };
393
393
  }
394
394
 
395
395
  // if the author filter is an array and it's empty, we should remove it from the filter as it will always return no results.