@crvouga/mockingbird-service-caretalk 1.7.0 → 1.8.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 CHANGED
@@ -1,14 +1,31 @@
1
1
  # Changelog — @crvouga/mockingbird-service-caretalk
2
2
 
3
- ## 1.7.0 (2026-10-01)
3
+ ## 1.8.0 (2026-10-02)
4
4
 
5
5
  ### Features
6
6
 
7
- - record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
7
+ - coordinate HTTP and protocol mocks ([3168653](https://github.com/crvouga/mockingbird/commit/31686534c55a9823fdaa92afc9cc456cf741b809))
8
+
9
+ ### Fixes and improvements
8
10
 
9
- ### Dependencies
11
+ - decouple smoke types from built packages ([a4b31c6](https://github.com/crvouga/mockingbird/commit/a4b31c6205d92aa0c6f7c9de3518d5ae44d38829))
12
+ - protect child admin endpoints ([5410b94](https://github.com/crvouga/mockingbird/commit/5410b948a30dcedc21e9a70e44fd415c1ea25394))
13
+ - harden protocol startup and state ([6f5b557](https://github.com/crvouga/mockingbird/commit/6f5b557e87b48b31985949e04f3b5ef58f51d295))
14
+ - preserve checkpoints and lifecycle ([50e8c32](https://github.com/crvouga/mockingbird/commit/50e8c322feded2f7f45afce9d8cdef43c37ab9ab))
15
+ - clean branch state on reset ([3b714d4](https://github.com/crvouga/mockingbird/commit/3b714d4a5df8e75fee0bb31e98d7c7566e164381))
16
+ - type signal listener cleanup ([5279852](https://github.com/crvouga/mockingbird/commit/5279852e56ea60262ccf17aa6e46b3c31091a34a))
17
+
18
+ ## 1.7.1 (2026-10-01)
19
+
20
+ ### Fixes and improvements
10
21
 
11
- - `@crvouga/mockingbird-service-sqlite`
22
+ - freeze clock before setting time ([a054a1d](https://github.com/crvouga/mockingbird/commit/a054a1defad62539d719b1d31c15107afea623bd))
23
+
24
+ ## 1.7.0 (2026-09-30)
25
+
26
+ ### Features
27
+
28
+ - record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
12
29
 
13
30
  ## 1.6.1 (2026-09-30)
14
31
 
@@ -183,29 +183,30 @@ var createControlPlane = (context) => {
183
183
  return json(200, { status: "ok", reset: target === "*" ? context.namespaces() : [target] });
184
184
  },
185
185
  "GET /namespaces": () => json(200, { default: context.defaultNamespace, namespaces: context.namespaces() }),
186
- "GET /clock": () => json(200, context.clock.state()),
187
- "POST /clock": ({ body }) => {
186
+ "GET /clock": ({ namespace }) => json(200, (context.namespaceClock?.(namespace) ?? context.clock).state()),
187
+ "POST /clock": ({ body, namespace }) => {
188
+ const clock = context.namespaceClock?.(namespace) ?? context.clock;
188
189
  if (!isRecord(body))
189
190
  return adminError(400, "expected a JSON object");
190
191
  if (body.reset === true)
191
- context.clock.reset();
192
+ clock.reset();
193
+ if (body.freeze === true)
194
+ clock.freeze();
192
195
  if (body.set !== void 0) {
193
196
  const instant = parseInstant(body.set);
194
197
  if (instant === void 0)
195
198
  return adminError(400, "set: expected epoch ms or ISO-8601");
196
- context.clock.set(instant);
199
+ clock.set(instant);
197
200
  }
198
201
  if (body.advance !== void 0) {
199
202
  const delta = parseDuration(body.advance);
200
203
  if (delta === void 0)
201
204
  return adminError(400, 'advance: expected ms or "15m"-style');
202
- context.clock.advance(delta);
205
+ clock.advance(delta);
203
206
  }
204
- if (body.freeze === true)
205
- context.clock.freeze();
206
207
  if (body.freeze === false)
207
- context.clock.unfreeze();
208
- return json(200, context.clock.state());
208
+ clock.unfreeze();
209
+ return json(200, clock.state());
209
210
  },
210
211
  "GET /faults": () => json(200, { faults: context.faults.list() }),
211
212
  "POST /faults": ({ body, namespace }) => {
@@ -474,7 +475,33 @@ var faultResponse = (rule) => {
474
475
  };
475
476
  var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
476
477
  const entries = [];
478
+ let isolated = false;
479
+ const scopedRngs = /* @__PURE__ */ new Map();
480
+ const random = (namespace) => {
481
+ if (!isolated || namespace === "default")
482
+ return rng;
483
+ let scoped = scopedRngs.get(namespace);
484
+ if (!scoped) {
485
+ scoped = createRng(rng.seed);
486
+ scopedRngs.set(namespace, scoped);
487
+ }
488
+ return scoped;
489
+ };
477
490
  return {
491
+ isolateNamespaces() {
492
+ isolated = true;
493
+ },
494
+ snapshot(namespace) {
495
+ return {
496
+ rules: structuredClone(entries.map((entry, position) => ({
497
+ ...entry.rule,
498
+ remaining: entry.remaining,
499
+ hits: entry.hits,
500
+ position
501
+ })).filter((entry) => entry.namespace === namespace)),
502
+ rngState: random(namespace).state()
503
+ };
504
+ },
478
505
  add(rule) {
479
506
  const existing = entries.findIndex((e) => e.rule.id === rule.id);
480
507
  const entry = { rule, remaining: rule.count ?? null, hits: 0 };
@@ -494,6 +521,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
494
521
  },
495
522
  clear() {
496
523
  entries.length = 0;
524
+ rng.reset();
525
+ scopedRngs.clear();
526
+ },
527
+ restore(namespace, state) {
528
+ for (let i = entries.length - 1; i >= 0; i--)
529
+ if (entries[i]?.rule.namespace === namespace)
530
+ entries.splice(i, 1);
531
+ for (const { remaining, hits, position, ...rule } of state.rules)
532
+ entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
533
+ random(namespace).setState(state.rngState);
497
534
  },
498
535
  async take(candidate) {
499
536
  const hits = [];
@@ -503,7 +540,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
503
540
  if (!matches(entry.rule, candidate))
504
541
  continue;
505
542
  const rate = entry.rule.rate ?? 1;
506
- if (rng.next() >= rate)
543
+ if (random(candidate.namespace).next() >= rate)
507
544
  continue;
508
545
  entry.hits++;
509
546
  if (entry.remaining !== null)
@@ -1052,7 +1089,8 @@ var annotateResponse = (response, extra) => {
1052
1089
  var responseNotes = (response) => notes.get(response);
1053
1090
 
1054
1091
  // ../core/dist/metrics.js
1055
- var createMetrics = () => {
1092
+ var createMetrics = (partition = true) => {
1093
+ const namespaces = /* @__PURE__ */ new Map();
1056
1094
  let requests = 0;
1057
1095
  let faults = 0;
1058
1096
  let totalDurationMs = 0;
@@ -1060,6 +1098,14 @@ var createMetrics = () => {
1060
1098
  const unmatched = /* @__PURE__ */ new Map();
1061
1099
  return {
1062
1100
  record(entry) {
1101
+ if (partition) {
1102
+ let scoped = namespaces.get(entry.namespace);
1103
+ if (!scoped) {
1104
+ scoped = createMetrics(false);
1105
+ namespaces.set(entry.namespace, scoped);
1106
+ }
1107
+ scoped.record(entry);
1108
+ }
1063
1109
  requests++;
1064
1110
  totalDurationMs += entry.durationMs;
1065
1111
  if (entry.faultId !== void 0)
@@ -1071,7 +1117,7 @@ var createMetrics = () => {
1071
1117
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
1072
1118
  }
1073
1119
  },
1074
- report: () => ({
1120
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
1075
1121
  requests,
1076
1122
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
1077
1123
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -1080,8 +1126,13 @@ var createMetrics = () => {
1080
1126
  }),
1081
1127
  faults,
1082
1128
  totalDurationMs
1083
- }),
1084
- reset() {
1129
+ },
1130
+ reset(namespace) {
1131
+ if (namespace !== void 0) {
1132
+ namespaces.delete(namespace);
1133
+ return;
1134
+ }
1135
+ namespaces.clear();
1085
1136
  requests = 0;
1086
1137
  faults = 0;
1087
1138
  totalDurationMs = 0;
@@ -4063,7 +4114,7 @@ var stateAdminRoutes = (routes) => {
4063
4114
  };
4064
4115
 
4065
4116
  // ../core/dist/version.js
4066
- var PACKAGE_VERSION = true ? "1.7.0" : UNRELEASED_VERSION;
4117
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
4067
4118
 
4068
4119
  // ../core/dist/signing.js
4069
4120
  var encoder = new TextEncoder();
@@ -4240,6 +4291,20 @@ var createRuntime = (options) => {
4240
4291
  const instances = /* @__PURE__ */ new Map();
4241
4292
  const publicNamespaces = /* @__PURE__ */ new Set();
4242
4293
  const branchRngs = /* @__PURE__ */ new Map();
4294
+ const namespaceClocks = /* @__PURE__ */ new Map();
4295
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4296
+ let isolatedNamespaces = false;
4297
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4298
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4299
+ return clock;
4300
+ let found = namespaceClocks.get(name);
4301
+ if (!found) {
4302
+ found = createClock();
4303
+ namespaceClocks.set(name, found);
4304
+ }
4305
+ return found;
4306
+ };
4307
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
4243
4308
  const timelines = /* @__PURE__ */ new Map();
4244
4309
  const branchStorage = /* @__PURE__ */ new Map();
4245
4310
  const captured = /* @__PURE__ */ new Map();
@@ -4253,17 +4318,21 @@ var createRuntime = (options) => {
4253
4318
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
4254
4319
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
4255
4320
  }
4321
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
4256
4322
  const created = options.create({
4257
4323
  namespace: storageNamespace(key),
4258
4324
  publicNamespace,
4259
4325
  sqlite,
4260
- clock,
4261
- rng: isolatedRng ?? rng
4326
+ clock: namespaceClock(publicNamespace),
4327
+ rng: createdContextRng
4262
4328
  });
4263
4329
  instances.set(key, created);
4330
+ namespaceForStorage.set(key, publicNamespace);
4264
4331
  publicNamespaces.add(publicNamespace);
4265
4332
  if (isolatedRng)
4266
4333
  branchRngs.set(key, isolatedRng);
4334
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4335
+ branchRngs.set(key, createdContextRng);
4267
4336
  return created;
4268
4337
  };
4269
4338
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4281,7 +4350,7 @@ var createRuntime = (options) => {
4281
4350
  captured.set(storage, snapshot2);
4282
4351
  return Object.freeze({
4283
4352
  snapshot: snapshot2,
4284
- clock: Object.freeze(clock.state()),
4353
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4285
4354
  rngState: (branchRngs.get(storage) ?? rng).state()
4286
4355
  });
4287
4356
  };
@@ -4291,7 +4360,7 @@ var createRuntime = (options) => {
4291
4360
  return found;
4292
4361
  instance(name);
4293
4362
  found = new Timeline({
4294
- now: clock.now,
4363
+ now: namespaceClock(name).now,
4295
4364
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4296
4365
  });
4297
4366
  found.commit(capture(name));
@@ -4318,12 +4387,13 @@ var createRuntime = (options) => {
4318
4387
  const point = history2.checkout("main", at);
4319
4388
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4320
4389
  captured.set(namespace, point.value.snapshot);
4321
- rng.setState(point.value.rngState);
4322
- clock.set(point.value.clock.now);
4390
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4391
+ const selectedClock = namespaceClock(namespace);
4392
+ selectedClock.set(point.value.clock.now);
4323
4393
  if (point.value.clock.frozen)
4324
- clock.freeze();
4394
+ selectedClock.freeze();
4325
4395
  else
4326
- clock.unfreeze();
4396
+ selectedClock.unfreeze();
4327
4397
  }
4328
4398
  return namespace;
4329
4399
  }
@@ -4381,11 +4451,12 @@ var createRuntime = (options) => {
4381
4451
  const storage = ensureBranch(namespace, branchName);
4382
4452
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4383
4453
  captured.set(storage, point.value.snapshot);
4384
- clock.set(point.value.clock.now);
4454
+ const selectedClock = namespaceClock(namespace);
4455
+ selectedClock.set(point.value.clock.now);
4385
4456
  if (point.value.clock.frozen)
4386
- clock.freeze();
4457
+ selectedClock.freeze();
4387
4458
  else
4388
- clock.unfreeze();
4459
+ selectedClock.unfreeze();
4389
4460
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4390
4461
  };
4391
4462
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4393,10 +4464,21 @@ var createRuntime = (options) => {
4393
4464
  options.webhooks?.clear();
4394
4465
  for (const each of instances.values())
4395
4466
  await each.reset();
4467
+ for (const each of branchRngs.values())
4468
+ each.reset();
4396
4469
  timelines.clear();
4470
+ for (const storage of branchStorage.values()) {
4471
+ instances.delete(storage);
4472
+ branchRngs.delete(storage);
4473
+ namespaceForStorage.delete(storage);
4474
+ }
4397
4475
  branchStorage.clear();
4398
- branchRngs.clear();
4399
4476
  captured.clear();
4477
+ faults.clear();
4478
+ clock.reset();
4479
+ rng.reset();
4480
+ for (const each of namespaceClocks.values())
4481
+ each.reset();
4400
4482
  return;
4401
4483
  }
4402
4484
  options.webhooks?.clear(name);
@@ -4414,11 +4496,18 @@ var createRuntime = (options) => {
4414
4496
  else
4415
4497
  clearNamespace(sqlite, storageNamespace(storage));
4416
4498
  branchStorage.delete(mapping);
4499
+ instances.delete(storage);
4500
+ namespaceForStorage.delete(storage);
4417
4501
  branchRngs.delete(storage);
4418
4502
  captured.delete(storage);
4419
4503
  }
4420
4504
  timelines.delete(name);
4421
4505
  captured.delete(name);
4506
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4507
+ namespaceClock(name).reset();
4508
+ (branchRngs.get(name) ?? rng).reset();
4509
+ journal.clear(name);
4510
+ metrics.reset(name);
4422
4511
  };
4423
4512
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4424
4513
  return checkpoint(name, "main").value.snapshot;
@@ -4456,6 +4545,53 @@ var createRuntime = (options) => {
4456
4545
  rng,
4457
4546
  credentials,
4458
4547
  webhooks: options.webhooks,
4548
+ isolateNamespaces() {
4549
+ if (publicNamespaces.size > 1)
4550
+ throw new Error("enable namespace isolation before creating named namespaces");
4551
+ isolatedNamespaces = true;
4552
+ faults.isolateNamespaces();
4553
+ },
4554
+ namespaceClock,
4555
+ namespaceOf(request) {
4556
+ const url = new URL(request.url);
4557
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4558
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4559
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4560
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4561
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4562
+ if (selected !== void 0)
4563
+ return selected;
4564
+ const credential = options.credential?.(request);
4565
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4566
+ },
4567
+ fleetSnapshot(namespace) {
4568
+ instance(namespace);
4569
+ const handle = {};
4570
+ fleetPoints.set(handle, {
4571
+ namespace,
4572
+ state: capture(namespace),
4573
+ faults: faults.snapshot(namespace),
4574
+ webhooks: options.webhooks?.snapshot(namespace)
4575
+ });
4576
+ return handle;
4577
+ },
4578
+ fleetRestore(handle, namespace) {
4579
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4580
+ if (!point || point.namespace !== namespace)
4581
+ throw new Error("invalid fleet checkpoint");
4582
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4583
+ captured.set(namespace, point.state.snapshot);
4584
+ const selectedClock = namespaceClock(namespace);
4585
+ selectedClock.freeze();
4586
+ selectedClock.set(point.state.clock.now);
4587
+ if (!point.state.clock.frozen)
4588
+ selectedClock.unfreeze();
4589
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4590
+ faults.restore(namespace, point.faults);
4591
+ if (point.webhooks !== void 0)
4592
+ options.webhooks?.restore(namespace, point.webhooks);
4593
+ timelines.delete(namespace);
4594
+ },
4459
4595
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4460
4596
  const preset = options.presets?.[name];
4461
4597
  if (!preset)
@@ -4631,6 +4767,7 @@ var createRuntime = (options) => {
4631
4767
  startedAt: wallNow(),
4632
4768
  wallNow,
4633
4769
  clock,
4770
+ namespaceClock,
4634
4771
  faults,
4635
4772
  metrics,
4636
4773
  journal,
@@ -5625,6 +5762,8 @@ var CareTalkAPI = class {
5625
5762
  };
5626
5763
 
5627
5764
  export {
5765
+ createClock,
5766
+ parseDuration,
5628
5767
  DEFAULT_FORMS,
5629
5768
  US_STATES,
5630
5769
  DEFAULT_DOCTORS,
@@ -5639,4 +5778,4 @@ export {
5639
5778
  normalizeDate,
5640
5779
  CareTalkAPI
5641
5780
  };
5642
- //# sourceMappingURL=chunk-UVVPSQHN.js.map
5781
+ //# sourceMappingURL=chunk-52LEJOTG.js.map