@crvouga/mockingbird-service-otel 1.7.1 → 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,5 +1,20 @@
1
1
  # Changelog — @crvouga/mockingbird-service-otel
2
2
 
3
+ ## 1.8.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.7.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 }) => {
@@ -492,7 +493,33 @@ var faultResponse = (rule) => {
492
493
  };
493
494
  var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
494
495
  const entries = [];
496
+ let isolated = false;
497
+ const scopedRngs = /* @__PURE__ */ new Map();
498
+ const random = (namespace) => {
499
+ if (!isolated || namespace === "default")
500
+ return rng;
501
+ let scoped = scopedRngs.get(namespace);
502
+ if (!scoped) {
503
+ scoped = createRng(rng.seed);
504
+ scopedRngs.set(namespace, scoped);
505
+ }
506
+ return scoped;
507
+ };
495
508
  return {
509
+ isolateNamespaces() {
510
+ isolated = true;
511
+ },
512
+ snapshot(namespace) {
513
+ return {
514
+ rules: structuredClone(entries.map((entry, position) => ({
515
+ ...entry.rule,
516
+ remaining: entry.remaining,
517
+ hits: entry.hits,
518
+ position
519
+ })).filter((entry) => entry.namespace === namespace)),
520
+ rngState: random(namespace).state()
521
+ };
522
+ },
496
523
  add(rule) {
497
524
  const existing = entries.findIndex((e) => e.rule.id === rule.id);
498
525
  const entry = { rule, remaining: rule.count ?? null, hits: 0 };
@@ -512,6 +539,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
512
539
  },
513
540
  clear() {
514
541
  entries.length = 0;
542
+ rng.reset();
543
+ scopedRngs.clear();
544
+ },
545
+ restore(namespace, state) {
546
+ for (let i = entries.length - 1; i >= 0; i--)
547
+ if (entries[i]?.rule.namespace === namespace)
548
+ entries.splice(i, 1);
549
+ for (const { remaining, hits, position, ...rule } of state.rules)
550
+ entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
551
+ random(namespace).setState(state.rngState);
515
552
  },
516
553
  async take(candidate) {
517
554
  const hits = [];
@@ -521,7 +558,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
521
558
  if (!matches(entry.rule, candidate))
522
559
  continue;
523
560
  const rate = entry.rule.rate ?? 1;
524
- if (rng.next() >= rate)
561
+ if (random(candidate.namespace).next() >= rate)
525
562
  continue;
526
563
  entry.hits++;
527
564
  if (entry.remaining !== null)
@@ -853,7 +890,8 @@ var annotateResponse = (response, extra) => {
853
890
  var responseNotes = (response) => notes.get(response);
854
891
 
855
892
  // ../core/dist/metrics.js
856
- var createMetrics = () => {
893
+ var createMetrics = (partition = true) => {
894
+ const namespaces = /* @__PURE__ */ new Map();
857
895
  let requests = 0;
858
896
  let faults = 0;
859
897
  let totalDurationMs = 0;
@@ -861,6 +899,14 @@ var createMetrics = () => {
861
899
  const unmatched = /* @__PURE__ */ new Map();
862
900
  return {
863
901
  record(entry) {
902
+ if (partition) {
903
+ let scoped = namespaces.get(entry.namespace);
904
+ if (!scoped) {
905
+ scoped = createMetrics(false);
906
+ namespaces.set(entry.namespace, scoped);
907
+ }
908
+ scoped.record(entry);
909
+ }
864
910
  requests++;
865
911
  totalDurationMs += entry.durationMs;
866
912
  if (entry.faultId !== void 0)
@@ -872,7 +918,7 @@ var createMetrics = () => {
872
918
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
873
919
  }
874
920
  },
875
- report: () => ({
921
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
876
922
  requests,
877
923
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
878
924
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -881,8 +927,13 @@ var createMetrics = () => {
881
927
  }),
882
928
  faults,
883
929
  totalDurationMs
884
- }),
885
- reset() {
930
+ },
931
+ reset(namespace) {
932
+ if (namespace !== void 0) {
933
+ namespaces.delete(namespace);
934
+ return;
935
+ }
936
+ namespaces.clear();
886
937
  requests = 0;
887
938
  faults = 0;
888
939
  totalDurationMs = 0;
@@ -3780,7 +3831,7 @@ var stateAdminRoutes = (routes) => {
3780
3831
  };
3781
3832
 
3782
3833
  // ../core/dist/version.js
3783
- var PACKAGE_VERSION = true ? "1.7.1" : UNRELEASED_VERSION;
3834
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
3784
3835
 
3785
3836
  // ../core/dist/webhooks.js
3786
3837
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -3946,6 +3997,20 @@ var createRuntime = (options) => {
3946
3997
  const instances = /* @__PURE__ */ new Map();
3947
3998
  const publicNamespaces = /* @__PURE__ */ new Set();
3948
3999
  const branchRngs = /* @__PURE__ */ new Map();
4000
+ const namespaceClocks = /* @__PURE__ */ new Map();
4001
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4002
+ let isolatedNamespaces = false;
4003
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4004
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4005
+ return clock;
4006
+ let found = namespaceClocks.get(name);
4007
+ if (!found) {
4008
+ found = createClock();
4009
+ namespaceClocks.set(name, found);
4010
+ }
4011
+ return found;
4012
+ };
4013
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
3949
4014
  const timelines = /* @__PURE__ */ new Map();
3950
4015
  const branchStorage = /* @__PURE__ */ new Map();
3951
4016
  const captured = /* @__PURE__ */ new Map();
@@ -3959,17 +4024,21 @@ var createRuntime = (options) => {
3959
4024
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
3960
4025
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
3961
4026
  }
4027
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
3962
4028
  const created = options.create({
3963
4029
  namespace: storageNamespace(key),
3964
4030
  publicNamespace,
3965
4031
  sqlite,
3966
- clock,
3967
- rng: isolatedRng ?? rng
4032
+ clock: namespaceClock(publicNamespace),
4033
+ rng: createdContextRng
3968
4034
  });
3969
4035
  instances.set(key, created);
4036
+ namespaceForStorage.set(key, publicNamespace);
3970
4037
  publicNamespaces.add(publicNamespace);
3971
4038
  if (isolatedRng)
3972
4039
  branchRngs.set(key, isolatedRng);
4040
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4041
+ branchRngs.set(key, createdContextRng);
3973
4042
  return created;
3974
4043
  };
3975
4044
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -3987,7 +4056,7 @@ var createRuntime = (options) => {
3987
4056
  captured.set(storage, snapshot2);
3988
4057
  return Object.freeze({
3989
4058
  snapshot: snapshot2,
3990
- clock: Object.freeze(clock.state()),
4059
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
3991
4060
  rngState: (branchRngs.get(storage) ?? rng).state()
3992
4061
  });
3993
4062
  };
@@ -3997,7 +4066,7 @@ var createRuntime = (options) => {
3997
4066
  return found;
3998
4067
  instance(name);
3999
4068
  found = new Timeline({
4000
- now: clock.now,
4069
+ now: namespaceClock(name).now,
4001
4070
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4002
4071
  });
4003
4072
  found.commit(capture(name));
@@ -4024,12 +4093,13 @@ var createRuntime = (options) => {
4024
4093
  const point = history2.checkout("main", at);
4025
4094
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4026
4095
  captured.set(namespace, point.value.snapshot);
4027
- rng.setState(point.value.rngState);
4028
- clock.set(point.value.clock.now);
4096
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4097
+ const selectedClock = namespaceClock(namespace);
4098
+ selectedClock.set(point.value.clock.now);
4029
4099
  if (point.value.clock.frozen)
4030
- clock.freeze();
4100
+ selectedClock.freeze();
4031
4101
  else
4032
- clock.unfreeze();
4102
+ selectedClock.unfreeze();
4033
4103
  }
4034
4104
  return namespace;
4035
4105
  }
@@ -4087,11 +4157,12 @@ var createRuntime = (options) => {
4087
4157
  const storage = ensureBranch(namespace, branchName);
4088
4158
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4089
4159
  captured.set(storage, point.value.snapshot);
4090
- clock.set(point.value.clock.now);
4160
+ const selectedClock = namespaceClock(namespace);
4161
+ selectedClock.set(point.value.clock.now);
4091
4162
  if (point.value.clock.frozen)
4092
- clock.freeze();
4163
+ selectedClock.freeze();
4093
4164
  else
4094
- clock.unfreeze();
4165
+ selectedClock.unfreeze();
4095
4166
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4096
4167
  };
4097
4168
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4099,10 +4170,21 @@ var createRuntime = (options) => {
4099
4170
  options.webhooks?.clear();
4100
4171
  for (const each of instances.values())
4101
4172
  await each.reset();
4173
+ for (const each of branchRngs.values())
4174
+ each.reset();
4102
4175
  timelines.clear();
4176
+ for (const storage of branchStorage.values()) {
4177
+ instances.delete(storage);
4178
+ branchRngs.delete(storage);
4179
+ namespaceForStorage.delete(storage);
4180
+ }
4103
4181
  branchStorage.clear();
4104
- branchRngs.clear();
4105
4182
  captured.clear();
4183
+ faults.clear();
4184
+ clock.reset();
4185
+ rng.reset();
4186
+ for (const each of namespaceClocks.values())
4187
+ each.reset();
4106
4188
  return;
4107
4189
  }
4108
4190
  options.webhooks?.clear(name);
@@ -4120,11 +4202,18 @@ var createRuntime = (options) => {
4120
4202
  else
4121
4203
  clearNamespace(sqlite, storageNamespace(storage));
4122
4204
  branchStorage.delete(mapping);
4205
+ instances.delete(storage);
4206
+ namespaceForStorage.delete(storage);
4123
4207
  branchRngs.delete(storage);
4124
4208
  captured.delete(storage);
4125
4209
  }
4126
4210
  timelines.delete(name);
4127
4211
  captured.delete(name);
4212
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4213
+ namespaceClock(name).reset();
4214
+ (branchRngs.get(name) ?? rng).reset();
4215
+ journal.clear(name);
4216
+ metrics.reset(name);
4128
4217
  };
4129
4218
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4130
4219
  return checkpoint(name, "main").value.snapshot;
@@ -4162,6 +4251,53 @@ var createRuntime = (options) => {
4162
4251
  rng,
4163
4252
  credentials,
4164
4253
  webhooks: options.webhooks,
4254
+ isolateNamespaces() {
4255
+ if (publicNamespaces.size > 1)
4256
+ throw new Error("enable namespace isolation before creating named namespaces");
4257
+ isolatedNamespaces = true;
4258
+ faults.isolateNamespaces();
4259
+ },
4260
+ namespaceClock,
4261
+ namespaceOf(request) {
4262
+ const url = new URL(request.url);
4263
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4264
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4265
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4266
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4267
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4268
+ if (selected !== void 0)
4269
+ return selected;
4270
+ const credential = options.credential?.(request);
4271
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4272
+ },
4273
+ fleetSnapshot(namespace) {
4274
+ instance(namespace);
4275
+ const handle = {};
4276
+ fleetPoints.set(handle, {
4277
+ namespace,
4278
+ state: capture(namespace),
4279
+ faults: faults.snapshot(namespace),
4280
+ webhooks: options.webhooks?.snapshot(namespace)
4281
+ });
4282
+ return handle;
4283
+ },
4284
+ fleetRestore(handle, namespace) {
4285
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4286
+ if (!point || point.namespace !== namespace)
4287
+ throw new Error("invalid fleet checkpoint");
4288
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4289
+ captured.set(namespace, point.state.snapshot);
4290
+ const selectedClock = namespaceClock(namespace);
4291
+ selectedClock.freeze();
4292
+ selectedClock.set(point.state.clock.now);
4293
+ if (!point.state.clock.frozen)
4294
+ selectedClock.unfreeze();
4295
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4296
+ faults.restore(namespace, point.faults);
4297
+ if (point.webhooks !== void 0)
4298
+ options.webhooks?.restore(namespace, point.webhooks);
4299
+ timelines.delete(namespace);
4300
+ },
4165
4301
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4166
4302
  const preset = options.presets?.[name];
4167
4303
  if (!preset)
@@ -4337,6 +4473,7 @@ var createRuntime = (options) => {
4337
4473
  startedAt: wallNow(),
4338
4474
  wallNow,
4339
4475
  clock,
4476
+ namespaceClock,
4340
4477
  faults,
4341
4478
  metrics,
4342
4479
  journal,
@@ -6101,6 +6238,8 @@ var adminRow = (stored) => ({
6101
6238
  });
6102
6239
 
6103
6240
  export {
6241
+ createClock,
6242
+ parseDuration,
6104
6243
  document2 as document,
6105
6244
  operationIds,
6106
6245
  supportedOperationIds,
@@ -6123,4 +6262,4 @@ export {
6123
6262
  OtelAPI,
6124
6263
  adminRow
6125
6264
  };
6126
- //# sourceMappingURL=chunk-36MHAEFN.js.map
6265
+ //# sourceMappingURL=chunk-NYIFOGHW.js.map