@durable-streams/server 0.1.4 → 0.1.6
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.cjs +412 -17
- package/dist/index.d.cts +140 -9
- package/dist/index.d.ts +140 -9
- package/dist/index.js +412 -17
- package/package.json +4 -4
- package/src/file-store.ts +238 -10
- package/src/server.ts +202 -17
- package/src/store.ts +272 -7
- package/src/types.ts +46 -0
package/dist/index.d.cts
CHANGED
|
@@ -56,6 +56,11 @@ interface Stream {
|
|
|
56
56
|
* Timestamp when the stream was created.
|
|
57
57
|
*/
|
|
58
58
|
createdAt: number;
|
|
59
|
+
/**
|
|
60
|
+
* Producer states for idempotent writes.
|
|
61
|
+
* Maps producer ID to their epoch and sequence state.
|
|
62
|
+
*/
|
|
63
|
+
producers?: Map<string, ProducerState>;
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* Event data for stream lifecycle hooks.
|
|
@@ -131,6 +136,49 @@ interface TestServerOptions {
|
|
|
131
136
|
cursorEpoch?: Date;
|
|
132
137
|
}
|
|
133
138
|
/**
|
|
139
|
+
* Producer state for idempotent writes.
|
|
140
|
+
* Tracks epoch and sequence number per producer ID for deduplication.
|
|
141
|
+
*/
|
|
142
|
+
interface ProducerState {
|
|
143
|
+
/**
|
|
144
|
+
* Current epoch for this producer.
|
|
145
|
+
* Client-declared, server-validated monotonically increasing.
|
|
146
|
+
*/
|
|
147
|
+
epoch: number;
|
|
148
|
+
/**
|
|
149
|
+
* Last sequence number received in this epoch.
|
|
150
|
+
*/
|
|
151
|
+
lastSeq: number;
|
|
152
|
+
/**
|
|
153
|
+
* Timestamp when this producer state was last updated.
|
|
154
|
+
* Used for TTL-based cleanup.
|
|
155
|
+
*/
|
|
156
|
+
lastUpdated: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Result of producer validation for append operations.
|
|
160
|
+
* For 'accepted' status, includes proposedState to commit after successful append.
|
|
161
|
+
*/
|
|
162
|
+
type ProducerValidationResult = {
|
|
163
|
+
status: `accepted`;
|
|
164
|
+
isNew: boolean;
|
|
165
|
+
/** State to commit after successful append (deferred mutation) */
|
|
166
|
+
proposedState: ProducerState;
|
|
167
|
+
producerId: string;
|
|
168
|
+
} | {
|
|
169
|
+
status: `duplicate`;
|
|
170
|
+
lastSeq: number;
|
|
171
|
+
} | {
|
|
172
|
+
status: `stale_epoch`;
|
|
173
|
+
currentEpoch: number;
|
|
174
|
+
} | {
|
|
175
|
+
status: `invalid_epoch_seq`;
|
|
176
|
+
} | {
|
|
177
|
+
status: `sequence_gap`;
|
|
178
|
+
expectedSeq: number;
|
|
179
|
+
receivedSeq: number;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
134
182
|
* Pending long-poll request.
|
|
135
183
|
*/
|
|
136
184
|
interface PendingLongPoll {
|
|
@@ -155,10 +203,32 @@ interface PendingLongPoll {
|
|
|
155
203
|
/**
|
|
156
204
|
* In-memory store for durable streams.
|
|
157
205
|
*/
|
|
206
|
+
/**
|
|
207
|
+
* Options for append operations.
|
|
208
|
+
*/
|
|
209
|
+
interface AppendOptions {
|
|
210
|
+
seq?: string;
|
|
211
|
+
contentType?: string;
|
|
212
|
+
producerId?: string;
|
|
213
|
+
producerEpoch?: number;
|
|
214
|
+
producerSeq?: number;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Result of an append operation.
|
|
218
|
+
*/
|
|
219
|
+
interface AppendResult {
|
|
220
|
+
message: StreamMessage | null;
|
|
221
|
+
producerResult?: ProducerValidationResult;
|
|
222
|
+
}
|
|
158
223
|
declare class StreamStore {
|
|
159
224
|
private streams;
|
|
160
225
|
private pendingLongPolls;
|
|
161
226
|
/**
|
|
227
|
+
* Per-producer locks for serializing validation+append operations.
|
|
228
|
+
* Key: "{streamPath}:{producerId}"
|
|
229
|
+
*/
|
|
230
|
+
private producerLocks;
|
|
231
|
+
/**
|
|
162
232
|
* Check if a stream is expired based on TTL or Expires-At.
|
|
163
233
|
*/
|
|
164
234
|
private isExpired;
|
|
@@ -192,15 +262,47 @@ declare class StreamStore {
|
|
|
192
262
|
*/
|
|
193
263
|
delete(path: string): boolean;
|
|
194
264
|
/**
|
|
265
|
+
* Validate producer state WITHOUT mutating.
|
|
266
|
+
* Returns proposed state to commit after successful append.
|
|
267
|
+
* Implements Kafka-style idempotent producer validation.
|
|
268
|
+
*
|
|
269
|
+
* IMPORTANT: This function does NOT mutate producer state. The caller must
|
|
270
|
+
* call commitProducerState() after successful append to apply the mutation.
|
|
271
|
+
* This ensures atomicity: if append fails (e.g., JSON validation), producer
|
|
272
|
+
* state is not incorrectly advanced.
|
|
273
|
+
*/
|
|
274
|
+
private validateProducer;
|
|
275
|
+
/**
|
|
276
|
+
* Commit producer state after successful append.
|
|
277
|
+
* This is the only place where producer state is mutated.
|
|
278
|
+
*/
|
|
279
|
+
private commitProducerState;
|
|
280
|
+
/**
|
|
281
|
+
* Clean up expired producer states from a stream.
|
|
282
|
+
*/
|
|
283
|
+
private cleanupExpiredProducers;
|
|
284
|
+
/**
|
|
285
|
+
* Acquire a lock for serialized producer operations.
|
|
286
|
+
* Returns a release function.
|
|
287
|
+
*/
|
|
288
|
+
private acquireProducerLock;
|
|
289
|
+
/**
|
|
195
290
|
* Append data to a stream.
|
|
196
291
|
* @throws Error if stream doesn't exist or is expired
|
|
197
292
|
* @throws Error if seq is lower than lastSeq
|
|
198
293
|
* @throws Error if JSON mode and array is empty
|
|
199
294
|
*/
|
|
200
|
-
append(path: string, data: Uint8Array, options?:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
295
|
+
append(path: string, data: Uint8Array, options?: AppendOptions): StreamMessage | AppendResult;
|
|
296
|
+
/**
|
|
297
|
+
* Append with producer serialization for concurrent request handling.
|
|
298
|
+
* This ensures that validation+append is atomic per producer.
|
|
299
|
+
*/
|
|
300
|
+
appendWithProducer(path: string, data: Uint8Array, options: AppendOptions): Promise<AppendResult>;
|
|
301
|
+
/**
|
|
302
|
+
* Get the current epoch for a producer on a stream.
|
|
303
|
+
* Returns undefined if the producer doesn't exist or stream not found.
|
|
304
|
+
*/
|
|
305
|
+
getProducerEpoch(path: string, producerId: string): number | undefined;
|
|
204
306
|
/**
|
|
205
307
|
* Read messages from a stream starting at the given offset.
|
|
206
308
|
* @throws Error if stream doesn't exist or is expired
|
|
@@ -261,6 +363,11 @@ declare class FileBackedStreamStore {
|
|
|
261
363
|
private fileHandlePool;
|
|
262
364
|
private pendingLongPolls;
|
|
263
365
|
private dataDir;
|
|
366
|
+
/**
|
|
367
|
+
* Per-producer locks for serializing validation+append operations.
|
|
368
|
+
* Key: "{streamPath}:{producerId}"
|
|
369
|
+
*/
|
|
370
|
+
private producerLocks;
|
|
264
371
|
constructor(options: FileBackedStreamStoreOptions);
|
|
265
372
|
/**
|
|
266
373
|
* Recover streams from disk on startup.
|
|
@@ -277,6 +384,25 @@ declare class FileBackedStreamStore {
|
|
|
277
384
|
*/
|
|
278
385
|
private streamMetaToStream;
|
|
279
386
|
/**
|
|
387
|
+
* Validate producer state WITHOUT mutating.
|
|
388
|
+
* Returns proposed state to commit after successful append.
|
|
389
|
+
*
|
|
390
|
+
* IMPORTANT: This function does NOT mutate producer state. The caller must
|
|
391
|
+
* commit the proposedState after successful append (file write + fsync + LMDB).
|
|
392
|
+
* This ensures atomicity: if any step fails, producer state is not advanced.
|
|
393
|
+
*/
|
|
394
|
+
private validateProducer;
|
|
395
|
+
/**
|
|
396
|
+
* Acquire a lock for serialized producer operations.
|
|
397
|
+
* Returns a release function.
|
|
398
|
+
*/
|
|
399
|
+
private acquireProducerLock;
|
|
400
|
+
/**
|
|
401
|
+
* Get the current epoch for a producer on a stream.
|
|
402
|
+
* Returns undefined if the producer doesn't exist or stream not found.
|
|
403
|
+
*/
|
|
404
|
+
getProducerEpoch(streamPath: string, producerId: string): number | undefined;
|
|
405
|
+
/**
|
|
280
406
|
* Check if a stream is expired based on TTL or Expires-At.
|
|
281
407
|
*/
|
|
282
408
|
private isExpired;
|
|
@@ -299,11 +425,14 @@ declare class FileBackedStreamStore {
|
|
|
299
425
|
get(streamPath: string): Stream | undefined;
|
|
300
426
|
has(streamPath: string): boolean;
|
|
301
427
|
delete(streamPath: string): boolean;
|
|
302
|
-
append(streamPath: string, data: Uint8Array, options?: {
|
|
303
|
-
seq?: string;
|
|
304
|
-
contentType?: string;
|
|
428
|
+
append(streamPath: string, data: Uint8Array, options?: AppendOptions & {
|
|
305
429
|
isInitialCreate?: boolean;
|
|
306
|
-
}): Promise<StreamMessage | null>;
|
|
430
|
+
}): Promise<StreamMessage | AppendResult | null>;
|
|
431
|
+
/**
|
|
432
|
+
* Append with producer serialization for concurrent request handling.
|
|
433
|
+
* This ensures that validation+append is atomic per producer.
|
|
434
|
+
*/
|
|
435
|
+
appendWithProducer(streamPath: string, data: Uint8Array, options: AppendOptions): Promise<AppendResult>;
|
|
307
436
|
read(streamPath: string, offset?: string): {
|
|
308
437
|
messages: Array<StreamMessage>;
|
|
309
438
|
upToDate: boolean;
|
|
@@ -451,7 +580,9 @@ declare class DurableStreamTestServer {
|
|
|
451
580
|
*/
|
|
452
581
|
private handleTestInjectError;
|
|
453
582
|
private readBody;
|
|
454
|
-
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
455
586
|
//#region src/path-encoding.d.ts
|
|
456
587
|
/**
|
|
457
588
|
* Encode a stream path to a filesystem-safe directory name using base64url encoding.
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,11 @@ interface Stream {
|
|
|
56
56
|
* Timestamp when the stream was created.
|
|
57
57
|
*/
|
|
58
58
|
createdAt: number;
|
|
59
|
+
/**
|
|
60
|
+
* Producer states for idempotent writes.
|
|
61
|
+
* Maps producer ID to their epoch and sequence state.
|
|
62
|
+
*/
|
|
63
|
+
producers?: Map<string, ProducerState>;
|
|
59
64
|
}
|
|
60
65
|
/**
|
|
61
66
|
* Event data for stream lifecycle hooks.
|
|
@@ -131,6 +136,49 @@ interface TestServerOptions {
|
|
|
131
136
|
cursorEpoch?: Date;
|
|
132
137
|
}
|
|
133
138
|
/**
|
|
139
|
+
* Producer state for idempotent writes.
|
|
140
|
+
* Tracks epoch and sequence number per producer ID for deduplication.
|
|
141
|
+
*/
|
|
142
|
+
interface ProducerState {
|
|
143
|
+
/**
|
|
144
|
+
* Current epoch for this producer.
|
|
145
|
+
* Client-declared, server-validated monotonically increasing.
|
|
146
|
+
*/
|
|
147
|
+
epoch: number;
|
|
148
|
+
/**
|
|
149
|
+
* Last sequence number received in this epoch.
|
|
150
|
+
*/
|
|
151
|
+
lastSeq: number;
|
|
152
|
+
/**
|
|
153
|
+
* Timestamp when this producer state was last updated.
|
|
154
|
+
* Used for TTL-based cleanup.
|
|
155
|
+
*/
|
|
156
|
+
lastUpdated: number;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Result of producer validation for append operations.
|
|
160
|
+
* For 'accepted' status, includes proposedState to commit after successful append.
|
|
161
|
+
*/
|
|
162
|
+
type ProducerValidationResult = {
|
|
163
|
+
status: `accepted`;
|
|
164
|
+
isNew: boolean;
|
|
165
|
+
/** State to commit after successful append (deferred mutation) */
|
|
166
|
+
proposedState: ProducerState;
|
|
167
|
+
producerId: string;
|
|
168
|
+
} | {
|
|
169
|
+
status: `duplicate`;
|
|
170
|
+
lastSeq: number;
|
|
171
|
+
} | {
|
|
172
|
+
status: `stale_epoch`;
|
|
173
|
+
currentEpoch: number;
|
|
174
|
+
} | {
|
|
175
|
+
status: `invalid_epoch_seq`;
|
|
176
|
+
} | {
|
|
177
|
+
status: `sequence_gap`;
|
|
178
|
+
expectedSeq: number;
|
|
179
|
+
receivedSeq: number;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
134
182
|
* Pending long-poll request.
|
|
135
183
|
*/
|
|
136
184
|
interface PendingLongPoll {
|
|
@@ -155,10 +203,32 @@ interface PendingLongPoll {
|
|
|
155
203
|
/**
|
|
156
204
|
* In-memory store for durable streams.
|
|
157
205
|
*/
|
|
206
|
+
/**
|
|
207
|
+
* Options for append operations.
|
|
208
|
+
*/
|
|
209
|
+
interface AppendOptions {
|
|
210
|
+
seq?: string;
|
|
211
|
+
contentType?: string;
|
|
212
|
+
producerId?: string;
|
|
213
|
+
producerEpoch?: number;
|
|
214
|
+
producerSeq?: number;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Result of an append operation.
|
|
218
|
+
*/
|
|
219
|
+
interface AppendResult {
|
|
220
|
+
message: StreamMessage | null;
|
|
221
|
+
producerResult?: ProducerValidationResult;
|
|
222
|
+
}
|
|
158
223
|
declare class StreamStore {
|
|
159
224
|
private streams;
|
|
160
225
|
private pendingLongPolls;
|
|
161
226
|
/**
|
|
227
|
+
* Per-producer locks for serializing validation+append operations.
|
|
228
|
+
* Key: "{streamPath}:{producerId}"
|
|
229
|
+
*/
|
|
230
|
+
private producerLocks;
|
|
231
|
+
/**
|
|
162
232
|
* Check if a stream is expired based on TTL or Expires-At.
|
|
163
233
|
*/
|
|
164
234
|
private isExpired;
|
|
@@ -192,15 +262,47 @@ declare class StreamStore {
|
|
|
192
262
|
*/
|
|
193
263
|
delete(path: string): boolean;
|
|
194
264
|
/**
|
|
265
|
+
* Validate producer state WITHOUT mutating.
|
|
266
|
+
* Returns proposed state to commit after successful append.
|
|
267
|
+
* Implements Kafka-style idempotent producer validation.
|
|
268
|
+
*
|
|
269
|
+
* IMPORTANT: This function does NOT mutate producer state. The caller must
|
|
270
|
+
* call commitProducerState() after successful append to apply the mutation.
|
|
271
|
+
* This ensures atomicity: if append fails (e.g., JSON validation), producer
|
|
272
|
+
* state is not incorrectly advanced.
|
|
273
|
+
*/
|
|
274
|
+
private validateProducer;
|
|
275
|
+
/**
|
|
276
|
+
* Commit producer state after successful append.
|
|
277
|
+
* This is the only place where producer state is mutated.
|
|
278
|
+
*/
|
|
279
|
+
private commitProducerState;
|
|
280
|
+
/**
|
|
281
|
+
* Clean up expired producer states from a stream.
|
|
282
|
+
*/
|
|
283
|
+
private cleanupExpiredProducers;
|
|
284
|
+
/**
|
|
285
|
+
* Acquire a lock for serialized producer operations.
|
|
286
|
+
* Returns a release function.
|
|
287
|
+
*/
|
|
288
|
+
private acquireProducerLock;
|
|
289
|
+
/**
|
|
195
290
|
* Append data to a stream.
|
|
196
291
|
* @throws Error if stream doesn't exist or is expired
|
|
197
292
|
* @throws Error if seq is lower than lastSeq
|
|
198
293
|
* @throws Error if JSON mode and array is empty
|
|
199
294
|
*/
|
|
200
|
-
append(path: string, data: Uint8Array, options?:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
295
|
+
append(path: string, data: Uint8Array, options?: AppendOptions): StreamMessage | AppendResult;
|
|
296
|
+
/**
|
|
297
|
+
* Append with producer serialization for concurrent request handling.
|
|
298
|
+
* This ensures that validation+append is atomic per producer.
|
|
299
|
+
*/
|
|
300
|
+
appendWithProducer(path: string, data: Uint8Array, options: AppendOptions): Promise<AppendResult>;
|
|
301
|
+
/**
|
|
302
|
+
* Get the current epoch for a producer on a stream.
|
|
303
|
+
* Returns undefined if the producer doesn't exist or stream not found.
|
|
304
|
+
*/
|
|
305
|
+
getProducerEpoch(path: string, producerId: string): number | undefined;
|
|
204
306
|
/**
|
|
205
307
|
* Read messages from a stream starting at the given offset.
|
|
206
308
|
* @throws Error if stream doesn't exist or is expired
|
|
@@ -261,6 +363,11 @@ declare class FileBackedStreamStore {
|
|
|
261
363
|
private fileHandlePool;
|
|
262
364
|
private pendingLongPolls;
|
|
263
365
|
private dataDir;
|
|
366
|
+
/**
|
|
367
|
+
* Per-producer locks for serializing validation+append operations.
|
|
368
|
+
* Key: "{streamPath}:{producerId}"
|
|
369
|
+
*/
|
|
370
|
+
private producerLocks;
|
|
264
371
|
constructor(options: FileBackedStreamStoreOptions);
|
|
265
372
|
/**
|
|
266
373
|
* Recover streams from disk on startup.
|
|
@@ -277,6 +384,25 @@ declare class FileBackedStreamStore {
|
|
|
277
384
|
*/
|
|
278
385
|
private streamMetaToStream;
|
|
279
386
|
/**
|
|
387
|
+
* Validate producer state WITHOUT mutating.
|
|
388
|
+
* Returns proposed state to commit after successful append.
|
|
389
|
+
*
|
|
390
|
+
* IMPORTANT: This function does NOT mutate producer state. The caller must
|
|
391
|
+
* commit the proposedState after successful append (file write + fsync + LMDB).
|
|
392
|
+
* This ensures atomicity: if any step fails, producer state is not advanced.
|
|
393
|
+
*/
|
|
394
|
+
private validateProducer;
|
|
395
|
+
/**
|
|
396
|
+
* Acquire a lock for serialized producer operations.
|
|
397
|
+
* Returns a release function.
|
|
398
|
+
*/
|
|
399
|
+
private acquireProducerLock;
|
|
400
|
+
/**
|
|
401
|
+
* Get the current epoch for a producer on a stream.
|
|
402
|
+
* Returns undefined if the producer doesn't exist or stream not found.
|
|
403
|
+
*/
|
|
404
|
+
getProducerEpoch(streamPath: string, producerId: string): number | undefined;
|
|
405
|
+
/**
|
|
280
406
|
* Check if a stream is expired based on TTL or Expires-At.
|
|
281
407
|
*/
|
|
282
408
|
private isExpired;
|
|
@@ -299,11 +425,14 @@ declare class FileBackedStreamStore {
|
|
|
299
425
|
get(streamPath: string): Stream | undefined;
|
|
300
426
|
has(streamPath: string): boolean;
|
|
301
427
|
delete(streamPath: string): boolean;
|
|
302
|
-
append(streamPath: string, data: Uint8Array, options?: {
|
|
303
|
-
seq?: string;
|
|
304
|
-
contentType?: string;
|
|
428
|
+
append(streamPath: string, data: Uint8Array, options?: AppendOptions & {
|
|
305
429
|
isInitialCreate?: boolean;
|
|
306
|
-
}): Promise<StreamMessage | null>;
|
|
430
|
+
}): Promise<StreamMessage | AppendResult | null>;
|
|
431
|
+
/**
|
|
432
|
+
* Append with producer serialization for concurrent request handling.
|
|
433
|
+
* This ensures that validation+append is atomic per producer.
|
|
434
|
+
*/
|
|
435
|
+
appendWithProducer(streamPath: string, data: Uint8Array, options: AppendOptions): Promise<AppendResult>;
|
|
307
436
|
read(streamPath: string, offset?: string): {
|
|
308
437
|
messages: Array<StreamMessage>;
|
|
309
438
|
upToDate: boolean;
|
|
@@ -451,7 +580,9 @@ declare class DurableStreamTestServer {
|
|
|
451
580
|
*/
|
|
452
581
|
private handleTestInjectError;
|
|
453
582
|
private readBody;
|
|
454
|
-
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
455
586
|
//#region src/path-encoding.d.ts
|
|
456
587
|
/**
|
|
457
588
|
* Encode a stream path to a filesystem-safe directory name using base64url encoding.
|