@durable-streams/server 0.2.0 → 0.2.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.cjs +490 -69
- package/dist/index.d.cts +76 -1
- package/dist/index.d.ts +76 -1
- package/dist/index.js +490 -69
- package/package.json +4 -4
- package/src/file-store.ts +259 -11
- package/src/server.ts +357 -54
- package/src/store.ts +201 -7
- package/src/types.ts +17 -0
package/dist/index.js
CHANGED
|
@@ -109,7 +109,8 @@ var StreamStore = class {
|
|
|
109
109
|
const contentTypeMatches = (normalizeContentType(options.contentType) || `application/octet-stream`) === (normalizeContentType(existing.contentType) || `application/octet-stream`);
|
|
110
110
|
const ttlMatches = options.ttlSeconds === existing.ttlSeconds;
|
|
111
111
|
const expiresMatches = options.expiresAt === existing.expiresAt;
|
|
112
|
-
|
|
112
|
+
const closedMatches = (options.closed ?? false) === (existing.closed ?? false);
|
|
113
|
+
if (contentTypeMatches && ttlMatches && expiresMatches && closedMatches) return existing;
|
|
113
114
|
else throw new Error(`Stream already exists with different configuration: ${path$2}`);
|
|
114
115
|
}
|
|
115
116
|
const stream = {
|
|
@@ -119,7 +120,8 @@ var StreamStore = class {
|
|
|
119
120
|
currentOffset: `0000000000000000_0000000000000000`,
|
|
120
121
|
ttlSeconds: options.ttlSeconds,
|
|
121
122
|
expiresAt: options.expiresAt,
|
|
122
|
-
createdAt: Date.now()
|
|
123
|
+
createdAt: Date.now(),
|
|
124
|
+
closed: options.closed ?? false
|
|
123
125
|
};
|
|
124
126
|
if (options.initialData && options.initialData.length > 0) this.appendToStream(stream, options.initialData, true);
|
|
125
127
|
this.streams.set(path$2, stream);
|
|
@@ -256,6 +258,20 @@ var StreamStore = class {
|
|
|
256
258
|
append(path$2, data, options = {}) {
|
|
257
259
|
const stream = this.getIfNotExpired(path$2);
|
|
258
260
|
if (!stream) throw new Error(`Stream not found: ${path$2}`);
|
|
261
|
+
if (stream.closed) {
|
|
262
|
+
if (options.producerId && stream.closedBy && stream.closedBy.producerId === options.producerId && stream.closedBy.epoch === options.producerEpoch && stream.closedBy.seq === options.producerSeq) return {
|
|
263
|
+
message: null,
|
|
264
|
+
streamClosed: true,
|
|
265
|
+
producerResult: {
|
|
266
|
+
status: `duplicate`,
|
|
267
|
+
lastSeq: options.producerSeq
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
message: null,
|
|
272
|
+
streamClosed: true
|
|
273
|
+
};
|
|
274
|
+
}
|
|
259
275
|
if (options.contentType && stream.contentType) {
|
|
260
276
|
const providedType = normalizeContentType(options.contentType);
|
|
261
277
|
const streamType = normalizeContentType(stream.contentType);
|
|
@@ -275,10 +291,20 @@ var StreamStore = class {
|
|
|
275
291
|
const message = this.appendToStream(stream, data);
|
|
276
292
|
if (producerResult) this.commitProducerState(stream, producerResult);
|
|
277
293
|
if (options.seq !== void 0) stream.lastSeq = options.seq;
|
|
294
|
+
if (options.close) {
|
|
295
|
+
stream.closed = true;
|
|
296
|
+
if (options.producerId !== void 0) stream.closedBy = {
|
|
297
|
+
producerId: options.producerId,
|
|
298
|
+
epoch: options.producerEpoch,
|
|
299
|
+
seq: options.producerSeq
|
|
300
|
+
};
|
|
301
|
+
this.notifyLongPollsClosed(path$2);
|
|
302
|
+
}
|
|
278
303
|
this.notifyLongPolls(path$2);
|
|
279
|
-
if (producerResult) return {
|
|
304
|
+
if (producerResult || options.close) return {
|
|
280
305
|
message,
|
|
281
|
-
producerResult
|
|
306
|
+
producerResult,
|
|
307
|
+
streamClosed: options.close
|
|
282
308
|
};
|
|
283
309
|
return message;
|
|
284
310
|
}
|
|
@@ -302,6 +328,69 @@ var StreamStore = class {
|
|
|
302
328
|
}
|
|
303
329
|
}
|
|
304
330
|
/**
|
|
331
|
+
* Close a stream without appending data.
|
|
332
|
+
* @returns The final offset, or null if stream doesn't exist
|
|
333
|
+
*/
|
|
334
|
+
closeStream(path$2) {
|
|
335
|
+
const stream = this.getIfNotExpired(path$2);
|
|
336
|
+
if (!stream) return null;
|
|
337
|
+
const alreadyClosed = stream.closed ?? false;
|
|
338
|
+
stream.closed = true;
|
|
339
|
+
this.notifyLongPollsClosed(path$2);
|
|
340
|
+
return {
|
|
341
|
+
finalOffset: stream.currentOffset,
|
|
342
|
+
alreadyClosed
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Close a stream with producer headers for idempotent close-only operations.
|
|
347
|
+
* Participates in producer sequencing for deduplication.
|
|
348
|
+
* @returns The final offset and producer result, or null if stream doesn't exist
|
|
349
|
+
*/
|
|
350
|
+
async closeStreamWithProducer(path$2, options) {
|
|
351
|
+
const releaseLock = await this.acquireProducerLock(path$2, options.producerId);
|
|
352
|
+
try {
|
|
353
|
+
const stream = this.getIfNotExpired(path$2);
|
|
354
|
+
if (!stream) return null;
|
|
355
|
+
if (stream.closed) {
|
|
356
|
+
if (stream.closedBy && stream.closedBy.producerId === options.producerId && stream.closedBy.epoch === options.producerEpoch && stream.closedBy.seq === options.producerSeq) return {
|
|
357
|
+
finalOffset: stream.currentOffset,
|
|
358
|
+
alreadyClosed: true,
|
|
359
|
+
producerResult: {
|
|
360
|
+
status: `duplicate`,
|
|
361
|
+
lastSeq: options.producerSeq
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
finalOffset: stream.currentOffset,
|
|
366
|
+
alreadyClosed: true,
|
|
367
|
+
producerResult: { status: `stream_closed` }
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const producerResult = this.validateProducer(stream, options.producerId, options.producerEpoch, options.producerSeq);
|
|
371
|
+
if (producerResult.status !== `accepted`) return {
|
|
372
|
+
finalOffset: stream.currentOffset,
|
|
373
|
+
alreadyClosed: stream.closed ?? false,
|
|
374
|
+
producerResult
|
|
375
|
+
};
|
|
376
|
+
this.commitProducerState(stream, producerResult);
|
|
377
|
+
stream.closed = true;
|
|
378
|
+
stream.closedBy = {
|
|
379
|
+
producerId: options.producerId,
|
|
380
|
+
epoch: options.producerEpoch,
|
|
381
|
+
seq: options.producerSeq
|
|
382
|
+
};
|
|
383
|
+
this.notifyLongPollsClosed(path$2);
|
|
384
|
+
return {
|
|
385
|
+
finalOffset: stream.currentOffset,
|
|
386
|
+
alreadyClosed: false,
|
|
387
|
+
producerResult
|
|
388
|
+
};
|
|
389
|
+
} finally {
|
|
390
|
+
releaseLock();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
305
394
|
* Get the current epoch for a producer on a stream.
|
|
306
395
|
* Returns undefined if the producer doesn't exist or stream not found.
|
|
307
396
|
*/
|
|
@@ -361,12 +450,20 @@ var StreamStore = class {
|
|
|
361
450
|
messages,
|
|
362
451
|
timedOut: false
|
|
363
452
|
};
|
|
453
|
+
if (stream.closed && offset === stream.currentOffset) return {
|
|
454
|
+
messages: [],
|
|
455
|
+
timedOut: false,
|
|
456
|
+
streamClosed: true
|
|
457
|
+
};
|
|
364
458
|
return new Promise((resolve) => {
|
|
365
459
|
const timeoutId = setTimeout(() => {
|
|
366
460
|
this.removePendingLongPoll(pending);
|
|
461
|
+
const currentStream = this.getIfNotExpired(path$2);
|
|
462
|
+
const streamClosed = currentStream?.closed ?? false;
|
|
367
463
|
resolve({
|
|
368
464
|
messages: [],
|
|
369
|
-
timedOut: true
|
|
465
|
+
timedOut: true,
|
|
466
|
+
streamClosed
|
|
370
467
|
});
|
|
371
468
|
}, timeoutMs);
|
|
372
469
|
const pending = {
|
|
@@ -375,9 +472,12 @@ var StreamStore = class {
|
|
|
375
472
|
resolve: (msgs) => {
|
|
376
473
|
clearTimeout(timeoutId);
|
|
377
474
|
this.removePendingLongPoll(pending);
|
|
475
|
+
const currentStream = this.getIfNotExpired(path$2);
|
|
476
|
+
const streamClosed = currentStream?.closed && msgs.length === 0 ? true : void 0;
|
|
378
477
|
resolve({
|
|
379
478
|
messages: msgs,
|
|
380
|
-
timedOut: false
|
|
479
|
+
timedOut: false,
|
|
480
|
+
streamClosed
|
|
381
481
|
});
|
|
382
482
|
},
|
|
383
483
|
timeoutId
|
|
@@ -450,6 +550,14 @@ var StreamStore = class {
|
|
|
450
550
|
if (messages.length > 0) pending.resolve(messages);
|
|
451
551
|
}
|
|
452
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* Notify pending long-polls that a stream has been closed.
|
|
555
|
+
* They should wake up immediately and return Stream-Closed: true.
|
|
556
|
+
*/
|
|
557
|
+
notifyLongPollsClosed(path$2) {
|
|
558
|
+
const toNotify = this.pendingLongPolls.filter((p) => p.path === path$2);
|
|
559
|
+
for (const pending of toNotify) pending.resolve([]);
|
|
560
|
+
}
|
|
453
561
|
cancelLongPollsForStream(path$2) {
|
|
454
562
|
const toCancel = this.pendingLongPolls.filter((p) => p.path === path$2);
|
|
455
563
|
for (const pending of toCancel) {
|
|
@@ -771,7 +879,9 @@ var FileBackedStreamStore = class {
|
|
|
771
879
|
ttlSeconds: meta.ttlSeconds,
|
|
772
880
|
expiresAt: meta.expiresAt,
|
|
773
881
|
createdAt: meta.createdAt,
|
|
774
|
-
producers
|
|
882
|
+
producers,
|
|
883
|
+
closed: meta.closed,
|
|
884
|
+
closedBy: meta.closedBy
|
|
775
885
|
};
|
|
776
886
|
}
|
|
777
887
|
/**
|
|
@@ -910,7 +1020,8 @@ var FileBackedStreamStore = class {
|
|
|
910
1020
|
const contentTypeMatches = normalizeMimeType(options.contentType) === normalizeMimeType(existing.contentType);
|
|
911
1021
|
const ttlMatches = options.ttlSeconds === existing.ttlSeconds;
|
|
912
1022
|
const expiresMatches = options.expiresAt === existing.expiresAt;
|
|
913
|
-
|
|
1023
|
+
const closedMatches = (options.closed ?? false) === (existing.closed ?? false);
|
|
1024
|
+
if (contentTypeMatches && ttlMatches && expiresMatches && closedMatches) return this.streamMetaToStream(existing);
|
|
914
1025
|
else throw new Error(`Stream already exists with different configuration: ${streamPath}`);
|
|
915
1026
|
}
|
|
916
1027
|
const key = `stream:${streamPath}`;
|
|
@@ -924,7 +1035,8 @@ var FileBackedStreamStore = class {
|
|
|
924
1035
|
createdAt: Date.now(),
|
|
925
1036
|
segmentCount: 1,
|
|
926
1037
|
totalBytes: 0,
|
|
927
|
-
directoryName: generateUniqueDirectoryName(streamPath)
|
|
1038
|
+
directoryName: generateUniqueDirectoryName(streamPath),
|
|
1039
|
+
closed: false
|
|
928
1040
|
};
|
|
929
1041
|
const streamDir = path.join(this.dataDir, `streams`, streamMeta.directoryName);
|
|
930
1042
|
try {
|
|
@@ -936,15 +1048,17 @@ var FileBackedStreamStore = class {
|
|
|
936
1048
|
throw err;
|
|
937
1049
|
}
|
|
938
1050
|
this.db.putSync(key, streamMeta);
|
|
939
|
-
if (options.initialData && options.initialData.length > 0) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
const
|
|
945
|
-
|
|
1051
|
+
if (options.initialData && options.initialData.length > 0) await this.append(streamPath, options.initialData, {
|
|
1052
|
+
contentType: options.contentType,
|
|
1053
|
+
isInitialCreate: true
|
|
1054
|
+
});
|
|
1055
|
+
if (options.closed) {
|
|
1056
|
+
const updatedMeta = this.db.get(key);
|
|
1057
|
+
updatedMeta.closed = true;
|
|
1058
|
+
this.db.putSync(key, updatedMeta);
|
|
946
1059
|
}
|
|
947
|
-
|
|
1060
|
+
const updated = this.db.get(key);
|
|
1061
|
+
return this.streamMetaToStream(updated);
|
|
948
1062
|
}
|
|
949
1063
|
get(streamPath) {
|
|
950
1064
|
const meta = this.getMetaIfNotExpired(streamPath);
|
|
@@ -971,6 +1085,20 @@ var FileBackedStreamStore = class {
|
|
|
971
1085
|
async append(streamPath, data, options = {}) {
|
|
972
1086
|
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
973
1087
|
if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
|
|
1088
|
+
if (streamMeta.closed) {
|
|
1089
|
+
if (options.producerId && streamMeta.closedBy && streamMeta.closedBy.producerId === options.producerId && streamMeta.closedBy.epoch === options.producerEpoch && streamMeta.closedBy.seq === options.producerSeq) return {
|
|
1090
|
+
message: null,
|
|
1091
|
+
streamClosed: true,
|
|
1092
|
+
producerResult: {
|
|
1093
|
+
status: `duplicate`,
|
|
1094
|
+
lastSeq: options.producerSeq
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
return {
|
|
1098
|
+
message: null,
|
|
1099
|
+
streamClosed: true
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
974
1102
|
if (options.contentType && streamMeta.contentType) {
|
|
975
1103
|
const providedType = normalizeContentType(options.contentType);
|
|
976
1104
|
const streamType = normalizeContentType(streamMeta.contentType);
|
|
@@ -1021,19 +1149,29 @@ var FileBackedStreamStore = class {
|
|
|
1021
1149
|
await this.fileHandlePool.fsyncFile(segmentPath);
|
|
1022
1150
|
const updatedProducers = { ...streamMeta.producers };
|
|
1023
1151
|
if (producerResult && producerResult.status === `accepted`) updatedProducers[producerResult.producerId] = producerResult.proposedState;
|
|
1152
|
+
let closedBy = void 0;
|
|
1153
|
+
if (options.close && options.producerId) closedBy = {
|
|
1154
|
+
producerId: options.producerId,
|
|
1155
|
+
epoch: options.producerEpoch,
|
|
1156
|
+
seq: options.producerSeq
|
|
1157
|
+
};
|
|
1024
1158
|
const updatedMeta = {
|
|
1025
1159
|
...streamMeta,
|
|
1026
1160
|
currentOffset: newOffset,
|
|
1027
1161
|
lastSeq: options.seq ?? streamMeta.lastSeq,
|
|
1028
1162
|
totalBytes: streamMeta.totalBytes + processedData.length + 5,
|
|
1029
|
-
producers: updatedProducers
|
|
1163
|
+
producers: updatedProducers,
|
|
1164
|
+
closed: options.close ? true : streamMeta.closed,
|
|
1165
|
+
closedBy: closedBy ?? streamMeta.closedBy
|
|
1030
1166
|
};
|
|
1031
1167
|
const key = `stream:${streamPath}`;
|
|
1032
1168
|
this.db.putSync(key, updatedMeta);
|
|
1033
1169
|
this.notifyLongPolls(streamPath);
|
|
1034
|
-
if (
|
|
1170
|
+
if (options.close) this.notifyLongPollsClosed(streamPath);
|
|
1171
|
+
if (producerResult || options.close) return {
|
|
1035
1172
|
message,
|
|
1036
|
-
producerResult
|
|
1173
|
+
producerResult,
|
|
1174
|
+
streamClosed: options.close
|
|
1037
1175
|
};
|
|
1038
1176
|
return message;
|
|
1039
1177
|
}
|
|
@@ -1056,6 +1194,81 @@ var FileBackedStreamStore = class {
|
|
|
1056
1194
|
releaseLock();
|
|
1057
1195
|
}
|
|
1058
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Close a stream without appending data.
|
|
1199
|
+
* @returns The final offset, or null if stream doesn't exist
|
|
1200
|
+
*/
|
|
1201
|
+
closeStream(streamPath) {
|
|
1202
|
+
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1203
|
+
if (!streamMeta) return null;
|
|
1204
|
+
const alreadyClosed = streamMeta.closed ?? false;
|
|
1205
|
+
const key = `stream:${streamPath}`;
|
|
1206
|
+
const updatedMeta = {
|
|
1207
|
+
...streamMeta,
|
|
1208
|
+
closed: true
|
|
1209
|
+
};
|
|
1210
|
+
this.db.putSync(key, updatedMeta);
|
|
1211
|
+
this.notifyLongPollsClosed(streamPath);
|
|
1212
|
+
return {
|
|
1213
|
+
finalOffset: streamMeta.currentOffset,
|
|
1214
|
+
alreadyClosed
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Close a stream with producer headers for idempotent close-only operations.
|
|
1219
|
+
* Participates in producer sequencing for deduplication.
|
|
1220
|
+
* @returns The final offset and producer result, or null if stream doesn't exist
|
|
1221
|
+
*/
|
|
1222
|
+
async closeStreamWithProducer(streamPath, options) {
|
|
1223
|
+
const releaseLock = await this.acquireProducerLock(streamPath, options.producerId);
|
|
1224
|
+
try {
|
|
1225
|
+
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1226
|
+
if (!streamMeta) return null;
|
|
1227
|
+
if (streamMeta.closed) {
|
|
1228
|
+
if (streamMeta.closedBy && streamMeta.closedBy.producerId === options.producerId && streamMeta.closedBy.epoch === options.producerEpoch && streamMeta.closedBy.seq === options.producerSeq) return {
|
|
1229
|
+
finalOffset: streamMeta.currentOffset,
|
|
1230
|
+
alreadyClosed: true,
|
|
1231
|
+
producerResult: {
|
|
1232
|
+
status: `duplicate`,
|
|
1233
|
+
lastSeq: options.producerSeq
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
return {
|
|
1237
|
+
finalOffset: streamMeta.currentOffset,
|
|
1238
|
+
alreadyClosed: true,
|
|
1239
|
+
producerResult: { status: `stream_closed` }
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
const producerResult = this.validateProducer(streamMeta, options.producerId, options.producerEpoch, options.producerSeq);
|
|
1243
|
+
if (producerResult.status !== `accepted`) return {
|
|
1244
|
+
finalOffset: streamMeta.currentOffset,
|
|
1245
|
+
alreadyClosed: streamMeta.closed ?? false,
|
|
1246
|
+
producerResult
|
|
1247
|
+
};
|
|
1248
|
+
const key = `stream:${streamPath}`;
|
|
1249
|
+
const updatedProducers = { ...streamMeta.producers };
|
|
1250
|
+
updatedProducers[producerResult.producerId] = producerResult.proposedState;
|
|
1251
|
+
const updatedMeta = {
|
|
1252
|
+
...streamMeta,
|
|
1253
|
+
closed: true,
|
|
1254
|
+
closedBy: {
|
|
1255
|
+
producerId: options.producerId,
|
|
1256
|
+
epoch: options.producerEpoch,
|
|
1257
|
+
seq: options.producerSeq
|
|
1258
|
+
},
|
|
1259
|
+
producers: updatedProducers
|
|
1260
|
+
};
|
|
1261
|
+
this.db.putSync(key, updatedMeta);
|
|
1262
|
+
this.notifyLongPollsClosed(streamPath);
|
|
1263
|
+
return {
|
|
1264
|
+
finalOffset: streamMeta.currentOffset,
|
|
1265
|
+
alreadyClosed: false,
|
|
1266
|
+
producerResult
|
|
1267
|
+
};
|
|
1268
|
+
} finally {
|
|
1269
|
+
releaseLock();
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1059
1272
|
read(streamPath, offset) {
|
|
1060
1273
|
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1061
1274
|
if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
|
|
@@ -1111,17 +1324,30 @@ var FileBackedStreamStore = class {
|
|
|
1111
1324
|
async waitForMessages(streamPath, offset, timeoutMs) {
|
|
1112
1325
|
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1113
1326
|
if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
|
|
1327
|
+
if (streamMeta.closed && offset === streamMeta.currentOffset) return {
|
|
1328
|
+
messages: [],
|
|
1329
|
+
timedOut: false,
|
|
1330
|
+
streamClosed: true
|
|
1331
|
+
};
|
|
1114
1332
|
const { messages } = this.read(streamPath, offset);
|
|
1115
1333
|
if (messages.length > 0) return {
|
|
1116
1334
|
messages,
|
|
1117
|
-
timedOut: false
|
|
1335
|
+
timedOut: false,
|
|
1336
|
+
streamClosed: streamMeta.closed
|
|
1337
|
+
};
|
|
1338
|
+
if (streamMeta.closed) return {
|
|
1339
|
+
messages: [],
|
|
1340
|
+
timedOut: false,
|
|
1341
|
+
streamClosed: true
|
|
1118
1342
|
};
|
|
1119
1343
|
return new Promise((resolve) => {
|
|
1120
1344
|
const timeoutId = setTimeout(() => {
|
|
1121
1345
|
this.removePendingLongPoll(pending);
|
|
1346
|
+
const currentMeta = this.getMetaIfNotExpired(streamPath);
|
|
1122
1347
|
resolve({
|
|
1123
1348
|
messages: [],
|
|
1124
|
-
timedOut: true
|
|
1349
|
+
timedOut: true,
|
|
1350
|
+
streamClosed: currentMeta?.closed
|
|
1125
1351
|
});
|
|
1126
1352
|
}, timeoutMs);
|
|
1127
1353
|
const pending = {
|
|
@@ -1130,9 +1356,11 @@ var FileBackedStreamStore = class {
|
|
|
1130
1356
|
resolve: (msgs) => {
|
|
1131
1357
|
clearTimeout(timeoutId);
|
|
1132
1358
|
this.removePendingLongPoll(pending);
|
|
1359
|
+
const currentMeta = this.getMetaIfNotExpired(streamPath);
|
|
1133
1360
|
resolve({
|
|
1134
1361
|
messages: msgs,
|
|
1135
|
-
timedOut: false
|
|
1362
|
+
timedOut: false,
|
|
1363
|
+
streamClosed: currentMeta?.closed
|
|
1136
1364
|
});
|
|
1137
1365
|
},
|
|
1138
1366
|
timeoutId
|
|
@@ -1205,6 +1433,14 @@ var FileBackedStreamStore = class {
|
|
|
1205
1433
|
if (messages.length > 0) pending.resolve(messages);
|
|
1206
1434
|
}
|
|
1207
1435
|
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Notify pending long-polls that a stream has been closed.
|
|
1438
|
+
* They should wake up immediately and return Stream-Closed: true.
|
|
1439
|
+
*/
|
|
1440
|
+
notifyLongPollsClosed(streamPath) {
|
|
1441
|
+
const toNotify = this.pendingLongPolls.filter((p) => p.path === streamPath);
|
|
1442
|
+
for (const pending of toNotify) pending.resolve([]);
|
|
1443
|
+
}
|
|
1208
1444
|
cancelLongPollsForStream(streamPath) {
|
|
1209
1445
|
const toCancel = this.pendingLongPolls.filter((p) => p.path === streamPath);
|
|
1210
1446
|
for (const pending of toCancel) {
|
|
@@ -1329,6 +1565,7 @@ const STREAM_UP_TO_DATE_HEADER = `Stream-Up-To-Date`;
|
|
|
1329
1565
|
const STREAM_SEQ_HEADER = `Stream-Seq`;
|
|
1330
1566
|
const STREAM_TTL_HEADER = `Stream-TTL`;
|
|
1331
1567
|
const STREAM_EXPIRES_AT_HEADER = `Stream-Expires-At`;
|
|
1568
|
+
const STREAM_SSE_DATA_ENCODING_HEADER = `Stream-SSE-Data-Encoding`;
|
|
1332
1569
|
const PRODUCER_ID_HEADER = `Producer-Id`;
|
|
1333
1570
|
const PRODUCER_EPOCH_HEADER = `Producer-Epoch`;
|
|
1334
1571
|
const PRODUCER_SEQ_HEADER = `Producer-Seq`;
|
|
@@ -1337,6 +1574,8 @@ const PRODUCER_RECEIVED_SEQ_HEADER = `Producer-Received-Seq`;
|
|
|
1337
1574
|
const SSE_OFFSET_FIELD = `streamNextOffset`;
|
|
1338
1575
|
const SSE_CURSOR_FIELD = `streamCursor`;
|
|
1339
1576
|
const SSE_UP_TO_DATE_FIELD = `upToDate`;
|
|
1577
|
+
const SSE_CLOSED_FIELD = `streamClosed`;
|
|
1578
|
+
const STREAM_CLOSED_HEADER = `Stream-Closed`;
|
|
1340
1579
|
const OFFSET_QUERY_PARAM = `offset`;
|
|
1341
1580
|
const LIVE_QUERY_PARAM = `live`;
|
|
1342
1581
|
const CURSOR_QUERY_PARAM = `cursor`;
|
|
@@ -1551,8 +1790,8 @@ var DurableStreamTestServer = class {
|
|
|
1551
1790
|
const method = req.method?.toUpperCase();
|
|
1552
1791
|
res.setHeader(`access-control-allow-origin`, `*`);
|
|
1553
1792
|
res.setHeader(`access-control-allow-methods`, `GET, POST, PUT, DELETE, HEAD, OPTIONS`);
|
|
1554
|
-
res.setHeader(`access-control-allow-headers`, `content-type, authorization, Stream-Seq, Stream-TTL, Stream-Expires-At, Producer-Id, Producer-Epoch, Producer-Seq`);
|
|
1555
|
-
res.setHeader(`access-control-expose-headers`, `Stream-Next-Offset, Stream-Cursor, Stream-Up-To-Date, Producer-Epoch, Producer-Seq, Producer-Expected-Seq, Producer-Received-Seq, etag, content-type, content-encoding, vary`);
|
|
1793
|
+
res.setHeader(`access-control-allow-headers`, `content-type, authorization, Stream-Seq, Stream-TTL, Stream-Expires-At, Stream-Closed, Producer-Id, Producer-Epoch, Producer-Seq`);
|
|
1794
|
+
res.setHeader(`access-control-expose-headers`, `Stream-Next-Offset, Stream-Cursor, Stream-Up-To-Date, Stream-Closed, Producer-Epoch, Producer-Seq, Producer-Expected-Seq, Producer-Received-Seq, etag, content-type, content-encoding, vary`);
|
|
1556
1795
|
res.setHeader(`x-content-type-options`, `nosniff`);
|
|
1557
1796
|
res.setHeader(`cross-origin-resource-policy`, `cross-origin`);
|
|
1558
1797
|
if (method === `OPTIONS`) {
|
|
@@ -1632,6 +1871,8 @@ var DurableStreamTestServer = class {
|
|
|
1632
1871
|
if (!contentType || contentType.trim() === `` || !/^[\w-]+\/[\w-]+/.test(contentType)) contentType = `application/octet-stream`;
|
|
1633
1872
|
const ttlHeader = req.headers[STREAM_TTL_HEADER.toLowerCase()];
|
|
1634
1873
|
const expiresAtHeader = req.headers[STREAM_EXPIRES_AT_HEADER.toLowerCase()];
|
|
1874
|
+
const closedHeader = req.headers[STREAM_CLOSED_HEADER.toLowerCase()];
|
|
1875
|
+
const createClosed = closedHeader === `true`;
|
|
1635
1876
|
if (ttlHeader && expiresAtHeader) {
|
|
1636
1877
|
res.writeHead(400, { "content-type": `text/plain` });
|
|
1637
1878
|
res.end(`Cannot specify both Stream-TTL and Stream-Expires-At`);
|
|
@@ -1666,7 +1907,8 @@ var DurableStreamTestServer = class {
|
|
|
1666
1907
|
contentType,
|
|
1667
1908
|
ttlSeconds,
|
|
1668
1909
|
expiresAt: expiresAtHeader,
|
|
1669
|
-
initialData: body.length > 0 ? body : void 0
|
|
1910
|
+
initialData: body.length > 0 ? body : void 0,
|
|
1911
|
+
closed: createClosed
|
|
1670
1912
|
}));
|
|
1671
1913
|
const stream = this.store.get(path$2);
|
|
1672
1914
|
if (isNew && this.options.onStreamCreated) await Promise.resolve(this.options.onStreamCreated({
|
|
@@ -1680,6 +1922,7 @@ var DurableStreamTestServer = class {
|
|
|
1680
1922
|
[STREAM_OFFSET_HEADER]: stream.currentOffset
|
|
1681
1923
|
};
|
|
1682
1924
|
if (isNew) headers[`location`] = `${this._url}${path$2}`;
|
|
1925
|
+
if (stream.closed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1683
1926
|
res.writeHead(isNew ? 201 : 200, headers);
|
|
1684
1927
|
res.end();
|
|
1685
1928
|
}
|
|
@@ -1698,7 +1941,9 @@ var DurableStreamTestServer = class {
|
|
|
1698
1941
|
"cache-control": `no-store`
|
|
1699
1942
|
};
|
|
1700
1943
|
if (stream.contentType) headers[`content-type`] = stream.contentType;
|
|
1701
|
-
headers[
|
|
1944
|
+
if (stream.closed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1945
|
+
const closedSuffix = stream.closed ? `:c` : ``;
|
|
1946
|
+
headers[`etag`] = `"${Buffer.from(path$2).toString(`base64`)}:-1:${stream.currentOffset}${closedSuffix}"`;
|
|
1702
1947
|
res.writeHead(200, headers);
|
|
1703
1948
|
res.end();
|
|
1704
1949
|
}
|
|
@@ -1739,9 +1984,15 @@ var DurableStreamTestServer = class {
|
|
|
1739
1984
|
res.end(`${live === `sse` ? `SSE` : `Long-poll`} requires offset parameter`);
|
|
1740
1985
|
return;
|
|
1741
1986
|
}
|
|
1987
|
+
let useBase64 = false;
|
|
1988
|
+
if (live === `sse`) {
|
|
1989
|
+
const ct = stream.contentType?.toLowerCase().split(`;`)[0]?.trim() ?? ``;
|
|
1990
|
+
const isTextCompatible = ct.startsWith(`text/`) || ct === `application/json`;
|
|
1991
|
+
useBase64 = !isTextCompatible;
|
|
1992
|
+
}
|
|
1742
1993
|
if (live === `sse`) {
|
|
1743
1994
|
const sseOffset = offset === `now` ? stream.currentOffset : offset;
|
|
1744
|
-
await this.handleSSE(path$2, stream, sseOffset, cursor, res);
|
|
1995
|
+
await this.handleSSE(path$2, stream, sseOffset, cursor, useBase64, res);
|
|
1745
1996
|
return;
|
|
1746
1997
|
}
|
|
1747
1998
|
const effectiveOffset = offset === `now` ? stream.currentOffset : offset;
|
|
@@ -1752,6 +2003,7 @@ var DurableStreamTestServer = class {
|
|
|
1752
2003
|
[`cache-control`]: `no-store`
|
|
1753
2004
|
};
|
|
1754
2005
|
if (stream.contentType) headers$1[`content-type`] = stream.contentType;
|
|
2006
|
+
if (stream.closed) headers$1[STREAM_CLOSED_HEADER] = `true`;
|
|
1755
2007
|
const isJsonMode = stream.contentType?.includes(`application/json`);
|
|
1756
2008
|
const responseBody = isJsonMode ? `[]` : ``;
|
|
1757
2009
|
res.writeHead(200, headers$1);
|
|
@@ -1761,17 +2013,40 @@ var DurableStreamTestServer = class {
|
|
|
1761
2013
|
let { messages, upToDate } = this.store.read(path$2, effectiveOffset);
|
|
1762
2014
|
const clientIsCaughtUp = effectiveOffset && effectiveOffset === stream.currentOffset || offset === `now`;
|
|
1763
2015
|
if (live === `long-poll` && clientIsCaughtUp && messages.length === 0) {
|
|
2016
|
+
if (stream.closed) {
|
|
2017
|
+
res.writeHead(204, {
|
|
2018
|
+
[STREAM_OFFSET_HEADER]: stream.currentOffset,
|
|
2019
|
+
[STREAM_UP_TO_DATE_HEADER]: `true`,
|
|
2020
|
+
[STREAM_CLOSED_HEADER]: `true`
|
|
2021
|
+
});
|
|
2022
|
+
res.end();
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
1764
2025
|
const result = await this.store.waitForMessages(path$2, effectiveOffset ?? stream.currentOffset, this.options.longPollTimeout);
|
|
1765
|
-
if (result.
|
|
2026
|
+
if (result.streamClosed) {
|
|
1766
2027
|
const responseCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
1767
2028
|
res.writeHead(204, {
|
|
1768
2029
|
[STREAM_OFFSET_HEADER]: effectiveOffset ?? stream.currentOffset,
|
|
1769
2030
|
[STREAM_UP_TO_DATE_HEADER]: `true`,
|
|
1770
|
-
[STREAM_CURSOR_HEADER]: responseCursor
|
|
2031
|
+
[STREAM_CURSOR_HEADER]: responseCursor,
|
|
2032
|
+
[STREAM_CLOSED_HEADER]: `true`
|
|
1771
2033
|
});
|
|
1772
2034
|
res.end();
|
|
1773
2035
|
return;
|
|
1774
2036
|
}
|
|
2037
|
+
if (result.timedOut) {
|
|
2038
|
+
const responseCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
2039
|
+
const currentStream$1 = this.store.get(path$2);
|
|
2040
|
+
const timeoutHeaders = {
|
|
2041
|
+
[STREAM_OFFSET_HEADER]: effectiveOffset ?? stream.currentOffset,
|
|
2042
|
+
[STREAM_UP_TO_DATE_HEADER]: `true`,
|
|
2043
|
+
[STREAM_CURSOR_HEADER]: responseCursor
|
|
2044
|
+
};
|
|
2045
|
+
if (currentStream$1?.closed) timeoutHeaders[STREAM_CLOSED_HEADER] = `true`;
|
|
2046
|
+
res.writeHead(204, timeoutHeaders);
|
|
2047
|
+
res.end();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
1775
2050
|
messages = result.messages;
|
|
1776
2051
|
upToDate = true;
|
|
1777
2052
|
}
|
|
@@ -1782,8 +2057,12 @@ var DurableStreamTestServer = class {
|
|
|
1782
2057
|
headers[STREAM_OFFSET_HEADER] = responseOffset;
|
|
1783
2058
|
if (live === `long-poll`) headers[STREAM_CURSOR_HEADER] = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
1784
2059
|
if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
|
|
2060
|
+
const currentStream = this.store.get(path$2);
|
|
2061
|
+
const clientAtTail = responseOffset === currentStream?.currentOffset;
|
|
2062
|
+
if (currentStream?.closed && clientAtTail && upToDate) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1785
2063
|
const startOffset = offset ?? `-1`;
|
|
1786
|
-
const
|
|
2064
|
+
const closedSuffix = currentStream?.closed && clientAtTail && upToDate ? `:c` : ``;
|
|
2065
|
+
const etag = `"${Buffer.from(path$2).toString(`base64`)}:${startOffset}:${responseOffset}${closedSuffix}"`;
|
|
1787
2066
|
headers[`etag`] = etag;
|
|
1788
2067
|
const ifNoneMatch = req.headers[`if-none-match`];
|
|
1789
2068
|
if (ifNoneMatch && ifNoneMatch === etag) {
|
|
@@ -1795,10 +2074,10 @@ var DurableStreamTestServer = class {
|
|
|
1795
2074
|
let finalData = responseData;
|
|
1796
2075
|
if (this.options.compression && responseData.length >= COMPRESSION_THRESHOLD) {
|
|
1797
2076
|
const acceptEncoding = req.headers[`accept-encoding`];
|
|
1798
|
-
const
|
|
1799
|
-
if (
|
|
1800
|
-
finalData = compressData(responseData,
|
|
1801
|
-
headers[`content-encoding`] =
|
|
2077
|
+
const compressionEncoding = getCompressionEncoding(acceptEncoding);
|
|
2078
|
+
if (compressionEncoding) {
|
|
2079
|
+
finalData = compressData(responseData, compressionEncoding);
|
|
2080
|
+
headers[`content-encoding`] = compressionEncoding;
|
|
1802
2081
|
headers[`vary`] = `accept-encoding`;
|
|
1803
2082
|
}
|
|
1804
2083
|
}
|
|
@@ -1809,16 +2088,18 @@ var DurableStreamTestServer = class {
|
|
|
1809
2088
|
/**
|
|
1810
2089
|
* Handle SSE (Server-Sent Events) mode
|
|
1811
2090
|
*/
|
|
1812
|
-
async handleSSE(path$2, stream, initialOffset, cursor, res) {
|
|
2091
|
+
async handleSSE(path$2, stream, initialOffset, cursor, useBase64, res) {
|
|
1813
2092
|
this.activeSSEResponses.add(res);
|
|
1814
|
-
|
|
2093
|
+
const sseHeaders = {
|
|
1815
2094
|
"content-type": `text/event-stream`,
|
|
1816
2095
|
"cache-control": `no-cache`,
|
|
1817
2096
|
connection: `keep-alive`,
|
|
1818
2097
|
"access-control-allow-origin": `*`,
|
|
1819
2098
|
"x-content-type-options": `nosniff`,
|
|
1820
2099
|
"cross-origin-resource-policy": `cross-origin`
|
|
1821
|
-
}
|
|
2100
|
+
};
|
|
2101
|
+
if (useBase64) sseHeaders[STREAM_SSE_DATA_ENCODING_HEADER] = `base64`;
|
|
2102
|
+
res.writeHead(200, sseHeaders);
|
|
1822
2103
|
const fault = res._injectedFault;
|
|
1823
2104
|
if (fault?.injectSseEvent) {
|
|
1824
2105
|
res.write(`event: ${fault.injectSseEvent.eventType}\n`);
|
|
@@ -1836,7 +2117,8 @@ var DurableStreamTestServer = class {
|
|
|
1836
2117
|
const { messages, upToDate } = this.store.read(path$2, currentOffset);
|
|
1837
2118
|
for (const message of messages) {
|
|
1838
2119
|
let dataPayload;
|
|
1839
|
-
if (
|
|
2120
|
+
if (useBase64) dataPayload = Buffer.from(message.data).toString(`base64`);
|
|
2121
|
+
else if (isJsonStream) {
|
|
1840
2122
|
const jsonBytes = this.store.formatResponse(path$2, [message]);
|
|
1841
2123
|
dataPayload = decoder.decode(jsonBytes);
|
|
1842
2124
|
} else dataPayload = decoder.decode(message.data);
|
|
@@ -1844,28 +2126,60 @@ var DurableStreamTestServer = class {
|
|
|
1844
2126
|
res.write(encodeSSEData(dataPayload));
|
|
1845
2127
|
currentOffset = message.offset;
|
|
1846
2128
|
}
|
|
1847
|
-
const
|
|
2129
|
+
const currentStream = this.store.get(path$2);
|
|
2130
|
+
const controlOffset = messages[messages.length - 1]?.offset ?? currentStream.currentOffset;
|
|
2131
|
+
const streamIsClosed = currentStream?.closed ?? false;
|
|
2132
|
+
const clientAtTail = controlOffset === currentStream.currentOffset;
|
|
1848
2133
|
const responseCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
1849
|
-
const controlData = {
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
2134
|
+
const controlData = { [SSE_OFFSET_FIELD]: controlOffset };
|
|
2135
|
+
if (streamIsClosed && clientAtTail) controlData[SSE_CLOSED_FIELD] = true;
|
|
2136
|
+
else {
|
|
2137
|
+
controlData[SSE_CURSOR_FIELD] = responseCursor;
|
|
2138
|
+
if (upToDate) controlData[SSE_UP_TO_DATE_FIELD] = true;
|
|
2139
|
+
}
|
|
1854
2140
|
res.write(`event: control\n`);
|
|
1855
2141
|
res.write(encodeSSEData(JSON.stringify(controlData)));
|
|
2142
|
+
if (streamIsClosed && clientAtTail) break;
|
|
1856
2143
|
currentOffset = controlOffset;
|
|
1857
2144
|
if (upToDate) {
|
|
2145
|
+
if (currentStream?.closed) {
|
|
2146
|
+
const finalControlData = {
|
|
2147
|
+
[SSE_OFFSET_FIELD]: currentOffset,
|
|
2148
|
+
[SSE_CLOSED_FIELD]: true
|
|
2149
|
+
};
|
|
2150
|
+
res.write(`event: control\n`);
|
|
2151
|
+
res.write(encodeSSEData(JSON.stringify(finalControlData)));
|
|
2152
|
+
break;
|
|
2153
|
+
}
|
|
1858
2154
|
const result = await this.store.waitForMessages(path$2, currentOffset, this.options.longPollTimeout);
|
|
1859
2155
|
if (this.isShuttingDown || !isConnected) break;
|
|
2156
|
+
if (result.streamClosed) {
|
|
2157
|
+
const finalControlData = {
|
|
2158
|
+
[SSE_OFFSET_FIELD]: currentOffset,
|
|
2159
|
+
[SSE_CLOSED_FIELD]: true
|
|
2160
|
+
};
|
|
2161
|
+
res.write(`event: control\n`);
|
|
2162
|
+
res.write(encodeSSEData(JSON.stringify(finalControlData)));
|
|
2163
|
+
break;
|
|
2164
|
+
}
|
|
1860
2165
|
if (result.timedOut) {
|
|
1861
2166
|
const keepAliveCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
2167
|
+
const streamAfterWait = this.store.get(path$2);
|
|
2168
|
+
if (streamAfterWait?.closed) {
|
|
2169
|
+
const closedControlData = {
|
|
2170
|
+
[SSE_OFFSET_FIELD]: currentOffset,
|
|
2171
|
+
[SSE_CLOSED_FIELD]: true
|
|
2172
|
+
};
|
|
2173
|
+
res.write(`event: control\n`);
|
|
2174
|
+
res.write(encodeSSEData(JSON.stringify(closedControlData)));
|
|
2175
|
+
break;
|
|
2176
|
+
}
|
|
1862
2177
|
const keepAliveData = {
|
|
1863
2178
|
[SSE_OFFSET_FIELD]: currentOffset,
|
|
1864
2179
|
[SSE_CURSOR_FIELD]: keepAliveCursor,
|
|
1865
2180
|
[SSE_UP_TO_DATE_FIELD]: true
|
|
1866
2181
|
};
|
|
1867
|
-
res.write(`event: control\n`);
|
|
1868
|
-
res.write(encodeSSEData(JSON.stringify(keepAliveData)));
|
|
2182
|
+
res.write(`event: control\n` + encodeSSEData(JSON.stringify(keepAliveData)));
|
|
1869
2183
|
}
|
|
1870
2184
|
}
|
|
1871
2185
|
}
|
|
@@ -1878,20 +2192,11 @@ var DurableStreamTestServer = class {
|
|
|
1878
2192
|
async handleAppend(path$2, req, res) {
|
|
1879
2193
|
const contentType = req.headers[`content-type`];
|
|
1880
2194
|
const seq = req.headers[STREAM_SEQ_HEADER.toLowerCase()];
|
|
2195
|
+
const closedHeader = req.headers[STREAM_CLOSED_HEADER.toLowerCase()];
|
|
2196
|
+
const closeStream = closedHeader === `true`;
|
|
1881
2197
|
const producerId = req.headers[PRODUCER_ID_HEADER.toLowerCase()];
|
|
1882
2198
|
const producerEpochStr = req.headers[PRODUCER_EPOCH_HEADER.toLowerCase()];
|
|
1883
2199
|
const producerSeqStr = req.headers[PRODUCER_SEQ_HEADER.toLowerCase()];
|
|
1884
|
-
const body = await this.readBody(req);
|
|
1885
|
-
if (body.length === 0) {
|
|
1886
|
-
res.writeHead(400, { "content-type": `text/plain` });
|
|
1887
|
-
res.end(`Empty body`);
|
|
1888
|
-
return;
|
|
1889
|
-
}
|
|
1890
|
-
if (!contentType) {
|
|
1891
|
-
res.writeHead(400, { "content-type": `text/plain` });
|
|
1892
|
-
res.end(`Content-Type header is required`);
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1895
2200
|
const hasProducerHeaders = producerId !== void 0 || producerEpochStr !== void 0 || producerSeqStr !== void 0;
|
|
1896
2201
|
const hasAllProducerHeaders = producerId !== void 0 && producerEpochStr !== void 0 && producerSeqStr !== void 0;
|
|
1897
2202
|
if (hasProducerHeaders && !hasAllProducerHeaders) {
|
|
@@ -1931,34 +2236,148 @@ var DurableStreamTestServer = class {
|
|
|
1931
2236
|
return;
|
|
1932
2237
|
}
|
|
1933
2238
|
}
|
|
2239
|
+
const body = await this.readBody(req);
|
|
2240
|
+
if (body.length === 0 && closeStream) {
|
|
2241
|
+
if (hasAllProducerHeaders) {
|
|
2242
|
+
const closeResult$1 = await this.store.closeStreamWithProducer(path$2, {
|
|
2243
|
+
producerId,
|
|
2244
|
+
producerEpoch,
|
|
2245
|
+
producerSeq
|
|
2246
|
+
});
|
|
2247
|
+
if (!closeResult$1) {
|
|
2248
|
+
res.writeHead(404, { "content-type": `text/plain` });
|
|
2249
|
+
res.end(`Stream not found`);
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
if (closeResult$1.producerResult?.status === `duplicate`) {
|
|
2253
|
+
res.writeHead(204, {
|
|
2254
|
+
[STREAM_OFFSET_HEADER]: closeResult$1.finalOffset,
|
|
2255
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2256
|
+
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
2257
|
+
[PRODUCER_SEQ_HEADER]: closeResult$1.producerResult.lastSeq.toString()
|
|
2258
|
+
});
|
|
2259
|
+
res.end();
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (closeResult$1.producerResult?.status === `stale_epoch`) {
|
|
2263
|
+
res.writeHead(403, {
|
|
2264
|
+
"content-type": `text/plain`,
|
|
2265
|
+
[PRODUCER_EPOCH_HEADER]: closeResult$1.producerResult.currentEpoch.toString()
|
|
2266
|
+
});
|
|
2267
|
+
res.end(`Stale producer epoch`);
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
if (closeResult$1.producerResult?.status === `invalid_epoch_seq`) {
|
|
2271
|
+
res.writeHead(400, { "content-type": `text/plain` });
|
|
2272
|
+
res.end(`New epoch must start with sequence 0`);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (closeResult$1.producerResult?.status === `sequence_gap`) {
|
|
2276
|
+
res.writeHead(409, {
|
|
2277
|
+
"content-type": `text/plain`,
|
|
2278
|
+
[PRODUCER_EXPECTED_SEQ_HEADER]: closeResult$1.producerResult.expectedSeq.toString(),
|
|
2279
|
+
[PRODUCER_RECEIVED_SEQ_HEADER]: closeResult$1.producerResult.receivedSeq.toString()
|
|
2280
|
+
});
|
|
2281
|
+
res.end(`Producer sequence gap`);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
if (closeResult$1.producerResult?.status === `stream_closed`) {
|
|
2285
|
+
const stream = this.store.get(path$2);
|
|
2286
|
+
res.writeHead(409, {
|
|
2287
|
+
"content-type": `text/plain`,
|
|
2288
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2289
|
+
[STREAM_OFFSET_HEADER]: stream?.currentOffset ?? ``
|
|
2290
|
+
});
|
|
2291
|
+
res.end(`Stream is closed`);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
res.writeHead(204, {
|
|
2295
|
+
[STREAM_OFFSET_HEADER]: closeResult$1.finalOffset,
|
|
2296
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2297
|
+
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
2298
|
+
[PRODUCER_SEQ_HEADER]: producerSeq.toString()
|
|
2299
|
+
});
|
|
2300
|
+
res.end();
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
const closeResult = this.store.closeStream(path$2);
|
|
2304
|
+
if (!closeResult) {
|
|
2305
|
+
res.writeHead(404, { "content-type": `text/plain` });
|
|
2306
|
+
res.end(`Stream not found`);
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
res.writeHead(204, {
|
|
2310
|
+
[STREAM_OFFSET_HEADER]: closeResult.finalOffset,
|
|
2311
|
+
[STREAM_CLOSED_HEADER]: `true`
|
|
2312
|
+
});
|
|
2313
|
+
res.end();
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if (body.length === 0) {
|
|
2317
|
+
res.writeHead(400, { "content-type": `text/plain` });
|
|
2318
|
+
res.end(`Empty body`);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (!contentType) {
|
|
2322
|
+
res.writeHead(400, { "content-type": `text/plain` });
|
|
2323
|
+
res.end(`Content-Type header is required`);
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
1934
2326
|
const appendOptions = {
|
|
1935
2327
|
seq,
|
|
1936
2328
|
contentType,
|
|
1937
2329
|
producerId,
|
|
1938
2330
|
producerEpoch,
|
|
1939
|
-
producerSeq
|
|
2331
|
+
producerSeq,
|
|
2332
|
+
close: closeStream
|
|
1940
2333
|
};
|
|
1941
2334
|
let result;
|
|
1942
2335
|
if (producerId !== void 0) result = await this.store.appendWithProducer(path$2, body, appendOptions);
|
|
1943
2336
|
else result = await Promise.resolve(this.store.append(path$2, body, appendOptions));
|
|
1944
|
-
if (result && typeof result === `object` && `
|
|
1945
|
-
const { message: message$1, producerResult } = result;
|
|
2337
|
+
if (result && typeof result === `object` && `message` in result) {
|
|
2338
|
+
const { message: message$1, producerResult, streamClosed } = result;
|
|
2339
|
+
if (streamClosed && !message$1) {
|
|
2340
|
+
if (producerResult?.status === `duplicate`) {
|
|
2341
|
+
const stream = this.store.get(path$2);
|
|
2342
|
+
res.writeHead(204, {
|
|
2343
|
+
[STREAM_OFFSET_HEADER]: stream?.currentOffset ?? ``,
|
|
2344
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2345
|
+
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
2346
|
+
[PRODUCER_SEQ_HEADER]: producerResult.lastSeq.toString()
|
|
2347
|
+
});
|
|
2348
|
+
res.end();
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
const closedStream = this.store.get(path$2);
|
|
2352
|
+
res.writeHead(409, {
|
|
2353
|
+
"content-type": `text/plain`,
|
|
2354
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2355
|
+
[STREAM_OFFSET_HEADER]: closedStream?.currentOffset ?? ``
|
|
2356
|
+
});
|
|
2357
|
+
res.end(`Stream is closed`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
1946
2360
|
if (!producerResult || producerResult.status === `accepted`) {
|
|
1947
|
-
const responseHeaders = { [STREAM_OFFSET_HEADER]: message$1.offset };
|
|
1948
|
-
if (producerEpoch !== void 0) responseHeaders[PRODUCER_EPOCH_HEADER] = producerEpoch.toString();
|
|
1949
|
-
if (producerSeq !== void 0) responseHeaders[PRODUCER_SEQ_HEADER] = producerSeq.toString();
|
|
1950
|
-
|
|
2361
|
+
const responseHeaders$1 = { [STREAM_OFFSET_HEADER]: message$1.offset };
|
|
2362
|
+
if (producerEpoch !== void 0) responseHeaders$1[PRODUCER_EPOCH_HEADER] = producerEpoch.toString();
|
|
2363
|
+
if (producerSeq !== void 0) responseHeaders$1[PRODUCER_SEQ_HEADER] = producerSeq.toString();
|
|
2364
|
+
if (streamClosed) responseHeaders$1[STREAM_CLOSED_HEADER] = `true`;
|
|
2365
|
+
const statusCode = producerId !== void 0 ? 200 : 204;
|
|
2366
|
+
res.writeHead(statusCode, responseHeaders$1);
|
|
1951
2367
|
res.end();
|
|
1952
2368
|
return;
|
|
1953
2369
|
}
|
|
1954
2370
|
switch (producerResult.status) {
|
|
1955
|
-
case `duplicate`:
|
|
1956
|
-
|
|
2371
|
+
case `duplicate`: {
|
|
2372
|
+
const dupHeaders = {
|
|
1957
2373
|
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
1958
2374
|
[PRODUCER_SEQ_HEADER]: producerResult.lastSeq.toString()
|
|
1959
|
-
}
|
|
2375
|
+
};
|
|
2376
|
+
if (streamClosed) dupHeaders[STREAM_CLOSED_HEADER] = `true`;
|
|
2377
|
+
res.writeHead(204, dupHeaders);
|
|
1960
2378
|
res.end();
|
|
1961
2379
|
return;
|
|
2380
|
+
}
|
|
1962
2381
|
case `stale_epoch`: {
|
|
1963
2382
|
res.writeHead(403, {
|
|
1964
2383
|
"content-type": `text/plain`,
|
|
@@ -1982,7 +2401,9 @@ var DurableStreamTestServer = class {
|
|
|
1982
2401
|
}
|
|
1983
2402
|
}
|
|
1984
2403
|
const message = result;
|
|
1985
|
-
|
|
2404
|
+
const responseHeaders = { [STREAM_OFFSET_HEADER]: message.offset };
|
|
2405
|
+
if (closeStream) responseHeaders[STREAM_CLOSED_HEADER] = `true`;
|
|
2406
|
+
res.writeHead(204, responseHeaders);
|
|
1986
2407
|
res.end();
|
|
1987
2408
|
}
|
|
1988
2409
|
/**
|