@crvouga/mockingbird-service-aws-speech 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-aws-speech
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)
@@ -862,7 +899,8 @@ var annotateResponse = (response, extra) => {
862
899
  var responseNotes = (response) => notes.get(response);
863
900
 
864
901
  // ../core/dist/metrics.js
865
- var createMetrics = () => {
902
+ var createMetrics = (partition = true) => {
903
+ const namespaces = /* @__PURE__ */ new Map();
866
904
  let requests = 0;
867
905
  let faults = 0;
868
906
  let totalDurationMs = 0;
@@ -870,6 +908,14 @@ var createMetrics = () => {
870
908
  const unmatched = /* @__PURE__ */ new Map();
871
909
  return {
872
910
  record(entry) {
911
+ if (partition) {
912
+ let scoped = namespaces.get(entry.namespace);
913
+ if (!scoped) {
914
+ scoped = createMetrics(false);
915
+ namespaces.set(entry.namespace, scoped);
916
+ }
917
+ scoped.record(entry);
918
+ }
873
919
  requests++;
874
920
  totalDurationMs += entry.durationMs;
875
921
  if (entry.faultId !== void 0)
@@ -881,7 +927,7 @@ var createMetrics = () => {
881
927
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
882
928
  }
883
929
  },
884
- report: () => ({
930
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
885
931
  requests,
886
932
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
887
933
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -890,8 +936,13 @@ var createMetrics = () => {
890
936
  }),
891
937
  faults,
892
938
  totalDurationMs
893
- }),
894
- reset() {
939
+ },
940
+ reset(namespace) {
941
+ if (namespace !== void 0) {
942
+ namespaces.delete(namespace);
943
+ return;
944
+ }
945
+ namespaces.clear();
895
946
  requests = 0;
896
947
  faults = 0;
897
948
  totalDurationMs = 0;
@@ -3789,7 +3840,7 @@ var stateAdminRoutes = (routes) => {
3789
3840
  };
3790
3841
 
3791
3842
  // ../core/dist/version.js
3792
- var PACKAGE_VERSION = true ? "1.7.0" : UNRELEASED_VERSION;
3843
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
3793
3844
 
3794
3845
  // ../core/dist/signing.js
3795
3846
  var encoder = new TextEncoder();
@@ -3959,6 +4010,20 @@ var createRuntime = (options) => {
3959
4010
  const instances = /* @__PURE__ */ new Map();
3960
4011
  const publicNamespaces = /* @__PURE__ */ new Set();
3961
4012
  const branchRngs = /* @__PURE__ */ new Map();
4013
+ const namespaceClocks = /* @__PURE__ */ new Map();
4014
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4015
+ let isolatedNamespaces = false;
4016
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4017
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4018
+ return clock;
4019
+ let found = namespaceClocks.get(name);
4020
+ if (!found) {
4021
+ found = createClock();
4022
+ namespaceClocks.set(name, found);
4023
+ }
4024
+ return found;
4025
+ };
4026
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
3962
4027
  const timelines = /* @__PURE__ */ new Map();
3963
4028
  const branchStorage = /* @__PURE__ */ new Map();
3964
4029
  const captured = /* @__PURE__ */ new Map();
@@ -3972,17 +4037,21 @@ var createRuntime = (options) => {
3972
4037
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
3973
4038
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
3974
4039
  }
4040
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
3975
4041
  const created = options.create({
3976
4042
  namespace: storageNamespace(key),
3977
4043
  publicNamespace,
3978
4044
  sqlite,
3979
- clock,
3980
- rng: isolatedRng ?? rng
4045
+ clock: namespaceClock(publicNamespace),
4046
+ rng: createdContextRng
3981
4047
  });
3982
4048
  instances.set(key, created);
4049
+ namespaceForStorage.set(key, publicNamespace);
3983
4050
  publicNamespaces.add(publicNamespace);
3984
4051
  if (isolatedRng)
3985
4052
  branchRngs.set(key, isolatedRng);
4053
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4054
+ branchRngs.set(key, createdContextRng);
3986
4055
  return created;
3987
4056
  };
3988
4057
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4000,7 +4069,7 @@ var createRuntime = (options) => {
4000
4069
  captured.set(storage, snapshot2);
4001
4070
  return Object.freeze({
4002
4071
  snapshot: snapshot2,
4003
- clock: Object.freeze(clock.state()),
4072
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4004
4073
  rngState: (branchRngs.get(storage) ?? rng).state()
4005
4074
  });
4006
4075
  };
@@ -4010,7 +4079,7 @@ var createRuntime = (options) => {
4010
4079
  return found;
4011
4080
  instance(name);
4012
4081
  found = new Timeline({
4013
- now: clock.now,
4082
+ now: namespaceClock(name).now,
4014
4083
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4015
4084
  });
4016
4085
  found.commit(capture(name));
@@ -4037,12 +4106,13 @@ var createRuntime = (options) => {
4037
4106
  const point = history2.checkout("main", at);
4038
4107
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4039
4108
  captured.set(namespace, point.value.snapshot);
4040
- rng.setState(point.value.rngState);
4041
- clock.set(point.value.clock.now);
4109
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4110
+ const selectedClock = namespaceClock(namespace);
4111
+ selectedClock.set(point.value.clock.now);
4042
4112
  if (point.value.clock.frozen)
4043
- clock.freeze();
4113
+ selectedClock.freeze();
4044
4114
  else
4045
- clock.unfreeze();
4115
+ selectedClock.unfreeze();
4046
4116
  }
4047
4117
  return namespace;
4048
4118
  }
@@ -4100,11 +4170,12 @@ var createRuntime = (options) => {
4100
4170
  const storage = ensureBranch(namespace, branchName);
4101
4171
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4102
4172
  captured.set(storage, point.value.snapshot);
4103
- clock.set(point.value.clock.now);
4173
+ const selectedClock = namespaceClock(namespace);
4174
+ selectedClock.set(point.value.clock.now);
4104
4175
  if (point.value.clock.frozen)
4105
- clock.freeze();
4176
+ selectedClock.freeze();
4106
4177
  else
4107
- clock.unfreeze();
4178
+ selectedClock.unfreeze();
4108
4179
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4109
4180
  };
4110
4181
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4112,10 +4183,21 @@ var createRuntime = (options) => {
4112
4183
  options.webhooks?.clear();
4113
4184
  for (const each of instances.values())
4114
4185
  await each.reset();
4186
+ for (const each of branchRngs.values())
4187
+ each.reset();
4115
4188
  timelines.clear();
4189
+ for (const storage of branchStorage.values()) {
4190
+ instances.delete(storage);
4191
+ branchRngs.delete(storage);
4192
+ namespaceForStorage.delete(storage);
4193
+ }
4116
4194
  branchStorage.clear();
4117
- branchRngs.clear();
4118
4195
  captured.clear();
4196
+ faults.clear();
4197
+ clock.reset();
4198
+ rng.reset();
4199
+ for (const each of namespaceClocks.values())
4200
+ each.reset();
4119
4201
  return;
4120
4202
  }
4121
4203
  options.webhooks?.clear(name);
@@ -4133,11 +4215,18 @@ var createRuntime = (options) => {
4133
4215
  else
4134
4216
  clearNamespace(sqlite, storageNamespace(storage));
4135
4217
  branchStorage.delete(mapping);
4218
+ instances.delete(storage);
4219
+ namespaceForStorage.delete(storage);
4136
4220
  branchRngs.delete(storage);
4137
4221
  captured.delete(storage);
4138
4222
  }
4139
4223
  timelines.delete(name);
4140
4224
  captured.delete(name);
4225
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4226
+ namespaceClock(name).reset();
4227
+ (branchRngs.get(name) ?? rng).reset();
4228
+ journal.clear(name);
4229
+ metrics.reset(name);
4141
4230
  };
4142
4231
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4143
4232
  return checkpoint(name, "main").value.snapshot;
@@ -4175,6 +4264,53 @@ var createRuntime = (options) => {
4175
4264
  rng,
4176
4265
  credentials,
4177
4266
  webhooks: options.webhooks,
4267
+ isolateNamespaces() {
4268
+ if (publicNamespaces.size > 1)
4269
+ throw new Error("enable namespace isolation before creating named namespaces");
4270
+ isolatedNamespaces = true;
4271
+ faults.isolateNamespaces();
4272
+ },
4273
+ namespaceClock,
4274
+ namespaceOf(request) {
4275
+ const url = new URL(request.url);
4276
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4277
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4278
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4279
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4280
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4281
+ if (selected !== void 0)
4282
+ return selected;
4283
+ const credential = options.credential?.(request);
4284
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4285
+ },
4286
+ fleetSnapshot(namespace) {
4287
+ instance(namespace);
4288
+ const handle = {};
4289
+ fleetPoints.set(handle, {
4290
+ namespace,
4291
+ state: capture(namespace),
4292
+ faults: faults.snapshot(namespace),
4293
+ webhooks: options.webhooks?.snapshot(namespace)
4294
+ });
4295
+ return handle;
4296
+ },
4297
+ fleetRestore(handle, namespace) {
4298
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4299
+ if (!point || point.namespace !== namespace)
4300
+ throw new Error("invalid fleet checkpoint");
4301
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4302
+ captured.set(namespace, point.state.snapshot);
4303
+ const selectedClock = namespaceClock(namespace);
4304
+ selectedClock.freeze();
4305
+ selectedClock.set(point.state.clock.now);
4306
+ if (!point.state.clock.frozen)
4307
+ selectedClock.unfreeze();
4308
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4309
+ faults.restore(namespace, point.faults);
4310
+ if (point.webhooks !== void 0)
4311
+ options.webhooks?.restore(namespace, point.webhooks);
4312
+ timelines.delete(namespace);
4313
+ },
4178
4314
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4179
4315
  const preset = options.presets?.[name];
4180
4316
  if (!preset)
@@ -4350,6 +4486,7 @@ var createRuntime = (options) => {
4350
4486
  startedAt: wallNow(),
4351
4487
  wallNow,
4352
4488
  clock,
4489
+ namespaceClock,
4353
4490
  faults,
4354
4491
  metrics,
4355
4492
  journal,
@@ -5923,6 +6060,8 @@ var SpeechAPI = class {
5923
6060
  };
5924
6061
 
5925
6062
  export {
6063
+ createClock,
6064
+ parseDuration,
5926
6065
  MS_PER_CHARACTER,
5927
6066
  durationFor,
5928
6067
  pcmTone,
@@ -5951,4 +6090,4 @@ export {
5951
6090
  accessKeyCredential,
5952
6091
  SpeechAPI
5953
6092
  };
5954
- //# sourceMappingURL=chunk-4VZZ5XPO.js.map
6093
+ //# sourceMappingURL=chunk-FLBEZUJF.js.map