@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,10 +1,11 @@
1
- import sinon from 'sinon';
2
1
  import { DataStream } from '../../src/utils/data-stream.js';
2
+ import { DateSort } from '../../src/types/records-types.js';
3
3
  import { DidKey } from '@enbox/dids';
4
4
  import { Dwn } from '../../src/dwn.js';
5
5
  import { DwnConstant } from '../../src/core/dwn-constant.js';
6
6
  import { DwnErrorCode } from '../../src/core/dwn-error.js';
7
7
  import { Jws } from '../../src/utils/jws.js';
8
+ import { Poller } from '../utils/poller.js';
8
9
  import { ProtocolsConfigure } from '../../src/interfaces/protocols-configure.js';
9
10
  import { RecordsRead } from '../../src/interfaces/records-read.js';
10
11
  import { sleep } from '@enbox/common';
@@ -16,6 +17,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'bun:test'
16
17
  import { DataStoreLevel, MessageStoreLevel, ResumableTaskStoreLevel } from '../../src/store/level.js';
17
18
  export function testRecordsRecordLimit() {
18
19
  describe('Records $recordLimit', () => {
20
+ const roomTitlePath = 'room/title';
19
21
  let didResolver;
20
22
  let messageStore;
21
23
  let dataStore;
@@ -34,7 +36,6 @@ export function testRecordsRecordLimit() {
34
36
  dwn = await Dwn.create({ didResolver, messageStore, dataStore, eventLog, resumableTaskStore });
35
37
  });
36
38
  beforeEach(async () => {
37
- sinon.restore();
38
39
  await messageStore.clear();
39
40
  await dataStore.clear();
40
41
  await resumableTaskStore.clear();
@@ -58,6 +59,7 @@ export function testRecordsRecordLimit() {
58
59
  protocol: input.protocol,
59
60
  protocolPath: input.protocolPath,
60
61
  parentContextId: input.parentContextId,
62
+ published: input.published,
61
63
  dateCreated: input.dateCreated,
62
64
  messageTimestamp: input.dateCreated,
63
65
  });
@@ -72,7 +74,9 @@ export function testRecordsRecordLimit() {
72
74
  protocol: input.protocol,
73
75
  protocolPath: input.protocolPath,
74
76
  ...(input.contextId === undefined ? {} : { contextId: input.contextId }),
77
+ ...(input.parentId === undefined ? {} : { parentId: input.parentId }),
75
78
  },
79
+ dateSort: input.dateSort,
76
80
  pagination: input.pagination,
77
81
  });
78
82
  const reply = await (input.targetDwn ?? dwn).processMessage(input.author.did, recordsQuery.message);
@@ -89,6 +93,7 @@ export function testRecordsRecordLimit() {
89
93
  protocol: input.protocol,
90
94
  protocolPath: input.protocolPath,
91
95
  ...(input.contextId === undefined ? {} : { contextId: input.contextId }),
96
+ ...(input.parentId === undefined ? {} : { parentId: input.parentId }),
92
97
  },
93
98
  });
94
99
  const reply = await (input.targetDwn ?? dwn).processMessage(input.author.did, recordsCount.message);
@@ -108,7 +113,10 @@ export function testRecordsRecordLimit() {
108
113
  filter: {
109
114
  protocol: input.protocol,
110
115
  protocolPath: input.protocolPath,
116
+ ...(input.contextId === undefined ? {} : { contextId: input.contextId }),
117
+ ...(input.parentId === undefined ? {} : { parentId: input.parentId }),
111
118
  },
119
+ pagination: input.pagination,
112
120
  });
113
121
  const reply = await (input.targetDwn ?? dwn).processMessage(input.author.did, recordsSubscribe.message, { subscriptionHandler: () => { } });
114
122
  expect(reply.status.code).toBe(200);
@@ -124,7 +132,7 @@ export function testRecordsRecordLimit() {
124
132
  }
125
133
  }
126
134
  describe('ProtocolsConfigure validation', () => {
127
- it('should allow $recordLimit with valid max and strategy', async () => {
135
+ it('should allow $recordLimit with a valid max', async () => {
128
136
  const alice = await TestDataGenerator.generateDidKeyPersona();
129
137
  const definition = {
130
138
  protocol: 'http://example.com/record-limit',
@@ -132,7 +140,7 @@ export function testRecordsRecordLimit() {
132
140
  types: { message: {} },
133
141
  structure: {
134
142
  message: {
135
- $recordLimit: { max: 5, strategy: 'reject' },
143
+ $recordLimit: { max: 5 },
136
144
  }
137
145
  }
138
146
  };
@@ -141,10 +149,7 @@ export function testRecordsRecordLimit() {
141
149
  definition,
142
150
  });
143
151
  expect(protocolsConfigure.message.descriptor.definition).toBeDefined();
144
- expect(protocolsConfigure.message.descriptor.definition.structure.message.$recordLimit).toEqual({
145
- max: 5,
146
- strategy: 'reject',
147
- });
152
+ expect(protocolsConfigure.message.descriptor.definition.structure.message.$recordLimit).toEqual({ max: 5 });
148
153
  });
149
154
  it('should allow $recordLimit with max of 1', async () => {
150
155
  const alice = await TestDataGenerator.generateDidKeyPersona();
@@ -154,7 +159,7 @@ export function testRecordsRecordLimit() {
154
159
  types: { profile: {} },
155
160
  structure: {
156
161
  profile: {
157
- $recordLimit: { max: 1, strategy: 'reject' },
162
+ $recordLimit: { max: 1 },
158
163
  }
159
164
  }
160
165
  };
@@ -162,10 +167,7 @@ export function testRecordsRecordLimit() {
162
167
  signer: Jws.createSigner(alice),
163
168
  definition,
164
169
  });
165
- expect(protocolsConfigure.message.descriptor.definition.structure.profile.$recordLimit).toEqual({
166
- max: 1,
167
- strategy: 'reject',
168
- });
170
+ expect(protocolsConfigure.message.descriptor.definition.structure.profile.$recordLimit).toEqual({ max: 1 });
169
171
  });
170
172
  it('should reject $recordLimit above the supported max', async () => {
171
173
  const alice = await TestDataGenerator.generateDidKeyPersona();
@@ -175,7 +177,7 @@ export function testRecordsRecordLimit() {
175
177
  types: { message: {} },
176
178
  structure: {
177
179
  message: {
178
- $recordLimit: { max: DwnConstant.maxRecordLimit + 1, strategy: 'reject' },
180
+ $recordLimit: { max: DwnConstant.maxRecordLimit + 1 },
179
181
  }
180
182
  }
181
183
  };
@@ -185,27 +187,6 @@ export function testRecordsRecordLimit() {
185
187
  });
186
188
  await expect(promise).rejects.toThrow(DwnErrorCode.ProtocolsConfigureInvalidRecordLimit);
187
189
  });
188
- it('should allow $recordLimit with purgeOldest strategy (schema validation only)', async () => {
189
- const alice = await TestDataGenerator.generateDidKeyPersona();
190
- const definition = {
191
- protocol: 'http://example.com/record-limit',
192
- published: true,
193
- types: { status: {} },
194
- structure: {
195
- status: {
196
- $recordLimit: { max: 10, strategy: 'purgeOldest' },
197
- }
198
- }
199
- };
200
- const protocolsConfigure = await ProtocolsConfigure.create({
201
- signer: Jws.createSigner(alice),
202
- definition,
203
- });
204
- expect(protocolsConfigure.message.descriptor.definition.structure.status.$recordLimit).toEqual({
205
- max: 10,
206
- strategy: 'purgeOldest',
207
- });
208
- });
209
190
  it('should reject $recordLimit with max less than 1', async () => {
210
191
  const alice = await TestDataGenerator.generateDidKeyPersona();
211
192
  const definition = {
@@ -214,7 +195,7 @@ export function testRecordsRecordLimit() {
214
195
  types: { message: {} },
215
196
  structure: {
216
197
  message: {
217
- $recordLimit: { max: 0, strategy: 'reject' },
198
+ $recordLimit: { max: 0 },
218
199
  }
219
200
  }
220
201
  };
@@ -232,7 +213,7 @@ export function testRecordsRecordLimit() {
232
213
  types: { message: {} },
233
214
  structure: {
234
215
  message: {
235
- $recordLimit: { max: 2.5, strategy: 'reject' },
216
+ $recordLimit: { max: 2.5 },
236
217
  }
237
218
  }
238
219
  };
@@ -242,7 +223,7 @@ export function testRecordsRecordLimit() {
242
223
  });
243
224
  await expect(promise).rejects.toThrow('SchemaValidator');
244
225
  });
245
- it('should reject $recordLimit with missing strategy', async () => {
226
+ it('should reject the removed $recordLimit strategy property', async () => {
246
227
  const alice = await TestDataGenerator.generateDidKeyPersona();
247
228
  const definition = {
248
229
  protocol: 'http://example.com/record-limit',
@@ -250,25 +231,7 @@ export function testRecordsRecordLimit() {
250
231
  types: { message: {} },
251
232
  structure: {
252
233
  message: {
253
- $recordLimit: { max: 5 },
254
- }
255
- }
256
- };
257
- const promise = ProtocolsConfigure.create({
258
- signer: Jws.createSigner(alice),
259
- definition,
260
- });
261
- await expect(promise).rejects.toThrow('SchemaValidator');
262
- });
263
- it('should reject $recordLimit with invalid strategy', async () => {
264
- const alice = await TestDataGenerator.generateDidKeyPersona();
265
- const definition = {
266
- protocol: 'http://example.com/record-limit',
267
- published: true,
268
- types: { message: {} },
269
- structure: {
270
- message: {
271
- $recordLimit: { max: 5, strategy: 'invalidStrategy' },
234
+ $recordLimit: { max: 5, strategy: 'reject' },
272
235
  }
273
236
  }
274
237
  };
@@ -290,7 +253,7 @@ export function testRecordsRecordLimit() {
290
253
  structure: {
291
254
  room: {
292
255
  message: {
293
- $recordLimit: { max: 100, strategy: 'reject' },
256
+ $recordLimit: { max: 100 },
294
257
  }
295
258
  }
296
259
  }
@@ -300,13 +263,10 @@ export function testRecordsRecordLimit() {
300
263
  definition,
301
264
  });
302
265
  const roomRuleSet = protocolsConfigure.message.descriptor.definition.structure.room;
303
- expect(roomRuleSet.message.$recordLimit).toEqual({
304
- max: 100,
305
- strategy: 'reject',
306
- });
266
+ expect(roomRuleSet.message.$recordLimit).toEqual({ max: 100 });
307
267
  });
308
268
  });
309
- describe('read-time occupancy projection — reject strategy', () => {
269
+ describe('read-time occupancy projection', () => {
310
270
  it('should admit candidates and project Query, Read, and Count to top-ranked occupants', async () => {
311
271
  const alice = await TestDataGenerator.generateDidKeyPersona();
312
272
  const protocolDefinition = {
@@ -315,7 +275,7 @@ export function testRecordsRecordLimit() {
315
275
  types: { blob: {} },
316
276
  structure: {
317
277
  blob: {
318
- $recordLimit: { max: 2, strategy: 'reject' },
278
+ $recordLimit: { max: 2 },
319
279
  }
320
280
  }
321
281
  };
@@ -362,7 +322,7 @@ export function testRecordsRecordLimit() {
362
322
  types: { profile: {} },
363
323
  structure: {
364
324
  profile: {
365
- $recordLimit: { max: 1, strategy: 'reject' },
325
+ $recordLimit: { max: 1 },
366
326
  }
367
327
  }
368
328
  };
@@ -406,7 +366,7 @@ export function testRecordsRecordLimit() {
406
366
  structure: {
407
367
  room: {
408
368
  message: {
409
- $recordLimit: { max: 2, strategy: 'reject' },
369
+ $recordLimit: { max: 2 },
410
370
  }
411
371
  }
412
372
  }
@@ -489,6 +449,30 @@ export function testRecordsRecordLimit() {
489
449
  room2Messages[0].message.recordId,
490
450
  room2Messages[1].message.recordId,
491
451
  ]);
452
+ expect((await queryProtocolRecordIds({
453
+ author: alice,
454
+ protocol,
455
+ protocolPath: 'room/message',
456
+ contextId: room1Messages[0].message.contextId,
457
+ })).recordIds).toEqual([room1Messages[0].message.recordId]);
458
+ expect(await countProtocolRecords({
459
+ author: alice,
460
+ protocol,
461
+ protocolPath: 'room/message',
462
+ contextId: room1Messages[0].message.contextId,
463
+ })).toBe(1);
464
+ expect((await queryProtocolRecordIds({
465
+ author: alice,
466
+ protocol,
467
+ protocolPath: 'room/message',
468
+ contextId: room1Messages[2].message.contextId,
469
+ })).recordIds).toEqual([]);
470
+ expect(await countProtocolRecords({
471
+ author: alice,
472
+ protocol,
473
+ protocolPath: 'room/message',
474
+ contextId: room1Messages[2].message.contextId,
475
+ })).toBe(0);
492
476
  const broadQuery = await TestDataGenerator.generateRecordsQuery({
493
477
  author: alice,
494
478
  filter: {
@@ -500,6 +484,331 @@ export function testRecordsRecordLimit() {
500
484
  expect(broadReply.status.code).toBe(400);
501
485
  expect(broadReply.status.detail).toContain(DwnErrorCode.RecordsQueryNestedProtocolPathContextIdInvalid);
502
486
  });
487
+ it('should project selected direct-parent groups independently in one request', async () => {
488
+ const alice = await TestDataGenerator.generateDidKeyPersona();
489
+ const protocolDefinition = {
490
+ protocol: `http://record-limit-${TestDataGenerator.randomString(12)}.xyz`,
491
+ published: true,
492
+ types: {
493
+ room: {},
494
+ title: {},
495
+ },
496
+ structure: {
497
+ room: {
498
+ title: {
499
+ $recordLimit: { max: 1 },
500
+ },
501
+ },
502
+ },
503
+ };
504
+ const protocol = protocolDefinition.protocol;
505
+ await installProtocol({ author: alice, definition: protocolDefinition });
506
+ const room1 = await writeProtocolRecord({
507
+ author: alice,
508
+ protocol,
509
+ protocolPath: 'room',
510
+ dateCreated: '2025-01-01T00:00:00.000000Z',
511
+ });
512
+ const room2 = await writeProtocolRecord({
513
+ author: alice,
514
+ protocol,
515
+ protocolPath: 'room',
516
+ dateCreated: '2025-01-02T00:00:00.000000Z',
517
+ });
518
+ const room1Title = await writeProtocolRecord({
519
+ author: alice,
520
+ protocol,
521
+ protocolPath: roomTitlePath,
522
+ parentContextId: room1.message.contextId,
523
+ dateCreated: '2025-02-01T00:00:00.000000Z',
524
+ });
525
+ const room2Title = await writeProtocolRecord({
526
+ author: alice,
527
+ protocol,
528
+ protocolPath: roomTitlePath,
529
+ parentContextId: room2.message.contextId,
530
+ dateCreated: '2025-02-02T00:00:00.000000Z',
531
+ });
532
+ await writeProtocolRecord({
533
+ author: alice,
534
+ protocol,
535
+ protocolPath: roomTitlePath,
536
+ parentContextId: room1.message.contextId,
537
+ dateCreated: '2025-02-03T00:00:00.000000Z',
538
+ });
539
+ await writeProtocolRecord({
540
+ author: alice,
541
+ protocol,
542
+ protocolPath: roomTitlePath,
543
+ parentContextId: room2.message.contextId,
544
+ dateCreated: '2025-02-04T00:00:00.000000Z',
545
+ });
546
+ const parentIds = [room1.message.recordId, room2.message.recordId];
547
+ const expectedRecordIds = [room1Title.message.recordId, room2Title.message.recordId];
548
+ const queryInput = {
549
+ author: alice,
550
+ protocol,
551
+ protocolPath: roomTitlePath,
552
+ parentId: parentIds,
553
+ };
554
+ expect((await queryProtocolRecordIds(queryInput)).recordIds).toEqual(expectedRecordIds);
555
+ expect(await countProtocolRecords(queryInput)).toBe(expectedRecordIds.length);
556
+ expect(await subscribeSnapshotRecordIds(queryInput)).toEqual(expectedRecordIds);
557
+ expect(await subscribeSnapshotRecordIds({
558
+ author: alice,
559
+ protocol,
560
+ protocolPath: roomTitlePath,
561
+ pagination: { limit: 2 },
562
+ })).toEqual(expectedRecordIds);
563
+ expect((await queryProtocolRecordIds({
564
+ ...queryInput,
565
+ parentId: room2.message.recordId,
566
+ })).recordIds).toEqual([room2Title.message.recordId]);
567
+ const scopedInput = {
568
+ ...queryInput,
569
+ contextId: room1.message.contextId,
570
+ };
571
+ expect((await queryProtocolRecordIds(scopedInput)).recordIds).toEqual([room1Title.message.recordId]);
572
+ expect(await countProtocolRecords(scopedInput)).toBe(1);
573
+ expect(await subscribeSnapshotRecordIds(scopedInput)).toEqual([room1Title.message.recordId]);
574
+ });
575
+ it('should reject empty and duplicate parentId arrays', async () => {
576
+ const alice = await TestDataGenerator.generateDidKeyPersona();
577
+ const filter = {
578
+ protocol: 'http://record-limit-parent-filter.xyz',
579
+ protocolPath: roomTitlePath,
580
+ parentId: [],
581
+ };
582
+ await expect(TestDataGenerator.generateRecordsQuery({ author: alice, filter })).rejects.toThrow('SchemaValidatorFailure');
583
+ await expect(TestDataGenerator.generateRecordsCount({ author: alice, filter })).rejects.toThrow('SchemaValidatorFailure');
584
+ await expect(TestDataGenerator.generateRecordsSubscribe({ author: alice, filter })).rejects.toThrow('SchemaValidatorFailure');
585
+ await expect(TestDataGenerator.generateRecordsQuery({
586
+ author: alice,
587
+ filter: { ...filter, parentId: ['same-parent', 'same-parent'] },
588
+ })).rejects.toThrow('SchemaValidatorFailure');
589
+ await expect(TestDataGenerator.generateRecordsCount({
590
+ author: alice,
591
+ filter: { ...filter, parentId: '' },
592
+ })).rejects.toThrow('SchemaValidatorFailure');
593
+ await expect(TestDataGenerator.generateRecordsSubscribe({
594
+ author: alice,
595
+ filter: { ...filter, parentId: [''] },
596
+ })).rejects.toThrow('SchemaValidatorFailure');
597
+ });
598
+ it('should project ancestor queries independently across parent contexts', async () => {
599
+ const alice = await TestDataGenerator.generateDidKeyPersona();
600
+ const protocolDefinition = {
601
+ protocol: `http://record-limit-${TestDataGenerator.randomString(12)}.xyz`,
602
+ published: true,
603
+ types: {
604
+ channel: {},
605
+ community: {},
606
+ message: {},
607
+ },
608
+ structure: {
609
+ community: {
610
+ channel: {
611
+ message: {
612
+ $recordLimit: { max: 2 },
613
+ },
614
+ },
615
+ },
616
+ },
617
+ };
618
+ const protocol = protocolDefinition.protocol;
619
+ await installProtocol({ author: alice, definition: protocolDefinition });
620
+ const community = await writeProtocolRecord({
621
+ author: alice,
622
+ protocol,
623
+ protocolPath: 'community',
624
+ dateCreated: '2025-01-01T00:00:00.000000Z',
625
+ });
626
+ const channel1 = await writeProtocolRecord({
627
+ author: alice,
628
+ protocol,
629
+ protocolPath: 'community/channel',
630
+ parentContextId: community.message.contextId,
631
+ dateCreated: '2025-01-02T00:00:00.000000Z',
632
+ });
633
+ const channel2 = await writeProtocolRecord({
634
+ author: alice,
635
+ protocol,
636
+ protocolPath: 'community/channel',
637
+ parentContextId: community.message.contextId,
638
+ dateCreated: '2025-01-03T00:00:00.000000Z',
639
+ });
640
+ const { cursor: cursorBeforeMessages } = await eventLog.read(alice.did);
641
+ // Creation times are interleaved across channels so pagination proves
642
+ // that projection happens per direct parent before global ordering.
643
+ // Channel 2 arrives newest-first to prove occupancy ignores arrival order.
644
+ const channel1Messages = [
645
+ await writeProtocolRecord({
646
+ author: alice,
647
+ protocol,
648
+ protocolPath: 'community/channel/message',
649
+ parentContextId: channel1.message.contextId,
650
+ dateCreated: '2025-02-01T00:00:00.000000Z',
651
+ }),
652
+ await writeProtocolRecord({
653
+ author: alice,
654
+ protocol,
655
+ protocolPath: 'community/channel/message',
656
+ parentContextId: channel1.message.contextId,
657
+ published: true,
658
+ dateCreated: '2025-02-04T00:00:00.000000Z',
659
+ }),
660
+ await writeProtocolRecord({
661
+ author: alice,
662
+ protocol,
663
+ protocolPath: 'community/channel/message',
664
+ parentContextId: channel1.message.contextId,
665
+ published: true,
666
+ dateCreated: '2025-02-06T00:00:00.000000Z',
667
+ }),
668
+ ];
669
+ const channel2Tie1 = await writeProtocolRecord({
670
+ author: alice,
671
+ protocol,
672
+ protocolPath: 'community/channel/message',
673
+ parentContextId: channel2.message.contextId,
674
+ published: true,
675
+ dateCreated: '2025-02-03T00:00:00.000000Z',
676
+ });
677
+ const channel2Tie2 = await writeProtocolRecord({
678
+ author: alice,
679
+ protocol,
680
+ protocolPath: 'community/channel/message',
681
+ parentContextId: channel2.message.contextId,
682
+ published: true,
683
+ dateCreated: '2025-02-03T00:00:00.000000Z',
684
+ });
685
+ const channel2Oldest = await writeProtocolRecord({
686
+ author: alice,
687
+ protocol,
688
+ protocolPath: 'community/channel/message',
689
+ parentContextId: channel2.message.contextId,
690
+ published: true,
691
+ dateCreated: '2025-02-02T00:00:00.000000Z',
692
+ });
693
+ const channel2Messages = [channel2Oldest, channel2Tie1, channel2Tie2];
694
+ const channel2BoundaryOccupant = channel2Messages[1].message.recordId < channel2Messages[2].message.recordId
695
+ ? channel2Messages[1]
696
+ : channel2Messages[2];
697
+ const expectedRecordIds = [
698
+ channel1Messages[0].message.recordId,
699
+ channel2Messages[0].message.recordId,
700
+ channel2BoundaryOccupant.message.recordId,
701
+ channel1Messages[1].message.recordId,
702
+ ];
703
+ const queryInput = {
704
+ author: alice,
705
+ protocol,
706
+ protocolPath: 'community/channel/message',
707
+ contextId: community.message.contextId,
708
+ };
709
+ expect((await queryProtocolRecordIds(queryInput)).recordIds).toEqual(expectedRecordIds);
710
+ expect((await queryProtocolRecordIds({
711
+ ...queryInput,
712
+ dateSort: DateSort.CreatedDescending,
713
+ })).recordIds).toEqual([...expectedRecordIds].reverse());
714
+ expect(await countProtocolRecords(queryInput)).toBe(expectedRecordIds.length);
715
+ expect(await subscribeSnapshotRecordIds(queryInput)).toEqual(expectedRecordIds);
716
+ const page1 = await queryProtocolRecordIds({ ...queryInput, pagination: { limit: 2 } });
717
+ expect(page1.recordIds).toEqual(expectedRecordIds.slice(0, 2));
718
+ expect(page1.cursor).toBeDefined();
719
+ const page2 = await queryProtocolRecordIds({
720
+ ...queryInput,
721
+ pagination: { limit: 2, cursor: page1.cursor },
722
+ });
723
+ expect(page2.recordIds).toEqual(expectedRecordIds.slice(2));
724
+ expect(page2.cursor).toBeUndefined();
725
+ const descendingPage1 = await queryProtocolRecordIds({
726
+ ...queryInput,
727
+ dateSort: DateSort.CreatedDescending,
728
+ pagination: { limit: 2 },
729
+ });
730
+ expect(descendingPage1.recordIds).toEqual([...expectedRecordIds].reverse().slice(0, 2));
731
+ expect(descendingPage1.cursor).toBeDefined();
732
+ const descendingPage2 = await queryProtocolRecordIds({
733
+ ...queryInput,
734
+ dateSort: DateSort.CreatedDescending,
735
+ pagination: { limit: 2, cursor: descendingPage1.cursor },
736
+ });
737
+ expect(descendingPage2.recordIds).toEqual([...expectedRecordIds].reverse().slice(2));
738
+ expect(descendingPage2.cursor).toBeUndefined();
739
+ // Occupancy is ranked before requester visibility. The unpublished
740
+ // first channel-1 occupant still consumes a slot, so the later
741
+ // published over-limit candidate must remain hidden anonymously.
742
+ const publishedFilter = {
743
+ contextId: community.message.contextId,
744
+ protocol,
745
+ protocolPath: 'community/channel/message',
746
+ published: true,
747
+ };
748
+ const anonymousQuery = await TestDataGenerator.generateRecordsQuery({
749
+ anonymous: true,
750
+ filter: publishedFilter,
751
+ });
752
+ const anonymousReply = await dwn.processMessage(alice.did, anonymousQuery.message);
753
+ expect(anonymousReply.status.code).toBe(200);
754
+ expect(anonymousReply.entries?.map((entry) => entry.recordId)).toEqual([
755
+ channel2Messages[0].message.recordId,
756
+ channel2BoundaryOccupant.message.recordId,
757
+ channel1Messages[1].message.recordId,
758
+ ]);
759
+ const anonymousCount = await TestDataGenerator.generateRecordsCount({
760
+ anonymous: true,
761
+ filter: publishedFilter,
762
+ });
763
+ const anonymousCountReply = await dwn.processMessage(alice.did, anonymousCount.message);
764
+ expect(anonymousCountReply.status.code).toBe(200);
765
+ expect(anonymousCountReply.count).toBe(3);
766
+ const replayedRecordIds = [];
767
+ const cursorSubscribe = await TestDataGenerator.generateRecordsSubscribe({
768
+ author: alice,
769
+ cursor: cursorBeforeMessages,
770
+ filter: {
771
+ contextId: community.message.contextId,
772
+ protocol,
773
+ protocolPath: 'community/channel/message',
774
+ },
775
+ });
776
+ const cursorSubscribeReply = await dwn.processMessage(alice.did, cursorSubscribe.message, {
777
+ subscriptionHandler: (subscriptionMessage) => {
778
+ if (subscriptionMessage.type === 'event' && subscriptionMessage.event.message.descriptor.method === 'Write') {
779
+ replayedRecordIds.push(subscriptionMessage.event.message.recordId);
780
+ }
781
+ },
782
+ });
783
+ expect(cursorSubscribeReply.status.code).toBe(200);
784
+ await Poller.pollUntilSuccessOrTimeout(async () => {
785
+ expect(replayedRecordIds).toHaveLength(expectedRecordIds.length);
786
+ });
787
+ expect([...replayedRecordIds].sort()).toEqual([...expectedRecordIds].sort());
788
+ await cursorSubscribeReply.subscription?.close();
789
+ const deleteChannel1Occupant = await TestDataGenerator.generateRecordsDelete({
790
+ author: alice,
791
+ recordId: channel1Messages[0].message.recordId,
792
+ });
793
+ const deleteReply = await dwn.processMessage(alice.did, deleteChannel1Occupant.message);
794
+ expect(deleteReply.status.code).toBe(202);
795
+ const expectedAfterPromotion = [
796
+ channel2Messages[0].message.recordId,
797
+ channel2BoundaryOccupant.message.recordId,
798
+ channel1Messages[1].message.recordId,
799
+ channel1Messages[2].message.recordId,
800
+ ];
801
+ expect((await queryProtocolRecordIds(queryInput)).recordIds).toEqual(expectedAfterPromotion);
802
+ expect((await queryProtocolRecordIds({
803
+ author: alice,
804
+ protocol,
805
+ protocolPath: 'community/channel/message',
806
+ contextId: channel2.message.contextId,
807
+ })).recordIds).toEqual([
808
+ channel2Messages[0].message.recordId,
809
+ channel2BoundaryOccupant.message.recordId,
810
+ ]);
811
+ });
503
812
  it('should paginate over the projected occupant set for max:N scopes', async () => {
504
813
  const alice = await TestDataGenerator.generateDidKeyPersona();
505
814
  const protocolDefinition = {
@@ -508,7 +817,7 @@ export function testRecordsRecordLimit() {
508
817
  types: { item: {} },
509
818
  structure: {
510
819
  item: {
511
- $recordLimit: { max: 3, strategy: 'reject' },
820
+ $recordLimit: { max: 3 },
512
821
  }
513
822
  }
514
823
  };
@@ -551,7 +860,7 @@ export function testRecordsRecordLimit() {
551
860
  types: { status: {} },
552
861
  structure: {
553
862
  status: {
554
- $recordLimit: { max: 1, strategy: 'reject' },
863
+ $recordLimit: { max: 1 },
555
864
  }
556
865
  }
557
866
  };
@@ -633,10 +942,10 @@ export function testRecordsRecordLimit() {
633
942
  },
634
943
  structure: {
635
944
  note: {
636
- $recordLimit: { max: 2, strategy: 'reject' },
945
+ $recordLimit: { max: 2 },
637
946
  },
638
947
  profile: {
639
- $recordLimit: { max: 1, strategy: 'reject' },
948
+ $recordLimit: { max: 1 },
640
949
  }
641
950
  }
642
951
  };
@@ -712,7 +1021,7 @@ export function testRecordsRecordLimit() {
712
1021
  types: { blob: {} },
713
1022
  structure: {
714
1023
  blob: {
715
- $recordLimit: { max: 1, strategy: 'reject' },
1024
+ $recordLimit: { max: 1 },
716
1025
  }
717
1026
  }
718
1027
  };
@@ -734,30 +1043,6 @@ export function testRecordsRecordLimit() {
734
1043
  record.message.recordId,
735
1044
  ]);
736
1045
  });
737
- it('should reject purgeOldest strategy at write time with not-implemented error', async () => {
738
- const alice = await TestDataGenerator.generateDidKeyPersona();
739
- const protocolDefinition = {
740
- protocol: `http://record-limit-${TestDataGenerator.randomString(12)}.xyz`,
741
- published: true,
742
- types: { blob: {} },
743
- structure: {
744
- blob: {
745
- $recordLimit: { max: 1, strategy: 'purgeOldest' },
746
- }
747
- }
748
- };
749
- const protocol = protocolDefinition.protocol;
750
- await installProtocol({ author: alice, definition: protocolDefinition });
751
- const record = await TestDataGenerator.generateRecordsWrite({
752
- author: alice,
753
- recipient: alice.did,
754
- protocol,
755
- protocolPath: 'blob',
756
- });
757
- const reply = await dwn.processMessage(alice.did, record.message, { dataStream: record.dataStream });
758
- expect(reply.status.code).toBe(400);
759
- expect(reply.status.detail).toContain(DwnErrorCode.ProtocolAuthorizationRecordLimitStrategyNotImplemented);
760
- });
761
1046
  });
762
1047
  });
763
1048
  }