@crvouga/mockingbird-service-prism 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-prism
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 }) => {
@@ -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;
@@ -4072,7 +4123,7 @@ var stateAdminRoutes = (routes) => {
4072
4123
  };
4073
4124
 
4074
4125
  // ../core/dist/version.js
4075
- var PACKAGE_VERSION = true ? "1.7.1" : UNRELEASED_VERSION;
4126
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
4076
4127
 
4077
4128
  // ../core/dist/webhooks.js
4078
4129
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -4238,6 +4289,20 @@ var createRuntime = (options) => {
4238
4289
  const instances = /* @__PURE__ */ new Map();
4239
4290
  const publicNamespaces = /* @__PURE__ */ new Set();
4240
4291
  const branchRngs = /* @__PURE__ */ new Map();
4292
+ const namespaceClocks = /* @__PURE__ */ new Map();
4293
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4294
+ let isolatedNamespaces = false;
4295
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4296
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4297
+ return clock;
4298
+ let found = namespaceClocks.get(name);
4299
+ if (!found) {
4300
+ found = createClock();
4301
+ namespaceClocks.set(name, found);
4302
+ }
4303
+ return found;
4304
+ };
4305
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
4241
4306
  const timelines = /* @__PURE__ */ new Map();
4242
4307
  const branchStorage = /* @__PURE__ */ new Map();
4243
4308
  const captured = /* @__PURE__ */ new Map();
@@ -4251,17 +4316,21 @@ var createRuntime = (options) => {
4251
4316
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
4252
4317
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
4253
4318
  }
4319
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
4254
4320
  const created = options.create({
4255
4321
  namespace: storageNamespace(key),
4256
4322
  publicNamespace,
4257
4323
  sqlite,
4258
- clock,
4259
- rng: isolatedRng ?? rng
4324
+ clock: namespaceClock(publicNamespace),
4325
+ rng: createdContextRng
4260
4326
  });
4261
4327
  instances.set(key, created);
4328
+ namespaceForStorage.set(key, publicNamespace);
4262
4329
  publicNamespaces.add(publicNamespace);
4263
4330
  if (isolatedRng)
4264
4331
  branchRngs.set(key, isolatedRng);
4332
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4333
+ branchRngs.set(key, createdContextRng);
4265
4334
  return created;
4266
4335
  };
4267
4336
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4279,7 +4348,7 @@ var createRuntime = (options) => {
4279
4348
  captured.set(storage, snapshot2);
4280
4349
  return Object.freeze({
4281
4350
  snapshot: snapshot2,
4282
- clock: Object.freeze(clock.state()),
4351
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4283
4352
  rngState: (branchRngs.get(storage) ?? rng).state()
4284
4353
  });
4285
4354
  };
@@ -4289,7 +4358,7 @@ var createRuntime = (options) => {
4289
4358
  return found;
4290
4359
  instance(name);
4291
4360
  found = new Timeline({
4292
- now: clock.now,
4361
+ now: namespaceClock(name).now,
4293
4362
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4294
4363
  });
4295
4364
  found.commit(capture(name));
@@ -4316,12 +4385,13 @@ var createRuntime = (options) => {
4316
4385
  const point = history2.checkout("main", at);
4317
4386
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4318
4387
  captured.set(namespace, point.value.snapshot);
4319
- rng.setState(point.value.rngState);
4320
- clock.set(point.value.clock.now);
4388
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4389
+ const selectedClock = namespaceClock(namespace);
4390
+ selectedClock.set(point.value.clock.now);
4321
4391
  if (point.value.clock.frozen)
4322
- clock.freeze();
4392
+ selectedClock.freeze();
4323
4393
  else
4324
- clock.unfreeze();
4394
+ selectedClock.unfreeze();
4325
4395
  }
4326
4396
  return namespace;
4327
4397
  }
@@ -4379,11 +4449,12 @@ var createRuntime = (options) => {
4379
4449
  const storage = ensureBranch(namespace, branchName);
4380
4450
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4381
4451
  captured.set(storage, point.value.snapshot);
4382
- clock.set(point.value.clock.now);
4452
+ const selectedClock = namespaceClock(namespace);
4453
+ selectedClock.set(point.value.clock.now);
4383
4454
  if (point.value.clock.frozen)
4384
- clock.freeze();
4455
+ selectedClock.freeze();
4385
4456
  else
4386
- clock.unfreeze();
4457
+ selectedClock.unfreeze();
4387
4458
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4388
4459
  };
4389
4460
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4391,10 +4462,21 @@ var createRuntime = (options) => {
4391
4462
  options.webhooks?.clear();
4392
4463
  for (const each of instances.values())
4393
4464
  await each.reset();
4465
+ for (const each of branchRngs.values())
4466
+ each.reset();
4394
4467
  timelines.clear();
4468
+ for (const storage of branchStorage.values()) {
4469
+ instances.delete(storage);
4470
+ branchRngs.delete(storage);
4471
+ namespaceForStorage.delete(storage);
4472
+ }
4395
4473
  branchStorage.clear();
4396
- branchRngs.clear();
4397
4474
  captured.clear();
4475
+ faults.clear();
4476
+ clock.reset();
4477
+ rng.reset();
4478
+ for (const each of namespaceClocks.values())
4479
+ each.reset();
4398
4480
  return;
4399
4481
  }
4400
4482
  options.webhooks?.clear(name);
@@ -4412,11 +4494,18 @@ var createRuntime = (options) => {
4412
4494
  else
4413
4495
  clearNamespace(sqlite, storageNamespace(storage));
4414
4496
  branchStorage.delete(mapping);
4497
+ instances.delete(storage);
4498
+ namespaceForStorage.delete(storage);
4415
4499
  branchRngs.delete(storage);
4416
4500
  captured.delete(storage);
4417
4501
  }
4418
4502
  timelines.delete(name);
4419
4503
  captured.delete(name);
4504
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4505
+ namespaceClock(name).reset();
4506
+ (branchRngs.get(name) ?? rng).reset();
4507
+ journal.clear(name);
4508
+ metrics.reset(name);
4420
4509
  };
4421
4510
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4422
4511
  return checkpoint(name, "main").value.snapshot;
@@ -4454,6 +4543,53 @@ var createRuntime = (options) => {
4454
4543
  rng,
4455
4544
  credentials,
4456
4545
  webhooks: options.webhooks,
4546
+ isolateNamespaces() {
4547
+ if (publicNamespaces.size > 1)
4548
+ throw new Error("enable namespace isolation before creating named namespaces");
4549
+ isolatedNamespaces = true;
4550
+ faults.isolateNamespaces();
4551
+ },
4552
+ namespaceClock,
4553
+ namespaceOf(request) {
4554
+ const url = new URL(request.url);
4555
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4556
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4557
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4558
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4559
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4560
+ if (selected !== void 0)
4561
+ return selected;
4562
+ const credential = options.credential?.(request);
4563
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4564
+ },
4565
+ fleetSnapshot(namespace) {
4566
+ instance(namespace);
4567
+ const handle = {};
4568
+ fleetPoints.set(handle, {
4569
+ namespace,
4570
+ state: capture(namespace),
4571
+ faults: faults.snapshot(namespace),
4572
+ webhooks: options.webhooks?.snapshot(namespace)
4573
+ });
4574
+ return handle;
4575
+ },
4576
+ fleetRestore(handle, namespace) {
4577
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4578
+ if (!point || point.namespace !== namespace)
4579
+ throw new Error("invalid fleet checkpoint");
4580
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4581
+ captured.set(namespace, point.state.snapshot);
4582
+ const selectedClock = namespaceClock(namespace);
4583
+ selectedClock.freeze();
4584
+ selectedClock.set(point.state.clock.now);
4585
+ if (!point.state.clock.frozen)
4586
+ selectedClock.unfreeze();
4587
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4588
+ faults.restore(namespace, point.faults);
4589
+ if (point.webhooks !== void 0)
4590
+ options.webhooks?.restore(namespace, point.webhooks);
4591
+ timelines.delete(namespace);
4592
+ },
4457
4593
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4458
4594
  const preset = options.presets?.[name];
4459
4595
  if (!preset)
@@ -4629,6 +4765,7 @@ var createRuntime = (options) => {
4629
4765
  startedAt: wallNow(),
4630
4766
  wallNow,
4631
4767
  clock,
4768
+ namespaceClock,
4632
4769
  faults,
4633
4770
  metrics,
4634
4771
  journal,
@@ -5351,6 +5488,8 @@ var PrismAPI = class {
5351
5488
  };
5352
5489
 
5353
5490
  export {
5491
+ createClock,
5492
+ parseDuration,
5354
5493
  document2 as document,
5355
5494
  operationIds,
5356
5495
  supportedOperationIds,
@@ -5362,4 +5501,4 @@ export {
5362
5501
  scanResults,
5363
5502
  PrismAPI
5364
5503
  };
5365
- //# sourceMappingURL=chunk-OZBUWLPY.js.map
5504
+ //# sourceMappingURL=chunk-EJSDM62Y.js.map