@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.
Files changed (175) 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 +978 -840
  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-collection.js +200 -0
  16. package/dist/esm/src/handlers/records-collection.js.map +1 -0
  17. package/dist/esm/src/handlers/records-count.js +26 -195
  18. package/dist/esm/src/handlers/records-count.js.map +1 -1
  19. package/dist/esm/src/handlers/records-query.js +19 -373
  20. package/dist/esm/src/handlers/records-query.js.map +1 -1
  21. package/dist/esm/src/handlers/records-subscribe.js +21 -338
  22. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  23. package/dist/esm/src/handlers/records-write.js +8 -26
  24. package/dist/esm/src/handlers/records-write.js.map +1 -1
  25. package/dist/esm/src/index.js +1 -1
  26. package/dist/esm/src/index.js.map +1 -1
  27. package/dist/esm/src/interfaces/protocols-configure.js +4 -9
  28. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  29. package/dist/esm/src/interfaces/records-count.js +1 -1
  30. package/dist/esm/src/interfaces/records-count.js.map +1 -1
  31. package/dist/esm/src/interfaces/records-query.js +1 -1
  32. package/dist/esm/src/interfaces/records-query.js.map +1 -1
  33. package/dist/esm/src/interfaces/records-subscribe.js +3 -1
  34. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  35. package/dist/esm/src/store/index-level.js +210 -21
  36. package/dist/esm/src/store/index-level.js.map +1 -1
  37. package/dist/esm/src/store/message-store-level.js +192 -21
  38. package/dist/esm/src/store/message-store-level.js.map +1 -1
  39. package/dist/esm/src/store/storage-controller.js +1 -3
  40. package/dist/esm/src/store/storage-controller.js.map +1 -1
  41. package/dist/esm/src/types/protocols-types.js +0 -10
  42. package/dist/esm/src/types/protocols-types.js.map +1 -1
  43. package/dist/esm/src/utils/filter.js +35 -1
  44. package/dist/esm/src/utils/filter.js.map +1 -1
  45. package/dist/esm/src/utils/grant-key-coverage.js +4 -6
  46. package/dist/esm/src/utils/grant-key-coverage.js.map +1 -1
  47. package/dist/esm/src/utils/messages.js +4 -14
  48. package/dist/esm/src/utils/messages.js.map +1 -1
  49. package/dist/esm/src/utils/permission-scope.js +3 -9
  50. package/dist/esm/src/utils/permission-scope.js.map +1 -1
  51. package/dist/esm/src/utils/record-limit-occupancy.js +73 -202
  52. package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
  53. package/dist/esm/src/utils/records.js +19 -52
  54. package/dist/esm/src/utils/records.js.map +1 -1
  55. package/dist/esm/tests/core/records-grant-authorization.spec.js +13 -0
  56. package/dist/esm/tests/core/records-grant-authorization.spec.js.map +1 -1
  57. package/dist/esm/tests/core/replication-replay-property.spec.js +2 -2
  58. package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
  59. package/dist/esm/tests/core/validation-read-closure.spec.js +1 -1
  60. package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
  61. package/dist/esm/tests/dwn.spec.js +2 -2
  62. package/dist/esm/tests/dwn.spec.js.map +1 -1
  63. package/dist/esm/tests/features/records-delivery.spec.js +2 -5
  64. package/dist/esm/tests/features/records-delivery.spec.js.map +1 -1
  65. package/dist/esm/tests/features/records-nested-query-scope.spec.js +149 -70
  66. package/dist/esm/tests/features/records-nested-query-scope.spec.js.map +1 -1
  67. package/dist/esm/tests/features/records-record-limit.spec.js +380 -95
  68. package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
  69. package/dist/esm/tests/fuzz/filter.fuzz.spec.js +19 -4
  70. package/dist/esm/tests/fuzz/filter.fuzz.spec.js.map +1 -1
  71. package/dist/esm/tests/handlers/records-count.spec.js +52 -1
  72. package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
  73. package/dist/esm/tests/handlers/records-subscribe.spec.js +76 -1
  74. package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
  75. package/dist/esm/tests/handlers/records-write.spec.js +57 -3
  76. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  77. package/dist/esm/tests/store/index-level.spec.js +208 -1
  78. package/dist/esm/tests/store/index-level.spec.js.map +1 -1
  79. package/dist/esm/tests/store/message-store-cross-context.spec.js +46 -0
  80. package/dist/esm/tests/store/message-store-cross-context.spec.js.map +1 -0
  81. package/dist/esm/tests/store/message-store-level.spec.js +76 -1
  82. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  83. package/dist/esm/tests/store/message-store.spec.js +81 -0
  84. package/dist/esm/tests/store/message-store.spec.js.map +1 -1
  85. package/dist/esm/tests/utils/filters.spec.js +25 -4
  86. package/dist/esm/tests/utils/filters.spec.js.map +1 -1
  87. package/dist/esm/tests/utils/permission-scope.spec.js +4 -0
  88. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
  89. package/dist/esm/tests/utils/records.spec.js +12 -0
  90. package/dist/esm/tests/utils/records.spec.js.map +1 -1
  91. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +3 -1
  92. package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
  93. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  94. package/dist/types/src/core/dwn-constant.d.ts +2 -2
  95. package/dist/types/src/core/dwn-error.d.ts +0 -1
  96. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  97. package/dist/types/src/core/protocol-authorization-validation.d.ts +0 -10
  98. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  99. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  100. package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
  101. package/dist/types/src/handlers/records-collection.d.ts +32 -0
  102. package/dist/types/src/handlers/records-collection.d.ts.map +1 -0
  103. package/dist/types/src/handlers/records-count.d.ts +0 -34
  104. package/dist/types/src/handlers/records-count.d.ts.map +1 -1
  105. package/dist/types/src/handlers/records-query.d.ts +0 -61
  106. package/dist/types/src/handlers/records-query.d.ts.map +1 -1
  107. package/dist/types/src/handlers/records-subscribe.d.ts +1 -37
  108. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  109. package/dist/types/src/handlers/records-write.d.ts +3 -0
  110. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  111. package/dist/types/src/index.d.ts +3 -3
  112. package/dist/types/src/index.d.ts.map +1 -1
  113. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  114. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  115. package/dist/types/src/store/index-level.d.ts +61 -0
  116. package/dist/types/src/store/index-level.d.ts.map +1 -1
  117. package/dist/types/src/store/message-store-level.d.ts +24 -6
  118. package/dist/types/src/store/message-store-level.d.ts.map +1 -1
  119. package/dist/types/src/store/storage-controller.d.ts.map +1 -1
  120. package/dist/types/src/types/message-store.d.ts +28 -2
  121. package/dist/types/src/types/message-store.d.ts.map +1 -1
  122. package/dist/types/src/types/protocols-types.d.ts +7 -24
  123. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  124. package/dist/types/src/types/query-types.d.ts +8 -1
  125. package/dist/types/src/types/query-types.d.ts.map +1 -1
  126. package/dist/types/src/types/records-types.d.ts +3 -2
  127. package/dist/types/src/types/records-types.d.ts.map +1 -1
  128. package/dist/types/src/utils/filter.d.ts +14 -1
  129. package/dist/types/src/utils/filter.d.ts.map +1 -1
  130. package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -1
  131. package/dist/types/src/utils/messages.d.ts.map +1 -1
  132. package/dist/types/src/utils/permission-scope.d.ts +0 -2
  133. package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
  134. package/dist/types/src/utils/record-limit-occupancy.d.ts +25 -12
  135. package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
  136. package/dist/types/src/utils/records.d.ts +6 -24
  137. package/dist/types/src/utils/records.d.ts.map +1 -1
  138. package/dist/types/tests/features/records-delivery.spec.d.ts.map +1 -1
  139. package/dist/types/tests/features/records-nested-query-scope.spec.d.ts.map +1 -1
  140. package/dist/types/tests/features/records-record-limit.spec.d.ts.map +1 -1
  141. package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
  142. package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
  143. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  144. package/dist/types/tests/store/message-store-cross-context.spec.d.ts +2 -0
  145. package/dist/types/tests/store/message-store-cross-context.spec.d.ts.map +1 -0
  146. package/dist/types/tests/store/message-store.spec.d.ts.map +1 -1
  147. package/package.json +4 -4
  148. package/src/core/dwn-constant.ts +2 -2
  149. package/src/core/dwn-error.ts +0 -2
  150. package/src/core/protocol-authorization-validation.ts +1 -33
  151. package/src/core/protocol-authorization.ts +0 -7
  152. package/src/core/validation-state-reader.ts +2 -3
  153. package/src/handlers/records-collection.ts +269 -0
  154. package/src/handlers/records-count.ts +38 -232
  155. package/src/handlers/records-query.ts +22 -444
  156. package/src/handlers/records-subscribe.ts +26 -397
  157. package/src/handlers/records-write.ts +17 -32
  158. package/src/index.ts +3 -3
  159. package/src/interfaces/protocols-configure.ts +4 -13
  160. package/src/interfaces/records-count.ts +1 -1
  161. package/src/interfaces/records-query.ts +1 -1
  162. package/src/interfaces/records-subscribe.ts +5 -2
  163. package/src/store/index-level.ts +323 -28
  164. package/src/store/message-store-level.ts +251 -25
  165. package/src/store/storage-controller.ts +1 -3
  166. package/src/types/message-store.ts +30 -2
  167. package/src/types/protocols-types.ts +7 -26
  168. package/src/types/query-types.ts +10 -2
  169. package/src/types/records-types.ts +3 -2
  170. package/src/utils/filter.ts +40 -3
  171. package/src/utils/grant-key-coverage.ts +4 -7
  172. package/src/utils/messages.ts +4 -14
  173. package/src/utils/permission-scope.ts +4 -11
  174. package/src/utils/record-limit-occupancy.ts +108 -293
  175. package/src/utils/records.ts +22 -57
@@ -1,3 +1,5 @@
1
+ import { FilterUtility } from './filter.js';
2
+
1
3
  /**
2
4
  * Minimal protocol-bound scope shape used by permission grant authorization.
3
5
  *
@@ -38,22 +40,13 @@ export class PermissionScopeMatcher {
38
40
  }
39
41
 
40
42
  if (scope.protocolPath !== undefined) {
41
- return PermissionScopeMatcher.matchesSubtree(scope.protocolPath, target.protocolPath);
43
+ return FilterUtility.matchesSubtree(scope.protocolPath, target.protocolPath);
42
44
  }
43
45
 
44
46
  if (scope.contextId !== undefined) {
45
- return PermissionScopeMatcher.matchesContextId(scope.contextId, target.contextId);
47
+ return FilterUtility.matchesSubtree(scope.contextId, target.contextId);
46
48
  }
47
49
 
48
50
  return true;
49
51
  }
50
-
51
- private static matchesContextId(scopeContextId: string, candidateContextId: unknown): boolean {
52
- return PermissionScopeMatcher.matchesSubtree(scopeContextId, candidateContextId);
53
- }
54
-
55
- private static matchesSubtree(scopeValue: string, candidateValue: unknown): boolean {
56
- return typeof candidateValue === 'string' &&
57
- (candidateValue === scopeValue || candidateValue.startsWith(scopeValue + '/'));
58
- }
59
52
  }
@@ -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,137 @@ 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, parentId, 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 (parentId !== undefined) {
138
+ recordLimit.parentId = parentId;
139
+ }
140
+ if (contextId === undefined) {
141
+ return recordLimit;
142
+ }
143
+
144
+ const contextDepth = contextId.split('/').length;
145
+ const protocolPathDepth = protocolPath.split('/').length;
146
+ recordLimit.contextId = contextDepth === protocolPathDepth
147
+ ? Records.getParentContextFromOfContextId(contextId)
148
+ : contextId;
149
+
150
+ return recordLimit;
186
151
  }
187
152
 
188
- async function getRecordLimitForFilter(input: RecordLimitOccupancyDependencies & {
153
+ async function getRecordLimitForMessage(input: RecordLimitOccupancyDependencies & {
189
154
  tenant: string;
190
- filter: Filter;
155
+ message: RecordsWriteMessage;
191
156
  messageTimestamp: string;
192
- recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
193
157
  }): 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') {
158
+ const { protocol, protocolPath } = input.message.descriptor;
159
+ if (protocol === undefined || protocolPath === undefined) {
203
160
  return undefined;
204
161
  }
205
162
 
206
163
  return getRecordLimit({
207
- validationStateReader : input.validationStateReader,
208
- tenant : input.tenant,
164
+ validationStateReader : input.validationStateReader,
165
+ tenant : input.tenant,
209
166
  protocol,
210
167
  protocolPath,
211
- messageTimestamp : input.messageTimestamp,
212
- recordLimitDefinitions : input.recordLimitDefinitions,
168
+ messageTimestamp : input.messageTimestamp,
213
169
  });
214
170
  }
215
171
 
@@ -219,171 +175,30 @@ async function getRecordLimit(input: {
219
175
  protocol: string;
220
176
  protocolPath: string;
221
177
  messageTimestamp: string;
222
- recordLimitDefinitions: Map<string, ProtocolRecordLimitDefinition | undefined>;
223
178
  }): 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(
179
+ let protocolDefinition;
180
+ try {
181
+ protocolDefinition = await input.validationStateReader.fetchProtocolDefinition(
281
182
  input.tenant,
282
- [{ ...scopeFilter, dateCreated: boundaryDateCreated }],
283
- messageSort
183
+ input.protocol,
184
+ input.messageTimestamp,
284
185
  );
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;
186
+ } catch (error) {
187
+ if (error instanceof DwnError && error.code === DwnErrorCode.ProtocolAuthorizationProtocolNotFound) {
188
+ return undefined;
189
+ }
190
+ throw error;
382
191
  }
383
192
 
384
- return lexicographicalCompare(left.recordId, right.recordId);
193
+ const ruleSet = getRuleSetAtPath(input.protocolPath, protocolDefinition.structure);
194
+ const recordLimit = ruleSet?.$recordLimit;
195
+ return recordLimit;
385
196
  }
386
197
 
198
+ /**
199
+ * Selects a bounded occupant set for an in-memory projection whose grouping
200
+ * and ranking policy is supplied by the caller.
201
+ */
387
202
  export function selectOccupantRecordIds<T extends RecordsWriteMessage>(input: OccupancyProjectionInput<T>): Set<string> {
388
203
  const occupants = new Set<string>();
389
204
  const groups = new Map<string, T[]>();
@@ -280,15 +280,19 @@ 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 collection requests must select one or more direct
284
+ * parents, the target path itself, or one of its ancestor contexts. A
285
+ * bounded path-wide subscription may omit that scope because its initial
286
+ * page is explicitly capped; record-limited results still project each
287
+ * direct-parent population independently.
285
288
  */
286
- public static validateNestedProtocolPathQueryScope(
289
+ public static validateNestedProtocolPathScope(
287
290
  filter: RecordsFilter,
288
291
  errorCode: DwnErrorCode,
289
292
  operationName: string,
293
+ allowBoundedPathWideSelection = false,
290
294
  ): void {
291
- const { contextId, protocolPath } = filter;
295
+ const { contextId, parentId, protocolPath } = filter;
292
296
  if (!protocolPath?.includes('/')) {
293
297
  return;
294
298
  }
@@ -297,10 +301,19 @@ export class Records {
297
301
  return;
298
302
  }
299
303
 
300
- const expectedParentDepth = protocolPath.split('/').length - 1;
304
+ if (parentId !== undefined && contextId === undefined) {
305
+ return;
306
+ }
307
+
308
+ if (contextId === undefined && allowBoundedPathWideSelection) {
309
+ return;
310
+ }
311
+
312
+ const protocolPathDepth = protocolPath.split('/').length;
301
313
  const contextIdSegments = contextId?.split('/');
302
314
  if (
303
- contextIdSegments?.length === expectedParentDepth &&
315
+ contextIdSegments !== undefined &&
316
+ contextIdSegments.length <= protocolPathDepth &&
304
317
  contextIdSegments.every(segment => segment.length > 0)
305
318
  ) {
306
319
  return;
@@ -308,7 +321,7 @@ export class Records {
308
321
 
309
322
  throw new DwnError(
310
323
  errorCode,
311
- `${operationName} for nested protocol path '${protocolPath}' must include the direct parent contextId in the filter`
324
+ `${operationName} for nested protocol path '${protocolPath}' must include parentId or a contextId selecting that path or one of its ancestors`
312
325
  );
313
326
  }
314
327
 
@@ -386,10 +399,8 @@ export class Records {
386
399
  delete filterCopy.dateUpdated;
387
400
  }
388
401
 
389
- // contextId conversion to prefix match
390
- const contextIdPrefixFilter = contextId ? FilterUtility.constructPrefixFilterAsRangeFilter(contextId) : undefined;
391
- if (contextIdPrefixFilter) {
392
- filterCopy.contextId = contextIdPrefixFilter;
402
+ if (contextId !== undefined) {
403
+ filterCopy.contextId = { subtree: contextId };
393
404
  }
394
405
 
395
406
  // 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.
@@ -405,16 +416,6 @@ export class Records {
405
416
  return filterCopy;
406
417
  }
407
418
 
408
- public static buildUnpublishedControlRecordsFilter(filter: RecordsFilter, dateSort?: DateSort): Filter {
409
- return {
410
- ...Records.convertFilter(filter, dateSort),
411
- interface : DwnInterfaceName.Records,
412
- method : DwnMethodName.Write,
413
- isLatestBaseState : true,
414
- published : false,
415
- };
416
- }
417
-
418
419
  public static buildControlRecordsFilters(filters: Filter[]): Filter[] {
419
420
  const controlFilters: Filter[] = [];
420
421
  for (const filter of filters) {
@@ -584,13 +585,6 @@ export class Records {
584
585
  }
585
586
  }
586
587
 
587
- /**
588
- * Determines if signature payload contains a protocolRole and should be authorized as such.
589
- */
590
- public static shouldProtocolAuthorize(signaturePayload: GenericSignaturePayload): boolean {
591
- return signaturePayload.protocolRole !== undefined;
592
- }
593
-
594
588
  /**
595
589
  * Checks if the filter supports returning published records.
596
590
  */
@@ -639,33 +633,4 @@ export class Records {
639
633
  return !incomingDeleteIsNewest;
640
634
  }
641
635
 
642
- /**
643
- * Checks whether or not the incoming records query filter should build an unpublished recipient MessageStore filter.
644
- *
645
- * @param filter The incoming RecordsFilter to evaluate against.
646
- * @param recipient The recipient to check against the filter, typically the query/subscribe message author.
647
- * @returns {boolean} True if the filter contains the recipient, or if the recipient filter is undefined/empty.
648
- */
649
- static shouldBuildUnpublishedRecipientFilter(filter: RecordsFilter, recipient: string): boolean {
650
- const { recipient: recipientFilter } = filter;
651
-
652
- return Array.isArray(recipientFilter) ?
653
- recipientFilter.length === 0 || recipientFilter.includes(recipient) :
654
- recipientFilter === undefined || recipientFilter === recipient;
655
- }
656
-
657
- /**
658
- * Checks whether or not the incoming records query filter should build an unpublished author MessageStore filter.
659
- *
660
- * @param filter The incoming RecordsFilter to evaluate against.
661
- * @param author The author to check against the filter, typically the query/subscribe message author.
662
- * @returns {boolean} True if the filter contains the author, or if the author filter is undefined/empty.
663
- */
664
- static shouldBuildUnpublishedAuthorFilter(filter: RecordsFilter, author: string): boolean {
665
- const { author: authorFilter } = filter;
666
-
667
- return Array.isArray(authorFilter) ?
668
- authorFilter.length === 0 || authorFilter.includes(author) :
669
- authorFilter === undefined || authorFilter === author;
670
- }
671
636
  }