@enbox/dwn-sdk-js 0.3.0 → 0.3.2

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 (69) hide show
  1. package/dist/browser.mjs +6 -6
  2. package/dist/browser.mjs.map +3 -3
  3. package/dist/esm/generated/precompiled-validators.js +704 -342
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/src/core/dwn-error.js +1 -0
  6. package/dist/esm/src/core/dwn-error.js.map +1 -1
  7. package/dist/esm/src/event-stream/event-emitter-event-log.js +152 -22
  8. package/dist/esm/src/event-stream/event-emitter-event-log.js.map +1 -1
  9. package/dist/esm/src/handlers/messages-subscribe.js +7 -0
  10. package/dist/esm/src/handlers/messages-subscribe.js.map +1 -1
  11. package/dist/esm/src/handlers/protocols-configure.js +1 -1
  12. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  13. package/dist/esm/src/handlers/records-subscribe.js +7 -0
  14. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  15. package/dist/esm/src/handlers/records-write.js +3 -2
  16. package/dist/esm/src/handlers/records-write.js.map +1 -1
  17. package/dist/esm/src/interfaces/messages-subscribe.js.map +1 -1
  18. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  19. package/dist/esm/src/store/storage-controller.js +1 -1
  20. package/dist/esm/src/store/storage-controller.js.map +1 -1
  21. package/dist/esm/src/utils/messages.js +41 -1
  22. package/dist/esm/src/utils/messages.js.map +1 -1
  23. package/dist/esm/tests/event-emitter-event-log.spec.js +278 -84
  24. package/dist/esm/tests/event-emitter-event-log.spec.js.map +1 -1
  25. package/dist/esm/tests/handlers/messages-subscribe.spec.js +288 -0
  26. package/dist/esm/tests/handlers/messages-subscribe.spec.js.map +1 -1
  27. package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
  28. package/dist/types/generated/precompiled-validators.d.ts +50 -34
  29. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  30. package/dist/types/src/core/dwn-error.d.ts +1 -0
  31. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  32. package/dist/types/src/event-stream/event-emitter-event-log.d.ts +34 -4
  33. package/dist/types/src/event-stream/event-emitter-event-log.d.ts.map +1 -1
  34. package/dist/types/src/handlers/messages-subscribe.d.ts +1 -1
  35. package/dist/types/src/handlers/messages-subscribe.d.ts.map +1 -1
  36. package/dist/types/src/handlers/records-subscribe.d.ts +1 -1
  37. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  38. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  39. package/dist/types/src/index.d.ts +1 -1
  40. package/dist/types/src/index.d.ts.map +1 -1
  41. package/dist/types/src/interfaces/messages-subscribe.d.ts +4 -3
  42. package/dist/types/src/interfaces/messages-subscribe.d.ts.map +1 -1
  43. package/dist/types/src/interfaces/records-subscribe.d.ts +4 -3
  44. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  45. package/dist/types/src/types/messages-types.d.ts +14 -4
  46. package/dist/types/src/types/messages-types.d.ts.map +1 -1
  47. package/dist/types/src/types/records-types.d.ts +8 -4
  48. package/dist/types/src/types/records-types.d.ts.map +1 -1
  49. package/dist/types/src/types/subscriptions.d.ts +80 -20
  50. package/dist/types/src/types/subscriptions.d.ts.map +1 -1
  51. package/dist/types/src/utils/messages.d.ts.map +1 -1
  52. package/dist/types/tests/handlers/messages-subscribe.spec.d.ts.map +1 -1
  53. package/dist/types/tests/utils/test-data-generator.d.ts +5 -4
  54. package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/core/dwn-error.ts +1 -0
  57. package/src/event-stream/event-emitter-event-log.ts +174 -27
  58. package/src/handlers/messages-subscribe.ts +8 -1
  59. package/src/handlers/protocols-configure.ts +1 -1
  60. package/src/handlers/records-subscribe.ts +8 -1
  61. package/src/handlers/records-write.ts +3 -2
  62. package/src/index.ts +1 -1
  63. package/src/interfaces/messages-subscribe.ts +4 -3
  64. package/src/interfaces/records-subscribe.ts +4 -3
  65. package/src/store/storage-controller.ts +1 -1
  66. package/src/types/messages-types.ts +12 -4
  67. package/src/types/records-types.ts +6 -4
  68. package/src/types/subscriptions.ts +86 -20
  69. package/src/utils/messages.ts +47 -1
@@ -7,6 +7,9 @@ import type {
7
7
  EventLogSubscribeOptions,
8
8
  EventSubscription,
9
9
  MessageEvent,
10
+ ProgressGapInfo,
11
+ ProgressGapReason,
12
+ ProgressToken,
10
13
  SubscriptionListener,
11
14
  } from '../types/subscriptions.js';
12
15
 
@@ -21,7 +24,7 @@ const EVENTS_LISTENER_CHANNEL = 'events';
21
24
  * Payload shape used internally by mitt. We bundle the three EventListener
22
25
  * arguments into a single object because mitt emits one value per event.
23
26
  */
24
- type EmitterPayload = { tenant: string; event: MessageEvent; indexes: KeyValues; seq: number };
27
+ type EmitterPayload = { tenant: string; event: MessageEvent; indexes: KeyValues; seq: number; messageCid: string };
25
28
 
26
29
  /**
27
30
  * mitt event map — every channel name maps to an `EmitterPayload`.
@@ -30,11 +33,12 @@ type EmitterPayload = { tenant: string; event: MessageEvent; indexes: KeyValues;
30
33
  type EmitterEvents = Record<string, EmitterPayload>;
31
34
 
32
35
  /**
33
- * Internal storage entry — the event plus its indexes.
36
+ * Internal storage entry — the event plus its indexes and message CID.
34
37
  */
35
38
  type StoredEntry = {
36
39
  event : MessageEvent;
37
40
  indexes : KeyValues;
41
+ messageCid : string;
38
42
  };
39
43
 
40
44
  export interface EventEmitterEventLogConfig {
@@ -77,14 +81,92 @@ export class EventEmitterEventLog implements EventLog {
77
81
  */
78
82
  private tenantSeqs: Map<string, number> = new Map();
79
83
 
84
+ /**
85
+ * Epoch for this EventLog instance. Generated once at construction as a
86
+ * UUID v4. Changes every restart (correct for in-memory — state is lost).
87
+ */
88
+ private readonly epoch: string;
89
+
80
90
  constructor(config: EventEmitterEventLogConfig = {}) {
81
91
  this.maxEventsPerTenant = config.maxEventsPerTenant ?? 10_000;
92
+ this.epoch = crypto.randomUUID();
82
93
 
83
94
  if (config.errorHandler) {
84
95
  this.errorHandler = config.errorHandler;
85
96
  }
86
97
  }
87
98
 
99
+ /**
100
+ * Derives a stable `streamId` for a given tenant. Deterministic — same
101
+ * tenant always produces the same streamId on any instance.
102
+ */
103
+ private async getStreamId(tenant: string): Promise<string> {
104
+ const bytes = new TextEncoder().encode(tenant);
105
+ const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
106
+ const hashArray = new Uint8Array(hashBuffer);
107
+ // Take first 16 hex chars (64 bits) of the hash for a compact stable ID.
108
+ return Array.from(hashArray.slice(0, 8), (b: number) => b.toString(16).padStart(2, '0')).join('');
109
+ }
110
+
111
+ /**
112
+ * Constructs a {@link ProgressToken} from internal state.
113
+ */
114
+ private async buildToken(tenant: string, seq: number, messageCid: string): Promise<ProgressToken> {
115
+ return {
116
+ streamId : await this.getStreamId(tenant),
117
+ epoch : this.epoch,
118
+ position : String(seq),
119
+ messageCid,
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Validates a cursor against the current EventLog state. Throws
125
+ * `DwnError(EventLogProgressGap)` with {@link ProgressGapInfo} metadata
126
+ * if the cursor cannot be resumed.
127
+ */
128
+ private async validateCursor(tenant: string, cursor: ProgressToken): Promise<void> {
129
+ const expectedStreamId = await this.getStreamId(tenant);
130
+
131
+ let reason: ProgressGapReason;
132
+ if (cursor.streamId !== expectedStreamId) {
133
+ reason = 'stream_mismatch';
134
+ } else if (cursor.epoch !== this.epoch) {
135
+ reason = 'epoch_mismatch';
136
+ } else {
137
+ // Check if position is still within replay bounds.
138
+ const log = this.tenantLogs.get(tenant);
139
+ if (log !== undefined && log.size > 0) {
140
+ const firstSeq = log.keys().next().value as number;
141
+ const cursorSeq = EventEmitterEventLog.parsePosition(cursor.position);
142
+ if (cursorSeq < firstSeq - 1) {
143
+ // Cursor position has been evicted — events between cursor and firstSeq are lost.
144
+ reason = 'token_too_old';
145
+ } else {
146
+ return; // Cursor is valid.
147
+ }
148
+ } else {
149
+ return; // No events for tenant — cursor is vacuously valid (will get empty catch-up + EOSE).
150
+ }
151
+ }
152
+
153
+ // Build gap metadata.
154
+ const bounds = await this.getReplayBounds(tenant);
155
+ const gapInfo: ProgressGapInfo = {
156
+ requested : cursor,
157
+ oldestAvailable : bounds?.oldest ?? cursor,
158
+ latestAvailable : bounds?.latest ?? cursor,
159
+ reason,
160
+ };
161
+
162
+ const error = new DwnError(
163
+ DwnErrorCode.EventLogProgressGap,
164
+ `progress token gap: ${reason}`
165
+ );
166
+ (error as any).gapInfo = gapInfo;
167
+ throw error;
168
+ }
169
+
88
170
  public async open(): Promise<void> {
89
171
  this.isOpen = true;
90
172
  }
@@ -96,13 +178,13 @@ export class EventEmitterEventLog implements EventLog {
96
178
  this.tenantSeqs.clear();
97
179
  }
98
180
 
99
- public async emit(tenant: string, event: MessageEvent, indexes: KeyValues): Promise<string> {
181
+ public async emit(tenant: string, event: MessageEvent, indexes: KeyValues, messageCid: string): Promise<ProgressToken | undefined> {
100
182
  if (!this.isOpen) {
101
183
  this.errorHandler(new DwnError(
102
184
  DwnErrorCode.EventLogNotOpenError,
103
185
  'a message emitted when EventLog is closed'
104
186
  ));
105
- return '';
187
+ return undefined;
106
188
  }
107
189
 
108
190
  // Assign a monotonic sequence number for this tenant.
@@ -116,7 +198,7 @@ export class EventEmitterEventLog implements EventLog {
116
198
  log = new Map();
117
199
  this.tenantLogs.set(tenant, log);
118
200
  }
119
- log.set(seq, { event, indexes });
201
+ log.set(seq, { event, indexes, messageCid });
120
202
 
121
203
  // Evict oldest entries if the log exceeds the retention limit.
122
204
  if (log.size > this.maxEventsPerTenant) {
@@ -131,14 +213,20 @@ export class EventEmitterEventLog implements EventLog {
131
213
 
132
214
  // Notify in-process subscribers.
133
215
  const channel = `${tenant}_${EVENTS_LISTENER_CHANNEL}`;
134
- this.emitter.emit(channel, { tenant, event, indexes, seq });
216
+ this.emitter.emit(channel, { tenant, event, indexes, seq, messageCid });
135
217
 
136
- return String(seq);
218
+ return this.buildToken(tenant, seq, messageCid);
137
219
  }
138
220
 
139
221
  public async read(tenant: string, options: EventLogReadOptions = {}): Promise<EventLogReadResult> {
140
222
  const { cursor, limit, filters } = options;
141
- const cursorSeq = cursor !== undefined ? EventEmitterEventLog.parseCursor(cursor) : undefined;
223
+
224
+ // Validate cursor before attempting to read.
225
+ if (cursor !== undefined) {
226
+ await this.validateCursor(tenant, cursor);
227
+ }
228
+
229
+ const cursorSeq = cursor !== undefined ? EventEmitterEventLog.parsePosition(cursor.position) : undefined;
142
230
  const log = this.tenantLogs.get(tenant);
143
231
 
144
232
  if (log === undefined || log.size === 0) {
@@ -159,26 +247,45 @@ export class EventEmitterEventLog implements EventLog {
159
247
 
160
248
  results.push({
161
249
  seq,
162
- event : entry.event,
163
- indexes : entry.indexes,
250
+ event : entry.event,
251
+ indexes : entry.indexes,
252
+ messageCid : entry.messageCid,
164
253
  });
165
254
 
166
255
  if (results.length >= maxResults) { break; }
167
256
  }
168
257
 
169
- const lastSeq = results.length > 0 ? results[results.length - 1].seq : undefined;
170
- return { events: results, cursor: lastSeq !== undefined ? String(lastSeq) : cursor };
258
+ if (results.length > 0) {
259
+ const lastEntry = results[results.length - 1];
260
+ // Use the messageCid captured during the synchronous iteration above —
261
+ // no re-lookup needed, so eviction during the await cannot lose it.
262
+ const lastToken = await this.buildToken(tenant, lastEntry.seq, lastEntry.messageCid!);
263
+ return { events: results, cursor: lastToken };
264
+ }
265
+
266
+ return { events: results, cursor };
171
267
  }
172
268
 
173
269
  /**
174
- * Parse an opaque cursor string into an internal sequence number.
270
+ * Parse a position string into an internal sequence number using BigInt
271
+ * for safe handling of values beyond `Number.MAX_SAFE_INTEGER`.
272
+ *
273
+ * The returned `number` is safe for the in-memory EventLog which uses
274
+ * `Map<number, StoredEntry>` keys — in-memory sequences will never
275
+ * exceed safe integer range. The BigInt parse validates correctness
276
+ * before the narrowing conversion.
175
277
  */
176
- private static parseCursor(cursor: string): number {
177
- const seq = Number(cursor);
178
- if (Number.isNaN(seq) || seq < 0) {
179
- throw new DwnError(DwnErrorCode.EventLogNotOpenError, `invalid cursor: '${cursor}'`);
278
+ private static parsePosition(position: string): number {
279
+ try {
280
+ const big = BigInt(position);
281
+ if (big < 0n) {
282
+ throw new DwnError(DwnErrorCode.EventLogNotOpenError, `invalid cursor position: '${position}'`);
283
+ }
284
+ return Number(big);
285
+ } catch (e) {
286
+ if (e instanceof DwnError) { throw e; }
287
+ throw new DwnError(DwnErrorCode.EventLogNotOpenError, `invalid cursor position: '${position}'`);
180
288
  }
181
- return seq;
182
289
  }
183
290
 
184
291
  public async subscribe(
@@ -190,12 +297,20 @@ export class EventEmitterEventLog implements EventLog {
190
297
  const channel = `${tenant}_${EVENTS_LISTENER_CHANNEL}`;
191
298
  const { cursor, filters } = options ?? {};
192
299
 
300
+ // Helper to build a token from a live emitter payload.
301
+ const tokenFromPayload = async (payload: EmitterPayload): Promise<ProgressToken> => {
302
+ return this.buildToken(tenant, payload.seq, payload.messageCid);
303
+ };
304
+
193
305
  if (cursor !== undefined) {
194
306
  // ---- Cursor mode: catch-up from stored events, then EOSE, then live ----
195
- const cursorSeq = EventEmitterEventLog.parseCursor(cursor);
307
+ // Validate cursor before subscribing — throws DwnError(EventLogProgressGap) on gap.
308
+ await this.validateCursor(tenant, cursor);
309
+
310
+ const cursorSeq = EventEmitterEventLog.parsePosition(cursor.position);
196
311
 
197
312
  // Buffer live events that arrive during catch-up to avoid losing them.
198
- type BufferedEvent = { event: MessageEvent; seq: number };
313
+ type BufferedEvent = { event: MessageEvent; seq: number; messageCid: string };
199
314
  const pendingLiveEvents: BufferedEvent[] = [];
200
315
  let catchUpComplete = false;
201
316
 
@@ -205,9 +320,11 @@ export class EventEmitterEventLog implements EventLog {
205
320
  if (!FilterUtility.matchAnyFilter(payload.indexes, filters)) { return; }
206
321
  }
207
322
  if (!catchUpComplete) {
208
- pendingLiveEvents.push({ event: payload.event, seq: payload.seq });
323
+ pendingLiveEvents.push({ event: payload.event, seq: payload.seq, messageCid: payload.messageCid });
209
324
  } else {
210
- listener({ type: 'event', cursor: String(payload.seq), event: payload.event });
325
+ void tokenFromPayload(payload).then((token) => {
326
+ listener({ type: 'event', cursor: token, event: payload.event });
327
+ });
211
328
  }
212
329
  };
213
330
 
@@ -215,19 +332,27 @@ export class EventEmitterEventLog implements EventLog {
215
332
 
216
333
  // Step 2: Read stored events from cursor and deliver them.
217
334
  const readResult = await this.read(tenant, { cursor, filters });
218
- // The read cursor is the last event's seq (string) or the input cursor if nothing new.
335
+ // The read cursor is the token of the last read event, or the input cursor if nothing new.
219
336
  const eoseCursor = readResult.cursor ?? cursor;
220
- const lastCatchUpSeq = readResult.cursor !== undefined ? Number(readResult.cursor) : cursorSeq;
337
+ const lastCatchUpSeq = readResult.cursor !== undefined
338
+ ? EventEmitterEventLog.parsePosition(readResult.cursor.position)
339
+ : cursorSeq;
221
340
 
341
+ // Use the messageCid captured by read() during its synchronous iteration.
342
+ // This eliminates re-lookup races: read() populates entry.messageCid before
343
+ // any async yield, so eviction during read()'s buildToken() cannot lose it.
222
344
  for (const entry of readResult.events) {
223
- listener({ type: 'event', cursor: String(entry.seq), event: entry.event });
345
+ const token = await this.buildToken(tenant, entry.seq, entry.messageCid ?? '');
346
+ listener({ type: 'event', cursor: token, event: entry.event });
224
347
  }
225
348
 
226
349
  // Step 3: Deliver any live events that arrived during catch-up (with seq > lastCatchUpSeq).
350
+ // messageCid was captured at buffer time from the EmitterPayload.
227
351
  catchUpComplete = true;
228
352
  for (const liveEvent of pendingLiveEvents) {
229
353
  if (liveEvent.seq > lastCatchUpSeq) {
230
- listener({ type: 'event', cursor: String(liveEvent.seq), event: liveEvent.event });
354
+ const token = await this.buildToken(tenant, liveEvent.seq, liveEvent.messageCid);
355
+ listener({ type: 'event', cursor: token, event: liveEvent.event });
231
356
  }
232
357
  }
233
358
 
@@ -246,7 +371,9 @@ export class EventEmitterEventLog implements EventLog {
246
371
  if (filters !== undefined && filters.length > 0) {
247
372
  if (!FilterUtility.matchAnyFilter(payload.indexes, filters)) { return; }
248
373
  }
249
- listener({ type: 'event', cursor: String(payload.seq), event: payload.event });
374
+ void tokenFromPayload(payload).then((token) => {
375
+ listener({ type: 'event', cursor: token, event: payload.event });
376
+ });
250
377
  };
251
378
 
252
379
  this.emitter.on(channel, handler);
@@ -257,6 +384,26 @@ export class EventEmitterEventLog implements EventLog {
257
384
  };
258
385
  }
259
386
 
387
+ public async getReplayBounds(tenant: string): Promise<{ oldest: ProgressToken; latest: ProgressToken } | undefined> {
388
+ const log = this.tenantLogs.get(tenant);
389
+ if (log === undefined || log.size === 0) {
390
+ return undefined;
391
+ }
392
+
393
+ // Map is ordered by insertion (ascending seq). First and last keys are min/max.
394
+ const keys = [...log.keys()];
395
+ const oldestSeq = keys[0];
396
+ const latestSeq = keys[keys.length - 1];
397
+
398
+ const oldestEntry = log.get(oldestSeq)!;
399
+ const latestEntry = log.get(latestSeq)!;
400
+
401
+ const oldest = await this.buildToken(tenant, oldestSeq, oldestEntry.messageCid);
402
+ const latest = await this.buildToken(tenant, latestSeq, latestEntry.messageCid);
403
+
404
+ return { oldest, latest };
405
+ }
406
+
260
407
  public async trim(tenant: string, olderThan: number | string): Promise<void> {
261
408
  const log = this.tenantLogs.get(tenant);
262
409
  if (log === undefined) { return; }
@@ -1,7 +1,7 @@
1
1
  import type { MessageStore } from '../types/message-store.js';
2
- import type { SubscriptionListener } from '../types/subscriptions.js';
3
2
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
4
3
  import type { MessagesSubscribeMessage, MessagesSubscribeReply } from '../types/messages-types.js';
4
+ import type { ProgressGapInfo, SubscriptionListener } from '../types/subscriptions.js';
5
5
 
6
6
  import { authenticate } from '../core/auth.js';
7
7
  import { Message } from '../core/message.js';
@@ -61,6 +61,13 @@ export class MessagesSubscribeHandler implements MethodHandler {
61
61
  subscription,
62
62
  };
63
63
  } catch (error) {
64
+ if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
65
+ const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
66
+ return {
67
+ status : { code: 410, detail: 'Progress token gap' },
68
+ error : gapInfo !== undefined ? { code: 'ProgressGap' as const, ...gapInfo } : undefined,
69
+ };
70
+ }
64
71
  return messageReplyFromError(error, 500);
65
72
  }
66
73
  }
@@ -83,7 +83,7 @@ export class ProtocolsConfigureHandler implements MethodHandler {
83
83
 
84
84
  // only emit if the event log is set
85
85
  if (this.deps.eventLog !== undefined) {
86
- await this.deps.eventLog.emit(tenant, { message }, indexes);
86
+ await this.deps.eventLog.emit(tenant, { message }, indexes, messageCid);
87
87
  }
88
88
 
89
89
  messageReply = {
@@ -1,9 +1,9 @@
1
1
  import type { CoreProtocolRegistry } from '../core/core-protocol.js';
2
2
  import type { MessageSort } from '../types/message-types.js';
3
3
  import type { MessageStore } from '../types//message-store.js';
4
- import type { SubscriptionListener } from '../types/subscriptions.js';
5
4
  import type { Filter, PaginationCursor } from '../types/query-types.js';
6
5
  import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
6
+ import type { ProgressGapInfo, SubscriptionListener } from '../types/subscriptions.js';
7
7
  import type { RecordsQueryReplyEntry, RecordsSubscribeMessage, RecordsSubscribeReply } from '../types/records-types.js';
8
8
 
9
9
  import { authenticate } from '../core/auth.js';
@@ -93,6 +93,13 @@ export class RecordsSubscribeHandler implements MethodHandler {
93
93
  subscription,
94
94
  };
95
95
  } catch (error) {
96
+ if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
97
+ const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
98
+ return {
99
+ status : { code: 410, detail: 'Progress token gap' },
100
+ error : gapInfo !== undefined ? { code: 'ProgressGap' as const, ...gapInfo } : undefined,
101
+ };
102
+ }
96
103
  return messageReplyFromError(error, 500);
97
104
  }
98
105
  }
@@ -165,7 +165,8 @@ export class RecordsWriteHandler implements MethodHandler {
165
165
 
166
166
  const indexes = await recordsWrite.constructIndexes(isLatestBaseState);
167
167
  await this.deps.messageStore.put(tenant, messageWithOptionalEncodedData, indexes);
168
- await this.deps.stateIndex!.insert(tenant, await Message.getCid(message), indexes);
168
+ const messageCid = await Message.getCid(message);
169
+ await this.deps.stateIndex!.insert(tenant, messageCid, indexes);
169
170
 
170
171
  // NOTE: We only emit a `RecordsWrite` when the message is the latest base state.
171
172
  // Because we allow a `RecordsWrite` which is not the latest state to be written, but not queried, we shouldn't emit it either.
@@ -176,7 +177,7 @@ export class RecordsWriteHandler implements MethodHandler {
176
177
  // records (<= 30 KB). This allows live sync to store the record
177
178
  // immediately without a separate MessagesRead round-trip.
178
179
  if (this.deps.eventLog !== undefined && isLatestBaseState) {
179
- await this.deps.eventLog.emit(tenant, { message: messageWithOptionalEncodedData, initialWrite }, indexes);
180
+ await this.deps.eventLog.emit(tenant, { message: messageWithOptionalEncodedData, initialWrite }, indexes, messageCid);
180
181
  }
181
182
  } catch (error) {
182
183
  if (error instanceof DwnError) {
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // export everything that we want to be consumable
2
2
  export type { DwnConfig } from './dwn.js';
3
- export type { EventListener, EventLog, EventLogEntry, EventLogReadOptions, EventLogReadResult, EventLogSubscribeOptions, EventSubscription, MessageEvent, SubscriptionEose, SubscriptionEvent, SubscriptionListener, SubscriptionMessage, SubscriptionReply } from './types/subscriptions.js';
3
+ export type { EventListener, EventLog, EventLogEntry, EventLogReadOptions, EventLogReadResult, EventLogSubscribeOptions, EventSubscription, MessageEvent, ProgressGapInfo, ProgressGapReason, ProgressToken, SubscriptionEose, SubscriptionEvent, SubscriptionListener, SubscriptionMessage, SubscriptionReply } from './types/subscriptions.js';
4
4
  export type { AuthorizationModel, Descriptor, DelegatedGrantRecordsWriteMessage, GenericMessage, GenericMessageReply, GenericSignaturePayload, MessageSort, MessageSubscription, Pagination, QueryResultEntry, Status } from './types/message-types.js';
5
5
  export type { MessagesFilter, MessagesReadMessage as MessagesReadMessage, MessagesReadReply as MessagesReadReply, MessagesReadReplyEntry as MessagesReadReplyEntry, MessagesReadDescriptor, MessagesSubscribeDescriptor, MessagesSubscribeMessage, MessagesSubscribeReply, MessagesSubscribeMessageOptions, MessagesSyncAction, MessagesSyncDescriptor, MessagesSyncDiffEntry, MessagesSyncMessage, MessagesSyncReply } from './types/messages-types.js';
6
6
  export type { GT, LT, Filter, FilterValue, KeyValues, EqualFilter, OneOfFilter, RangeFilter, RangeCriterion, PaginationCursor, QueryOptions, RangeValue, StartsWithFilter } from './types/query-types.js';
@@ -1,5 +1,6 @@
1
1
  import type { MessagesFilter } from '../types/messages-types.js';
2
2
  import type { MessageSigner } from '../types/signer.js';
3
+ import type { ProgressToken } from '../types/subscriptions.js';
3
4
  import type { MessagesSubscribeDescriptor, MessagesSubscribeMessage } from '../types/messages-types.js';
4
5
 
5
6
  import { AbstractMessage } from '../core/abstract-message.js';
@@ -16,10 +17,10 @@ export type MessagesSubscribeOptions = {
16
17
  filters?: MessagesFilter[];
17
18
  permissionGrantId?: string;
18
19
  /**
19
- * Opaque EventLog cursor string to resume from. When provided, catch-up events are
20
- * replayed from the EventLog and an EOSE marker is delivered before live events.
20
+ * Progress token to resume from. When provided, catch-up events are replayed
21
+ * from the EventLog and an EOSE marker is delivered before live events.
21
22
  */
22
- cursor?: string;
23
+ cursor?: ProgressToken;
23
24
  };
24
25
 
25
26
  export class MessagesSubscribe extends AbstractMessage<MessagesSubscribeMessage> {
@@ -1,6 +1,7 @@
1
1
  import type { MessageSigner } from '../types/signer.js';
2
2
  import type { MessageStore } from '../types/message-store.js';
3
3
  import type { Pagination } from '../types/message-types.js';
4
+ import type { ProgressToken } from '../types/subscriptions.js';
4
5
  import type { DataEncodedRecordsWriteMessage, DateSort, RecordsFilter, RecordsSubscribeDescriptor, RecordsSubscribeMessage } from '../types/records-types.js';
5
6
 
6
7
  import { AbstractMessage } from '../core/abstract-message.js';
@@ -23,10 +24,10 @@ export type RecordsSubscribeOptions = {
23
24
  protocolRole?: string;
24
25
 
25
26
  /**
26
- * Opaque EventLog cursor string to resume from. When provided, catch-up events are
27
- * replayed from the EventLog and an EOSE marker is delivered before live events.
27
+ * Progress token to resume from. When provided, catch-up events are replayed
28
+ * from the EventLog and an EOSE marker is delivered before live events.
28
29
  */
29
- cursor?: string;
30
+ cursor?: ProgressToken;
30
31
 
31
32
  /**
32
33
  * The delegated grant to sign on behalf of the logical author, which is the grantor (`grantedBy`) of the delegated grant.
@@ -75,7 +75,7 @@ export class StorageController {
75
75
 
76
76
  // only emit if the event log is set
77
77
  if (this.eventLog !== undefined) {
78
- await this.eventLog.emit(tenant, { message, initialWrite }, indexes);
78
+ await this.eventLog.emit(tenant, { message, initialWrite }, indexes, messageCid);
79
79
  }
80
80
 
81
81
  if (message.descriptor.prune) {
@@ -1,7 +1,7 @@
1
1
  import type { RangeCriterion } from './query-types.js';
2
- import type { SubscriptionListener } from './subscriptions.js';
3
2
  import type { AuthorizationModel, GenericMessage, GenericMessageReply, MessageSubscription } from './message-types.js';
4
3
  import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
4
+ import type { ProgressGapInfo, ProgressToken, SubscriptionListener } from './subscriptions.js';
5
5
 
6
6
  /**
7
7
  * filters used when filtering for any type of Message across interfaces
@@ -10,6 +10,12 @@ export type MessagesFilter = {
10
10
  interface?: string;
11
11
  method?: string;
12
12
  protocol?: string;
13
+ /** Prefix filter for protocolPath. Matches records whose protocolPath equals
14
+ * the prefix or starts with the prefix followed by '/'. */
15
+ protocolPathPrefix?: string;
16
+ /** Prefix filter for contextId. Matches records whose contextId equals
17
+ * the prefix or starts with the prefix followed by '/'. */
18
+ contextIdPrefix?: string;
13
19
  messageTimestamp?: RangeCriterion;
14
20
  };
15
21
 
@@ -100,6 +106,8 @@ export type MessagesSubscribeMessage = {
100
106
 
101
107
  export type MessagesSubscribeReply = GenericMessageReply & {
102
108
  subscription?: MessageSubscription;
109
+ /** Present when status.code is 410 — structured gap metadata. */
110
+ error?: { code: 'ProgressGap' } & ProgressGapInfo;
103
111
  };
104
112
 
105
113
  export type MessagesSubscribeDescriptor = {
@@ -109,9 +117,9 @@ export type MessagesSubscribeDescriptor = {
109
117
  filters: MessagesFilter[];
110
118
  permissionGrantId?: string;
111
119
  /**
112
- * Opaque EventLog cursor string to resume from. When provided, the handler replays
113
- * events from the EventLog starting after this cursor instead of returning no
120
+ * Progress token to resume from. When provided, the handler replays events
121
+ * from the EventLog starting after this position instead of returning no
114
122
  * initial snapshot. An EOSE marker is sent after catch-up.
115
123
  */
116
- cursor?: string;
124
+ cursor?: ProgressToken;
117
125
  };
@@ -1,9 +1,9 @@
1
1
  import type { GeneralJws } from './jws-types.js';
2
2
  import type { JweEncryption } from '../utils/encryption.js';
3
- import type { SubscriptionListener } from './subscriptions.js';
4
3
  import type { AuthorizationModel, GenericMessage, GenericMessageReply, GenericSignaturePayload, MessageSubscription, Pagination } from './message-types.js';
5
4
  import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
6
5
  import type { PaginationCursor, RangeCriterion, RangeFilter, StartsWithFilter } from './query-types.js';
6
+ import type { ProgressGapInfo, ProgressToken, SubscriptionListener } from './subscriptions.js';
7
7
 
8
8
  export enum DateSort {
9
9
  CreatedAscending = 'createdAscending',
@@ -132,11 +132,11 @@ export type RecordsSubscribeDescriptor = {
132
132
  dateSort?: DateSort;
133
133
  pagination?: Pagination;
134
134
  /**
135
- * Opaque EventLog cursor string to resume from. When provided, the handler replays
136
- * events from the EventLog starting after this cursor instead of querying the
135
+ * Progress token to resume from. When provided, the handler replays events
136
+ * from the EventLog starting after this position instead of querying the
137
137
  * MessageStore for an initial snapshot. An EOSE marker is sent after catch-up.
138
138
  */
139
- cursor?: string;
139
+ cursor?: ProgressToken;
140
140
  };
141
141
 
142
142
  export type RecordsFilter = {
@@ -203,6 +203,8 @@ export type RecordsSubscribeReply = GenericMessageReply & {
203
203
  subscription?: MessageSubscription;
204
204
  entries?: RecordsQueryReplyEntry[];
205
205
  cursor?: PaginationCursor;
206
+ /** Present when status.code is 410 — structured gap metadata. */
207
+ error?: { code: 'ProgressGap' } & ProgressGapInfo;
206
208
  };
207
209
 
208
210
  export type RecordsReadMessage = {