@durable-streams/server 0.1.7 → 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.cjs
CHANGED
|
@@ -132,7 +132,8 @@ var StreamStore = class {
|
|
|
132
132
|
const contentTypeMatches = (normalizeContentType(options.contentType) || `application/octet-stream`) === (normalizeContentType(existing.contentType) || `application/octet-stream`);
|
|
133
133
|
const ttlMatches = options.ttlSeconds === existing.ttlSeconds;
|
|
134
134
|
const expiresMatches = options.expiresAt === existing.expiresAt;
|
|
135
|
-
|
|
135
|
+
const closedMatches = (options.closed ?? false) === (existing.closed ?? false);
|
|
136
|
+
if (contentTypeMatches && ttlMatches && expiresMatches && closedMatches) return existing;
|
|
136
137
|
else throw new Error(`Stream already exists with different configuration: ${path}`);
|
|
137
138
|
}
|
|
138
139
|
const stream = {
|
|
@@ -142,7 +143,8 @@ var StreamStore = class {
|
|
|
142
143
|
currentOffset: `0000000000000000_0000000000000000`,
|
|
143
144
|
ttlSeconds: options.ttlSeconds,
|
|
144
145
|
expiresAt: options.expiresAt,
|
|
145
|
-
createdAt: Date.now()
|
|
146
|
+
createdAt: Date.now(),
|
|
147
|
+
closed: options.closed ?? false
|
|
146
148
|
};
|
|
147
149
|
if (options.initialData && options.initialData.length > 0) this.appendToStream(stream, options.initialData, true);
|
|
148
150
|
this.streams.set(path, stream);
|
|
@@ -279,6 +281,20 @@ var StreamStore = class {
|
|
|
279
281
|
append(path, data, options = {}) {
|
|
280
282
|
const stream = this.getIfNotExpired(path);
|
|
281
283
|
if (!stream) throw new Error(`Stream not found: ${path}`);
|
|
284
|
+
if (stream.closed) {
|
|
285
|
+
if (options.producerId && stream.closedBy && stream.closedBy.producerId === options.producerId && stream.closedBy.epoch === options.producerEpoch && stream.closedBy.seq === options.producerSeq) return {
|
|
286
|
+
message: null,
|
|
287
|
+
streamClosed: true,
|
|
288
|
+
producerResult: {
|
|
289
|
+
status: `duplicate`,
|
|
290
|
+
lastSeq: options.producerSeq
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
message: null,
|
|
295
|
+
streamClosed: true
|
|
296
|
+
};
|
|
297
|
+
}
|
|
282
298
|
if (options.contentType && stream.contentType) {
|
|
283
299
|
const providedType = normalizeContentType(options.contentType);
|
|
284
300
|
const streamType = normalizeContentType(stream.contentType);
|
|
@@ -298,10 +314,20 @@ var StreamStore = class {
|
|
|
298
314
|
const message = this.appendToStream(stream, data);
|
|
299
315
|
if (producerResult) this.commitProducerState(stream, producerResult);
|
|
300
316
|
if (options.seq !== void 0) stream.lastSeq = options.seq;
|
|
317
|
+
if (options.close) {
|
|
318
|
+
stream.closed = true;
|
|
319
|
+
if (options.producerId !== void 0) stream.closedBy = {
|
|
320
|
+
producerId: options.producerId,
|
|
321
|
+
epoch: options.producerEpoch,
|
|
322
|
+
seq: options.producerSeq
|
|
323
|
+
};
|
|
324
|
+
this.notifyLongPollsClosed(path);
|
|
325
|
+
}
|
|
301
326
|
this.notifyLongPolls(path);
|
|
302
|
-
if (producerResult) return {
|
|
327
|
+
if (producerResult || options.close) return {
|
|
303
328
|
message,
|
|
304
|
-
producerResult
|
|
329
|
+
producerResult,
|
|
330
|
+
streamClosed: options.close
|
|
305
331
|
};
|
|
306
332
|
return message;
|
|
307
333
|
}
|
|
@@ -325,6 +351,69 @@ var StreamStore = class {
|
|
|
325
351
|
}
|
|
326
352
|
}
|
|
327
353
|
/**
|
|
354
|
+
* Close a stream without appending data.
|
|
355
|
+
* @returns The final offset, or null if stream doesn't exist
|
|
356
|
+
*/
|
|
357
|
+
closeStream(path) {
|
|
358
|
+
const stream = this.getIfNotExpired(path);
|
|
359
|
+
if (!stream) return null;
|
|
360
|
+
const alreadyClosed = stream.closed ?? false;
|
|
361
|
+
stream.closed = true;
|
|
362
|
+
this.notifyLongPollsClosed(path);
|
|
363
|
+
return {
|
|
364
|
+
finalOffset: stream.currentOffset,
|
|
365
|
+
alreadyClosed
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Close a stream with producer headers for idempotent close-only operations.
|
|
370
|
+
* Participates in producer sequencing for deduplication.
|
|
371
|
+
* @returns The final offset and producer result, or null if stream doesn't exist
|
|
372
|
+
*/
|
|
373
|
+
async closeStreamWithProducer(path, options) {
|
|
374
|
+
const releaseLock = await this.acquireProducerLock(path, options.producerId);
|
|
375
|
+
try {
|
|
376
|
+
const stream = this.getIfNotExpired(path);
|
|
377
|
+
if (!stream) return null;
|
|
378
|
+
if (stream.closed) {
|
|
379
|
+
if (stream.closedBy && stream.closedBy.producerId === options.producerId && stream.closedBy.epoch === options.producerEpoch && stream.closedBy.seq === options.producerSeq) return {
|
|
380
|
+
finalOffset: stream.currentOffset,
|
|
381
|
+
alreadyClosed: true,
|
|
382
|
+
producerResult: {
|
|
383
|
+
status: `duplicate`,
|
|
384
|
+
lastSeq: options.producerSeq
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
return {
|
|
388
|
+
finalOffset: stream.currentOffset,
|
|
389
|
+
alreadyClosed: true,
|
|
390
|
+
producerResult: { status: `stream_closed` }
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const producerResult = this.validateProducer(stream, options.producerId, options.producerEpoch, options.producerSeq);
|
|
394
|
+
if (producerResult.status !== `accepted`) return {
|
|
395
|
+
finalOffset: stream.currentOffset,
|
|
396
|
+
alreadyClosed: stream.closed ?? false,
|
|
397
|
+
producerResult
|
|
398
|
+
};
|
|
399
|
+
this.commitProducerState(stream, producerResult);
|
|
400
|
+
stream.closed = true;
|
|
401
|
+
stream.closedBy = {
|
|
402
|
+
producerId: options.producerId,
|
|
403
|
+
epoch: options.producerEpoch,
|
|
404
|
+
seq: options.producerSeq
|
|
405
|
+
};
|
|
406
|
+
this.notifyLongPollsClosed(path);
|
|
407
|
+
return {
|
|
408
|
+
finalOffset: stream.currentOffset,
|
|
409
|
+
alreadyClosed: false,
|
|
410
|
+
producerResult
|
|
411
|
+
};
|
|
412
|
+
} finally {
|
|
413
|
+
releaseLock();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
328
417
|
* Get the current epoch for a producer on a stream.
|
|
329
418
|
* Returns undefined if the producer doesn't exist or stream not found.
|
|
330
419
|
*/
|
|
@@ -384,12 +473,20 @@ var StreamStore = class {
|
|
|
384
473
|
messages,
|
|
385
474
|
timedOut: false
|
|
386
475
|
};
|
|
476
|
+
if (stream.closed && offset === stream.currentOffset) return {
|
|
477
|
+
messages: [],
|
|
478
|
+
timedOut: false,
|
|
479
|
+
streamClosed: true
|
|
480
|
+
};
|
|
387
481
|
return new Promise((resolve) => {
|
|
388
482
|
const timeoutId = setTimeout(() => {
|
|
389
483
|
this.removePendingLongPoll(pending);
|
|
484
|
+
const currentStream = this.getIfNotExpired(path);
|
|
485
|
+
const streamClosed = currentStream?.closed ?? false;
|
|
390
486
|
resolve({
|
|
391
487
|
messages: [],
|
|
392
|
-
timedOut: true
|
|
488
|
+
timedOut: true,
|
|
489
|
+
streamClosed
|
|
393
490
|
});
|
|
394
491
|
}, timeoutMs);
|
|
395
492
|
const pending = {
|
|
@@ -398,9 +495,12 @@ var StreamStore = class {
|
|
|
398
495
|
resolve: (msgs) => {
|
|
399
496
|
clearTimeout(timeoutId);
|
|
400
497
|
this.removePendingLongPoll(pending);
|
|
498
|
+
const currentStream = this.getIfNotExpired(path);
|
|
499
|
+
const streamClosed = currentStream?.closed && msgs.length === 0 ? true : void 0;
|
|
401
500
|
resolve({
|
|
402
501
|
messages: msgs,
|
|
403
|
-
timedOut: false
|
|
502
|
+
timedOut: false,
|
|
503
|
+
streamClosed
|
|
404
504
|
});
|
|
405
505
|
},
|
|
406
506
|
timeoutId
|
|
@@ -473,6 +573,14 @@ var StreamStore = class {
|
|
|
473
573
|
if (messages.length > 0) pending.resolve(messages);
|
|
474
574
|
}
|
|
475
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* Notify pending long-polls that a stream has been closed.
|
|
578
|
+
* They should wake up immediately and return Stream-Closed: true.
|
|
579
|
+
*/
|
|
580
|
+
notifyLongPollsClosed(path) {
|
|
581
|
+
const toNotify = this.pendingLongPolls.filter((p) => p.path === path);
|
|
582
|
+
for (const pending of toNotify) pending.resolve([]);
|
|
583
|
+
}
|
|
476
584
|
cancelLongPollsForStream(path) {
|
|
477
585
|
const toCancel = this.pendingLongPolls.filter((p) => p.path === path);
|
|
478
586
|
for (const pending of toCancel) {
|
|
@@ -794,7 +902,9 @@ var FileBackedStreamStore = class {
|
|
|
794
902
|
ttlSeconds: meta.ttlSeconds,
|
|
795
903
|
expiresAt: meta.expiresAt,
|
|
796
904
|
createdAt: meta.createdAt,
|
|
797
|
-
producers
|
|
905
|
+
producers,
|
|
906
|
+
closed: meta.closed,
|
|
907
|
+
closedBy: meta.closedBy
|
|
798
908
|
};
|
|
799
909
|
}
|
|
800
910
|
/**
|
|
@@ -933,7 +1043,8 @@ var FileBackedStreamStore = class {
|
|
|
933
1043
|
const contentTypeMatches = normalizeMimeType(options.contentType) === normalizeMimeType(existing.contentType);
|
|
934
1044
|
const ttlMatches = options.ttlSeconds === existing.ttlSeconds;
|
|
935
1045
|
const expiresMatches = options.expiresAt === existing.expiresAt;
|
|
936
|
-
|
|
1046
|
+
const closedMatches = (options.closed ?? false) === (existing.closed ?? false);
|
|
1047
|
+
if (contentTypeMatches && ttlMatches && expiresMatches && closedMatches) return this.streamMetaToStream(existing);
|
|
937
1048
|
else throw new Error(`Stream already exists with different configuration: ${streamPath}`);
|
|
938
1049
|
}
|
|
939
1050
|
const key = `stream:${streamPath}`;
|
|
@@ -947,7 +1058,8 @@ var FileBackedStreamStore = class {
|
|
|
947
1058
|
createdAt: Date.now(),
|
|
948
1059
|
segmentCount: 1,
|
|
949
1060
|
totalBytes: 0,
|
|
950
|
-
directoryName: generateUniqueDirectoryName(streamPath)
|
|
1061
|
+
directoryName: generateUniqueDirectoryName(streamPath),
|
|
1062
|
+
closed: false
|
|
951
1063
|
};
|
|
952
1064
|
const streamDir = node_path.join(this.dataDir, `streams`, streamMeta.directoryName);
|
|
953
1065
|
try {
|
|
@@ -959,15 +1071,17 @@ var FileBackedStreamStore = class {
|
|
|
959
1071
|
throw err;
|
|
960
1072
|
}
|
|
961
1073
|
this.db.putSync(key, streamMeta);
|
|
962
|
-
if (options.initialData && options.initialData.length > 0) {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
1074
|
+
if (options.initialData && options.initialData.length > 0) await this.append(streamPath, options.initialData, {
|
|
1075
|
+
contentType: options.contentType,
|
|
1076
|
+
isInitialCreate: true
|
|
1077
|
+
});
|
|
1078
|
+
if (options.closed) {
|
|
1079
|
+
const updatedMeta = this.db.get(key);
|
|
1080
|
+
updatedMeta.closed = true;
|
|
1081
|
+
this.db.putSync(key, updatedMeta);
|
|
969
1082
|
}
|
|
970
|
-
|
|
1083
|
+
const updated = this.db.get(key);
|
|
1084
|
+
return this.streamMetaToStream(updated);
|
|
971
1085
|
}
|
|
972
1086
|
get(streamPath) {
|
|
973
1087
|
const meta = this.getMetaIfNotExpired(streamPath);
|
|
@@ -994,6 +1108,20 @@ var FileBackedStreamStore = class {
|
|
|
994
1108
|
async append(streamPath, data, options = {}) {
|
|
995
1109
|
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
996
1110
|
if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
|
|
1111
|
+
if (streamMeta.closed) {
|
|
1112
|
+
if (options.producerId && streamMeta.closedBy && streamMeta.closedBy.producerId === options.producerId && streamMeta.closedBy.epoch === options.producerEpoch && streamMeta.closedBy.seq === options.producerSeq) return {
|
|
1113
|
+
message: null,
|
|
1114
|
+
streamClosed: true,
|
|
1115
|
+
producerResult: {
|
|
1116
|
+
status: `duplicate`,
|
|
1117
|
+
lastSeq: options.producerSeq
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
return {
|
|
1121
|
+
message: null,
|
|
1122
|
+
streamClosed: true
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
997
1125
|
if (options.contentType && streamMeta.contentType) {
|
|
998
1126
|
const providedType = normalizeContentType(options.contentType);
|
|
999
1127
|
const streamType = normalizeContentType(streamMeta.contentType);
|
|
@@ -1044,19 +1172,29 @@ var FileBackedStreamStore = class {
|
|
|
1044
1172
|
await this.fileHandlePool.fsyncFile(segmentPath);
|
|
1045
1173
|
const updatedProducers = { ...streamMeta.producers };
|
|
1046
1174
|
if (producerResult && producerResult.status === `accepted`) updatedProducers[producerResult.producerId] = producerResult.proposedState;
|
|
1175
|
+
let closedBy = void 0;
|
|
1176
|
+
if (options.close && options.producerId) closedBy = {
|
|
1177
|
+
producerId: options.producerId,
|
|
1178
|
+
epoch: options.producerEpoch,
|
|
1179
|
+
seq: options.producerSeq
|
|
1180
|
+
};
|
|
1047
1181
|
const updatedMeta = {
|
|
1048
1182
|
...streamMeta,
|
|
1049
1183
|
currentOffset: newOffset,
|
|
1050
1184
|
lastSeq: options.seq ?? streamMeta.lastSeq,
|
|
1051
1185
|
totalBytes: streamMeta.totalBytes + processedData.length + 5,
|
|
1052
|
-
producers: updatedProducers
|
|
1186
|
+
producers: updatedProducers,
|
|
1187
|
+
closed: options.close ? true : streamMeta.closed,
|
|
1188
|
+
closedBy: closedBy ?? streamMeta.closedBy
|
|
1053
1189
|
};
|
|
1054
1190
|
const key = `stream:${streamPath}`;
|
|
1055
1191
|
this.db.putSync(key, updatedMeta);
|
|
1056
1192
|
this.notifyLongPolls(streamPath);
|
|
1057
|
-
if (
|
|
1193
|
+
if (options.close) this.notifyLongPollsClosed(streamPath);
|
|
1194
|
+
if (producerResult || options.close) return {
|
|
1058
1195
|
message,
|
|
1059
|
-
producerResult
|
|
1196
|
+
producerResult,
|
|
1197
|
+
streamClosed: options.close
|
|
1060
1198
|
};
|
|
1061
1199
|
return message;
|
|
1062
1200
|
}
|
|
@@ -1079,6 +1217,81 @@ var FileBackedStreamStore = class {
|
|
|
1079
1217
|
releaseLock();
|
|
1080
1218
|
}
|
|
1081
1219
|
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Close a stream without appending data.
|
|
1222
|
+
* @returns The final offset, or null if stream doesn't exist
|
|
1223
|
+
*/
|
|
1224
|
+
closeStream(streamPath) {
|
|
1225
|
+
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1226
|
+
if (!streamMeta) return null;
|
|
1227
|
+
const alreadyClosed = streamMeta.closed ?? false;
|
|
1228
|
+
const key = `stream:${streamPath}`;
|
|
1229
|
+
const updatedMeta = {
|
|
1230
|
+
...streamMeta,
|
|
1231
|
+
closed: true
|
|
1232
|
+
};
|
|
1233
|
+
this.db.putSync(key, updatedMeta);
|
|
1234
|
+
this.notifyLongPollsClosed(streamPath);
|
|
1235
|
+
return {
|
|
1236
|
+
finalOffset: streamMeta.currentOffset,
|
|
1237
|
+
alreadyClosed
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Close a stream with producer headers for idempotent close-only operations.
|
|
1242
|
+
* Participates in producer sequencing for deduplication.
|
|
1243
|
+
* @returns The final offset and producer result, or null if stream doesn't exist
|
|
1244
|
+
*/
|
|
1245
|
+
async closeStreamWithProducer(streamPath, options) {
|
|
1246
|
+
const releaseLock = await this.acquireProducerLock(streamPath, options.producerId);
|
|
1247
|
+
try {
|
|
1248
|
+
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1249
|
+
if (!streamMeta) return null;
|
|
1250
|
+
if (streamMeta.closed) {
|
|
1251
|
+
if (streamMeta.closedBy && streamMeta.closedBy.producerId === options.producerId && streamMeta.closedBy.epoch === options.producerEpoch && streamMeta.closedBy.seq === options.producerSeq) return {
|
|
1252
|
+
finalOffset: streamMeta.currentOffset,
|
|
1253
|
+
alreadyClosed: true,
|
|
1254
|
+
producerResult: {
|
|
1255
|
+
status: `duplicate`,
|
|
1256
|
+
lastSeq: options.producerSeq
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
return {
|
|
1260
|
+
finalOffset: streamMeta.currentOffset,
|
|
1261
|
+
alreadyClosed: true,
|
|
1262
|
+
producerResult: { status: `stream_closed` }
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
const producerResult = this.validateProducer(streamMeta, options.producerId, options.producerEpoch, options.producerSeq);
|
|
1266
|
+
if (producerResult.status !== `accepted`) return {
|
|
1267
|
+
finalOffset: streamMeta.currentOffset,
|
|
1268
|
+
alreadyClosed: streamMeta.closed ?? false,
|
|
1269
|
+
producerResult
|
|
1270
|
+
};
|
|
1271
|
+
const key = `stream:${streamPath}`;
|
|
1272
|
+
const updatedProducers = { ...streamMeta.producers };
|
|
1273
|
+
updatedProducers[producerResult.producerId] = producerResult.proposedState;
|
|
1274
|
+
const updatedMeta = {
|
|
1275
|
+
...streamMeta,
|
|
1276
|
+
closed: true,
|
|
1277
|
+
closedBy: {
|
|
1278
|
+
producerId: options.producerId,
|
|
1279
|
+
epoch: options.producerEpoch,
|
|
1280
|
+
seq: options.producerSeq
|
|
1281
|
+
},
|
|
1282
|
+
producers: updatedProducers
|
|
1283
|
+
};
|
|
1284
|
+
this.db.putSync(key, updatedMeta);
|
|
1285
|
+
this.notifyLongPollsClosed(streamPath);
|
|
1286
|
+
return {
|
|
1287
|
+
finalOffset: streamMeta.currentOffset,
|
|
1288
|
+
alreadyClosed: false,
|
|
1289
|
+
producerResult
|
|
1290
|
+
};
|
|
1291
|
+
} finally {
|
|
1292
|
+
releaseLock();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1082
1295
|
read(streamPath, offset) {
|
|
1083
1296
|
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1084
1297
|
if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
|
|
@@ -1134,17 +1347,30 @@ var FileBackedStreamStore = class {
|
|
|
1134
1347
|
async waitForMessages(streamPath, offset, timeoutMs) {
|
|
1135
1348
|
const streamMeta = this.getMetaIfNotExpired(streamPath);
|
|
1136
1349
|
if (!streamMeta) throw new Error(`Stream not found: ${streamPath}`);
|
|
1350
|
+
if (streamMeta.closed && offset === streamMeta.currentOffset) return {
|
|
1351
|
+
messages: [],
|
|
1352
|
+
timedOut: false,
|
|
1353
|
+
streamClosed: true
|
|
1354
|
+
};
|
|
1137
1355
|
const { messages } = this.read(streamPath, offset);
|
|
1138
1356
|
if (messages.length > 0) return {
|
|
1139
1357
|
messages,
|
|
1140
|
-
timedOut: false
|
|
1358
|
+
timedOut: false,
|
|
1359
|
+
streamClosed: streamMeta.closed
|
|
1360
|
+
};
|
|
1361
|
+
if (streamMeta.closed) return {
|
|
1362
|
+
messages: [],
|
|
1363
|
+
timedOut: false,
|
|
1364
|
+
streamClosed: true
|
|
1141
1365
|
};
|
|
1142
1366
|
return new Promise((resolve) => {
|
|
1143
1367
|
const timeoutId = setTimeout(() => {
|
|
1144
1368
|
this.removePendingLongPoll(pending);
|
|
1369
|
+
const currentMeta = this.getMetaIfNotExpired(streamPath);
|
|
1145
1370
|
resolve({
|
|
1146
1371
|
messages: [],
|
|
1147
|
-
timedOut: true
|
|
1372
|
+
timedOut: true,
|
|
1373
|
+
streamClosed: currentMeta?.closed
|
|
1148
1374
|
});
|
|
1149
1375
|
}, timeoutMs);
|
|
1150
1376
|
const pending = {
|
|
@@ -1153,9 +1379,11 @@ var FileBackedStreamStore = class {
|
|
|
1153
1379
|
resolve: (msgs) => {
|
|
1154
1380
|
clearTimeout(timeoutId);
|
|
1155
1381
|
this.removePendingLongPoll(pending);
|
|
1382
|
+
const currentMeta = this.getMetaIfNotExpired(streamPath);
|
|
1156
1383
|
resolve({
|
|
1157
1384
|
messages: msgs,
|
|
1158
|
-
timedOut: false
|
|
1385
|
+
timedOut: false,
|
|
1386
|
+
streamClosed: currentMeta?.closed
|
|
1159
1387
|
});
|
|
1160
1388
|
},
|
|
1161
1389
|
timeoutId
|
|
@@ -1228,6 +1456,14 @@ var FileBackedStreamStore = class {
|
|
|
1228
1456
|
if (messages.length > 0) pending.resolve(messages);
|
|
1229
1457
|
}
|
|
1230
1458
|
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Notify pending long-polls that a stream has been closed.
|
|
1461
|
+
* They should wake up immediately and return Stream-Closed: true.
|
|
1462
|
+
*/
|
|
1463
|
+
notifyLongPollsClosed(streamPath) {
|
|
1464
|
+
const toNotify = this.pendingLongPolls.filter((p) => p.path === streamPath);
|
|
1465
|
+
for (const pending of toNotify) pending.resolve([]);
|
|
1466
|
+
}
|
|
1231
1467
|
cancelLongPollsForStream(streamPath) {
|
|
1232
1468
|
const toCancel = this.pendingLongPolls.filter((p) => p.path === streamPath);
|
|
1233
1469
|
for (const pending of toCancel) {
|
|
@@ -1352,6 +1588,7 @@ const STREAM_UP_TO_DATE_HEADER = `Stream-Up-To-Date`;
|
|
|
1352
1588
|
const STREAM_SEQ_HEADER = `Stream-Seq`;
|
|
1353
1589
|
const STREAM_TTL_HEADER = `Stream-TTL`;
|
|
1354
1590
|
const STREAM_EXPIRES_AT_HEADER = `Stream-Expires-At`;
|
|
1591
|
+
const STREAM_SSE_DATA_ENCODING_HEADER = `Stream-SSE-Data-Encoding`;
|
|
1355
1592
|
const PRODUCER_ID_HEADER = `Producer-Id`;
|
|
1356
1593
|
const PRODUCER_EPOCH_HEADER = `Producer-Epoch`;
|
|
1357
1594
|
const PRODUCER_SEQ_HEADER = `Producer-Seq`;
|
|
@@ -1360,6 +1597,8 @@ const PRODUCER_RECEIVED_SEQ_HEADER = `Producer-Received-Seq`;
|
|
|
1360
1597
|
const SSE_OFFSET_FIELD = `streamNextOffset`;
|
|
1361
1598
|
const SSE_CURSOR_FIELD = `streamCursor`;
|
|
1362
1599
|
const SSE_UP_TO_DATE_FIELD = `upToDate`;
|
|
1600
|
+
const SSE_CLOSED_FIELD = `streamClosed`;
|
|
1601
|
+
const STREAM_CLOSED_HEADER = `Stream-Closed`;
|
|
1363
1602
|
const OFFSET_QUERY_PARAM = `offset`;
|
|
1364
1603
|
const LIVE_QUERY_PARAM = `live`;
|
|
1365
1604
|
const CURSOR_QUERY_PARAM = `cursor`;
|
|
@@ -1574,8 +1813,8 @@ var DurableStreamTestServer = class {
|
|
|
1574
1813
|
const method = req.method?.toUpperCase();
|
|
1575
1814
|
res.setHeader(`access-control-allow-origin`, `*`);
|
|
1576
1815
|
res.setHeader(`access-control-allow-methods`, `GET, POST, PUT, DELETE, HEAD, OPTIONS`);
|
|
1577
|
-
res.setHeader(`access-control-allow-headers`, `content-type, authorization, Stream-Seq, Stream-TTL, Stream-Expires-At, Producer-Id, Producer-Epoch, Producer-Seq`);
|
|
1578
|
-
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`);
|
|
1816
|
+
res.setHeader(`access-control-allow-headers`, `content-type, authorization, Stream-Seq, Stream-TTL, Stream-Expires-At, Stream-Closed, Producer-Id, Producer-Epoch, Producer-Seq`);
|
|
1817
|
+
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`);
|
|
1579
1818
|
res.setHeader(`x-content-type-options`, `nosniff`);
|
|
1580
1819
|
res.setHeader(`cross-origin-resource-policy`, `cross-origin`);
|
|
1581
1820
|
if (method === `OPTIONS`) {
|
|
@@ -1655,6 +1894,8 @@ var DurableStreamTestServer = class {
|
|
|
1655
1894
|
if (!contentType || contentType.trim() === `` || !/^[\w-]+\/[\w-]+/.test(contentType)) contentType = `application/octet-stream`;
|
|
1656
1895
|
const ttlHeader = req.headers[STREAM_TTL_HEADER.toLowerCase()];
|
|
1657
1896
|
const expiresAtHeader = req.headers[STREAM_EXPIRES_AT_HEADER.toLowerCase()];
|
|
1897
|
+
const closedHeader = req.headers[STREAM_CLOSED_HEADER.toLowerCase()];
|
|
1898
|
+
const createClosed = closedHeader === `true`;
|
|
1658
1899
|
if (ttlHeader && expiresAtHeader) {
|
|
1659
1900
|
res.writeHead(400, { "content-type": `text/plain` });
|
|
1660
1901
|
res.end(`Cannot specify both Stream-TTL and Stream-Expires-At`);
|
|
@@ -1689,7 +1930,8 @@ var DurableStreamTestServer = class {
|
|
|
1689
1930
|
contentType,
|
|
1690
1931
|
ttlSeconds,
|
|
1691
1932
|
expiresAt: expiresAtHeader,
|
|
1692
|
-
initialData: body.length > 0 ? body : void 0
|
|
1933
|
+
initialData: body.length > 0 ? body : void 0,
|
|
1934
|
+
closed: createClosed
|
|
1693
1935
|
}));
|
|
1694
1936
|
const stream = this.store.get(path);
|
|
1695
1937
|
if (isNew && this.options.onStreamCreated) await Promise.resolve(this.options.onStreamCreated({
|
|
@@ -1703,6 +1945,7 @@ var DurableStreamTestServer = class {
|
|
|
1703
1945
|
[STREAM_OFFSET_HEADER]: stream.currentOffset
|
|
1704
1946
|
};
|
|
1705
1947
|
if (isNew) headers[`location`] = `${this._url}${path}`;
|
|
1948
|
+
if (stream.closed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1706
1949
|
res.writeHead(isNew ? 201 : 200, headers);
|
|
1707
1950
|
res.end();
|
|
1708
1951
|
}
|
|
@@ -1721,7 +1964,9 @@ var DurableStreamTestServer = class {
|
|
|
1721
1964
|
"cache-control": `no-store`
|
|
1722
1965
|
};
|
|
1723
1966
|
if (stream.contentType) headers[`content-type`] = stream.contentType;
|
|
1724
|
-
headers[
|
|
1967
|
+
if (stream.closed) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1968
|
+
const closedSuffix = stream.closed ? `:c` : ``;
|
|
1969
|
+
headers[`etag`] = `"${Buffer.from(path).toString(`base64`)}:-1:${stream.currentOffset}${closedSuffix}"`;
|
|
1725
1970
|
res.writeHead(200, headers);
|
|
1726
1971
|
res.end();
|
|
1727
1972
|
}
|
|
@@ -1762,9 +2007,15 @@ var DurableStreamTestServer = class {
|
|
|
1762
2007
|
res.end(`${live === `sse` ? `SSE` : `Long-poll`} requires offset parameter`);
|
|
1763
2008
|
return;
|
|
1764
2009
|
}
|
|
2010
|
+
let useBase64 = false;
|
|
2011
|
+
if (live === `sse`) {
|
|
2012
|
+
const ct = stream.contentType?.toLowerCase().split(`;`)[0]?.trim() ?? ``;
|
|
2013
|
+
const isTextCompatible = ct.startsWith(`text/`) || ct === `application/json`;
|
|
2014
|
+
useBase64 = !isTextCompatible;
|
|
2015
|
+
}
|
|
1765
2016
|
if (live === `sse`) {
|
|
1766
2017
|
const sseOffset = offset === `now` ? stream.currentOffset : offset;
|
|
1767
|
-
await this.handleSSE(path, stream, sseOffset, cursor, res);
|
|
2018
|
+
await this.handleSSE(path, stream, sseOffset, cursor, useBase64, res);
|
|
1768
2019
|
return;
|
|
1769
2020
|
}
|
|
1770
2021
|
const effectiveOffset = offset === `now` ? stream.currentOffset : offset;
|
|
@@ -1775,6 +2026,7 @@ var DurableStreamTestServer = class {
|
|
|
1775
2026
|
[`cache-control`]: `no-store`
|
|
1776
2027
|
};
|
|
1777
2028
|
if (stream.contentType) headers$1[`content-type`] = stream.contentType;
|
|
2029
|
+
if (stream.closed) headers$1[STREAM_CLOSED_HEADER] = `true`;
|
|
1778
2030
|
const isJsonMode = stream.contentType?.includes(`application/json`);
|
|
1779
2031
|
const responseBody = isJsonMode ? `[]` : ``;
|
|
1780
2032
|
res.writeHead(200, headers$1);
|
|
@@ -1784,17 +2036,40 @@ var DurableStreamTestServer = class {
|
|
|
1784
2036
|
let { messages, upToDate } = this.store.read(path, effectiveOffset);
|
|
1785
2037
|
const clientIsCaughtUp = effectiveOffset && effectiveOffset === stream.currentOffset || offset === `now`;
|
|
1786
2038
|
if (live === `long-poll` && clientIsCaughtUp && messages.length === 0) {
|
|
2039
|
+
if (stream.closed) {
|
|
2040
|
+
res.writeHead(204, {
|
|
2041
|
+
[STREAM_OFFSET_HEADER]: stream.currentOffset,
|
|
2042
|
+
[STREAM_UP_TO_DATE_HEADER]: `true`,
|
|
2043
|
+
[STREAM_CLOSED_HEADER]: `true`
|
|
2044
|
+
});
|
|
2045
|
+
res.end();
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
1787
2048
|
const result = await this.store.waitForMessages(path, effectiveOffset ?? stream.currentOffset, this.options.longPollTimeout);
|
|
1788
|
-
if (result.
|
|
2049
|
+
if (result.streamClosed) {
|
|
1789
2050
|
const responseCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
1790
2051
|
res.writeHead(204, {
|
|
1791
2052
|
[STREAM_OFFSET_HEADER]: effectiveOffset ?? stream.currentOffset,
|
|
1792
2053
|
[STREAM_UP_TO_DATE_HEADER]: `true`,
|
|
1793
|
-
[STREAM_CURSOR_HEADER]: responseCursor
|
|
2054
|
+
[STREAM_CURSOR_HEADER]: responseCursor,
|
|
2055
|
+
[STREAM_CLOSED_HEADER]: `true`
|
|
1794
2056
|
});
|
|
1795
2057
|
res.end();
|
|
1796
2058
|
return;
|
|
1797
2059
|
}
|
|
2060
|
+
if (result.timedOut) {
|
|
2061
|
+
const responseCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
2062
|
+
const currentStream$1 = this.store.get(path);
|
|
2063
|
+
const timeoutHeaders = {
|
|
2064
|
+
[STREAM_OFFSET_HEADER]: effectiveOffset ?? stream.currentOffset,
|
|
2065
|
+
[STREAM_UP_TO_DATE_HEADER]: `true`,
|
|
2066
|
+
[STREAM_CURSOR_HEADER]: responseCursor
|
|
2067
|
+
};
|
|
2068
|
+
if (currentStream$1?.closed) timeoutHeaders[STREAM_CLOSED_HEADER] = `true`;
|
|
2069
|
+
res.writeHead(204, timeoutHeaders);
|
|
2070
|
+
res.end();
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
1798
2073
|
messages = result.messages;
|
|
1799
2074
|
upToDate = true;
|
|
1800
2075
|
}
|
|
@@ -1805,8 +2080,12 @@ var DurableStreamTestServer = class {
|
|
|
1805
2080
|
headers[STREAM_OFFSET_HEADER] = responseOffset;
|
|
1806
2081
|
if (live === `long-poll`) headers[STREAM_CURSOR_HEADER] = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
1807
2082
|
if (upToDate) headers[STREAM_UP_TO_DATE_HEADER] = `true`;
|
|
2083
|
+
const currentStream = this.store.get(path);
|
|
2084
|
+
const clientAtTail = responseOffset === currentStream?.currentOffset;
|
|
2085
|
+
if (currentStream?.closed && clientAtTail && upToDate) headers[STREAM_CLOSED_HEADER] = `true`;
|
|
1808
2086
|
const startOffset = offset ?? `-1`;
|
|
1809
|
-
const
|
|
2087
|
+
const closedSuffix = currentStream?.closed && clientAtTail && upToDate ? `:c` : ``;
|
|
2088
|
+
const etag = `"${Buffer.from(path).toString(`base64`)}:${startOffset}:${responseOffset}${closedSuffix}"`;
|
|
1810
2089
|
headers[`etag`] = etag;
|
|
1811
2090
|
const ifNoneMatch = req.headers[`if-none-match`];
|
|
1812
2091
|
if (ifNoneMatch && ifNoneMatch === etag) {
|
|
@@ -1818,10 +2097,10 @@ var DurableStreamTestServer = class {
|
|
|
1818
2097
|
let finalData = responseData;
|
|
1819
2098
|
if (this.options.compression && responseData.length >= COMPRESSION_THRESHOLD) {
|
|
1820
2099
|
const acceptEncoding = req.headers[`accept-encoding`];
|
|
1821
|
-
const
|
|
1822
|
-
if (
|
|
1823
|
-
finalData = compressData(responseData,
|
|
1824
|
-
headers[`content-encoding`] =
|
|
2100
|
+
const compressionEncoding = getCompressionEncoding(acceptEncoding);
|
|
2101
|
+
if (compressionEncoding) {
|
|
2102
|
+
finalData = compressData(responseData, compressionEncoding);
|
|
2103
|
+
headers[`content-encoding`] = compressionEncoding;
|
|
1825
2104
|
headers[`vary`] = `accept-encoding`;
|
|
1826
2105
|
}
|
|
1827
2106
|
}
|
|
@@ -1832,16 +2111,18 @@ var DurableStreamTestServer = class {
|
|
|
1832
2111
|
/**
|
|
1833
2112
|
* Handle SSE (Server-Sent Events) mode
|
|
1834
2113
|
*/
|
|
1835
|
-
async handleSSE(path, stream, initialOffset, cursor, res) {
|
|
2114
|
+
async handleSSE(path, stream, initialOffset, cursor, useBase64, res) {
|
|
1836
2115
|
this.activeSSEResponses.add(res);
|
|
1837
|
-
|
|
2116
|
+
const sseHeaders = {
|
|
1838
2117
|
"content-type": `text/event-stream`,
|
|
1839
2118
|
"cache-control": `no-cache`,
|
|
1840
2119
|
connection: `keep-alive`,
|
|
1841
2120
|
"access-control-allow-origin": `*`,
|
|
1842
2121
|
"x-content-type-options": `nosniff`,
|
|
1843
2122
|
"cross-origin-resource-policy": `cross-origin`
|
|
1844
|
-
}
|
|
2123
|
+
};
|
|
2124
|
+
if (useBase64) sseHeaders[STREAM_SSE_DATA_ENCODING_HEADER] = `base64`;
|
|
2125
|
+
res.writeHead(200, sseHeaders);
|
|
1845
2126
|
const fault = res._injectedFault;
|
|
1846
2127
|
if (fault?.injectSseEvent) {
|
|
1847
2128
|
res.write(`event: ${fault.injectSseEvent.eventType}\n`);
|
|
@@ -1859,7 +2140,8 @@ var DurableStreamTestServer = class {
|
|
|
1859
2140
|
const { messages, upToDate } = this.store.read(path, currentOffset);
|
|
1860
2141
|
for (const message of messages) {
|
|
1861
2142
|
let dataPayload;
|
|
1862
|
-
if (
|
|
2143
|
+
if (useBase64) dataPayload = Buffer.from(message.data).toString(`base64`);
|
|
2144
|
+
else if (isJsonStream) {
|
|
1863
2145
|
const jsonBytes = this.store.formatResponse(path, [message]);
|
|
1864
2146
|
dataPayload = decoder.decode(jsonBytes);
|
|
1865
2147
|
} else dataPayload = decoder.decode(message.data);
|
|
@@ -1867,28 +2149,60 @@ var DurableStreamTestServer = class {
|
|
|
1867
2149
|
res.write(encodeSSEData(dataPayload));
|
|
1868
2150
|
currentOffset = message.offset;
|
|
1869
2151
|
}
|
|
1870
|
-
const
|
|
2152
|
+
const currentStream = this.store.get(path);
|
|
2153
|
+
const controlOffset = messages[messages.length - 1]?.offset ?? currentStream.currentOffset;
|
|
2154
|
+
const streamIsClosed = currentStream?.closed ?? false;
|
|
2155
|
+
const clientAtTail = controlOffset === currentStream.currentOffset;
|
|
1871
2156
|
const responseCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
1872
|
-
const controlData = {
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2157
|
+
const controlData = { [SSE_OFFSET_FIELD]: controlOffset };
|
|
2158
|
+
if (streamIsClosed && clientAtTail) controlData[SSE_CLOSED_FIELD] = true;
|
|
2159
|
+
else {
|
|
2160
|
+
controlData[SSE_CURSOR_FIELD] = responseCursor;
|
|
2161
|
+
if (upToDate) controlData[SSE_UP_TO_DATE_FIELD] = true;
|
|
2162
|
+
}
|
|
1877
2163
|
res.write(`event: control\n`);
|
|
1878
2164
|
res.write(encodeSSEData(JSON.stringify(controlData)));
|
|
2165
|
+
if (streamIsClosed && clientAtTail) break;
|
|
1879
2166
|
currentOffset = controlOffset;
|
|
1880
2167
|
if (upToDate) {
|
|
2168
|
+
if (currentStream?.closed) {
|
|
2169
|
+
const finalControlData = {
|
|
2170
|
+
[SSE_OFFSET_FIELD]: currentOffset,
|
|
2171
|
+
[SSE_CLOSED_FIELD]: true
|
|
2172
|
+
};
|
|
2173
|
+
res.write(`event: control\n`);
|
|
2174
|
+
res.write(encodeSSEData(JSON.stringify(finalControlData)));
|
|
2175
|
+
break;
|
|
2176
|
+
}
|
|
1881
2177
|
const result = await this.store.waitForMessages(path, currentOffset, this.options.longPollTimeout);
|
|
1882
2178
|
if (this.isShuttingDown || !isConnected) break;
|
|
2179
|
+
if (result.streamClosed) {
|
|
2180
|
+
const finalControlData = {
|
|
2181
|
+
[SSE_OFFSET_FIELD]: currentOffset,
|
|
2182
|
+
[SSE_CLOSED_FIELD]: true
|
|
2183
|
+
};
|
|
2184
|
+
res.write(`event: control\n`);
|
|
2185
|
+
res.write(encodeSSEData(JSON.stringify(finalControlData)));
|
|
2186
|
+
break;
|
|
2187
|
+
}
|
|
1883
2188
|
if (result.timedOut) {
|
|
1884
2189
|
const keepAliveCursor = generateResponseCursor(cursor, this.options.cursorOptions);
|
|
2190
|
+
const streamAfterWait = this.store.get(path);
|
|
2191
|
+
if (streamAfterWait?.closed) {
|
|
2192
|
+
const closedControlData = {
|
|
2193
|
+
[SSE_OFFSET_FIELD]: currentOffset,
|
|
2194
|
+
[SSE_CLOSED_FIELD]: true
|
|
2195
|
+
};
|
|
2196
|
+
res.write(`event: control\n`);
|
|
2197
|
+
res.write(encodeSSEData(JSON.stringify(closedControlData)));
|
|
2198
|
+
break;
|
|
2199
|
+
}
|
|
1885
2200
|
const keepAliveData = {
|
|
1886
2201
|
[SSE_OFFSET_FIELD]: currentOffset,
|
|
1887
2202
|
[SSE_CURSOR_FIELD]: keepAliveCursor,
|
|
1888
2203
|
[SSE_UP_TO_DATE_FIELD]: true
|
|
1889
2204
|
};
|
|
1890
|
-
res.write(`event: control\n`);
|
|
1891
|
-
res.write(encodeSSEData(JSON.stringify(keepAliveData)));
|
|
2205
|
+
res.write(`event: control\n` + encodeSSEData(JSON.stringify(keepAliveData)));
|
|
1892
2206
|
}
|
|
1893
2207
|
}
|
|
1894
2208
|
}
|
|
@@ -1901,20 +2215,11 @@ var DurableStreamTestServer = class {
|
|
|
1901
2215
|
async handleAppend(path, req, res) {
|
|
1902
2216
|
const contentType = req.headers[`content-type`];
|
|
1903
2217
|
const seq = req.headers[STREAM_SEQ_HEADER.toLowerCase()];
|
|
2218
|
+
const closedHeader = req.headers[STREAM_CLOSED_HEADER.toLowerCase()];
|
|
2219
|
+
const closeStream = closedHeader === `true`;
|
|
1904
2220
|
const producerId = req.headers[PRODUCER_ID_HEADER.toLowerCase()];
|
|
1905
2221
|
const producerEpochStr = req.headers[PRODUCER_EPOCH_HEADER.toLowerCase()];
|
|
1906
2222
|
const producerSeqStr = req.headers[PRODUCER_SEQ_HEADER.toLowerCase()];
|
|
1907
|
-
const body = await this.readBody(req);
|
|
1908
|
-
if (body.length === 0) {
|
|
1909
|
-
res.writeHead(400, { "content-type": `text/plain` });
|
|
1910
|
-
res.end(`Empty body`);
|
|
1911
|
-
return;
|
|
1912
|
-
}
|
|
1913
|
-
if (!contentType) {
|
|
1914
|
-
res.writeHead(400, { "content-type": `text/plain` });
|
|
1915
|
-
res.end(`Content-Type header is required`);
|
|
1916
|
-
return;
|
|
1917
|
-
}
|
|
1918
2223
|
const hasProducerHeaders = producerId !== void 0 || producerEpochStr !== void 0 || producerSeqStr !== void 0;
|
|
1919
2224
|
const hasAllProducerHeaders = producerId !== void 0 && producerEpochStr !== void 0 && producerSeqStr !== void 0;
|
|
1920
2225
|
if (hasProducerHeaders && !hasAllProducerHeaders) {
|
|
@@ -1954,34 +2259,148 @@ var DurableStreamTestServer = class {
|
|
|
1954
2259
|
return;
|
|
1955
2260
|
}
|
|
1956
2261
|
}
|
|
2262
|
+
const body = await this.readBody(req);
|
|
2263
|
+
if (body.length === 0 && closeStream) {
|
|
2264
|
+
if (hasAllProducerHeaders) {
|
|
2265
|
+
const closeResult$1 = await this.store.closeStreamWithProducer(path, {
|
|
2266
|
+
producerId,
|
|
2267
|
+
producerEpoch,
|
|
2268
|
+
producerSeq
|
|
2269
|
+
});
|
|
2270
|
+
if (!closeResult$1) {
|
|
2271
|
+
res.writeHead(404, { "content-type": `text/plain` });
|
|
2272
|
+
res.end(`Stream not found`);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
if (closeResult$1.producerResult?.status === `duplicate`) {
|
|
2276
|
+
res.writeHead(204, {
|
|
2277
|
+
[STREAM_OFFSET_HEADER]: closeResult$1.finalOffset,
|
|
2278
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2279
|
+
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
2280
|
+
[PRODUCER_SEQ_HEADER]: closeResult$1.producerResult.lastSeq.toString()
|
|
2281
|
+
});
|
|
2282
|
+
res.end();
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
if (closeResult$1.producerResult?.status === `stale_epoch`) {
|
|
2286
|
+
res.writeHead(403, {
|
|
2287
|
+
"content-type": `text/plain`,
|
|
2288
|
+
[PRODUCER_EPOCH_HEADER]: closeResult$1.producerResult.currentEpoch.toString()
|
|
2289
|
+
});
|
|
2290
|
+
res.end(`Stale producer epoch`);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (closeResult$1.producerResult?.status === `invalid_epoch_seq`) {
|
|
2294
|
+
res.writeHead(400, { "content-type": `text/plain` });
|
|
2295
|
+
res.end(`New epoch must start with sequence 0`);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
if (closeResult$1.producerResult?.status === `sequence_gap`) {
|
|
2299
|
+
res.writeHead(409, {
|
|
2300
|
+
"content-type": `text/plain`,
|
|
2301
|
+
[PRODUCER_EXPECTED_SEQ_HEADER]: closeResult$1.producerResult.expectedSeq.toString(),
|
|
2302
|
+
[PRODUCER_RECEIVED_SEQ_HEADER]: closeResult$1.producerResult.receivedSeq.toString()
|
|
2303
|
+
});
|
|
2304
|
+
res.end(`Producer sequence gap`);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (closeResult$1.producerResult?.status === `stream_closed`) {
|
|
2308
|
+
const stream = this.store.get(path);
|
|
2309
|
+
res.writeHead(409, {
|
|
2310
|
+
"content-type": `text/plain`,
|
|
2311
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2312
|
+
[STREAM_OFFSET_HEADER]: stream?.currentOffset ?? ``
|
|
2313
|
+
});
|
|
2314
|
+
res.end(`Stream is closed`);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
res.writeHead(204, {
|
|
2318
|
+
[STREAM_OFFSET_HEADER]: closeResult$1.finalOffset,
|
|
2319
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2320
|
+
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
2321
|
+
[PRODUCER_SEQ_HEADER]: producerSeq.toString()
|
|
2322
|
+
});
|
|
2323
|
+
res.end();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
const closeResult = this.store.closeStream(path);
|
|
2327
|
+
if (!closeResult) {
|
|
2328
|
+
res.writeHead(404, { "content-type": `text/plain` });
|
|
2329
|
+
res.end(`Stream not found`);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
res.writeHead(204, {
|
|
2333
|
+
[STREAM_OFFSET_HEADER]: closeResult.finalOffset,
|
|
2334
|
+
[STREAM_CLOSED_HEADER]: `true`
|
|
2335
|
+
});
|
|
2336
|
+
res.end();
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
if (body.length === 0) {
|
|
2340
|
+
res.writeHead(400, { "content-type": `text/plain` });
|
|
2341
|
+
res.end(`Empty body`);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (!contentType) {
|
|
2345
|
+
res.writeHead(400, { "content-type": `text/plain` });
|
|
2346
|
+
res.end(`Content-Type header is required`);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
1957
2349
|
const appendOptions = {
|
|
1958
2350
|
seq,
|
|
1959
2351
|
contentType,
|
|
1960
2352
|
producerId,
|
|
1961
2353
|
producerEpoch,
|
|
1962
|
-
producerSeq
|
|
2354
|
+
producerSeq,
|
|
2355
|
+
close: closeStream
|
|
1963
2356
|
};
|
|
1964
2357
|
let result;
|
|
1965
2358
|
if (producerId !== void 0) result = await this.store.appendWithProducer(path, body, appendOptions);
|
|
1966
2359
|
else result = await Promise.resolve(this.store.append(path, body, appendOptions));
|
|
1967
|
-
if (result && typeof result === `object` && `
|
|
1968
|
-
const { message: message$1, producerResult } = result;
|
|
2360
|
+
if (result && typeof result === `object` && `message` in result) {
|
|
2361
|
+
const { message: message$1, producerResult, streamClosed } = result;
|
|
2362
|
+
if (streamClosed && !message$1) {
|
|
2363
|
+
if (producerResult?.status === `duplicate`) {
|
|
2364
|
+
const stream = this.store.get(path);
|
|
2365
|
+
res.writeHead(204, {
|
|
2366
|
+
[STREAM_OFFSET_HEADER]: stream?.currentOffset ?? ``,
|
|
2367
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2368
|
+
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
2369
|
+
[PRODUCER_SEQ_HEADER]: producerResult.lastSeq.toString()
|
|
2370
|
+
});
|
|
2371
|
+
res.end();
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
const closedStream = this.store.get(path);
|
|
2375
|
+
res.writeHead(409, {
|
|
2376
|
+
"content-type": `text/plain`,
|
|
2377
|
+
[STREAM_CLOSED_HEADER]: `true`,
|
|
2378
|
+
[STREAM_OFFSET_HEADER]: closedStream?.currentOffset ?? ``
|
|
2379
|
+
});
|
|
2380
|
+
res.end(`Stream is closed`);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
1969
2383
|
if (!producerResult || producerResult.status === `accepted`) {
|
|
1970
|
-
const responseHeaders = { [STREAM_OFFSET_HEADER]: message$1.offset };
|
|
1971
|
-
if (producerEpoch !== void 0) responseHeaders[PRODUCER_EPOCH_HEADER] = producerEpoch.toString();
|
|
1972
|
-
if (producerSeq !== void 0) responseHeaders[PRODUCER_SEQ_HEADER] = producerSeq.toString();
|
|
1973
|
-
|
|
2384
|
+
const responseHeaders$1 = { [STREAM_OFFSET_HEADER]: message$1.offset };
|
|
2385
|
+
if (producerEpoch !== void 0) responseHeaders$1[PRODUCER_EPOCH_HEADER] = producerEpoch.toString();
|
|
2386
|
+
if (producerSeq !== void 0) responseHeaders$1[PRODUCER_SEQ_HEADER] = producerSeq.toString();
|
|
2387
|
+
if (streamClosed) responseHeaders$1[STREAM_CLOSED_HEADER] = `true`;
|
|
2388
|
+
const statusCode = producerId !== void 0 ? 200 : 204;
|
|
2389
|
+
res.writeHead(statusCode, responseHeaders$1);
|
|
1974
2390
|
res.end();
|
|
1975
2391
|
return;
|
|
1976
2392
|
}
|
|
1977
2393
|
switch (producerResult.status) {
|
|
1978
|
-
case `duplicate`:
|
|
1979
|
-
|
|
2394
|
+
case `duplicate`: {
|
|
2395
|
+
const dupHeaders = {
|
|
1980
2396
|
[PRODUCER_EPOCH_HEADER]: producerEpoch.toString(),
|
|
1981
2397
|
[PRODUCER_SEQ_HEADER]: producerResult.lastSeq.toString()
|
|
1982
|
-
}
|
|
2398
|
+
};
|
|
2399
|
+
if (streamClosed) dupHeaders[STREAM_CLOSED_HEADER] = `true`;
|
|
2400
|
+
res.writeHead(204, dupHeaders);
|
|
1983
2401
|
res.end();
|
|
1984
2402
|
return;
|
|
2403
|
+
}
|
|
1985
2404
|
case `stale_epoch`: {
|
|
1986
2405
|
res.writeHead(403, {
|
|
1987
2406
|
"content-type": `text/plain`,
|
|
@@ -2005,7 +2424,9 @@ var DurableStreamTestServer = class {
|
|
|
2005
2424
|
}
|
|
2006
2425
|
}
|
|
2007
2426
|
const message = result;
|
|
2008
|
-
|
|
2427
|
+
const responseHeaders = { [STREAM_OFFSET_HEADER]: message.offset };
|
|
2428
|
+
if (closeStream) responseHeaders[STREAM_CLOSED_HEADER] = `true`;
|
|
2429
|
+
res.writeHead(204, responseHeaders);
|
|
2009
2430
|
res.end();
|
|
2010
2431
|
}
|
|
2011
2432
|
/**
|