@durable-streams/server 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -57,6 +57,12 @@ interface Stream {
57
57
  */
58
58
  createdAt: number;
59
59
  /**
60
+ * Timestamp of the last read or write (for TTL renewal).
61
+ * Initialized to createdAt. Updated on GET reads and POST appends.
62
+ * HEAD requests do NOT update this field.
63
+ */
64
+ lastAccessedAt: number;
65
+ /**
60
66
  * Producer states for idempotent writes.
61
67
  * Maps producer ID to their epoch and sequence state.
62
68
  */
@@ -75,6 +81,24 @@ interface Stream {
75
81
  epoch: number;
76
82
  seq: number;
77
83
  };
84
+ /**
85
+ * Source stream path (set when this stream is a fork).
86
+ */
87
+ forkedFrom?: string;
88
+ /**
89
+ * Divergence offset from the source stream.
90
+ * Format: "0000000000000000_0000000000000000"
91
+ */
92
+ forkOffset?: string;
93
+ /**
94
+ * Number of forks referencing this stream.
95
+ * Defaults to 0.
96
+ */
97
+ refCount: number;
98
+ /**
99
+ * Whether this stream is logically deleted but retained for fork readers.
100
+ */
101
+ softDeleted?: boolean;
78
102
  }
79
103
  /**
80
104
  * Event data for stream lifecycle hooks.
@@ -251,13 +275,19 @@ declare class StreamStore {
251
275
  */
252
276
  private isExpired;
253
277
  /**
254
- * Get a stream, deleting it if expired.
255
- * Returns undefined if stream doesn't exist or is expired.
278
+ * Get a stream, handling expiry.
279
+ * Returns undefined if stream doesn't exist or is expired (and has no refs).
280
+ * Expired streams with refCount > 0 are soft-deleted instead of fully deleted.
256
281
  */
257
282
  private getIfNotExpired;
258
283
  /**
284
+ * Update lastAccessedAt to now. Called on reads and appends (not HEAD).
285
+ */
286
+ touchAccess(path: string): void;
287
+ /**
259
288
  * Create a new stream.
260
289
  * @throws Error if stream already exists with different config
290
+ * @throws Error if fork source not found, soft-deleted, or offset invalid
261
291
  * @returns existing stream if config matches (idempotent)
262
292
  */
263
293
  create(path: string, options?: {
@@ -266,21 +296,36 @@ declare class StreamStore {
266
296
  expiresAt?: string;
267
297
  initialData?: Uint8Array;
268
298
  closed?: boolean;
299
+ forkedFrom?: string;
300
+ forkOffset?: string;
269
301
  }): Stream;
270
302
  /**
303
+ * Resolve fork expiry per the decision table.
304
+ * Forks have independent lifetimes — no capping at source expiry.
305
+ */
306
+ private resolveForkExpiry;
307
+ /**
271
308
  * Get a stream by path.
272
309
  * Returns undefined if stream doesn't exist or is expired.
310
+ * Returns soft-deleted streams (caller should check stream.softDeleted).
273
311
  */
274
312
  get(path: string): Stream | undefined;
275
313
  /**
276
- * Check if a stream exists (and is not expired).
314
+ * Check if a stream exists, is not expired, and is not soft-deleted.
277
315
  */
278
316
  has(path: string): boolean;
279
317
  /**
280
318
  * Delete a stream.
319
+ * If the stream has forks (refCount > 0), it is soft-deleted instead of fully removed.
320
+ * Returns true if the stream was found and deleted (or soft-deleted).
281
321
  */
282
322
  delete(path: string): boolean;
283
323
  /**
324
+ * Fully delete a stream and cascade to soft-deleted parents
325
+ * whose refcount drops to zero.
326
+ */
327
+ private deleteWithCascade;
328
+ /**
284
329
  * Validate producer state WITHOUT mutating.
285
330
  * Returns proposed state to commit after successful append.
286
331
  * Implements Kafka-style idempotent producer validation.
@@ -346,6 +391,7 @@ declare class StreamStore {
346
391
  getProducerEpoch(path: string, producerId: string): number | undefined;
347
392
  /**
348
393
  * Read messages from a stream starting at the given offset.
394
+ * For forked streams, stitches messages from the source chain and the fork's own messages.
349
395
  * @throws Error if stream doesn't exist or is expired
350
396
  */
351
397
  read(path: string, offset?: string): {
@@ -353,6 +399,20 @@ declare class StreamStore {
353
399
  upToDate: boolean;
354
400
  };
355
401
  /**
402
+ * Read from a forked stream, stitching inherited and own messages.
403
+ */
404
+ private readFromFork;
405
+ /**
406
+ * Read a stream's own messages starting after the given offset.
407
+ */
408
+ private readOwnMessages;
409
+ /**
410
+ * Recursively read messages from a fork's source chain.
411
+ * Reads from source (and its sources if also forked), capped at forkOffset.
412
+ * Does NOT check softDeleted — forks must read through soft-deleted sources.
413
+ */
414
+ private readForkedMessages;
415
+ /**
356
416
  * Format messages for response.
357
417
  * For JSON mode, wraps concatenated data in array brackets.
358
418
  * @throws Error if stream doesn't exist or is expired
@@ -450,15 +510,25 @@ declare class FileBackedStreamStore {
450
510
  */
451
511
  getProducerEpoch(streamPath: string, producerId: string): number | undefined;
452
512
  /**
513
+ * Update lastAccessedAt to now. Called on reads and appends (not HEAD).
514
+ */
515
+ touchAccess(streamPath: string): void;
516
+ /**
453
517
  * Check if a stream is expired based on TTL or Expires-At.
454
518
  */
455
519
  private isExpired;
456
520
  /**
457
521
  * Get stream metadata, deleting it if expired.
458
- * Returns undefined if stream doesn't exist or is expired.
522
+ * Returns undefined if stream doesn't exist or is expired (and has no refs).
523
+ * Expired streams with refCount > 0 are soft-deleted instead of fully deleted.
459
524
  */
460
525
  private getMetaIfNotExpired;
461
526
  /**
527
+ * Resolve fork expiry per the decision table.
528
+ * Forks have independent lifetimes — no capping at source expiry.
529
+ */
530
+ private resolveForkExpiry;
531
+ /**
462
532
  * Close the store, closing all file handles and database.
463
533
  * All data is already fsynced on each append, so no final flush needed.
464
534
  */
@@ -469,10 +539,17 @@ declare class FileBackedStreamStore {
469
539
  expiresAt?: string;
470
540
  initialData?: Uint8Array;
471
541
  closed?: boolean;
542
+ forkedFrom?: string;
543
+ forkOffset?: string;
472
544
  }): Promise<Stream>;
473
545
  get(streamPath: string): Stream | undefined;
474
546
  has(streamPath: string): boolean;
475
547
  delete(streamPath: string): boolean;
548
+ /**
549
+ * Fully delete a stream and cascade to soft-deleted parents
550
+ * whose refcount drops to zero.
551
+ */
552
+ private deleteWithCascade;
476
553
  append(streamPath: string, data: Uint8Array, options?: AppendOptions & {
477
554
  isInitialCreate?: boolean;
478
555
  }): Promise<StreamMessage | AppendResult | null>;
@@ -503,6 +580,21 @@ declare class FileBackedStreamStore {
503
580
  alreadyClosed: boolean;
504
581
  producerResult?: ProducerValidationResult;
505
582
  } | null>;
583
+ /**
584
+ * Read messages from a specific segment file.
585
+ * @param segmentPath - Path to the segment file
586
+ * @param startByte - Start byte offset (skip messages at or before this offset)
587
+ * @param baseByteOffset - Base byte offset to add to physical offsets (for fork stitching)
588
+ * @param capByte - Optional cap: stop reading when logical offset exceeds this value
589
+ * @returns Array of messages with properly computed offsets
590
+ */
591
+ private readMessagesFromSegmentFile;
592
+ /**
593
+ * Recursively read messages from a fork's source chain.
594
+ * Reads from source (and its sources if also forked), capped at capByte.
595
+ * Does NOT check softDeleted -- forks must read through soft-deleted sources.
596
+ */
597
+ private readForkedMessages;
506
598
  read(streamPath: string, offset?: string): {
507
599
  messages: Array<StreamMessage>;
508
600
  upToDate: boolean;
@@ -533,8 +625,7 @@ declare class FileBackedStreamStore {
533
625
  private notifyLongPollsClosed;
534
626
  private cancelLongPollsForStream;
535
627
  private removePendingLongPoll;
536
- }
537
- //#endregion
628
+ } //#endregion
538
629
  //#region src/server.d.ts
539
630
  /**
540
631
  * HTTP server for testing durable streams.
package/dist/index.d.ts CHANGED
@@ -57,6 +57,12 @@ interface Stream {
57
57
  */
58
58
  createdAt: number;
59
59
  /**
60
+ * Timestamp of the last read or write (for TTL renewal).
61
+ * Initialized to createdAt. Updated on GET reads and POST appends.
62
+ * HEAD requests do NOT update this field.
63
+ */
64
+ lastAccessedAt: number;
65
+ /**
60
66
  * Producer states for idempotent writes.
61
67
  * Maps producer ID to their epoch and sequence state.
62
68
  */
@@ -75,6 +81,24 @@ interface Stream {
75
81
  epoch: number;
76
82
  seq: number;
77
83
  };
84
+ /**
85
+ * Source stream path (set when this stream is a fork).
86
+ */
87
+ forkedFrom?: string;
88
+ /**
89
+ * Divergence offset from the source stream.
90
+ * Format: "0000000000000000_0000000000000000"
91
+ */
92
+ forkOffset?: string;
93
+ /**
94
+ * Number of forks referencing this stream.
95
+ * Defaults to 0.
96
+ */
97
+ refCount: number;
98
+ /**
99
+ * Whether this stream is logically deleted but retained for fork readers.
100
+ */
101
+ softDeleted?: boolean;
78
102
  }
79
103
  /**
80
104
  * Event data for stream lifecycle hooks.
@@ -251,13 +275,19 @@ declare class StreamStore {
251
275
  */
252
276
  private isExpired;
253
277
  /**
254
- * Get a stream, deleting it if expired.
255
- * Returns undefined if stream doesn't exist or is expired.
278
+ * Get a stream, handling expiry.
279
+ * Returns undefined if stream doesn't exist or is expired (and has no refs).
280
+ * Expired streams with refCount > 0 are soft-deleted instead of fully deleted.
256
281
  */
257
282
  private getIfNotExpired;
258
283
  /**
284
+ * Update lastAccessedAt to now. Called on reads and appends (not HEAD).
285
+ */
286
+ touchAccess(path: string): void;
287
+ /**
259
288
  * Create a new stream.
260
289
  * @throws Error if stream already exists with different config
290
+ * @throws Error if fork source not found, soft-deleted, or offset invalid
261
291
  * @returns existing stream if config matches (idempotent)
262
292
  */
263
293
  create(path: string, options?: {
@@ -266,21 +296,36 @@ declare class StreamStore {
266
296
  expiresAt?: string;
267
297
  initialData?: Uint8Array;
268
298
  closed?: boolean;
299
+ forkedFrom?: string;
300
+ forkOffset?: string;
269
301
  }): Stream;
270
302
  /**
303
+ * Resolve fork expiry per the decision table.
304
+ * Forks have independent lifetimes — no capping at source expiry.
305
+ */
306
+ private resolveForkExpiry;
307
+ /**
271
308
  * Get a stream by path.
272
309
  * Returns undefined if stream doesn't exist or is expired.
310
+ * Returns soft-deleted streams (caller should check stream.softDeleted).
273
311
  */
274
312
  get(path: string): Stream | undefined;
275
313
  /**
276
- * Check if a stream exists (and is not expired).
314
+ * Check if a stream exists, is not expired, and is not soft-deleted.
277
315
  */
278
316
  has(path: string): boolean;
279
317
  /**
280
318
  * Delete a stream.
319
+ * If the stream has forks (refCount > 0), it is soft-deleted instead of fully removed.
320
+ * Returns true if the stream was found and deleted (or soft-deleted).
281
321
  */
282
322
  delete(path: string): boolean;
283
323
  /**
324
+ * Fully delete a stream and cascade to soft-deleted parents
325
+ * whose refcount drops to zero.
326
+ */
327
+ private deleteWithCascade;
328
+ /**
284
329
  * Validate producer state WITHOUT mutating.
285
330
  * Returns proposed state to commit after successful append.
286
331
  * Implements Kafka-style idempotent producer validation.
@@ -346,6 +391,7 @@ declare class StreamStore {
346
391
  getProducerEpoch(path: string, producerId: string): number | undefined;
347
392
  /**
348
393
  * Read messages from a stream starting at the given offset.
394
+ * For forked streams, stitches messages from the source chain and the fork's own messages.
349
395
  * @throws Error if stream doesn't exist or is expired
350
396
  */
351
397
  read(path: string, offset?: string): {
@@ -353,6 +399,20 @@ declare class StreamStore {
353
399
  upToDate: boolean;
354
400
  };
355
401
  /**
402
+ * Read from a forked stream, stitching inherited and own messages.
403
+ */
404
+ private readFromFork;
405
+ /**
406
+ * Read a stream's own messages starting after the given offset.
407
+ */
408
+ private readOwnMessages;
409
+ /**
410
+ * Recursively read messages from a fork's source chain.
411
+ * Reads from source (and its sources if also forked), capped at forkOffset.
412
+ * Does NOT check softDeleted — forks must read through soft-deleted sources.
413
+ */
414
+ private readForkedMessages;
415
+ /**
356
416
  * Format messages for response.
357
417
  * For JSON mode, wraps concatenated data in array brackets.
358
418
  * @throws Error if stream doesn't exist or is expired
@@ -450,15 +510,25 @@ declare class FileBackedStreamStore {
450
510
  */
451
511
  getProducerEpoch(streamPath: string, producerId: string): number | undefined;
452
512
  /**
513
+ * Update lastAccessedAt to now. Called on reads and appends (not HEAD).
514
+ */
515
+ touchAccess(streamPath: string): void;
516
+ /**
453
517
  * Check if a stream is expired based on TTL or Expires-At.
454
518
  */
455
519
  private isExpired;
456
520
  /**
457
521
  * Get stream metadata, deleting it if expired.
458
- * Returns undefined if stream doesn't exist or is expired.
522
+ * Returns undefined if stream doesn't exist or is expired (and has no refs).
523
+ * Expired streams with refCount > 0 are soft-deleted instead of fully deleted.
459
524
  */
460
525
  private getMetaIfNotExpired;
461
526
  /**
527
+ * Resolve fork expiry per the decision table.
528
+ * Forks have independent lifetimes — no capping at source expiry.
529
+ */
530
+ private resolveForkExpiry;
531
+ /**
462
532
  * Close the store, closing all file handles and database.
463
533
  * All data is already fsynced on each append, so no final flush needed.
464
534
  */
@@ -469,10 +539,17 @@ declare class FileBackedStreamStore {
469
539
  expiresAt?: string;
470
540
  initialData?: Uint8Array;
471
541
  closed?: boolean;
542
+ forkedFrom?: string;
543
+ forkOffset?: string;
472
544
  }): Promise<Stream>;
473
545
  get(streamPath: string): Stream | undefined;
474
546
  has(streamPath: string): boolean;
475
547
  delete(streamPath: string): boolean;
548
+ /**
549
+ * Fully delete a stream and cascade to soft-deleted parents
550
+ * whose refcount drops to zero.
551
+ */
552
+ private deleteWithCascade;
476
553
  append(streamPath: string, data: Uint8Array, options?: AppendOptions & {
477
554
  isInitialCreate?: boolean;
478
555
  }): Promise<StreamMessage | AppendResult | null>;
@@ -503,6 +580,21 @@ declare class FileBackedStreamStore {
503
580
  alreadyClosed: boolean;
504
581
  producerResult?: ProducerValidationResult;
505
582
  } | null>;
583
+ /**
584
+ * Read messages from a specific segment file.
585
+ * @param segmentPath - Path to the segment file
586
+ * @param startByte - Start byte offset (skip messages at or before this offset)
587
+ * @param baseByteOffset - Base byte offset to add to physical offsets (for fork stitching)
588
+ * @param capByte - Optional cap: stop reading when logical offset exceeds this value
589
+ * @returns Array of messages with properly computed offsets
590
+ */
591
+ private readMessagesFromSegmentFile;
592
+ /**
593
+ * Recursively read messages from a fork's source chain.
594
+ * Reads from source (and its sources if also forked), capped at capByte.
595
+ * Does NOT check softDeleted -- forks must read through soft-deleted sources.
596
+ */
597
+ private readForkedMessages;
506
598
  read(streamPath: string, offset?: string): {
507
599
  messages: Array<StreamMessage>;
508
600
  upToDate: boolean;
@@ -533,8 +625,7 @@ declare class FileBackedStreamStore {
533
625
  private notifyLongPollsClosed;
534
626
  private cancelLongPollsForStream;
535
627
  private removePendingLongPoll;
536
- }
537
- //#endregion
628
+ } //#endregion
538
629
  //#region src/server.d.ts
539
630
  /**
540
631
  * HTTP server for testing durable streams.