@crvouga/mockingbird-service-rxvortex 1.8.1 → 1.9.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,5 +1,20 @@
1
1
  # Changelog — @crvouga/mockingbird-service-rxvortex
2
2
 
3
+ ## 1.9.0 (2026-10-02)
4
+
5
+ ### Features
6
+
7
+ - coordinate HTTP and protocol mocks ([3168653](https://github.com/crvouga/mockingbird/commit/31686534c55a9823fdaa92afc9cc456cf741b809))
8
+
9
+ ### Fixes and improvements
10
+
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
+
3
18
  ## 1.8.1 (2026-10-01)
4
19
 
5
20
  ### Fixes and improvements
@@ -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();
192
193
  if (body.freeze === true)
193
- context.clock.freeze();
194
+ clock.freeze();
194
195
  if (body.set !== void 0) {
195
196
  const instant = parseInstant(body.set);
196
197
  if (instant === void 0)
197
198
  return adminError(400, "set: expected epoch ms or ISO-8601");
198
- context.clock.set(instant);
199
+ clock.set(instant);
199
200
  }
200
201
  if (body.advance !== void 0) {
201
202
  const delta = parseDuration(body.advance);
202
203
  if (delta === void 0)
203
204
  return adminError(400, 'advance: expected ms or "15m"-style');
204
- context.clock.advance(delta);
205
+ clock.advance(delta);
205
206
  }
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)
@@ -1071,7 +1108,8 @@ var annotateResponse = (response, extra) => {
1071
1108
  var responseNotes = (response) => notes.get(response);
1072
1109
 
1073
1110
  // ../core/dist/metrics.js
1074
- var createMetrics = () => {
1111
+ var createMetrics = (partition = true) => {
1112
+ const namespaces = /* @__PURE__ */ new Map();
1075
1113
  let requests = 0;
1076
1114
  let faults = 0;
1077
1115
  let totalDurationMs = 0;
@@ -1079,6 +1117,14 @@ var createMetrics = () => {
1079
1117
  const unmatched = /* @__PURE__ */ new Map();
1080
1118
  return {
1081
1119
  record(entry) {
1120
+ if (partition) {
1121
+ let scoped = namespaces.get(entry.namespace);
1122
+ if (!scoped) {
1123
+ scoped = createMetrics(false);
1124
+ namespaces.set(entry.namespace, scoped);
1125
+ }
1126
+ scoped.record(entry);
1127
+ }
1082
1128
  requests++;
1083
1129
  totalDurationMs += entry.durationMs;
1084
1130
  if (entry.faultId !== void 0)
@@ -1090,7 +1136,7 @@ var createMetrics = () => {
1090
1136
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
1091
1137
  }
1092
1138
  },
1093
- report: () => ({
1139
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
1094
1140
  requests,
1095
1141
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
1096
1142
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -1099,8 +1145,13 @@ var createMetrics = () => {
1099
1145
  }),
1100
1146
  faults,
1101
1147
  totalDurationMs
1102
- }),
1103
- reset() {
1148
+ },
1149
+ reset(namespace) {
1150
+ if (namespace !== void 0) {
1151
+ namespaces.delete(namespace);
1152
+ return;
1153
+ }
1154
+ namespaces.clear();
1104
1155
  requests = 0;
1105
1156
  faults = 0;
1106
1157
  totalDurationMs = 0;
@@ -4082,7 +4133,7 @@ var stateAdminRoutes = (routes) => {
4082
4133
  };
4083
4134
 
4084
4135
  // ../core/dist/version.js
4085
- var PACKAGE_VERSION = true ? "1.8.1" : UNRELEASED_VERSION;
4136
+ var PACKAGE_VERSION = true ? "1.9.0" : UNRELEASED_VERSION;
4086
4137
 
4087
4138
  // ../core/dist/signing.js
4088
4139
  var encoder = new TextEncoder();
@@ -4175,7 +4226,9 @@ var createWebhookHub = (options) => {
4175
4226
  const faults = /* @__PURE__ */ new Map();
4176
4227
  const held = /* @__PURE__ */ new Map();
4177
4228
  const inFlight = /* @__PURE__ */ new Set();
4229
+ const controllers = /* @__PURE__ */ new Map();
4178
4230
  let immediate = Promise.resolve();
4231
+ const snapshots = /* @__PURE__ */ new WeakMap();
4179
4232
  const track = (work) => {
4180
4233
  inFlight.add(work);
4181
4234
  void work.finally(() => inFlight.delete(work));
@@ -4196,6 +4249,7 @@ var createWebhookHub = (options) => {
4196
4249
  responseBody: null
4197
4250
  };
4198
4251
  const controller = new AbortController();
4252
+ controllers.set(delivery.id, controller);
4199
4253
  const timer = scheduleTimer(() => controller.abort(), timeoutMs);
4200
4254
  try {
4201
4255
  const signed = await options.signer({
@@ -4225,6 +4279,7 @@ var createWebhookHub = (options) => {
4225
4279
  } catch (error) {
4226
4280
  record2.error = controller.signal.aborted ? `timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : String(error);
4227
4281
  } finally {
4282
+ controllers.delete(delivery.id);
4228
4283
  cancel(timer);
4229
4284
  record2.durationMs = now() - started;
4230
4285
  delivery.attempts.push(record2);
@@ -4242,6 +4297,8 @@ var createWebhookHub = (options) => {
4242
4297
  if (!payloads.has(delivery.id))
4243
4298
  return;
4244
4299
  await attempt(delivery).then((ok) => {
4300
+ if (!payloads.has(delivery.id))
4301
+ return;
4245
4302
  if (ok)
4246
4303
  delivery.state = "delivered";
4247
4304
  else
@@ -4303,6 +4360,37 @@ var createWebhookHub = (options) => {
4303
4360
  fanOut(message);
4304
4361
  };
4305
4362
  const hub = {
4363
+ pending(namespace) {
4364
+ return inFlight.size + (held.get(namespace)?.length ?? 0) + [...deliveries.values()].filter((d) => d.namespace === namespace && d.state === "pending").length;
4365
+ },
4366
+ snapshot(namespace) {
4367
+ if (hub.pending(namespace) > 0)
4368
+ throw new Error("webhooks are not quiescent");
4369
+ const handle = {};
4370
+ snapshots.set(handle, structuredClone({
4371
+ namespace,
4372
+ endpoints: own.get(namespace) ?? [],
4373
+ messages: messages.filter((m) => m.namespace === namespace),
4374
+ deliveries: [...deliveries].filter(([, d]) => d.namespace === namespace),
4375
+ payloads: [...payloads].filter(([, p]) => p.message.namespace === namespace),
4376
+ faults: faults.get(namespace) ?? []
4377
+ }));
4378
+ return handle;
4379
+ },
4380
+ restore(namespace, handle) {
4381
+ const point = typeof handle === "object" && handle !== null ? snapshots.get(handle) : void 0;
4382
+ if (!point || point.namespace !== namespace || hub.pending(namespace) > 0)
4383
+ throw new Error("invalid webhook restore");
4384
+ hub.clear(namespace);
4385
+ const copied = structuredClone(point);
4386
+ own.set(namespace, copied.endpoints);
4387
+ messages.push(...copied.messages);
4388
+ for (const [id2, delivery] of copied.deliveries)
4389
+ deliveries.set(id2, delivery);
4390
+ for (const [id2, payload] of copied.payloads)
4391
+ payloads.set(id2, payload);
4392
+ faults.set(namespace, copied.faults);
4393
+ },
4306
4394
  publish(input) {
4307
4395
  const contentType = input.contentType ?? (input.form ? "application/x-www-form-urlencoded" : "application/json");
4308
4396
  const body = typeof input.body === "string" ? input.body : input.form ? new URLSearchParams(input.form).toString() : JSON.stringify(input.body);
@@ -4393,6 +4481,7 @@ var createWebhookHub = (options) => {
4393
4481
  for (const [id2, delivery] of deliveries) {
4394
4482
  if (namespace !== void 0 && delivery.namespace !== namespace)
4395
4483
  continue;
4484
+ controllers.get(id2)?.abort();
4396
4485
  const timer = pending.get(id2);
4397
4486
  if (timer !== void 0)
4398
4487
  cancel(timer);
@@ -4580,6 +4669,20 @@ var createRuntime = (options) => {
4580
4669
  const instances = /* @__PURE__ */ new Map();
4581
4670
  const publicNamespaces = /* @__PURE__ */ new Set();
4582
4671
  const branchRngs = /* @__PURE__ */ new Map();
4672
+ const namespaceClocks = /* @__PURE__ */ new Map();
4673
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4674
+ let isolatedNamespaces = false;
4675
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4676
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4677
+ return clock;
4678
+ let found = namespaceClocks.get(name);
4679
+ if (!found) {
4680
+ found = createClock();
4681
+ namespaceClocks.set(name, found);
4682
+ }
4683
+ return found;
4684
+ };
4685
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
4583
4686
  const timelines = /* @__PURE__ */ new Map();
4584
4687
  const branchStorage = /* @__PURE__ */ new Map();
4585
4688
  const captured = /* @__PURE__ */ new Map();
@@ -4593,17 +4696,21 @@ var createRuntime = (options) => {
4593
4696
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
4594
4697
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
4595
4698
  }
4699
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
4596
4700
  const created = options.create({
4597
4701
  namespace: storageNamespace(key),
4598
4702
  publicNamespace,
4599
4703
  sqlite,
4600
- clock,
4601
- rng: isolatedRng ?? rng
4704
+ clock: namespaceClock(publicNamespace),
4705
+ rng: createdContextRng
4602
4706
  });
4603
4707
  instances.set(key, created);
4708
+ namespaceForStorage.set(key, publicNamespace);
4604
4709
  publicNamespaces.add(publicNamespace);
4605
4710
  if (isolatedRng)
4606
4711
  branchRngs.set(key, isolatedRng);
4712
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4713
+ branchRngs.set(key, createdContextRng);
4607
4714
  return created;
4608
4715
  };
4609
4716
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4621,7 +4728,7 @@ var createRuntime = (options) => {
4621
4728
  captured.set(storage, snapshot2);
4622
4729
  return Object.freeze({
4623
4730
  snapshot: snapshot2,
4624
- clock: Object.freeze(clock.state()),
4731
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4625
4732
  rngState: (branchRngs.get(storage) ?? rng).state()
4626
4733
  });
4627
4734
  };
@@ -4631,7 +4738,7 @@ var createRuntime = (options) => {
4631
4738
  return found;
4632
4739
  instance(name);
4633
4740
  found = new Timeline({
4634
- now: clock.now,
4741
+ now: namespaceClock(name).now,
4635
4742
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4636
4743
  });
4637
4744
  found.commit(capture(name));
@@ -4658,12 +4765,13 @@ var createRuntime = (options) => {
4658
4765
  const point = history2.checkout("main", at);
4659
4766
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4660
4767
  captured.set(namespace, point.value.snapshot);
4661
- rng.setState(point.value.rngState);
4662
- clock.set(point.value.clock.now);
4768
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4769
+ const selectedClock = namespaceClock(namespace);
4770
+ selectedClock.set(point.value.clock.now);
4663
4771
  if (point.value.clock.frozen)
4664
- clock.freeze();
4772
+ selectedClock.freeze();
4665
4773
  else
4666
- clock.unfreeze();
4774
+ selectedClock.unfreeze();
4667
4775
  }
4668
4776
  return namespace;
4669
4777
  }
@@ -4721,11 +4829,12 @@ var createRuntime = (options) => {
4721
4829
  const storage = ensureBranch(namespace, branchName);
4722
4830
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4723
4831
  captured.set(storage, point.value.snapshot);
4724
- clock.set(point.value.clock.now);
4832
+ const selectedClock = namespaceClock(namespace);
4833
+ selectedClock.set(point.value.clock.now);
4725
4834
  if (point.value.clock.frozen)
4726
- clock.freeze();
4835
+ selectedClock.freeze();
4727
4836
  else
4728
- clock.unfreeze();
4837
+ selectedClock.unfreeze();
4729
4838
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4730
4839
  };
4731
4840
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4733,10 +4842,21 @@ var createRuntime = (options) => {
4733
4842
  options.webhooks?.clear();
4734
4843
  for (const each of instances.values())
4735
4844
  await each.reset();
4845
+ for (const each of branchRngs.values())
4846
+ each.reset();
4736
4847
  timelines.clear();
4848
+ for (const storage of branchStorage.values()) {
4849
+ instances.delete(storage);
4850
+ branchRngs.delete(storage);
4851
+ namespaceForStorage.delete(storage);
4852
+ }
4737
4853
  branchStorage.clear();
4738
- branchRngs.clear();
4739
4854
  captured.clear();
4855
+ faults.clear();
4856
+ clock.reset();
4857
+ rng.reset();
4858
+ for (const each of namespaceClocks.values())
4859
+ each.reset();
4740
4860
  return;
4741
4861
  }
4742
4862
  options.webhooks?.clear(name);
@@ -4754,11 +4874,18 @@ var createRuntime = (options) => {
4754
4874
  else
4755
4875
  clearNamespace(sqlite, storageNamespace(storage));
4756
4876
  branchStorage.delete(mapping);
4877
+ instances.delete(storage);
4878
+ namespaceForStorage.delete(storage);
4757
4879
  branchRngs.delete(storage);
4758
4880
  captured.delete(storage);
4759
4881
  }
4760
4882
  timelines.delete(name);
4761
4883
  captured.delete(name);
4884
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4885
+ namespaceClock(name).reset();
4886
+ (branchRngs.get(name) ?? rng).reset();
4887
+ journal.clear(name);
4888
+ metrics.reset(name);
4762
4889
  };
4763
4890
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4764
4891
  return checkpoint(name, "main").value.snapshot;
@@ -4796,6 +4923,53 @@ var createRuntime = (options) => {
4796
4923
  rng,
4797
4924
  credentials,
4798
4925
  webhooks: options.webhooks,
4926
+ isolateNamespaces() {
4927
+ if (publicNamespaces.size > 1)
4928
+ throw new Error("enable namespace isolation before creating named namespaces");
4929
+ isolatedNamespaces = true;
4930
+ faults.isolateNamespaces();
4931
+ },
4932
+ namespaceClock,
4933
+ namespaceOf(request) {
4934
+ const url = new URL(request.url);
4935
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4936
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4937
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4938
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4939
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4940
+ if (selected !== void 0)
4941
+ return selected;
4942
+ const credential = options.credential?.(request);
4943
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4944
+ },
4945
+ fleetSnapshot(namespace) {
4946
+ instance(namespace);
4947
+ const handle = {};
4948
+ fleetPoints.set(handle, {
4949
+ namespace,
4950
+ state: capture(namespace),
4951
+ faults: faults.snapshot(namespace),
4952
+ webhooks: options.webhooks?.snapshot(namespace)
4953
+ });
4954
+ return handle;
4955
+ },
4956
+ fleetRestore(handle, namespace) {
4957
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4958
+ if (!point || point.namespace !== namespace)
4959
+ throw new Error("invalid fleet checkpoint");
4960
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4961
+ captured.set(namespace, point.state.snapshot);
4962
+ const selectedClock = namespaceClock(namespace);
4963
+ selectedClock.freeze();
4964
+ selectedClock.set(point.state.clock.now);
4965
+ if (!point.state.clock.frozen)
4966
+ selectedClock.unfreeze();
4967
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4968
+ faults.restore(namespace, point.faults);
4969
+ if (point.webhooks !== void 0)
4970
+ options.webhooks?.restore(namespace, point.webhooks);
4971
+ timelines.delete(namespace);
4972
+ },
4799
4973
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4800
4974
  const preset = options.presets?.[name];
4801
4975
  if (!preset)
@@ -4971,6 +5145,7 @@ var createRuntime = (options) => {
4971
5145
  startedAt: wallNow(),
4972
5146
  wallNow,
4973
5147
  clock,
5148
+ namespaceClock,
4974
5149
  faults,
4975
5150
  metrics,
4976
5151
  journal,
@@ -5892,6 +6067,8 @@ var RxVortexAPI = class {
5892
6067
  };
5893
6068
 
5894
6069
  export {
6070
+ createClock,
6071
+ parseDuration,
5895
6072
  document2 as document,
5896
6073
  operationIds,
5897
6074
  supportedOperationIds,
@@ -5905,4 +6082,4 @@ export {
5905
6082
  tokenCredential,
5906
6083
  RxVortexAPI
5907
6084
  };
5908
- //# sourceMappingURL=chunk-CNQMYSGI.js.map
6085
+ //# sourceMappingURL=chunk-HJ32ST6Y.js.map