@crvouga/mockingbird-service-step-functions 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-step-functions
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 }) => {
@@ -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)
@@ -706,7 +743,8 @@ var notes = /* @__PURE__ */ new WeakMap();
706
743
  var responseNotes = (response) => notes.get(response);
707
744
 
708
745
  // ../core/dist/metrics.js
709
- var createMetrics = () => {
746
+ var createMetrics = (partition = true) => {
747
+ const namespaces = /* @__PURE__ */ new Map();
710
748
  let requests = 0;
711
749
  let faults = 0;
712
750
  let totalDurationMs = 0;
@@ -714,6 +752,14 @@ var createMetrics = () => {
714
752
  const unmatched = /* @__PURE__ */ new Map();
715
753
  return {
716
754
  record(entry) {
755
+ if (partition) {
756
+ let scoped = namespaces.get(entry.namespace);
757
+ if (!scoped) {
758
+ scoped = createMetrics(false);
759
+ namespaces.set(entry.namespace, scoped);
760
+ }
761
+ scoped.record(entry);
762
+ }
717
763
  requests++;
718
764
  totalDurationMs += entry.durationMs;
719
765
  if (entry.faultId !== void 0)
@@ -725,7 +771,7 @@ var createMetrics = () => {
725
771
  unmatched.set(route, (unmatched.get(route) ?? 0) + 1);
726
772
  }
727
773
  },
728
- report: () => ({
774
+ report: (namespace) => namespace !== void 0 ? (namespaces.get(namespace) ?? createMetrics(false)).report() : {
729
775
  requests,
730
776
  byOperation: Object.fromEntries([...byOperation].sort(([a], [b]) => a.localeCompare(b))),
731
777
  unmatched: [...unmatched].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([route, count]) => {
@@ -734,8 +780,13 @@ var createMetrics = () => {
734
780
  }),
735
781
  faults,
736
782
  totalDurationMs
737
- }),
738
- reset() {
783
+ },
784
+ reset(namespace) {
785
+ if (namespace !== void 0) {
786
+ namespaces.delete(namespace);
787
+ return;
788
+ }
789
+ namespaces.clear();
739
790
  requests = 0;
740
791
  faults = 0;
741
792
  totalDurationMs = 0;
@@ -3937,7 +3988,7 @@ var stateAdminRoutes = (routes) => {
3937
3988
  };
3938
3989
 
3939
3990
  // ../core/dist/version.js
3940
- var PACKAGE_VERSION = true ? "1.7.1" : UNRELEASED_VERSION;
3991
+ var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
3941
3992
 
3942
3993
  // ../core/dist/webhooks.js
3943
3994
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -4101,6 +4152,20 @@ var createRuntime = (options) => {
4101
4152
  const instances = /* @__PURE__ */ new Map();
4102
4153
  const publicNamespaces = /* @__PURE__ */ new Set();
4103
4154
  const branchRngs = /* @__PURE__ */ new Map();
4155
+ const namespaceClocks = /* @__PURE__ */ new Map();
4156
+ const namespaceForStorage = /* @__PURE__ */ new Map();
4157
+ let isolatedNamespaces = false;
4158
+ const namespaceClock = (name = DEFAULT_NAMESPACE) => {
4159
+ if (!isolatedNamespaces || name === DEFAULT_NAMESPACE)
4160
+ return clock;
4161
+ let found = namespaceClocks.get(name);
4162
+ if (!found) {
4163
+ found = createClock();
4164
+ namespaceClocks.set(name, found);
4165
+ }
4166
+ return found;
4167
+ };
4168
+ const fleetPoints = /* @__PURE__ */ new WeakMap();
4104
4169
  const timelines = /* @__PURE__ */ new Map();
4105
4170
  const branchStorage = /* @__PURE__ */ new Map();
4106
4171
  const captured = /* @__PURE__ */ new Map();
@@ -4114,17 +4179,21 @@ var createRuntime = (options) => {
4114
4179
  if (!NAMESPACE_PATTERN.test(key) || !NAMESPACE_PATTERN.test(publicNamespace)) {
4115
4180
  throw new RangeError(`namespace must match ${NAMESPACE_PATTERN}: ${JSON.stringify(publicNamespace)}`);
4116
4181
  }
4182
+ const createdContextRng = isolatedRng ?? (isolatedNamespaces && key !== DEFAULT_NAMESPACE ? createRng(options.seed ?? 0) : rng);
4117
4183
  const created = options.create({
4118
4184
  namespace: storageNamespace(key),
4119
4185
  publicNamespace,
4120
4186
  sqlite,
4121
- clock,
4122
- rng: isolatedRng ?? rng
4187
+ clock: namespaceClock(publicNamespace),
4188
+ rng: createdContextRng
4123
4189
  });
4124
4190
  instances.set(key, created);
4191
+ namespaceForStorage.set(key, publicNamespace);
4125
4192
  publicNamespaces.add(publicNamespace);
4126
4193
  if (isolatedRng)
4127
4194
  branchRngs.set(key, isolatedRng);
4195
+ else if (isolatedNamespaces && key !== DEFAULT_NAMESPACE)
4196
+ branchRngs.set(key, createdContextRng);
4128
4197
  return created;
4129
4198
  };
4130
4199
  const instance = (name = DEFAULT_NAMESPACE) => instanceFor(name);
@@ -4142,7 +4211,7 @@ var createRuntime = (options) => {
4142
4211
  captured.set(storage, snapshot2);
4143
4212
  return Object.freeze({
4144
4213
  snapshot: snapshot2,
4145
- clock: Object.freeze(clock.state()),
4214
+ clock: Object.freeze(namespaceClock(namespaceForStorage.get(storage) ?? storage).state()),
4146
4215
  rngState: (branchRngs.get(storage) ?? rng).state()
4147
4216
  });
4148
4217
  };
@@ -4152,7 +4221,7 @@ var createRuntime = (options) => {
4152
4221
  return found;
4153
4222
  instance(name);
4154
4223
  found = new Timeline({
4155
- now: clock.now,
4224
+ now: namespaceClock(name).now,
4156
4225
  ...options.maxCheckpoints !== void 0 ? { maxCheckpoints: options.maxCheckpoints } : {}
4157
4226
  });
4158
4227
  found.commit(capture(name));
@@ -4179,12 +4248,13 @@ var createRuntime = (options) => {
4179
4248
  const point = history2.checkout("main", at);
4180
4249
  restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
4181
4250
  captured.set(namespace, point.value.snapshot);
4182
- rng.setState(point.value.rngState);
4183
- clock.set(point.value.clock.now);
4251
+ (branchRngs.get(namespace) ?? rng).setState(point.value.rngState);
4252
+ const selectedClock = namespaceClock(namespace);
4253
+ selectedClock.set(point.value.clock.now);
4184
4254
  if (point.value.clock.frozen)
4185
- clock.freeze();
4255
+ selectedClock.freeze();
4186
4256
  else
4187
- clock.unfreeze();
4257
+ selectedClock.unfreeze();
4188
4258
  }
4189
4259
  return namespace;
4190
4260
  }
@@ -4242,11 +4312,12 @@ var createRuntime = (options) => {
4242
4312
  const storage = ensureBranch(namespace, branchName);
4243
4313
  restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
4244
4314
  captured.set(storage, point.value.snapshot);
4245
- clock.set(point.value.clock.now);
4315
+ const selectedClock = namespaceClock(namespace);
4316
+ selectedClock.set(point.value.clock.now);
4246
4317
  if (point.value.clock.frozen)
4247
- clock.freeze();
4318
+ selectedClock.freeze();
4248
4319
  else
4249
- clock.unfreeze();
4320
+ selectedClock.unfreeze();
4250
4321
  (branchRngs.get(storage) ?? rng).setState(point.value.rngState);
4251
4322
  };
4252
4323
  const reset = async (name = DEFAULT_NAMESPACE) => {
@@ -4254,10 +4325,21 @@ var createRuntime = (options) => {
4254
4325
  options.webhooks?.clear();
4255
4326
  for (const each of instances.values())
4256
4327
  await each.reset();
4328
+ for (const each of branchRngs.values())
4329
+ each.reset();
4257
4330
  timelines.clear();
4331
+ for (const storage of branchStorage.values()) {
4332
+ instances.delete(storage);
4333
+ branchRngs.delete(storage);
4334
+ namespaceForStorage.delete(storage);
4335
+ }
4258
4336
  branchStorage.clear();
4259
- branchRngs.clear();
4260
4337
  captured.clear();
4338
+ faults.clear();
4339
+ clock.reset();
4340
+ rng.reset();
4341
+ for (const each of namespaceClocks.values())
4342
+ each.reset();
4261
4343
  return;
4262
4344
  }
4263
4345
  options.webhooks?.clear(name);
@@ -4275,11 +4357,18 @@ var createRuntime = (options) => {
4275
4357
  else
4276
4358
  clearNamespace(sqlite, storageNamespace(storage));
4277
4359
  branchStorage.delete(mapping);
4360
+ instances.delete(storage);
4361
+ namespaceForStorage.delete(storage);
4278
4362
  branchRngs.delete(storage);
4279
4363
  captured.delete(storage);
4280
4364
  }
4281
4365
  timelines.delete(name);
4282
4366
  captured.delete(name);
4367
+ faults.restore(name, { rules: [], rngState: createRng(options.seed ?? 0).state() });
4368
+ namespaceClock(name).reset();
4369
+ (branchRngs.get(name) ?? rng).reset();
4370
+ journal.clear(name);
4371
+ metrics.reset(name);
4283
4372
  };
4284
4373
  const snapshot = (name = DEFAULT_NAMESPACE) => {
4285
4374
  return checkpoint(name, "main").value.snapshot;
@@ -4317,6 +4406,53 @@ var createRuntime = (options) => {
4317
4406
  rng,
4318
4407
  credentials,
4319
4408
  webhooks: options.webhooks,
4409
+ isolateNamespaces() {
4410
+ if (publicNamespaces.size > 1)
4411
+ throw new Error("enable namespace isolation before creating named namespaces");
4412
+ isolatedNamespaces = true;
4413
+ faults.isolateNamespaces();
4414
+ },
4415
+ namespaceClock,
4416
+ namespaceOf(request) {
4417
+ const url = new URL(request.url);
4418
+ const prefixed = PATH_PREFIX.exec(url.pathname);
4419
+ const selected = request.headers.get(NAMESPACE_HEADER) ?? (prefixed ? decodeURIComponent(prefixed[1]) : void 0);
4420
+ const path = prefixed ? prefixed[2] ?? "/" : url.pathname;
4421
+ if (path === "/__admin" || path.startsWith("/__admin/"))
4422
+ return url.searchParams.get("namespace") ?? selected ?? DEFAULT_NAMESPACE;
4423
+ if (selected !== void 0)
4424
+ return selected;
4425
+ const credential = options.credential?.(request);
4426
+ return (credential !== void 0 ? credentials.get(credential) : void 0) ?? DEFAULT_NAMESPACE;
4427
+ },
4428
+ fleetSnapshot(namespace) {
4429
+ instance(namespace);
4430
+ const handle = {};
4431
+ fleetPoints.set(handle, {
4432
+ namespace,
4433
+ state: capture(namespace),
4434
+ faults: faults.snapshot(namespace),
4435
+ webhooks: options.webhooks?.snapshot(namespace)
4436
+ });
4437
+ return handle;
4438
+ },
4439
+ fleetRestore(handle, namespace) {
4440
+ const point = typeof handle === "object" && handle !== null ? fleetPoints.get(handle) : void 0;
4441
+ if (!point || point.namespace !== namespace)
4442
+ throw new Error("invalid fleet checkpoint");
4443
+ restoreNamespace(sqlite, storageNamespace(namespace), point.state.snapshot);
4444
+ captured.set(namespace, point.state.snapshot);
4445
+ const selectedClock = namespaceClock(namespace);
4446
+ selectedClock.freeze();
4447
+ selectedClock.set(point.state.clock.now);
4448
+ if (!point.state.clock.frozen)
4449
+ selectedClock.unfreeze();
4450
+ (branchRngs.get(namespace) ?? rng).setState(point.state.rngState);
4451
+ faults.restore(namespace, point.faults);
4452
+ if (point.webhooks !== void 0)
4453
+ options.webhooks?.restore(namespace, point.webhooks);
4454
+ timelines.delete(namespace);
4455
+ },
4320
4456
  applyPreset: (name, namespace = DEFAULT_NAMESPACE, overrides = {}) => {
4321
4457
  const preset = options.presets?.[name];
4322
4458
  if (!preset)
@@ -4492,6 +4628,7 @@ var createRuntime = (options) => {
4492
4628
  startedAt: wallNow(),
4493
4629
  wallNow,
4494
4630
  clock,
4631
+ namespaceClock,
4495
4632
  faults,
4496
4633
  metrics,
4497
4634
  journal,
@@ -4960,6 +5097,8 @@ var StepFunctionsAPI = class {
4960
5097
  };
4961
5098
 
4962
5099
  export {
5100
+ createClock,
5101
+ parseDuration,
4963
5102
  document2 as document,
4964
5103
  operationIds,
4965
5104
  supportedOperationIds,
@@ -4969,4 +5108,4 @@ export {
4969
5108
  accessKeyCredential,
4970
5109
  StepFunctionsAPI
4971
5110
  };
4972
- //# sourceMappingURL=chunk-E4E6U7WJ.js.map
5111
+ //# sourceMappingURL=chunk-5ZU42EQK.js.map