@durable-streams/server 0.1.1 → 0.1.3

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.js CHANGED
@@ -65,12 +65,40 @@ var StreamStore = class {
65
65
  streams = new Map();
66
66
  pendingLongPolls = [];
67
67
  /**
68
+ * Check if a stream is expired based on TTL or Expires-At.
69
+ */
70
+ isExpired(stream) {
71
+ const now = Date.now();
72
+ if (stream.expiresAt) {
73
+ const expiryTime = new Date(stream.expiresAt).getTime();
74
+ if (!Number.isFinite(expiryTime) || now >= expiryTime) return true;
75
+ }
76
+ if (stream.ttlSeconds !== void 0) {
77
+ const expiryTime = stream.createdAt + stream.ttlSeconds * 1e3;
78
+ if (now >= expiryTime) return true;
79
+ }
80
+ return false;
81
+ }
82
+ /**
83
+ * Get a stream, deleting it if expired.
84
+ * Returns undefined if stream doesn't exist or is expired.
85
+ */
86
+ getIfNotExpired(path$2) {
87
+ const stream = this.streams.get(path$2);
88
+ if (!stream) return void 0;
89
+ if (this.isExpired(stream)) {
90
+ this.delete(path$2);
91
+ return void 0;
92
+ }
93
+ return stream;
94
+ }
95
+ /**
68
96
  * Create a new stream.
69
97
  * @throws Error if stream already exists with different config
70
98
  * @returns existing stream if config matches (idempotent)
71
99
  */
72
100
  create(path$2, options = {}) {
73
- const existing = this.streams.get(path$2);
101
+ const existing = this.getIfNotExpired(path$2);
74
102
  if (existing) {
75
103
  const contentTypeMatches = (normalizeContentType(options.contentType) || `application/octet-stream`) === (normalizeContentType(existing.contentType) || `application/octet-stream`);
76
104
  const ttlMatches = options.ttlSeconds === existing.ttlSeconds;
@@ -93,15 +121,16 @@ var StreamStore = class {
93
121
  }
94
122
  /**
95
123
  * Get a stream by path.
124
+ * Returns undefined if stream doesn't exist or is expired.
96
125
  */
97
126
  get(path$2) {
98
- return this.streams.get(path$2);
127
+ return this.getIfNotExpired(path$2);
99
128
  }
100
129
  /**
101
- * Check if a stream exists.
130
+ * Check if a stream exists (and is not expired).
102
131
  */
103
132
  has(path$2) {
104
- return this.streams.has(path$2);
133
+ return this.getIfNotExpired(path$2) !== void 0;
105
134
  }
106
135
  /**
107
136
  * Delete a stream.
@@ -112,12 +141,12 @@ var StreamStore = class {
112
141
  }
113
142
  /**
114
143
  * Append data to a stream.
115
- * @throws Error if stream doesn't exist
144
+ * @throws Error if stream doesn't exist or is expired
116
145
  * @throws Error if seq is lower than lastSeq
117
146
  * @throws Error if JSON mode and array is empty
118
147
  */
119
148
  append(path$2, data, options = {}) {
120
- const stream = this.streams.get(path$2);
149
+ const stream = this.getIfNotExpired(path$2);
121
150
  if (!stream) throw new Error(`Stream not found: ${path$2}`);
122
151
  if (options.contentType && stream.contentType) {
123
152
  const providedType = normalizeContentType(options.contentType);
@@ -134,9 +163,10 @@ var StreamStore = class {
134
163
  }
135
164
  /**
136
165
  * Read messages from a stream starting at the given offset.
166
+ * @throws Error if stream doesn't exist or is expired
137
167
  */
138
168
  read(path$2, offset) {
139
- const stream = this.streams.get(path$2);
169
+ const stream = this.getIfNotExpired(path$2);
140
170
  if (!stream) throw new Error(`Stream not found: ${path$2}`);
141
171
  if (!offset || offset === `-1`) return {
142
172
  messages: [...stream.messages],
@@ -155,9 +185,10 @@ var StreamStore = class {
155
185
  /**
156
186
  * Format messages for response.
157
187
  * For JSON mode, wraps concatenated data in array brackets.
188
+ * @throws Error if stream doesn't exist or is expired
158
189
  */
159
190
  formatResponse(path$2, messages) {
160
- const stream = this.streams.get(path$2);
191
+ const stream = this.getIfNotExpired(path$2);
161
192
  if (!stream) throw new Error(`Stream not found: ${path$2}`);
162
193
  const totalSize = messages.reduce((sum, m) => sum + m.data.length, 0);
163
194
  const concatenated = new Uint8Array(totalSize);
@@ -171,9 +202,10 @@ var StreamStore = class {
171
202
  }
172
203
  /**
173
204
  * Wait for new messages (long-poll).
205
+ * @throws Error if stream doesn't exist or is expired
174
206
  */
175
207
  async waitForMessages(path$2, offset, timeoutMs) {
176
- const stream = this.streams.get(path$2);
208
+ const stream = this.getIfNotExpired(path$2);
177
209
  if (!stream) throw new Error(`Stream not found: ${path$2}`);
178
210
  const { messages } = this.read(path$2, offset);
179
211
  if (messages.length > 0) return {
@@ -206,9 +238,10 @@ var StreamStore = class {
206
238
  }
207
239
  /**
208
240
  * Get the current offset for a stream.
241
+ * Returns undefined if stream doesn't exist or is expired.
209
242
  */
210
243
  getCurrentOffset(path$2) {
211
- return this.streams.get(path$2)?.currentOffset;
244
+ return this.getIfNotExpired(path$2)?.currentOffset;
212
245
  }
213
246
  /**
214
247
  * Clear all streams.
@@ -582,6 +615,35 @@ var FileBackedStreamStore = class {
582
615
  };
583
616
  }
584
617
  /**
618
+ * Check if a stream is expired based on TTL or Expires-At.
619
+ */
620
+ isExpired(meta) {
621
+ const now = Date.now();
622
+ if (meta.expiresAt) {
623
+ const expiryTime = new Date(meta.expiresAt).getTime();
624
+ if (!Number.isFinite(expiryTime) || now >= expiryTime) return true;
625
+ }
626
+ if (meta.ttlSeconds !== void 0) {
627
+ const expiryTime = meta.createdAt + meta.ttlSeconds * 1e3;
628
+ if (now >= expiryTime) return true;
629
+ }
630
+ return false;
631
+ }
632
+ /**
633
+ * Get stream metadata, deleting it if expired.
634
+ * Returns undefined if stream doesn't exist or is expired.
635
+ */
636
+ getMetaIfNotExpired(streamPath) {
637
+ const key = `stream:${streamPath}`;
638
+ const meta = this.db.get(key);
639
+ if (!meta) return void 0;
640
+ if (this.isExpired(meta)) {
641
+ this.delete(streamPath);
642
+ return void 0;
643
+ }
644
+ return meta;
645
+ }
646
+ /**
585
647
  * Close the store, closing all file handles and database.
586
648
  * All data is already fsynced on each append, so no final flush needed.
587
649
  */
@@ -590,8 +652,7 @@ var FileBackedStreamStore = class {
590
652
  await this.db.close();
591
653
  }
592
654
  async create(streamPath, options = {}) {
593
- const key = `stream:${streamPath}`;
594
- const existing = this.db.get(key);
655
+ const existing = this.getMetaIfNotExpired(streamPath);
595
656
  if (existing) {
596
657
  const normalizeMimeType = (ct) => (ct ?? `application/octet-stream`).toLowerCase();
597
658
  const contentTypeMatches = normalizeMimeType(options.contentType) === normalizeMimeType(existing.contentType);
@@ -600,6 +661,7 @@ var FileBackedStreamStore = class {
600
661
  if (contentTypeMatches && ttlMatches && expiresMatches) return this.streamMetaToStream(existing);
601
662
  else throw new Error(`Stream already exists with different configuration: ${streamPath}`);
602
663
  }
664
+ const key = `stream:${streamPath}`;
603
665
  const streamMeta = {
604
666
  path: streamPath,
605
667
  contentType: options.contentType,
@@ -633,13 +695,11 @@ var FileBackedStreamStore = class {
633
695
  return this.streamMetaToStream(streamMeta);
634
696
  }
635
697
  get(streamPath) {
636
- const key = `stream:${streamPath}`;
637
- const meta = this.db.get(key);
698
+ const meta = this.getMetaIfNotExpired(streamPath);
638
699
  return meta ? this.streamMetaToStream(meta) : void 0;
639
700
  }
640
701
  has(streamPath) {
641
- const key = `stream:${streamPath}`;
642
- return this.db.get(key) !== void 0;
702
+ return this.getMetaIfNotExpired(streamPath) !== void 0;
643
703
  }
644
704
  delete(streamPath) {
645
705
  const key = `stream:${streamPath}`;
@@ -657,8 +717,7 @@ var FileBackedStreamStore = class {
657
717
  return true;
658
718
  }
659
719
  async append(streamPath, data, options = {}) {
660
- const key = `stream:${streamPath}`;
661
- const streamMeta = this.db.get(key);
720
+ const streamMeta = this.getMetaIfNotExpired(streamPath);
662
721
  if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
663
722
  if (options.contentType && streamMeta.contentType) {
664
723
  const providedType = normalizeContentType(options.contentType);
@@ -706,13 +765,13 @@ var FileBackedStreamStore = class {
706
765
  lastSeq: options.seq ?? streamMeta.lastSeq,
707
766
  totalBytes: streamMeta.totalBytes + processedData.length + 5
708
767
  };
768
+ const key = `stream:${streamPath}`;
709
769
  this.db.putSync(key, updatedMeta);
710
770
  this.notifyLongPolls(streamPath);
711
771
  return message;
712
772
  }
713
773
  read(streamPath, offset) {
714
- const key = `stream:${streamPath}`;
715
- const streamMeta = this.db.get(key);
774
+ const streamMeta = this.getMetaIfNotExpired(streamPath);
716
775
  if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
717
776
  const startOffset = offset ?? `0000000000000000_0000000000000000`;
718
777
  const startParts = startOffset.split(`_`).map(Number);
@@ -764,8 +823,7 @@ var FileBackedStreamStore = class {
764
823
  };
765
824
  }
766
825
  async waitForMessages(streamPath, offset, timeoutMs) {
767
- const key = `stream:${streamPath}`;
768
- const streamMeta = this.db.get(key);
826
+ const streamMeta = this.getMetaIfNotExpired(streamPath);
769
827
  if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
770
828
  const { messages } = this.read(streamPath, offset);
771
829
  if (messages.length > 0) return {
@@ -799,10 +857,10 @@ var FileBackedStreamStore = class {
799
857
  /**
800
858
  * Format messages for response.
801
859
  * For JSON mode, wraps concatenated data in array brackets.
860
+ * @throws Error if stream doesn't exist or is expired
802
861
  */
803
862
  formatResponse(streamPath, messages) {
804
- const key = `stream:${streamPath}`;
805
- const streamMeta = this.db.get(key);
863
+ const streamMeta = this.getMetaIfNotExpired(streamPath);
806
864
  if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
807
865
  const totalSize = messages.reduce((sum, m) => sum + m.data.length, 0);
808
866
  const concatenated = new Uint8Array(totalSize);
@@ -815,8 +873,7 @@ var FileBackedStreamStore = class {
815
873
  return concatenated;
816
874
  }
817
875
  getCurrentOffset(streamPath) {
818
- const key = `stream:${streamPath}`;
819
- const streamMeta = this.db.get(key);
876
+ const streamMeta = this.getMetaIfNotExpired(streamPath);
820
877
  return streamMeta?.currentOffset;
821
878
  }
822
879
  clear() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@durable-streams/server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Node.js reference server implementation for Durable Streams",
5
5
  "author": "Durable Stream contributors",
6
6
  "license": "Apache-2.0",
@@ -39,15 +39,15 @@
39
39
  "dependencies": {
40
40
  "@neophi/sieve-cache": "^1.0.0",
41
41
  "lmdb": "^3.3.0",
42
- "@durable-streams/client": "0.1.1",
43
- "@durable-streams/state": "0.1.1"
42
+ "@durable-streams/client": "0.1.2",
43
+ "@durable-streams/state": "0.1.2"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.0.0",
47
47
  "tsdown": "^0.9.0",
48
48
  "typescript": "^5.0.0",
49
49
  "vitest": "^3.2.4",
50
- "@durable-streams/server-conformance-tests": "0.1.1"
50
+ "@durable-streams/server-conformance-tests": "0.1.3"
51
51
  },
52
52
  "files": [
53
53
  "dist",
package/src/file-store.ts CHANGED
@@ -332,6 +332,50 @@ export class FileBackedStreamStore {
332
332
  }
333
333
  }
334
334
 
335
+ /**
336
+ * Check if a stream is expired based on TTL or Expires-At.
337
+ */
338
+ private isExpired(meta: StreamMetadata): boolean {
339
+ const now = Date.now()
340
+
341
+ // Check absolute expiry time
342
+ if (meta.expiresAt) {
343
+ const expiryTime = new Date(meta.expiresAt).getTime()
344
+ // Treat invalid dates (NaN) as expired (fail closed)
345
+ if (!Number.isFinite(expiryTime) || now >= expiryTime) {
346
+ return true
347
+ }
348
+ }
349
+
350
+ // Check TTL (relative to creation time)
351
+ if (meta.ttlSeconds !== undefined) {
352
+ const expiryTime = meta.createdAt + meta.ttlSeconds * 1000
353
+ if (now >= expiryTime) {
354
+ return true
355
+ }
356
+ }
357
+
358
+ return false
359
+ }
360
+
361
+ /**
362
+ * Get stream metadata, deleting it if expired.
363
+ * Returns undefined if stream doesn't exist or is expired.
364
+ */
365
+ private getMetaIfNotExpired(streamPath: string): StreamMetadata | undefined {
366
+ const key = `stream:${streamPath}`
367
+ const meta = this.db.get(key) as StreamMetadata | undefined
368
+ if (!meta) {
369
+ return undefined
370
+ }
371
+ if (this.isExpired(meta)) {
372
+ // Delete expired stream
373
+ this.delete(streamPath)
374
+ return undefined
375
+ }
376
+ return meta
377
+ }
378
+
335
379
  /**
336
380
  * Close the store, closing all file handles and database.
337
381
  * All data is already fsynced on each append, so no final flush needed.
@@ -354,8 +398,8 @@ export class FileBackedStreamStore {
354
398
  initialData?: Uint8Array
355
399
  } = {}
356
400
  ): Promise<Stream> {
357
- const key = `stream:${streamPath}`
358
- const existing = this.db.get(key) as StreamMetadata | undefined
401
+ // Use getMetaIfNotExpired to treat expired streams as non-existent
402
+ const existing = this.getMetaIfNotExpired(streamPath)
359
403
 
360
404
  if (existing) {
361
405
  // Check if config matches (idempotent create)
@@ -379,6 +423,9 @@ export class FileBackedStreamStore {
379
423
  }
380
424
  }
381
425
 
426
+ // Define key for LMDB operations
427
+ const key = `stream:${streamPath}`
428
+
382
429
  // Initialize metadata
383
430
  const streamMeta: StreamMetadata = {
384
431
  path: streamPath,
@@ -430,14 +477,12 @@ export class FileBackedStreamStore {
430
477
  }
431
478
 
432
479
  get(streamPath: string): Stream | undefined {
433
- const key = `stream:${streamPath}`
434
- const meta = this.db.get(key) as StreamMetadata | undefined
480
+ const meta = this.getMetaIfNotExpired(streamPath)
435
481
  return meta ? this.streamMetaToStream(meta) : undefined
436
482
  }
437
483
 
438
484
  has(streamPath: string): boolean {
439
- const key = `stream:${streamPath}`
440
- return this.db.get(key) !== undefined
485
+ return this.getMetaIfNotExpired(streamPath) !== undefined
441
486
  }
442
487
 
443
488
  delete(streamPath: string): boolean {
@@ -489,8 +534,7 @@ export class FileBackedStreamStore {
489
534
  isInitialCreate?: boolean
490
535
  } = {}
491
536
  ): Promise<StreamMessage | null> {
492
- const key = `stream:${streamPath}`
493
- const streamMeta = this.db.get(key) as StreamMetadata | undefined
537
+ const streamMeta = this.getMetaIfNotExpired(streamPath)
494
538
 
495
539
  if (!streamMeta) {
496
540
  throw new Error(`Stream not found: ${streamPath}`)
@@ -583,6 +627,7 @@ export class FileBackedStreamStore {
583
627
  lastSeq: options.seq ?? streamMeta.lastSeq,
584
628
  totalBytes: streamMeta.totalBytes + processedData.length + 5, // +4 for length, +1 for newline
585
629
  }
630
+ const key = `stream:${streamPath}`
586
631
  this.db.putSync(key, updatedMeta)
587
632
 
588
633
  // 5. Notify long-polls (data is now readable from disk)
@@ -596,8 +641,7 @@ export class FileBackedStreamStore {
596
641
  streamPath: string,
597
642
  offset?: string
598
643
  ): { messages: Array<StreamMessage>; upToDate: boolean } {
599
- const key = `stream:${streamPath}`
600
- const streamMeta = this.db.get(key) as StreamMetadata | undefined
644
+ const streamMeta = this.getMetaIfNotExpired(streamPath)
601
645
 
602
646
  if (!streamMeta) {
603
647
  throw new Error(`Stream not found: ${streamPath}`)
@@ -690,8 +734,7 @@ export class FileBackedStreamStore {
690
734
  offset: string,
691
735
  timeoutMs: number
692
736
  ): Promise<{ messages: Array<StreamMessage>; timedOut: boolean }> {
693
- const key = `stream:${streamPath}`
694
- const streamMeta = this.db.get(key) as StreamMetadata | undefined
737
+ const streamMeta = this.getMetaIfNotExpired(streamPath)
695
738
 
696
739
  if (!streamMeta) {
697
740
  throw new Error(`Stream not found: ${streamPath}`)
@@ -729,13 +772,13 @@ export class FileBackedStreamStore {
729
772
  /**
730
773
  * Format messages for response.
731
774
  * For JSON mode, wraps concatenated data in array brackets.
775
+ * @throws Error if stream doesn't exist or is expired
732
776
  */
733
777
  formatResponse(
734
778
  streamPath: string,
735
779
  messages: Array<StreamMessage>
736
780
  ): Uint8Array {
737
- const key = `stream:${streamPath}`
738
- const streamMeta = this.db.get(key) as StreamMetadata | undefined
781
+ const streamMeta = this.getMetaIfNotExpired(streamPath)
739
782
 
740
783
  if (!streamMeta) {
741
784
  throw new Error(`Stream not found: ${streamPath}`)
@@ -759,8 +802,7 @@ export class FileBackedStreamStore {
759
802
  }
760
803
 
761
804
  getCurrentOffset(streamPath: string): string | undefined {
762
- const key = `stream:${streamPath}`
763
- const streamMeta = this.db.get(key) as StreamMetadata | undefined
805
+ const streamMeta = this.getMetaIfNotExpired(streamPath)
764
806
  return streamMeta?.currentOffset
765
807
  }
766
808
 
package/src/store.ts CHANGED
@@ -83,6 +83,49 @@ export class StreamStore {
83
83
  private streams = new Map<string, Stream>()
84
84
  private pendingLongPolls: Array<PendingLongPoll> = []
85
85
 
86
+ /**
87
+ * Check if a stream is expired based on TTL or Expires-At.
88
+ */
89
+ private isExpired(stream: Stream): boolean {
90
+ const now = Date.now()
91
+
92
+ // Check absolute expiry time
93
+ if (stream.expiresAt) {
94
+ const expiryTime = new Date(stream.expiresAt).getTime()
95
+ // Treat invalid dates (NaN) as expired (fail closed)
96
+ if (!Number.isFinite(expiryTime) || now >= expiryTime) {
97
+ return true
98
+ }
99
+ }
100
+
101
+ // Check TTL (relative to creation time)
102
+ if (stream.ttlSeconds !== undefined) {
103
+ const expiryTime = stream.createdAt + stream.ttlSeconds * 1000
104
+ if (now >= expiryTime) {
105
+ return true
106
+ }
107
+ }
108
+
109
+ return false
110
+ }
111
+
112
+ /**
113
+ * Get a stream, deleting it if expired.
114
+ * Returns undefined if stream doesn't exist or is expired.
115
+ */
116
+ private getIfNotExpired(path: string): Stream | undefined {
117
+ const stream = this.streams.get(path)
118
+ if (!stream) {
119
+ return undefined
120
+ }
121
+ if (this.isExpired(stream)) {
122
+ // Delete expired stream
123
+ this.delete(path)
124
+ return undefined
125
+ }
126
+ return stream
127
+ }
128
+
86
129
  /**
87
130
  * Create a new stream.
88
131
  * @throws Error if stream already exists with different config
@@ -97,7 +140,8 @@ export class StreamStore {
97
140
  initialData?: Uint8Array
98
141
  } = {}
99
142
  ): Stream {
100
- const existing = this.streams.get(path)
143
+ // Use getIfNotExpired to treat expired streams as non-existent
144
+ const existing = this.getIfNotExpired(path)
101
145
  if (existing) {
102
146
  // Check if config matches (idempotent create)
103
147
  const contentTypeMatches =
@@ -140,16 +184,17 @@ export class StreamStore {
140
184
 
141
185
  /**
142
186
  * Get a stream by path.
187
+ * Returns undefined if stream doesn't exist or is expired.
143
188
  */
144
189
  get(path: string): Stream | undefined {
145
- return this.streams.get(path)
190
+ return this.getIfNotExpired(path)
146
191
  }
147
192
 
148
193
  /**
149
- * Check if a stream exists.
194
+ * Check if a stream exists (and is not expired).
150
195
  */
151
196
  has(path: string): boolean {
152
- return this.streams.has(path)
197
+ return this.getIfNotExpired(path) !== undefined
153
198
  }
154
199
 
155
200
  /**
@@ -163,7 +208,7 @@ export class StreamStore {
163
208
 
164
209
  /**
165
210
  * Append data to a stream.
166
- * @throws Error if stream doesn't exist
211
+ * @throws Error if stream doesn't exist or is expired
167
212
  * @throws Error if seq is lower than lastSeq
168
213
  * @throws Error if JSON mode and array is empty
169
214
  */
@@ -172,7 +217,7 @@ export class StreamStore {
172
217
  data: Uint8Array,
173
218
  options: { seq?: string; contentType?: string } = {}
174
219
  ): StreamMessage {
175
- const stream = this.streams.get(path)
220
+ const stream = this.getIfNotExpired(path)
176
221
  if (!stream) {
177
222
  throw new Error(`Stream not found: ${path}`)
178
223
  }
@@ -210,12 +255,13 @@ export class StreamStore {
210
255
 
211
256
  /**
212
257
  * Read messages from a stream starting at the given offset.
258
+ * @throws Error if stream doesn't exist or is expired
213
259
  */
214
260
  read(
215
261
  path: string,
216
262
  offset?: string
217
263
  ): { messages: Array<StreamMessage>; upToDate: boolean } {
218
- const stream = this.streams.get(path)
264
+ const stream = this.getIfNotExpired(path)
219
265
  if (!stream) {
220
266
  throw new Error(`Stream not found: ${path}`)
221
267
  }
@@ -247,9 +293,10 @@ export class StreamStore {
247
293
  /**
248
294
  * Format messages for response.
249
295
  * For JSON mode, wraps concatenated data in array brackets.
296
+ * @throws Error if stream doesn't exist or is expired
250
297
  */
251
298
  formatResponse(path: string, messages: Array<StreamMessage>): Uint8Array {
252
- const stream = this.streams.get(path)
299
+ const stream = this.getIfNotExpired(path)
253
300
  if (!stream) {
254
301
  throw new Error(`Stream not found: ${path}`)
255
302
  }
@@ -273,13 +320,14 @@ export class StreamStore {
273
320
 
274
321
  /**
275
322
  * Wait for new messages (long-poll).
323
+ * @throws Error if stream doesn't exist or is expired
276
324
  */
277
325
  async waitForMessages(
278
326
  path: string,
279
327
  offset: string,
280
328
  timeoutMs: number
281
329
  ): Promise<{ messages: Array<StreamMessage>; timedOut: boolean }> {
282
- const stream = this.streams.get(path)
330
+ const stream = this.getIfNotExpired(path)
283
331
  if (!stream) {
284
332
  throw new Error(`Stream not found: ${path}`)
285
333
  }
@@ -315,9 +363,10 @@ export class StreamStore {
315
363
 
316
364
  /**
317
365
  * Get the current offset for a stream.
366
+ * Returns undefined if stream doesn't exist or is expired.
318
367
  */
319
368
  getCurrentOffset(path: string): string | undefined {
320
- return this.streams.get(path)?.currentOffset
369
+ return this.getIfNotExpired(path)?.currentOffset
321
370
  }
322
371
 
323
372
  /**