@crvouga/mockingbird-service-junction 0.5.0 → 0.7.0
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/CHANGELOG.md +12 -4
- package/dist/{chunk-ICINEQ7D.js → chunk-MS774AJA.js} +526 -46
- package/dist/chunk-MS774AJA.js.map +7 -0
- package/dist/{chunk-CR35KYXY.js → chunk-TG2EOH3J.js} +2 -2
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +136 -22
- package/dist/index.js +1 -1
- package/dist/server.d.ts +267 -44
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-ICINEQ7D.js.map +0 -7
- /package/dist/{chunk-CR35KYXY.js.map → chunk-TG2EOH3J.js.map} +0 -0
|
@@ -247,20 +247,66 @@ var createControlPlane = (context) => {
|
|
|
247
247
|
return context.faults.remove(id) ? json(200, { status: "ok" }) : adminError(404, `no fault ${id}`);
|
|
248
248
|
},
|
|
249
249
|
"POST /snapshots": ({ namespace }) => {
|
|
250
|
-
const
|
|
250
|
+
const point = context.timeTravel.checkpoint(namespace, "main");
|
|
251
|
+
context.timeTravel.retain(namespace, point.id);
|
|
251
252
|
snapshotCounter++;
|
|
252
253
|
const id = `snap_${snapshotCounter}`;
|
|
253
|
-
snapshots.set(id,
|
|
254
|
-
return json(201, { id, namespace, records:
|
|
254
|
+
snapshots.set(id, { namespace, checkpoint: point.id });
|
|
255
|
+
return json(201, { id, namespace, records: point.records ?? 0 });
|
|
255
256
|
},
|
|
256
257
|
"POST /snapshots/:id/restore": ({ params, namespace }) => {
|
|
257
|
-
const
|
|
258
|
-
if (!
|
|
258
|
+
const alias = snapshots.get(params.id);
|
|
259
|
+
if (!alias)
|
|
259
260
|
return adminError(404, `no snapshot ${params.id}`);
|
|
260
|
-
|
|
261
|
+
if (alias.namespace !== namespace) {
|
|
262
|
+
return adminError(409, `snapshot ${params.id} belongs to namespace ${alias.namespace}`);
|
|
263
|
+
}
|
|
264
|
+
context.timeTravel.checkout(alias.checkpoint, { namespace, branch: "main" });
|
|
261
265
|
return json(200, { status: "ok", id: params.id, namespace });
|
|
262
266
|
},
|
|
263
|
-
"DELETE /snapshots/:id": ({ params }) =>
|
|
267
|
+
"DELETE /snapshots/:id": ({ params }) => {
|
|
268
|
+
const id = params.id;
|
|
269
|
+
const alias = snapshots.get(id);
|
|
270
|
+
if (!alias)
|
|
271
|
+
return adminError(404, `no snapshot ${id}`);
|
|
272
|
+
snapshots.delete(id);
|
|
273
|
+
context.timeTravel.release(alias.namespace, alias.checkpoint);
|
|
274
|
+
return json(200, { status: "ok" });
|
|
275
|
+
},
|
|
276
|
+
"GET /timeline": ({ namespace }) => json(200, context.timeTravel.inspect(namespace)),
|
|
277
|
+
"POST /checkpoints": ({ body, namespace }) => {
|
|
278
|
+
const branch = isRecord(body) && typeof body.branch === "string" ? body.branch : "main";
|
|
279
|
+
try {
|
|
280
|
+
return json(201, context.timeTravel.checkpoint(namespace, branch));
|
|
281
|
+
} catch (error) {
|
|
282
|
+
return adminError(409, error instanceof Error ? error.message : String(error));
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
"POST /branches/:name": ({ params, body, namespace }) => {
|
|
286
|
+
const at = isRecord(body) && typeof body.at === "string" ? body.at : void 0;
|
|
287
|
+
try {
|
|
288
|
+
return json(201, context.timeTravel.branch(params.name, {
|
|
289
|
+
namespace,
|
|
290
|
+
...at !== void 0 ? { at } : {}
|
|
291
|
+
}));
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return adminError(409, error instanceof Error ? error.message : String(error));
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
"POST /branches/:name/checkout": ({ params, body, namespace }) => {
|
|
297
|
+
if (!isRecord(body) || typeof body.checkpoint !== "string") {
|
|
298
|
+
return adminError(400, 'expected {"checkpoint":"cp_..."}');
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
context.timeTravel.checkout(body.checkpoint, {
|
|
302
|
+
namespace,
|
|
303
|
+
branch: params.name
|
|
304
|
+
});
|
|
305
|
+
return json(200, { status: "ok", branch: params.name, checkpoint: body.checkpoint });
|
|
306
|
+
} catch (error) {
|
|
307
|
+
return adminError(409, error instanceof Error ? error.message : String(error));
|
|
308
|
+
}
|
|
309
|
+
},
|
|
264
310
|
"GET /requests": ({ url, namespace }) => {
|
|
265
311
|
const status = url.searchParams.get("status");
|
|
266
312
|
const since = url.searchParams.get("since");
|
|
@@ -308,7 +354,7 @@ var createControlPlane = (context) => {
|
|
|
308
354
|
return json(200, {
|
|
309
355
|
status: "ok",
|
|
310
356
|
service: context.name,
|
|
311
|
-
uptimeMs:
|
|
357
|
+
uptimeMs: context.wallNow() - context.startedAt,
|
|
312
358
|
clock: context.clock.state(),
|
|
313
359
|
namespaces: context.namespaces().length,
|
|
314
360
|
...context.describe()
|
|
@@ -386,6 +432,13 @@ var createRng = (seed = 0) => {
|
|
|
386
432
|
reset: () => {
|
|
387
433
|
state = numeric;
|
|
388
434
|
},
|
|
435
|
+
state: () => state,
|
|
436
|
+
setState: (next2) => {
|
|
437
|
+
if (!Number.isSafeInteger(next2) || next2 < 0 || next2 > 4294967295) {
|
|
438
|
+
throw new RangeError("rng state must be an unsigned 32-bit integer");
|
|
439
|
+
}
|
|
440
|
+
state = next2 >>> 0;
|
|
441
|
+
},
|
|
389
442
|
seed: numeric
|
|
390
443
|
};
|
|
391
444
|
};
|
|
@@ -414,7 +467,7 @@ var faultResponse = (rule) => {
|
|
|
414
467
|
const body = rule.body === void 0 ? { detail: "Injected by Mockingbird" } : rule.body;
|
|
415
468
|
return new Response(JSON.stringify(body), { status, headers });
|
|
416
469
|
};
|
|
417
|
-
var createFaultRegistry = (rng = createRng(0)) => {
|
|
470
|
+
var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
|
|
418
471
|
const entries = [];
|
|
419
472
|
return {
|
|
420
473
|
add(rule) {
|
|
@@ -452,7 +505,7 @@ var createFaultRegistry = (rng = createRng(0)) => {
|
|
|
452
505
|
entry.remaining--;
|
|
453
506
|
const delay = entry.rule.delayMs ?? entry.rule.latencyMs;
|
|
454
507
|
if (delay !== void 0 && delay > 0) {
|
|
455
|
-
await
|
|
508
|
+
await sleep(delay);
|
|
456
509
|
}
|
|
457
510
|
const hit = { id: entry.rule.id };
|
|
458
511
|
if (entry.rule.effect !== void 0) {
|
|
@@ -861,6 +914,167 @@ var createMetrics = () => {
|
|
|
861
914
|
};
|
|
862
915
|
};
|
|
863
916
|
|
|
917
|
+
// ../../core/dist/timeline.js
|
|
918
|
+
var BRANCH_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
919
|
+
var Timeline = class {
|
|
920
|
+
maxCheckpoints;
|
|
921
|
+
now;
|
|
922
|
+
makeId;
|
|
923
|
+
nodes = /* @__PURE__ */ new Map();
|
|
924
|
+
heads = /* @__PURE__ */ new Map();
|
|
925
|
+
/** Unreferenced nodes in the exact order they became collectible. */
|
|
926
|
+
evictable = /* @__PURE__ */ new Set();
|
|
927
|
+
/** Branch heads plus explicit retainers. Absent means zero. */
|
|
928
|
+
references = /* @__PURE__ */ new Map();
|
|
929
|
+
explicitPins = /* @__PURE__ */ new Map();
|
|
930
|
+
sequence = 0;
|
|
931
|
+
constructor(options = {}) {
|
|
932
|
+
const max = options.maxCheckpoints ?? 1e3;
|
|
933
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
934
|
+
throw new RangeError("maxCheckpoints must be a positive integer");
|
|
935
|
+
this.maxCheckpoints = max;
|
|
936
|
+
this.now = options.now ?? (() => this.sequence);
|
|
937
|
+
this.makeId = options.id ?? ((sequence) => `cp_${sequence.toString(36).padStart(8, "0")}`);
|
|
938
|
+
}
|
|
939
|
+
/** Capture a new immutable value and move `branch` to it. */
|
|
940
|
+
commit(value, options = {}) {
|
|
941
|
+
const branch = options.branch ?? "main";
|
|
942
|
+
this.assertBranch(branch);
|
|
943
|
+
const parent = options.parent === void 0 ? this.heads.get(branch) ?? null : options.parent;
|
|
944
|
+
if (parent !== null && !this.nodes.has(parent))
|
|
945
|
+
throw new RangeError(`no checkpoint ${parent}`);
|
|
946
|
+
const id = this.makeId(++this.sequence);
|
|
947
|
+
if (this.nodes.has(id))
|
|
948
|
+
throw new RangeError(`duplicate checkpoint id ${id}`);
|
|
949
|
+
const checkpoint = Object.freeze({ id, branch, parent, at: this.now(), value });
|
|
950
|
+
this.nodes.set(id, checkpoint);
|
|
951
|
+
this.moveHead(branch, id);
|
|
952
|
+
this.collect(this.maxCheckpoints);
|
|
953
|
+
return checkpoint;
|
|
954
|
+
}
|
|
955
|
+
/** Create a branch pointer without copying its checkpoint value. */
|
|
956
|
+
fork(branch, options = {}) {
|
|
957
|
+
this.assertBranch(branch);
|
|
958
|
+
if (this.heads.has(branch))
|
|
959
|
+
throw new RangeError(`branch already exists: ${branch}`);
|
|
960
|
+
const from = options.from ?? this.heads.get("main");
|
|
961
|
+
if (from === void 0)
|
|
962
|
+
return void 0;
|
|
963
|
+
const checkpoint = this.get(from);
|
|
964
|
+
this.moveHead(branch, checkpoint.id);
|
|
965
|
+
return checkpoint;
|
|
966
|
+
}
|
|
967
|
+
/** Move a branch pointer to an existing checkpoint. */
|
|
968
|
+
checkout(branch, id) {
|
|
969
|
+
this.assertBranch(branch);
|
|
970
|
+
const checkpoint = this.get(id);
|
|
971
|
+
this.moveHead(branch, checkpoint.id);
|
|
972
|
+
return checkpoint;
|
|
973
|
+
}
|
|
974
|
+
get(id) {
|
|
975
|
+
const checkpoint = this.nodes.get(id);
|
|
976
|
+
if (!checkpoint)
|
|
977
|
+
throw new RangeError(`no checkpoint ${id}`);
|
|
978
|
+
return checkpoint;
|
|
979
|
+
}
|
|
980
|
+
head(branch = "main") {
|
|
981
|
+
const id = this.heads.get(branch);
|
|
982
|
+
return id === void 0 ? void 0 : this.get(id);
|
|
983
|
+
}
|
|
984
|
+
hasBranch(branch) {
|
|
985
|
+
return this.heads.has(branch);
|
|
986
|
+
}
|
|
987
|
+
branches() {
|
|
988
|
+
return Object.freeze(Object.fromEntries([...this.heads].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)));
|
|
989
|
+
}
|
|
990
|
+
checkpoints() {
|
|
991
|
+
return [...this.nodes.values()];
|
|
992
|
+
}
|
|
993
|
+
/** Number of retained checkpoints without allocating an array. */
|
|
994
|
+
get size() {
|
|
995
|
+
return this.nodes.size;
|
|
996
|
+
}
|
|
997
|
+
/** Pin a checkpoint independently of branch heads (used by compatibility snapshot handles). */
|
|
998
|
+
retain(id) {
|
|
999
|
+
const checkpoint = this.get(id);
|
|
1000
|
+
this.explicitPins.set(id, (this.explicitPins.get(id) ?? 0) + 1);
|
|
1001
|
+
this.addReference(id);
|
|
1002
|
+
return checkpoint;
|
|
1003
|
+
}
|
|
1004
|
+
/** Release one explicit pin. Branch heads remain pinned until moved or deleted. */
|
|
1005
|
+
release(id) {
|
|
1006
|
+
if (!this.nodes.has(id))
|
|
1007
|
+
return false;
|
|
1008
|
+
const pins = this.explicitPins.get(id) ?? 0;
|
|
1009
|
+
if (pins === 0)
|
|
1010
|
+
return false;
|
|
1011
|
+
if (pins === 1)
|
|
1012
|
+
this.explicitPins.delete(id);
|
|
1013
|
+
else
|
|
1014
|
+
this.explicitPins.set(id, pins - 1);
|
|
1015
|
+
this.removeReference(id);
|
|
1016
|
+
this.collect(this.maxCheckpoints);
|
|
1017
|
+
return true;
|
|
1018
|
+
}
|
|
1019
|
+
deleteBranch(branch) {
|
|
1020
|
+
if (branch === "main")
|
|
1021
|
+
throw new RangeError("cannot delete main branch");
|
|
1022
|
+
const previous = this.heads.get(branch);
|
|
1023
|
+
const deleted = this.heads.delete(branch);
|
|
1024
|
+
if (previous !== void 0)
|
|
1025
|
+
this.removeReference(previous);
|
|
1026
|
+
this.collect(this.maxCheckpoints);
|
|
1027
|
+
return deleted;
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Deterministically discard oldest unpinned checkpoints. Collection is O(number removed):
|
|
1031
|
+
* commits never scan pinned nodes or the retained history. Parents are metadata rather than a
|
|
1032
|
+
* storage dependency, so a retained node remains usable after pruning.
|
|
1033
|
+
*/
|
|
1034
|
+
gc(max = this.maxCheckpoints) {
|
|
1035
|
+
if (!Number.isSafeInteger(max) || max < 1)
|
|
1036
|
+
throw new RangeError("max must be a positive integer");
|
|
1037
|
+
const removed = [];
|
|
1038
|
+
this.collect(max, removed);
|
|
1039
|
+
return removed;
|
|
1040
|
+
}
|
|
1041
|
+
collect(max, removed) {
|
|
1042
|
+
while (this.nodes.size > max && this.evictable.size > 0) {
|
|
1043
|
+
const id = this.evictable.values().next().value;
|
|
1044
|
+
this.evictable.delete(id);
|
|
1045
|
+
this.nodes.delete(id);
|
|
1046
|
+
removed?.push(id);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
moveHead(branch, id) {
|
|
1050
|
+
const previous = this.heads.get(branch);
|
|
1051
|
+
if (previous === id)
|
|
1052
|
+
return;
|
|
1053
|
+
if (previous !== void 0)
|
|
1054
|
+
this.removeReference(previous);
|
|
1055
|
+
this.heads.set(branch, id);
|
|
1056
|
+
this.addReference(id);
|
|
1057
|
+
}
|
|
1058
|
+
addReference(id) {
|
|
1059
|
+
this.references.set(id, (this.references.get(id) ?? 0) + 1);
|
|
1060
|
+
this.evictable.delete(id);
|
|
1061
|
+
}
|
|
1062
|
+
removeReference(id) {
|
|
1063
|
+
const next = (this.references.get(id) ?? 0) - 1;
|
|
1064
|
+
if (next > 0)
|
|
1065
|
+
this.references.set(id, next);
|
|
1066
|
+
else {
|
|
1067
|
+
this.references.delete(id);
|
|
1068
|
+
if (this.nodes.has(id))
|
|
1069
|
+
this.evictable.add(id);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
assertBranch(branch) {
|
|
1073
|
+
if (!BRANCH_PATTERN.test(branch))
|
|
1074
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN}`);
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
864
1078
|
// ../../sqlite/dist/default.js
|
|
865
1079
|
import { Database } from "@crvouga/mockingbird-service-sqlite";
|
|
866
1080
|
var createDefaultSqlite = () => new Database();
|
|
@@ -1082,7 +1296,7 @@ var restoreNamespace = (sqlite, namespace, snapshot) => {
|
|
|
1082
1296
|
};
|
|
1083
1297
|
|
|
1084
1298
|
// ../core/dist/version.js
|
|
1085
|
-
var PACKAGE_VERSION = true ? "0.
|
|
1299
|
+
var PACKAGE_VERSION = true ? "0.7.0" : UNRELEASED_VERSION;
|
|
1086
1300
|
|
|
1087
1301
|
// ../core/dist/webhooks.js
|
|
1088
1302
|
var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
@@ -1188,10 +1402,33 @@ var parsePayload = (message) => {
|
|
|
1188
1402
|
|
|
1189
1403
|
// ../core/dist/runtime.js
|
|
1190
1404
|
var MOCKINGBIRD_HEADER = "x-mockingbird";
|
|
1405
|
+
var BRANCH_HEADER = "x-mockingbird-branch";
|
|
1406
|
+
var AT_HEADER = "x-mockingbird-at";
|
|
1407
|
+
var CHECKPOINT_HEADER = "x-mockingbird-checkpoint";
|
|
1191
1408
|
var DEFAULT_NAMESPACE = "default";
|
|
1192
1409
|
var NAMESPACE_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1193
1410
|
var PATH_PREFIX = /^\/ns\/([^/]+)(\/.*)?$/;
|
|
1411
|
+
var BRANCH_PATTERN2 = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
1412
|
+
var MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1194
1413
|
var effects = /* @__PURE__ */ new WeakMap();
|
|
1414
|
+
var reuseSorted = (fresh, previous, compare, equal) => {
|
|
1415
|
+
if (!previous || previous.length === 0)
|
|
1416
|
+
return fresh.map((row) => Object.freeze(row));
|
|
1417
|
+
const result = new Array(fresh.length);
|
|
1418
|
+
let unchanged = fresh.length === previous.length;
|
|
1419
|
+
let oldIndex = 0;
|
|
1420
|
+
for (let index = 0; index < fresh.length; index++) {
|
|
1421
|
+
const row = fresh[index];
|
|
1422
|
+
while (oldIndex < previous.length && compare(previous[oldIndex], row) < 0) {
|
|
1423
|
+
oldIndex++;
|
|
1424
|
+
}
|
|
1425
|
+
const old = previous[oldIndex];
|
|
1426
|
+
result[index] = old !== void 0 && compare(old, row) === 0 && equal(old, row) ? old : Object.freeze(row);
|
|
1427
|
+
if (result[index] !== previous[index])
|
|
1428
|
+
unchanged = false;
|
|
1429
|
+
}
|
|
1430
|
+
return unchanged ? previous : result;
|
|
1431
|
+
};
|
|
1195
1432
|
var DroppedConnectionError = class extends TypeError {
|
|
1196
1433
|
code = "MOCKINGBIRD_DROP";
|
|
1197
1434
|
constructor() {
|
|
@@ -1212,36 +1449,173 @@ var createRuntime = (options) => {
|
|
|
1212
1449
|
const sqlite = bootSqlite(options.sqlite);
|
|
1213
1450
|
const clock = options.clock ?? createClock();
|
|
1214
1451
|
const rng = createRng(options.seed ?? 0);
|
|
1215
|
-
const
|
|
1452
|
+
const wallNow = options.io?.wallNow ?? Date.now;
|
|
1453
|
+
const monotonicNow = options.io?.monotonicNow ?? (() => performance.now());
|
|
1454
|
+
const sleep = options.io?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
1455
|
+
const faults = createFaultRegistry(createRng(options.seed ?? 0), sleep);
|
|
1216
1456
|
const metrics = createMetrics();
|
|
1217
1457
|
const journal = createJournal(options.journalSize ?? DEFAULT_JOURNAL_SIZE);
|
|
1218
1458
|
const version = options.version ?? PACKAGE_VERSION;
|
|
1219
1459
|
const instances = /* @__PURE__ */ new Map();
|
|
1460
|
+
const publicNamespaces = /* @__PURE__ */ new Set();
|
|
1461
|
+
const branchRngs = /* @__PURE__ */ new Map();
|
|
1462
|
+
const timelines = /* @__PURE__ */ new Map();
|
|
1463
|
+
const branchStorage = /* @__PURE__ */ new Map();
|
|
1464
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1220
1465
|
const credentials = createCredentialRegistry();
|
|
1221
1466
|
const operationIdFor = options.document ? operationMatcher(options.document) : () => void 0;
|
|
1222
1467
|
const storageNamespace = (name) => name === DEFAULT_NAMESPACE ? options.name : `${options.name}:${name}`;
|
|
1223
|
-
const
|
|
1224
|
-
const existing = instances.get(
|
|
1468
|
+
const instanceFor = (key, publicNamespace = key, isolatedRng) => {
|
|
1469
|
+
const existing = instances.get(key);
|
|
1225
1470
|
if (existing)
|
|
1226
1471
|
return existing;
|
|
1227
|
-
if (!NAMESPACE_PATTERN.test(
|
|
1228
|
-
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(
|
|
1472
|
+
if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
|
|
1473
|
+
throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
|
|
1229
1474
|
}
|
|
1230
1475
|
const created = options.create({
|
|
1231
|
-
namespace: storageNamespace(
|
|
1232
|
-
publicNamespace
|
|
1476
|
+
namespace: storageNamespace(key),
|
|
1477
|
+
publicNamespace,
|
|
1233
1478
|
sqlite,
|
|
1234
1479
|
clock,
|
|
1235
|
-
rng
|
|
1480
|
+
rng: isolatedRng ?? rng
|
|
1236
1481
|
});
|
|
1237
|
-
instances.set(
|
|
1482
|
+
instances.set(key, created);
|
|
1483
|
+
publicNamespaces.add(publicNamespace);
|
|
1484
|
+
if (isolatedRng)
|
|
1485
|
+
branchRngs.set(key, isolatedRng);
|
|
1238
1486
|
return created;
|
|
1239
1487
|
};
|
|
1488
|
+
const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
|
|
1489
|
+
const capture = (storage) => {
|
|
1490
|
+
const fresh = snapshotNamespace(sqlite, storageNamespace(storage));
|
|
1491
|
+
const previous = captured.get(storage);
|
|
1492
|
+
const snapshot2 = {
|
|
1493
|
+
namespace: fresh.namespace,
|
|
1494
|
+
records: reuseSorted(fresh.records, previous?.records, (left, right) => left.collection < right.collection ? -1 : left.collection > right.collection ? 1 : left.seq - right.seq, (left, right) => left.id === right.id && left.value === right.value),
|
|
1495
|
+
sequences: reuseSorted(fresh.sequences, previous?.sequences, (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : left.kind < right.kind ? -1 : left.kind > right.kind ? 1 : 0, (left, right) => left.value === right.value)
|
|
1496
|
+
};
|
|
1497
|
+
Object.freeze(snapshot2.records);
|
|
1498
|
+
Object.freeze(snapshot2.sequences);
|
|
1499
|
+
Object.freeze(snapshot2);
|
|
1500
|
+
captured.set(storage, snapshot2);
|
|
1501
|
+
return Object.freeze({
|
|
1502
|
+
snapshot: snapshot2,
|
|
1503
|
+
clock: Object.freeze(clock.state()),
|
|
1504
|
+
rngState: (branchRngs.get(storage) ?? rng).state()
|
|
1505
|
+
});
|
|
1506
|
+
};
|
|
1507
|
+
const timeline = (name = DEFAULT_NAMESPACE) => {
|
|
1508
|
+
let found = timelines.get(name);
|
|
1509
|
+
if (found)
|
|
1510
|
+
return found;
|
|
1511
|
+
instance(name);
|
|
1512
|
+
found = new Timeline({
|
|
1513
|
+
now: clock.now,
|
|
1514
|
+
...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
|
|
1515
|
+
});
|
|
1516
|
+
found.commit(capture(name));
|
|
1517
|
+
timelines.set(name, found);
|
|
1518
|
+
return found;
|
|
1519
|
+
};
|
|
1520
|
+
const physicalBranch = (namespace, branch2) => {
|
|
1521
|
+
if (branch2 === "main")
|
|
1522
|
+
return namespace;
|
|
1523
|
+
const mapKey = `${namespace}\0${branch2}`;
|
|
1524
|
+
const existing = branchStorage.get(mapKey);
|
|
1525
|
+
if (existing)
|
|
1526
|
+
return existing;
|
|
1527
|
+
const key = `branch_${seedFrom(`${options.name}\0${namespace}\0${branch2}`).toString(36)}`;
|
|
1528
|
+
branchStorage.set(mapKey, key);
|
|
1529
|
+
return key;
|
|
1530
|
+
};
|
|
1531
|
+
const ensureBranch = (namespace, branch2, at) => {
|
|
1532
|
+
if (!BRANCH_PATTERN2.test(branch2))
|
|
1533
|
+
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
1534
|
+
const history = timeline(namespace);
|
|
1535
|
+
if (branch2 === "main") {
|
|
1536
|
+
if (at !== void 0) {
|
|
1537
|
+
const point = history.checkout("main", at);
|
|
1538
|
+
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
1539
|
+
captured.set(namespace, point.value.snapshot);
|
|
1540
|
+
rng.setState(point.value.rngState);
|
|
1541
|
+
clock.set(point.value.clock.now);
|
|
1542
|
+
if (point.value.clock.frozen)
|
|
1543
|
+
clock.freeze();
|
|
1544
|
+
else
|
|
1545
|
+
clock.unfreeze();
|
|
1546
|
+
}
|
|
1547
|
+
return namespace;
|
|
1548
|
+
}
|
|
1549
|
+
const storage = physicalBranch(namespace, branch2);
|
|
1550
|
+
if (!history.hasBranch(branch2)) {
|
|
1551
|
+
if (at === void 0)
|
|
1552
|
+
history.commit(capture(namespace));
|
|
1553
|
+
const point = history.fork(branch2, at === void 0 ? {} : { from: at });
|
|
1554
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1555
|
+
if (point)
|
|
1556
|
+
branchRng.setState(point.value.rngState);
|
|
1557
|
+
instanceFor(storage, namespace, branchRng);
|
|
1558
|
+
if (point)
|
|
1559
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1560
|
+
if (point)
|
|
1561
|
+
captured.set(storage, point.value.snapshot);
|
|
1562
|
+
} else if (at !== void 0 && history.head(branch2)?.id !== at) {
|
|
1563
|
+
const point = history.checkout(branch2, at);
|
|
1564
|
+
if (!instances.has(storage)) {
|
|
1565
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1566
|
+
branchRng.setState(point.value.rngState);
|
|
1567
|
+
instanceFor(storage, namespace, branchRng);
|
|
1568
|
+
}
|
|
1569
|
+
branchRngs.get(storage)?.setState(point.value.rngState);
|
|
1570
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1571
|
+
captured.set(storage, point.value.snapshot);
|
|
1572
|
+
} else {
|
|
1573
|
+
if (!instances.has(storage)) {
|
|
1574
|
+
const point = history.head(branch2);
|
|
1575
|
+
const branchRng = createRng(options.seed ?? 0);
|
|
1576
|
+
if (point)
|
|
1577
|
+
branchRng.setState(point.value.rngState);
|
|
1578
|
+
instanceFor(storage, namespace, branchRng);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return storage;
|
|
1582
|
+
};
|
|
1583
|
+
const checkpoint = (namespace = DEFAULT_NAMESPACE, branch2 = "main") => {
|
|
1584
|
+
const storage = ensureBranch(namespace, branch2);
|
|
1585
|
+
return timeline(namespace).commit(capture(storage), { branch: branch2 });
|
|
1586
|
+
};
|
|
1587
|
+
const branch = (name, branchOptions = {}) => {
|
|
1588
|
+
const namespace = branchOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1589
|
+
ensureBranch(namespace, name, branchOptions.at);
|
|
1590
|
+
const head = timeline(namespace).head(name);
|
|
1591
|
+
if (!head)
|
|
1592
|
+
throw new RangeError(`branch ${name} has no checkpoint`);
|
|
1593
|
+
return head;
|
|
1594
|
+
};
|
|
1595
|
+
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
1596
|
+
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
1597
|
+
const branchName = checkoutOptions.branch ?? "main";
|
|
1598
|
+
const history = timeline(namespace);
|
|
1599
|
+
const point = history.checkout(branchName, checkpointId);
|
|
1600
|
+
const storage = ensureBranch(namespace, branchName);
|
|
1601
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1602
|
+
captured.set(storage, point.value.snapshot);
|
|
1603
|
+
clock.set(point.value.clock.now);
|
|
1604
|
+
if (point.value.clock.frozen)
|
|
1605
|
+
clock.freeze();
|
|
1606
|
+
else
|
|
1607
|
+
clock.unfreeze();
|
|
1608
|
+
(branchRngs.get(storage) ?? rng).setState(point.value.rngState);
|
|
1609
|
+
};
|
|
1240
1610
|
const reset = async (name = DEFAULT_NAMESPACE) => {
|
|
1241
1611
|
if (name === "*") {
|
|
1242
1612
|
options.webhooks?.clear();
|
|
1243
1613
|
for (const each of instances.values())
|
|
1244
1614
|
await each.reset();
|
|
1615
|
+
timelines.clear();
|
|
1616
|
+
branchStorage.clear();
|
|
1617
|
+
branchRngs.clear();
|
|
1618
|
+
captured.clear();
|
|
1245
1619
|
return;
|
|
1246
1620
|
}
|
|
1247
1621
|
options.webhooks?.clear(name);
|
|
@@ -1250,14 +1624,33 @@ var createRuntime = (options) => {
|
|
|
1250
1624
|
await target.reset();
|
|
1251
1625
|
else
|
|
1252
1626
|
clearNamespace(sqlite, storageNamespace(name));
|
|
1627
|
+
for (const [mapping, storage] of branchStorage) {
|
|
1628
|
+
if (!mapping.startsWith(`${name}\0`))
|
|
1629
|
+
continue;
|
|
1630
|
+
const branchInstance = instances.get(storage);
|
|
1631
|
+
if (branchInstance)
|
|
1632
|
+
await branchInstance.reset();
|
|
1633
|
+
else
|
|
1634
|
+
clearNamespace(sqlite, storageNamespace(storage));
|
|
1635
|
+
branchStorage.delete(mapping);
|
|
1636
|
+
branchRngs.delete(storage);
|
|
1637
|
+
captured.delete(storage);
|
|
1638
|
+
}
|
|
1639
|
+
timelines.delete(name);
|
|
1640
|
+
captured.delete(name);
|
|
1253
1641
|
};
|
|
1254
1642
|
const snapshot = (name = DEFAULT_NAMESPACE) => {
|
|
1255
|
-
|
|
1256
|
-
return snapshotNamespace(sqlite, storageNamespace(name));
|
|
1643
|
+
return checkpoint(name, "main").value.snapshot;
|
|
1257
1644
|
};
|
|
1258
1645
|
const restore = (from, name = DEFAULT_NAMESPACE) => {
|
|
1259
1646
|
instance(name);
|
|
1260
1647
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
1648
|
+
captured.set(name, from);
|
|
1649
|
+
const history = timelines.get(name);
|
|
1650
|
+
if (history)
|
|
1651
|
+
history.commit(capture(name), { branch: "main" });
|
|
1652
|
+
else
|
|
1653
|
+
timeline(name);
|
|
1261
1654
|
};
|
|
1262
1655
|
const runtime = {
|
|
1263
1656
|
name: options.name,
|
|
@@ -1289,10 +1682,14 @@ var createRuntime = (options) => {
|
|
|
1289
1682
|
return added;
|
|
1290
1683
|
},
|
|
1291
1684
|
instance,
|
|
1292
|
-
namespaces: () => [...
|
|
1685
|
+
namespaces: () => [...publicNamespaces].sort(),
|
|
1293
1686
|
reset,
|
|
1294
1687
|
snapshot,
|
|
1295
1688
|
restore,
|
|
1689
|
+
checkpoint,
|
|
1690
|
+
branch,
|
|
1691
|
+
checkout,
|
|
1692
|
+
timeline,
|
|
1296
1693
|
fetch: async (incoming) => {
|
|
1297
1694
|
let request2 = incoming;
|
|
1298
1695
|
const prefixed = PATH_PREFIX.exec(new URL(request2.url).pathname);
|
|
@@ -1318,6 +1715,8 @@ var createRuntime = (options) => {
|
|
|
1318
1715
|
if (mapped !== void 0)
|
|
1319
1716
|
namespace = mapped;
|
|
1320
1717
|
}
|
|
1718
|
+
const selectedBranch = request2.headers.get(BRANCH_HEADER) ?? "main";
|
|
1719
|
+
const at = request2.headers.get(AT_HEADER) ?? void 0;
|
|
1321
1720
|
const stamp = (response2) => {
|
|
1322
1721
|
const value = NAMESPACE_PATTERN.test(namespace) ? `${options.name}@${version}; ns=${namespace}` : `${options.name}@${version}`;
|
|
1323
1722
|
try {
|
|
@@ -1332,7 +1731,7 @@ var createRuntime = (options) => {
|
|
|
1332
1731
|
const handled = await control.handle(request2);
|
|
1333
1732
|
if (handled)
|
|
1334
1733
|
return stamp(handled);
|
|
1335
|
-
const started =
|
|
1734
|
+
const started = monotonicNow();
|
|
1336
1735
|
const url = new URL(request2.url);
|
|
1337
1736
|
const operationId = operationIdFor(request2, url.pathname);
|
|
1338
1737
|
const log = (status, faultId, response2) => {
|
|
@@ -1344,7 +1743,7 @@ var createRuntime = (options) => {
|
|
|
1344
1743
|
method: request2.method,
|
|
1345
1744
|
path: url.pathname,
|
|
1346
1745
|
status,
|
|
1347
|
-
durationMs: Math.round((
|
|
1746
|
+
durationMs: Math.round((monotonicNow() - started) * 100) / 100,
|
|
1348
1747
|
unmatched: options.document !== void 0 && operationId === void 0,
|
|
1349
1748
|
...faultId !== void 0 ? { faultId } : {},
|
|
1350
1749
|
...noted?.ids && Object.keys(noted.ids).length > 0 ? { ids: noted.ids } : {},
|
|
@@ -1363,6 +1762,30 @@ var createRuntime = (options) => {
|
|
|
1363
1762
|
}
|
|
1364
1763
|
}), { status: 400, headers: { "content-type": "application/json" } }));
|
|
1365
1764
|
}
|
|
1765
|
+
if (!BRANCH_PATTERN2.test(selectedBranch)) {
|
|
1766
|
+
log(400);
|
|
1767
|
+
return stamp(adminFail(400, `${BRANCH_HEADER} must match ${BRANCH_PATTERN2}`));
|
|
1768
|
+
}
|
|
1769
|
+
let storage;
|
|
1770
|
+
try {
|
|
1771
|
+
if (at !== void 0 && selectedBranch === "main" && !MUTATING_METHODS.has(request2.method)) {
|
|
1772
|
+
const point = timeline(namespace).get(at);
|
|
1773
|
+
storage = physicalBranch(namespace, `at_${at}`);
|
|
1774
|
+
let viewRng = branchRngs.get(storage);
|
|
1775
|
+
if (!viewRng) {
|
|
1776
|
+
viewRng = createRng(options.seed ?? 0);
|
|
1777
|
+
instanceFor(storage, namespace, viewRng);
|
|
1778
|
+
}
|
|
1779
|
+
viewRng.setState(point.value.rngState);
|
|
1780
|
+
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
1781
|
+
captured.set(storage, point.value.snapshot);
|
|
1782
|
+
} else {
|
|
1783
|
+
storage = ensureBranch(namespace, selectedBranch, at);
|
|
1784
|
+
}
|
|
1785
|
+
} catch (error) {
|
|
1786
|
+
log(409);
|
|
1787
|
+
return stamp(adminFail(409, error instanceof Error ? error.message : String(error)));
|
|
1788
|
+
}
|
|
1366
1789
|
const hits = await faults.take({
|
|
1367
1790
|
operationId,
|
|
1368
1791
|
method: request2.method,
|
|
@@ -1381,14 +1804,28 @@ var createRuntime = (options) => {
|
|
|
1381
1804
|
const fired = hits.filter((hit) => hit.effect !== void 0);
|
|
1382
1805
|
if (fired.length > 0)
|
|
1383
1806
|
effects.set(request2, fired.map((hit) => hit.effect));
|
|
1384
|
-
|
|
1807
|
+
let response = await instanceFor(storage, namespace).fetch(request2);
|
|
1808
|
+
if (MUTATING_METHODS.has(request2.method) && response.status >= 200 && response.status < 400) {
|
|
1809
|
+
const point = timeline(namespace).commit(capture(storage), { branch: selectedBranch });
|
|
1810
|
+
response = mutableResponse(response);
|
|
1811
|
+
response.headers.set(CHECKPOINT_HEADER, point.id);
|
|
1812
|
+
}
|
|
1813
|
+
if (selectedBranch !== "main") {
|
|
1814
|
+
response = mutableResponse(response);
|
|
1815
|
+
response.headers.set(BRANCH_HEADER, selectedBranch);
|
|
1816
|
+
}
|
|
1817
|
+
if (at !== void 0) {
|
|
1818
|
+
response = mutableResponse(response);
|
|
1819
|
+
response.headers.set(AT_HEADER, at);
|
|
1820
|
+
}
|
|
1385
1821
|
log(response.status, fired[0]?.id, response);
|
|
1386
1822
|
return stamp(response);
|
|
1387
1823
|
}
|
|
1388
1824
|
};
|
|
1389
1825
|
const control = createControlPlane({
|
|
1390
1826
|
name: options.name,
|
|
1391
|
-
startedAt:
|
|
1827
|
+
startedAt: wallNow(),
|
|
1828
|
+
wallNow,
|
|
1392
1829
|
clock,
|
|
1393
1830
|
faults,
|
|
1394
1831
|
metrics,
|
|
@@ -1396,8 +1833,39 @@ var createRuntime = (options) => {
|
|
|
1396
1833
|
defaultNamespace: DEFAULT_NAMESPACE,
|
|
1397
1834
|
namespaces: runtime.namespaces,
|
|
1398
1835
|
reset,
|
|
1399
|
-
|
|
1400
|
-
|
|
1836
|
+
timeTravel: {
|
|
1837
|
+
checkpoint: (name, branchName) => {
|
|
1838
|
+
const point = checkpoint(name, branchName);
|
|
1839
|
+
return {
|
|
1840
|
+
id: point.id,
|
|
1841
|
+
branch: point.branch,
|
|
1842
|
+
parent: point.parent,
|
|
1843
|
+
at: point.at,
|
|
1844
|
+
records: point.value.snapshot.records.length
|
|
1845
|
+
};
|
|
1846
|
+
},
|
|
1847
|
+
branch: (branchName, branchOptions) => {
|
|
1848
|
+
const point = branch(branchName, branchOptions);
|
|
1849
|
+
return { id: point.id, branch: point.branch, parent: point.parent, at: point.at };
|
|
1850
|
+
},
|
|
1851
|
+
checkout: (checkpointId, checkoutOptions) => checkout(checkpointId, checkoutOptions),
|
|
1852
|
+
retain: (name, checkpointId) => {
|
|
1853
|
+
timeline(name).retain(checkpointId);
|
|
1854
|
+
},
|
|
1855
|
+
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
1856
|
+
inspect: (name) => {
|
|
1857
|
+
const history = timeline(name);
|
|
1858
|
+
return {
|
|
1859
|
+
branches: history.branches(),
|
|
1860
|
+
checkpoints: history.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
1861
|
+
id,
|
|
1862
|
+
branch: branchName,
|
|
1863
|
+
parent,
|
|
1864
|
+
at
|
|
1865
|
+
}))
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1868
|
+
},
|
|
1401
1869
|
describe: options.describe ?? (() => ({})),
|
|
1402
1870
|
...options.presets ? {
|
|
1403
1871
|
applyPreset: (name, namespace, overrides) => runtime.applyPreset(name, namespace, overrides)
|
|
@@ -1412,6 +1880,15 @@ var createRuntime = (options) => {
|
|
|
1412
1880
|
});
|
|
1413
1881
|
return runtime;
|
|
1414
1882
|
};
|
|
1883
|
+
var mutableResponse = (response) => {
|
|
1884
|
+
try {
|
|
1885
|
+
response.headers.set("x-mockingbird-mutable-probe", "1");
|
|
1886
|
+
response.headers.delete("x-mockingbird-mutable-probe");
|
|
1887
|
+
return response;
|
|
1888
|
+
} catch {
|
|
1889
|
+
return new Response(response.body, response);
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1415
1892
|
var adminJson = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
1416
1893
|
var adminFail = (status, message) => adminJson(status, { error: { type: "mockingbird_admin", message } });
|
|
1417
1894
|
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -4346,7 +4823,7 @@ var linkAppointmentToOrder = (state, order, appointment, status, nowIso) => {
|
|
|
4346
4823
|
transactionOrder.updated_at = nowIso;
|
|
4347
4824
|
}
|
|
4348
4825
|
state.orders.update(order.id, order);
|
|
4349
|
-
state.publishOrderWebhook(order, "labtest.order.updated");
|
|
4826
|
+
state.publishOrderWebhook(order, "labtest.order.updated", new Date(nowIso).getTime());
|
|
4350
4827
|
};
|
|
4351
4828
|
var appendAppointmentEvent = (appointment, status, at) => {
|
|
4352
4829
|
const event = { created_at: at, status, data: null };
|
|
@@ -5192,7 +5669,7 @@ var pyIso = (value) => {
|
|
|
5192
5669
|
const ms = (match?.[1] ?? "000").padEnd(6, "0");
|
|
5193
5670
|
return `${date}T${time.slice(0, 8)}.${ms}+00:00`;
|
|
5194
5671
|
};
|
|
5195
|
-
var orderValidation = (body) => {
|
|
5672
|
+
var orderValidation = (body, now = Date.now) => {
|
|
5196
5673
|
const errors = [];
|
|
5197
5674
|
const userId = body.user_id;
|
|
5198
5675
|
if (userId === void 0) errors.push(missingError(["body", "user_id"], body));
|
|
@@ -5290,7 +5767,7 @@ var orderValidation = (body) => {
|
|
|
5290
5767
|
const emailError = userEmailError(p[field]);
|
|
5291
5768
|
if (emailError) errors.push(emailError);
|
|
5292
5769
|
} else if (field === "dob") {
|
|
5293
|
-
const dobError = orderDobError(p[field]);
|
|
5770
|
+
const dobError = orderDobError(p[field], now());
|
|
5294
5771
|
if (dobError) errors.push(dobError);
|
|
5295
5772
|
} else if (field === "phone_number" && !isValidPhone(p[field])) {
|
|
5296
5773
|
errors.push(phoneError(["body", "patient_details", "phone_number"], p[field]));
|
|
@@ -5338,11 +5815,11 @@ var userEmailError = (value) => {
|
|
|
5338
5815
|
}
|
|
5339
5816
|
return void 0;
|
|
5340
5817
|
};
|
|
5341
|
-
var orderDobError = (dob) => {
|
|
5818
|
+
var orderDobError = (dob, nowMs) => {
|
|
5342
5819
|
if (/^\d{4}-\d{2}-\d{2}T/.test(dob)) {
|
|
5343
5820
|
if (/^\d{4}-\d{2}-\d{2}T00:00:00/.test(dob)) {
|
|
5344
5821
|
const dateOnly = dob.slice(0, 10);
|
|
5345
|
-
if ((/* @__PURE__ */ new Date(`${dateOnly}T00:00:00Z`)).getTime() >
|
|
5822
|
+
if ((/* @__PURE__ */ new Date(`${dateOnly}T00:00:00Z`)).getTime() > nowMs) {
|
|
5346
5823
|
return {
|
|
5347
5824
|
type: "value_error",
|
|
5348
5825
|
loc: ["body", "patient_details", "dob"],
|
|
@@ -5383,7 +5860,7 @@ var orderDobError = (dob) => {
|
|
|
5383
5860
|
ctx: { error: {} }
|
|
5384
5861
|
};
|
|
5385
5862
|
}
|
|
5386
|
-
if ((/* @__PURE__ */ new Date(`${dob}T00:00:00Z`)).getTime() >
|
|
5863
|
+
if ((/* @__PURE__ */ new Date(`${dob}T00:00:00Z`)).getTime() > nowMs) {
|
|
5387
5864
|
return {
|
|
5388
5865
|
type: "value_error",
|
|
5389
5866
|
loc: ["body", "patient_details", "dob"],
|
|
@@ -5615,7 +6092,7 @@ var orderHandlers = (state) => ({
|
|
|
5615
6092
|
});
|
|
5616
6093
|
}
|
|
5617
6094
|
}
|
|
5618
|
-
const errors = orderValidation(body);
|
|
6095
|
+
const errors = orderValidation(body, context.now);
|
|
5619
6096
|
if (errors.length > 0) throw new HttpError(422, { detail: errors });
|
|
5620
6097
|
const rawAddress = body.patient_address;
|
|
5621
6098
|
if (typeof rawAddress === "object" && rawAddress !== null && !Array.isArray(rawAddress) && typeof rawAddress.country === "string" && /[^A-Za-z ]/.test(rawAddress.country ?? "")) {
|
|
@@ -8145,14 +8622,17 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8145
8622
|
const delays = endpoint.retryDelaysMs ?? WEBHOOK_RETRY_DELAYS_MS;
|
|
8146
8623
|
const timeoutMs = endpoint.timeoutMs ?? 15e3;
|
|
8147
8624
|
const send = endpoint.fetch ?? ((request2) => fetch(request2));
|
|
8625
|
+
const now = endpoint.now ?? Date.now;
|
|
8626
|
+
const scheduleTimer = endpoint.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
8627
|
+
const cancel = endpoint.cancel ?? ((handle) => clearTimeout(handle));
|
|
8148
8628
|
const deliveries = /* @__PURE__ */ new Map();
|
|
8149
8629
|
const pending = /* @__PURE__ */ new Map();
|
|
8150
8630
|
const inFlight = /* @__PURE__ */ new Set();
|
|
8151
8631
|
let sequence = 0;
|
|
8152
8632
|
const attempt = async (delivery) => {
|
|
8153
8633
|
const body = JSON.stringify(delivery.event);
|
|
8154
|
-
const timestamp = Math.floor(
|
|
8155
|
-
const started =
|
|
8634
|
+
const timestamp = Math.floor(now() / 1e3);
|
|
8635
|
+
const started = now();
|
|
8156
8636
|
const record = {
|
|
8157
8637
|
attempt: delivery.attempts.length + 1,
|
|
8158
8638
|
at: new Date(started).toISOString(),
|
|
@@ -8161,7 +8641,7 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8161
8641
|
durationMs: 0
|
|
8162
8642
|
};
|
|
8163
8643
|
const controller = new AbortController();
|
|
8164
|
-
const timer =
|
|
8644
|
+
const timer = scheduleTimer(() => controller.abort(), timeoutMs);
|
|
8165
8645
|
try {
|
|
8166
8646
|
const response = await send(
|
|
8167
8647
|
new Request(endpoint.url, {
|
|
@@ -8182,8 +8662,8 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8182
8662
|
} catch (error) {
|
|
8183
8663
|
record.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
|
|
8184
8664
|
} finally {
|
|
8185
|
-
|
|
8186
|
-
record.durationMs =
|
|
8665
|
+
cancel(timer);
|
|
8666
|
+
record.durationMs = now() - started;
|
|
8187
8667
|
delivery.attempts.push(record);
|
|
8188
8668
|
}
|
|
8189
8669
|
return record.status !== null && record.status >= 200 && record.status < 300;
|
|
@@ -8213,7 +8693,7 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8213
8693
|
run();
|
|
8214
8694
|
return;
|
|
8215
8695
|
}
|
|
8216
|
-
const timer =
|
|
8696
|
+
const timer = scheduleTimer(run, delay);
|
|
8217
8697
|
unref(timer);
|
|
8218
8698
|
pending.set(delivery.messageId, { delivery, timer });
|
|
8219
8699
|
};
|
|
@@ -8236,7 +8716,7 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8236
8716
|
const delivery = deliveries.get(messageId);
|
|
8237
8717
|
if (!delivery) return void 0;
|
|
8238
8718
|
const queued = pending.get(messageId);
|
|
8239
|
-
if (queued?.timer !== void 0)
|
|
8719
|
+
if (queued?.timer !== void 0) cancel(queued.timer);
|
|
8240
8720
|
pending.delete(messageId);
|
|
8241
8721
|
const ok = await attempt(delivery);
|
|
8242
8722
|
delivery.state = ok ? "delivered" : "failed";
|
|
@@ -8244,7 +8724,7 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8244
8724
|
},
|
|
8245
8725
|
async flush() {
|
|
8246
8726
|
for (const [id, queued] of [...pending]) {
|
|
8247
|
-
if (queued.timer !== void 0)
|
|
8727
|
+
if (queued.timer !== void 0) cancel(queued.timer);
|
|
8248
8728
|
pending.delete(id);
|
|
8249
8729
|
track(
|
|
8250
8730
|
attempt(queued.delivery).then((ok) => {
|
|
@@ -8262,7 +8742,7 @@ var createWebhookDispatcher = (endpoint) => {
|
|
|
8262
8742
|
for (const [id, delivery] of [...deliveries]) {
|
|
8263
8743
|
if (namespace !== void 0 && delivery.namespace !== namespace) continue;
|
|
8264
8744
|
const queued = pending.get(id);
|
|
8265
|
-
if (queued?.timer !== void 0)
|
|
8745
|
+
if (queued?.timer !== void 0) cancel(queued.timer);
|
|
8266
8746
|
pending.delete(id);
|
|
8267
8747
|
deliveries.delete(id);
|
|
8268
8748
|
}
|
|
@@ -9128,4 +9608,4 @@ export {
|
|
|
9128
9608
|
JunctionAPI,
|
|
9129
9609
|
LabAccountConflict
|
|
9130
9610
|
};
|
|
9131
|
-
//# sourceMappingURL=chunk-
|
|
9611
|
+
//# sourceMappingURL=chunk-MS774AJA.js.map
|