@crvouga/mockingbird-service-bedrock 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-bedrock
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 }) => {
@@ -475,7 +476,33 @@ var faultResponse = (rule) => {
475
476
  };
476
477
  var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
477
478
  const entries = [];
479
+ let isolated = false;
480
+ const scopedRngs = /* @__PURE__ */ new Map();
481
+ const random = (namespace) => {
482
+ if (!isolated || namespace === "default")
483
+ return rng;
484
+ let scoped = scopedRngs.get(namespace);
485
+ if (!scoped) {
486
+ scoped = createRng(rng.seed);
487
+ scopedRngs.set(namespace, scoped);
488
+ }
489
+ return scoped;
490
+ };
478
491
  return {
492
+ isolateNamespaces() {
493
+ isolated = true;
494
+ },
495
+ snapshot(namespace) {
496
+ return {
497
+ rules: structuredClone(entries.map((entry, position) => ({
498
+ ...entry.rule,
499
+ remaining: entry.remaining,
500
+ hits: entry.hits,
501
+ position
502
+ })).filter((entry) => entry.namespace === namespace)),
503
+ rngState: random(namespace).state()
504
+ };
505
+ },
479
506
  add(rule) {
480
507
  const existing = entries.findIndex((e) => e.rule.id === rule.id);
481
508
  const entry = { rule, remaining: rule.count ?? null, hits: 0 };
@@ -495,6 +522,16 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
495
522
  },
496
523
  clear() {
497
524
  entries.length = 0;
525
+ rng.reset();
526
+ scopedRngs.clear();
527
+ },
528
+ restore(namespace, state) {
529
+ for (let i = entries.length - 1; i >= 0; i--)
530
+ if (entries[i]?.rule.namespace === namespace)
531
+ entries.splice(i, 1);
532
+ for (const { remaining, hits, position, ...rule } of state.rules)
533
+ entries.splice(Math.min(position, entries.length), 0, { rule, remaining, hits });
534
+ random(namespace).setState(state.rngState);
498
535
  },
499
536
  async take(candidate) {
500
537
  const hits = [];
@@ -504,7 +541,7 @@ var createFaultRegistry = (rng = createRng(0), sleep = (ms) => new Promise((reso
504
541
  if (!matches(entry.rule, candidate))
505
542
  continue;
506
543
  const rate = entry.rule.rate ?? 1;
507
- if (rng.next() >= rate)
544
+ if (random(candidate.namespace).next() >= rate)
508
545
  continue;
509
546
  entry.hits++;
510
547
  if (entry.remaining !== null)
@@ -1072,7 +1109,8 @@ var annotateResponse = (response, extra) => {
1072
1109
  var responseNotes = (response) => notes.get(response);
1073
1110
 
1074
1111
  // ../core/dist/metrics.js
1075
- var createMetrics = () => {
1112
+ var createMetrics = (partition = true) => {
1113
+ const namespaces = /* @__PURE__ */ new Map();
1076
1114
  let requests = 0;
1077
1115
  let faults = 0;
1078
1116
  let totalDurationMs = 0;
@@ -1080,6 +1118,14 @@ var createMetrics = () => {
1080
1118
  const unmatched = /* @__PURE__ */ new Map();
1081
1119
  return {
1082
1120
  record(entry) {
1121
+ if (partition) {
1122
+ let scoped = namespaces.get(entry.namespace);
1123
+ if (!scoped) {
1124
+ scoped = createMetrics(false);
1125
+ namespaces.set(entry.namespace, scoped);
1126
+ }
1127
+ scoped.record(entry);
1128
+ }
1083
1129
  requests++;
1084
1130
  totalDurationMs += entry.durationMs;
1085
1131
  if (entry.faultId !== void 0)
@@ -1091,7 +1137,7 @@ var createMetrics = () => {
1091
1137
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
1092
1138
  }
1093
1139
  },
1094
- report: () => ({
1140
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
1095
1141
  requests,
1096
1142
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
1097
1143
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -1100,8 +1146,13 @@ var createMetrics = () => {
1100
1146
  }),
1101
1147
  faults,
1102
1148
  totalDurationMs
1103
- }),
1104
- reset() {
1149
+ },
1150
+ reset(namespace) {
1151
+ if (namespace !== void 0) {
1152
+ namespaces.delete(namespace);
1153
+ return;
1154
+ }
1155
+ namespaces.clear();
1105
1156
  requests = 0;
1106
1157
  faults = 0;
1107
1158
  totalDurationMs = 0;
@@ -4073,7 +4124,7 @@ var stateAdminRoutes = (routes) => {
4073
4124
  };
4074
4125
 
4075
4126
  // ../core/dist/version.js
4076
- var PACKAGE_VERSION = true ? "1.7.0" : UNRELEASED_VERSION;
4127
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
4077
4128
 
4078
4129
  // ../core/dist/signing.js
4079
4130
  var encoder = new TextEncoder();
@@ -4244,6 +4295,20 @@ var createRuntime = (options) => {
4244
4295
  const instances = /* @__PURE__ */ new Map();
4245
4296
  const publicNamespaces = /* @__PURE__ */ new Set();
4246
4297
  const branchRngs = /* @__PURE__ */ new Map();
4298
+ const namespaceClocks = /* @__PURE__ */ new Map();
4299
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4300
+ let isolatedNamespaces = false;
4301
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4302
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4303
+ return clock;
4304
+ let found = namespaceClocks.get(name);
4305
+ if (!found) {
4306
+ found = createClock();
4307
+ namespaceClocks.set(name, found);
4308
+ }
4309
+ return found;
4310
+ };
4311
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
4247
4312
  const timelines = /* @__PURE__ */ new Map();
4248
4313
  const branchStorage = /* @__PURE__ */ new Map();
4249
4314
  const captured = /* @__PURE__ */ new Map();
@@ -4257,17 +4322,21 @@ var createRuntime = (options) => {
4257
4322
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
4258
4323
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
4259
4324
  }
4325
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
4260
4326
  const created = options.create({
4261
4327
  namespace: storageNamespace(key),
4262
4328
  publicNamespace,
4263
4329
  sqlite,
4264
- clock,
4265
- rng: isolatedRng ?? rng
4330
+ clock: namespaceClock(publicNamespace),
4331
+ rng: createdContextRng
4266
4332
  });
4267
4333
  instances.set(key, created);
4334
+ namespaceForStorage.set(key, publicNamespace);
4268
4335
  publicNamespaces.add(publicNamespace);
4269
4336
  if (isolatedRng)
4270
4337
  branchRngs.set(key, isolatedRng);
4338
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4339
+ branchRngs.set(key, createdContextRng);
4271
4340
  return created;
4272
4341
  };
4273
4342
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4285,7 +4354,7 @@ var createRuntime = (options) => {
4285
4354
  captured.set(storage, snapshot2);
4286
4355
  return Object.freeze({
4287
4356
  snapshot: snapshot2,
4288
- clock: Object.freeze(clock.state()),
4357
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4289
4358
  rngState: (branchRngs.get(storage) ?? rng).state()
4290
4359
  });
4291
4360
  };
@@ -4295,7 +4364,7 @@ var createRuntime = (options) => {
4295
4364
  return found;
4296
4365
  instance(name);
4297
4366
  found = new Timeline({
4298
- now: clock.now,
4367
+ now: namespaceClock(name).now,
4299
4368
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4300
4369
  });
4301
4370
  found.commit(capture(name));
@@ -4322,12 +4391,13 @@ var createRuntime = (options) => {
4322
4391
  const point = history2.checkout("main", at);
4323
4392
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4324
4393
  captured.set(namespace, point.value.snapshot);
4325
- rng.setState(point.value.rngState);
4326
- clock.set(point.value.clock.now);
4394
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4395
+ const selectedClock = namespaceClock(namespace);
4396
+ selectedClock.set(point.value.clock.now);
4327
4397
  if (point.value.clock.frozen)
4328
- clock.freeze();
4398
+ selectedClock.freeze();
4329
4399
  else
4330
- clock.unfreeze();
4400
+ selectedClock.unfreeze();
4331
4401
  }
4332
4402
  return namespace;
4333
4403
  }
@@ -4385,11 +4455,12 @@ var createRuntime = (options) => {
4385
4455
  const storage = ensureBranch(namespace, branchName);
4386
4456
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4387
4457
  captured.set(storage, point.value.snapshot);
4388
- clock.set(point.value.clock.now);
4458
+ const selectedClock = namespaceClock(namespace);
4459
+ selectedClock.set(point.value.clock.now);
4389
4460
  if (point.value.clock.frozen)
4390
- clock.freeze();
4461
+ selectedClock.freeze();
4391
4462
  else
4392
- clock.unfreeze();
4463
+ selectedClock.unfreeze();
4393
4464
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4394
4465
  };
4395
4466
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4397,10 +4468,21 @@ var createRuntime = (options) => {
4397
4468
  options.webhooks?.clear();
4398
4469
  for (const each of instances.values())
4399
4470
  await each.reset();
4471
+ for (const each of branchRngs.values())
4472
+ each.reset();
4400
4473
  timelines.clear();
4474
+ for (const storage of branchStorage.values()) {
4475
+ instances.delete(storage);
4476
+ branchRngs.delete(storage);
4477
+ namespaceForStorage.delete(storage);
4478
+ }
4401
4479
  branchStorage.clear();
4402
- branchRngs.clear();
4403
4480
  captured.clear();
4481
+ faults.clear();
4482
+ clock.reset();
4483
+ rng.reset();
4484
+ for (const each of namespaceClocks.values())
4485
+ each.reset();
4404
4486
  return;
4405
4487
  }
4406
4488
  options.webhooks?.clear(name);
@@ -4418,11 +4500,18 @@ var createRuntime = (options) => {
4418
4500
  else
4419
4501
  clearNamespace(sqlite, storageNamespace(storage));
4420
4502
  branchStorage.delete(mapping);
4503
+ instances.delete(storage);
4504
+ namespaceForStorage.delete(storage);
4421
4505
  branchRngs.delete(storage);
4422
4506
  captured.delete(storage);
4423
4507
  }
4424
4508
  timelines.delete(name);
4425
4509
  captured.delete(name);
4510
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4511
+ namespaceClock(name).reset();
4512
+ (branchRngs.get(name) ?? rng).reset();
4513
+ journal.clear(name);
4514
+ metrics.reset(name);
4426
4515
  };
4427
4516
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4428
4517
  return checkpoint(name, "main").value.snapshot;
@@ -4460,6 +4549,53 @@ var createRuntime = (options) => {
4460
4549
  rng,
4461
4550
  credentials,
4462
4551
  webhooks: options.webhooks,
4552
+ isolateNamespaces() {
4553
+ if (publicNamespaces.size > 1)
4554
+ throw new Error("enable namespace isolation before creating named namespaces");
4555
+ isolatedNamespaces = true;
4556
+ faults.isolateNamespaces();
4557
+ },
4558
+ namespaceClock,
4559
+ namespaceOf(request) {
4560
+ const url = new URL(request.url);
4561
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4562
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4563
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4564
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4565
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4566
+ if (selected !== void 0)
4567
+ return selected;
4568
+ const credential = options.credential?.(request);
4569
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4570
+ },
4571
+ fleetSnapshot(namespace) {
4572
+ instance(namespace);
4573
+ const handle = {};
4574
+ fleetPoints.set(handle, {
4575
+ namespace,
4576
+ state: capture(namespace),
4577
+ faults: faults.snapshot(namespace),
4578
+ webhooks: options.webhooks?.snapshot(namespace)
4579
+ });
4580
+ return handle;
4581
+ },
4582
+ fleetRestore(handle, namespace) {
4583
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4584
+ if (!point || point.namespace !== namespace)
4585
+ throw new Error("invalid fleet checkpoint");
4586
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4587
+ captured.set(namespace, point.state.snapshot);
4588
+ const selectedClock = namespaceClock(namespace);
4589
+ selectedClock.freeze();
4590
+ selectedClock.set(point.state.clock.now);
4591
+ if (!point.state.clock.frozen)
4592
+ selectedClock.unfreeze();
4593
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4594
+ faults.restore(namespace, point.faults);
4595
+ if (point.webhooks !== void 0)
4596
+ options.webhooks?.restore(namespace, point.webhooks);
4597
+ timelines.delete(namespace);
4598
+ },
4463
4599
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4464
4600
  const preset = options.presets?.[name];
4465
4601
  if (!preset)
@@ -4635,6 +4771,7 @@ var createRuntime = (options) => {
4635
4771
  startedAt: wallNow(),
4636
4772
  wallNow,
4637
4773
  clock,
4774
+ namespaceClock,
4638
4775
  faults,
4639
4776
  metrics,
4640
4777
  journal,
@@ -7206,6 +7343,8 @@ var BedrockAPI = class {
7206
7343
  };
7207
7344
 
7208
7345
  export {
7346
+ createClock,
7347
+ parseDuration,
7209
7348
  document2 as document,
7210
7349
  operationIds,
7211
7350
  supportedOperationIds,
@@ -7237,4 +7376,4 @@ export {
7237
7376
  titanEmbedding,
7238
7377
  BedrockAPI
7239
7378
  };
7240
- //# sourceMappingURL=chunk-VWANBYFI.js.map
7379
+ //# sourceMappingURL=chunk-BDXV2KJB.js.map