@gkoos/caracal 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { admissionSignal, emitRuntimeEvent, nextAttempt, withAdmissionSignal, withSignal } from './chunk-5CXDW7W6.js';
2
- export { operation } from './chunk-5CXDW7W6.js';
1
+ export { operation } from './chunk-JV2OLYOF.js';
2
+ import { MAX_TIMER_MS, admissionSignal, emitRuntimeEvent, nextAttempt, withAdmissionSignal, withSignal } from './chunk-FD7IZJKY.js';
3
3
  import { randomUUID } from 'crypto';
4
4
 
5
5
  var BulkheadRejectedError = class extends Error {
@@ -34,7 +34,7 @@ function local(options) {
34
34
  const { name, limit } = options;
35
35
  const queue = options.queue && { ...options.queue };
36
36
  validate(name, limit);
37
- if (queue && (!Number.isSafeInteger(queue.limit) || queue.limit < 1 || !Number.isSafeInteger(queue.timeoutMs) || queue.timeoutMs < 1 || queue.timeoutMs > 2147483647))
37
+ if (queue && (!Number.isSafeInteger(queue.limit) || queue.limit < 1 || !Number.isSafeInteger(queue.timeoutMs) || queue.timeoutMs < 1 || queue.timeoutMs > MAX_TIMER_MS))
38
38
  throw new RangeError("Invalid bounded queue");
39
39
  let occupancy = 0;
40
40
  const waiting = [];
@@ -97,8 +97,8 @@ function local(options) {
97
97
  return await next(context);
98
98
  } finally {
99
99
  occupancy--;
100
- waiting[0]?.();
101
100
  event(context, "local", name, "process", "released", occupancy);
101
+ waiting[0]?.();
102
102
  }
103
103
  }
104
104
  });
@@ -212,13 +212,23 @@ function distributed(options) {
212
212
  }
213
213
  };
214
214
  try {
215
- if (performance.now() >= deadline)
215
+ if (performance.now() >= deadline) {
216
+ event(
217
+ context,
218
+ "distributed",
219
+ name,
220
+ scope,
221
+ "rejected",
222
+ admitted.occupancy,
223
+ "admission-expired"
224
+ );
216
225
  throw new BulkheadRejectedError(
217
226
  "distributed",
218
227
  name,
219
228
  scope,
220
229
  "admission-expired"
221
230
  );
231
+ }
222
232
  admissionSignal(context)?.throwIfAborted();
223
233
  event(
224
234
  context,
@@ -320,6 +330,14 @@ var DEFAULT_OPEN_MS = 1e4;
320
330
  var DEFAULT_HALF_OPEN_SUCCESSES = 1;
321
331
  var DEFAULT_HALF_OPEN_PROBES = 1;
322
332
  var DEFAULT_WINDOW_SIZE = 100;
333
+ var FAILURE_THRESHOLD_SCALE = 1e3;
334
+ function assertResolvableThreshold(failureThreshold) {
335
+ const numerator = Math.round(failureThreshold * FAILURE_THRESHOLD_SCALE);
336
+ if (numerator < 1 || numerator >= FAILURE_THRESHOLD_SCALE)
337
+ throw new RangeError(
338
+ `failureThreshold must be at least 0.0005 and below 0.9995 (thresholds are resolved to thousandths); got ${failureThreshold}`
339
+ );
340
+ }
323
341
  function validate2(opts) {
324
342
  if (!opts.name.trim())
325
343
  throw new RangeError("Circuit breaker name must not be empty");
@@ -329,6 +347,7 @@ function validate2(opts) {
329
347
  const { failureThreshold = DEFAULT_FAILURE_THRESHOLD } = opts;
330
348
  if (!Number.isFinite(failureThreshold) || failureThreshold <= 0 || failureThreshold >= 1)
331
349
  throw new RangeError("failureThreshold must be a number in (0, 1)");
350
+ assertResolvableThreshold(failureThreshold);
332
351
  const { openMs = DEFAULT_OPEN_MS } = opts;
333
352
  if (!Number.isInteger(openMs) || openMs < 1)
334
353
  throw new RangeError("openMs must be a positive integer");
@@ -571,6 +590,7 @@ function validateDistributed(opts) {
571
590
  const { failureThreshold = DEFAULT_DIST_FAILURE_THRESHOLD } = opts;
572
591
  if (!Number.isFinite(failureThreshold) || failureThreshold <= 0 || failureThreshold >= 1)
573
592
  throw new RangeError("failureThreshold must be a number in (0, 1)");
593
+ assertResolvableThreshold(failureThreshold);
574
594
  const { openMs = DEFAULT_DIST_OPEN_MS } = opts;
575
595
  if (!Number.isInteger(openMs) || openMs < 1)
576
596
  throw new RangeError("openMs must be a positive integer");
@@ -603,7 +623,9 @@ function distributed2(options) {
603
623
  const resolveScope = options.scope;
604
624
  const minimumThroughput = options.minimumThroughput ?? DEFAULT_DIST_MINIMUM_THROUGHPUT;
605
625
  const failureThreshold = options.failureThreshold ?? DEFAULT_DIST_FAILURE_THRESHOLD;
606
- const failureThresholdNumerator = Math.round(failureThreshold * 1e3);
626
+ const failureThresholdNumerator = Math.round(
627
+ failureThreshold * FAILURE_THRESHOLD_SCALE
628
+ );
607
629
  const windowSize = options.windowSize ?? DEFAULT_DIST_WINDOW_SIZE;
608
630
  const openMs = options.openMs ?? DEFAULT_DIST_OPEN_MS;
609
631
  const windowTtlMs = options.windowTtlMs ?? Math.max(openMs * 3, 6e4);
@@ -767,8 +789,9 @@ function distributed2(options) {
767
789
  classifier,
768
790
  isSuccess ? { status: "success", value } : { status: "failure", error: thrownError }
769
791
  );
770
- if (breakerOutcome !== "ignored") {
792
+ if (breakerOutcome !== "ignored" || admission.kind === "probe") {
771
793
  const outcomeStr = breakerOutcome === "success" ? "success" : "failure";
794
+ const settleOutcome = breakerOutcome === "ignored" ? "ignored" : outcomeStr;
772
795
  if (admission.kind === "closed") {
773
796
  try {
774
797
  const result = await coordinator.observe(identity, {
@@ -826,49 +849,51 @@ function distributed2(options) {
826
849
  try {
827
850
  const result = await coordinator.settleProbe(identity, {
828
851
  probeToken: admission.probeToken,
829
- outcome: outcomeStr,
852
+ outcome: settleOutcome,
830
853
  generation: admission.generation,
831
854
  halfOpenSuccesses,
832
855
  openMs,
833
856
  windowTtlMs
834
857
  });
835
- if (result.type === "stale") {
836
- emitRuntimeEvent(context, {
837
- type: "breaker.observation-stale",
838
- coordination: "distributed",
839
- policyName: name,
840
- scope,
841
- attemptGeneration: admission.generation,
842
- currentGeneration: result.generation
843
- });
844
- } else {
845
- emitRuntimeEvent(context, {
846
- type: "breaker.observation",
847
- coordination: "distributed",
848
- policyName: name,
849
- scope,
850
- outcome: outcomeStr,
851
- generation: admission.generation
852
- });
853
- if (result.type === "transitioned") {
854
- if (result.newState === "closed") {
855
- lastKnownState.forget(identity.operation, scope);
856
- } else {
857
- lastKnownState.remember(
858
- identity.operation,
859
- scope,
860
- result.newState
861
- );
862
- }
858
+ if (breakerOutcome !== "ignored") {
859
+ if (result.type === "stale") {
863
860
  emitRuntimeEvent(context, {
864
- type: "breaker.state-changed",
861
+ type: "breaker.observation-stale",
865
862
  coordination: "distributed",
866
863
  policyName: name,
867
864
  scope,
868
- state: result.newState,
869
- previousState: "half-open",
870
- generation: result.newGeneration
865
+ attemptGeneration: admission.generation,
866
+ currentGeneration: result.generation
867
+ });
868
+ } else {
869
+ emitRuntimeEvent(context, {
870
+ type: "breaker.observation",
871
+ coordination: "distributed",
872
+ policyName: name,
873
+ scope,
874
+ outcome: outcomeStr,
875
+ generation: admission.generation
871
876
  });
877
+ if (result.type === "transitioned") {
878
+ if (result.newState === "closed") {
879
+ lastKnownState.forget(identity.operation, scope);
880
+ } else {
881
+ lastKnownState.remember(
882
+ identity.operation,
883
+ scope,
884
+ result.newState
885
+ );
886
+ }
887
+ emitRuntimeEvent(context, {
888
+ type: "breaker.state-changed",
889
+ coordination: "distributed",
890
+ policyName: name,
891
+ scope,
892
+ state: result.newState,
893
+ previousState: "half-open",
894
+ generation: result.newGeneration
895
+ });
896
+ }
872
897
  }
873
898
  }
874
899
  } catch (settleError) {
@@ -911,6 +936,11 @@ function delayFor(options, context, attempt, outcome) {
911
936
  if (!Number.isFinite(delay) || delay < 0) {
912
937
  throw new RangeError("retry delay must be a finite non-negative number");
913
938
  }
939
+ if (delay > MAX_TIMER_MS) {
940
+ throw new RangeError(
941
+ `retry delay must not exceed ${MAX_TIMER_MS} ms, the largest delay setTimeout honours`
942
+ );
943
+ }
914
944
  return delay;
915
945
  }
916
946
  function wait(delayMs, signal) {
@@ -947,6 +977,11 @@ function retry(options) {
947
977
  if (!Number.isInteger(options.maxAttempts) || options.maxAttempts < 1) {
948
978
  throw new RangeError("maxAttempts must be a positive integer");
949
979
  }
980
+ if (typeof options.delay === "number" && (!Number.isFinite(options.delay) || options.delay < 0 || options.delay > MAX_TIMER_MS)) {
981
+ throw new RangeError(
982
+ `retry delay must be a finite number within 0..${MAX_TIMER_MS} ms`
983
+ );
984
+ }
950
985
  return Object.freeze({
951
986
  name: "retry",
952
987
  async execute(initialContext, next) {
@@ -958,6 +993,12 @@ function retry(options) {
958
993
  const outcome = { status: "success", value };
959
994
  const outcomeClassification = classification(context, outcome);
960
995
  if (outcomeClassification !== "retryable" || context.capabilities.replay !== "safe") {
996
+ emitRuntimeEvent(context, {
997
+ type: "retry.declined",
998
+ outcome: summarized(outcome),
999
+ classification: outcomeClassification,
1000
+ reason: context.capabilities.replay !== "safe" ? "replay-unsafe" : "not-retryable"
1001
+ });
961
1002
  return value;
962
1003
  }
963
1004
  if (context.attempt >= options.maxAttempts) {
@@ -981,7 +1022,16 @@ function retry(options) {
981
1022
  } catch (error) {
982
1023
  const outcome = { status: "failure", error };
983
1024
  const outcomeClassification = classification(context, outcome);
984
- if (admissionSignal(context)?.aborted || context.capabilities.replay !== "safe" || outcomeClassification !== "retryable") {
1025
+ if (admissionSignal(context)?.aborted) {
1026
+ throw error;
1027
+ }
1028
+ if (context.capabilities.replay !== "safe" || outcomeClassification !== "retryable") {
1029
+ emitRuntimeEvent(context, {
1030
+ type: "retry.declined",
1031
+ outcome: summarized(outcome),
1032
+ classification: outcomeClassification,
1033
+ reason: context.capabilities.replay !== "safe" ? "replay-unsafe" : "not-retryable"
1034
+ });
985
1035
  throw error;
986
1036
  }
987
1037
  if (context.attempt >= options.maxAttempts) {
@@ -1027,6 +1077,11 @@ function timeout(options) {
1027
1077
  if (!Number.isFinite(options.ms) || options.ms <= 0) {
1028
1078
  throw new RangeError("timeout ms must be a finite positive number");
1029
1079
  }
1080
+ if (options.ms > MAX_TIMER_MS) {
1081
+ throw new RangeError(
1082
+ `timeout ms must not exceed ${MAX_TIMER_MS} ms, the largest delay setTimeout honours`
1083
+ );
1084
+ }
1030
1085
  return Object.freeze({
1031
1086
  name: "timeout",
1032
1087
  async execute(context, next) {