@classytic/primitives 0.22.0 → 0.24.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/outbox.d.mts CHANGED
@@ -35,15 +35,35 @@ interface OutboxClaimOptions {
35
35
  /** Only claim events of these types (optional filter). */
36
36
  readonly types?: readonly string[];
37
37
  }
38
+ /**
39
+ * A claim result carrying the FENCING TOKEN — returned by
40
+ * {@link OutboxStore.claimPendingFenced}. The token is minted by the STORE
41
+ * per claim epoch of that event: a re-claim after lease expiry mints a
42
+ * STRICTLY GREATER token, so a stale ex-holder presenting its old token to
43
+ * `acknowledge`/`fail` is rejected — the overlap window ownership checks
44
+ * narrow but cannot close (holder ids recur; epochs do not).
45
+ */
46
+ interface OutboxClaimedEvent {
47
+ readonly event: DomainEvent;
48
+ readonly fencingToken: number;
49
+ }
38
50
  /** Options for {@link OutboxStore.acknowledge}. */
39
51
  interface OutboxAcknowledgeOptions {
40
52
  /** Worker identifier — stores may enforce "only owner can ack". */
41
53
  readonly consumerId?: string;
54
+ /**
55
+ * Fencing token from {@link OutboxStore.claimPendingFenced}. Stores that
56
+ * fence MUST throw {@link OutboxOwnershipError} when it is not the event's
57
+ * CURRENT claim token.
58
+ */
59
+ readonly fencingToken?: number;
42
60
  }
43
61
  /** Options for {@link OutboxStore.fail}. */
44
62
  interface OutboxFailOptions {
45
63
  /** Worker identifier — stores may enforce "only owner can fail". */
46
64
  readonly consumerId?: string;
65
+ /** Fencing token — same contract as {@link OutboxAcknowledgeOptions.fencingToken}. */
66
+ readonly fencingToken?: number;
47
67
  /** Schedule retry for a later time (implements backoff). */
48
68
  readonly retryAt?: Date;
49
69
  /** Move the event to dead-letter state (no further retries). */
@@ -213,6 +233,13 @@ interface OutboxStore {
213
233
  * prefers this over `getPending`.
214
234
  */
215
235
  claimPending?(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
236
+ /**
237
+ * FENCED claim — same atomic semantics as `claimPending`, returning each
238
+ * event with a store-minted fencing token (see {@link OutboxClaimedEvent}).
239
+ * Additive and feature-detected, mirroring `LockAdapter.tryAcquireFenced`:
240
+ * a store that cannot mint monotonic per-event tokens omits this method.
241
+ */
242
+ claimPendingFenced?(options?: OutboxClaimOptions): Promise<OutboxClaimedEvent[]>;
216
243
  /**
217
244
  * Mark event as successfully relayed.
218
245
  *
@@ -291,4 +318,4 @@ interface OutboxStore {
291
318
  oldestPendingAgeMs?(): Promise<number | null>;
292
319
  }
293
320
  //#endregion
294
- export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStatus, OutboxStore, OutboxWriteOptions };
321
+ export { InvalidOutboxEventError, OUTBOX_STATUSES, OutboxAcknowledgeOptions, OutboxClaimOptions, OutboxClaimedEvent, OutboxErrorInfo, OutboxFailOptions, OutboxFailureContext, OutboxFailureDecision, OutboxFailurePolicy, OutboxOwnershipError, OutboxStatus, OutboxStore, OutboxWriteOptions };
@@ -46,9 +46,26 @@ declare function unitCostRateFromTotal<C extends string = string>(totalMinor: nu
46
46
  * {@link QTY_SCALE} precision.
47
47
  */
48
48
  declare function extendedAmount<C extends string = string>(rate: UnitCostRate<C>, quantity: number, mode?: RoundingMode): MinorUnits<C>;
49
+ /**
50
+ * Incremental weighted-average of two scaled rates — the moving-average (WAC)
51
+ * receipt step:
52
+ *
53
+ * newRate = (onHandQty × onHandRate + inQty × inRate) / (onHandQty + inQty)
54
+ *
55
+ * Both rates must share the same scale (ecosystem-wide {@link RATE_SCALE});
56
+ * the result is a scaled integer on that same scale. Bigint-exact with the
57
+ * quantity honoured to {@link QTY_SCALE} precision; rounds once, at the end.
58
+ *
59
+ * Degenerate-stock semantics (the moving-average standard): a zero or
60
+ * NEGATIVE on-hand contributes NOTHING to the average — the incoming rate
61
+ * RESETS the cost. Averaging against a negative quantity would let a
62
+ * catch-up receipt swing the rate arbitrarily far from any price ever paid.
63
+ * Outflows never call this function: an issue leaves the average unchanged.
64
+ */
65
+ declare function weightedAverageScaledRate(onHandQuantity: number, onHandScaledRate: number, inQuantity: number, inScaledRate: number, mode?: RoundingMode): number;
49
66
  /** The rate as a plain (fractional) minor-units-per-unit number — for display only. */
50
67
  declare function rateMinorPerUnit(rate: UnitCostRate): number;
51
68
  /** Structural guard. */
52
69
  declare function isUnitCostRate(value: unknown): value is UnitCostRate;
53
70
  //#endregion
54
- export { RATE_SCALE, RoundingMode, UnitCostRate, UnitCostRateError, UnitCostRateErrorCode, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal };
71
+ export { RATE_SCALE, RoundingMode, UnitCostRate, UnitCostRateError, UnitCostRateErrorCode, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal, weightedAverageScaledRate };
@@ -85,6 +85,35 @@ function extendedAmount(rate, quantity, mode = DEFAULT_ROUNDING) {
85
85
  if (!Number.isSafeInteger(minor)) throw new UnitCostRateError("UNSAFE_INTEGER", `extended amount ${minor} exceeds safe integer range`);
86
86
  return MinorUnits(minor, rate.currency);
87
87
  }
88
+ /**
89
+ * Incremental weighted-average of two scaled rates — the moving-average (WAC)
90
+ * receipt step:
91
+ *
92
+ * newRate = (onHandQty × onHandRate + inQty × inRate) / (onHandQty + inQty)
93
+ *
94
+ * Both rates must share the same scale (ecosystem-wide {@link RATE_SCALE});
95
+ * the result is a scaled integer on that same scale. Bigint-exact with the
96
+ * quantity honoured to {@link QTY_SCALE} precision; rounds once, at the end.
97
+ *
98
+ * Degenerate-stock semantics (the moving-average standard): a zero or
99
+ * NEGATIVE on-hand contributes NOTHING to the average — the incoming rate
100
+ * RESETS the cost. Averaging against a negative quantity would let a
101
+ * catch-up receipt swing the rate arbitrarily far from any price ever paid.
102
+ * Outflows never call this function: an issue leaves the average unchanged.
103
+ */
104
+ function weightedAverageScaledRate(onHandQuantity, onHandScaledRate, inQuantity, inScaledRate, mode = DEFAULT_ROUNDING) {
105
+ if (!Number.isFinite(inQuantity) || inQuantity <= 0) throw new UnitCostRateError("NON_POSITIVE_QUANTITY", `incoming quantity must be > 0, got ${inQuantity}`);
106
+ assertSafe(inScaledRate);
107
+ const oldQty = Number.isFinite(onHandQuantity) ? Math.max(onHandQuantity, 0) : 0;
108
+ if (oldQty === 0) return inScaledRate;
109
+ assertSafe(onHandScaledRate);
110
+ const oldQ = scaleQuantity(oldQty);
111
+ const inQ = scaleQuantity(inQuantity);
112
+ const numerator = oldQ * BigInt(onHandScaledRate) + inQ * BigInt(inScaledRate);
113
+ const scaled = Number(roundedDiv(numerator, oldQ + inQ, mode));
114
+ assertSafe(scaled);
115
+ return scaled;
116
+ }
88
117
  /** The rate as a plain (fractional) minor-units-per-unit number — for display only. */
89
118
  function rateMinorPerUnit(rate) {
90
119
  return rate.scaledAmount / rate.scale;
@@ -113,4 +142,4 @@ function roundedDiv(numerator, denominator, mode) {
113
142
  return neg ? -result : result;
114
143
  }
115
144
  //#endregion
116
- export { RATE_SCALE, UnitCostRateError, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal };
145
+ export { RATE_SCALE, UnitCostRateError, extendedAmount, isUnitCostRate, rateMinorPerUnit, unitCostRate, unitCostRateFromTotal, weightedAverageScaledRate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",
@@ -45,6 +45,10 @@
45
45
  "types": "./dist/calendar.d.mts",
46
46
  "default": "./dist/calendar.mjs"
47
47
  },
48
+ "./calendar-facts": {
49
+ "types": "./dist/calendar-facts.d.mts",
50
+ "default": "./dist/calendar-facts.mjs"
51
+ },
48
52
  "./timezone": {
49
53
  "types": "./dist/timezone.d.mts",
50
54
  "default": "./dist/timezone.mjs"
@@ -53,6 +57,10 @@
53
57
  "types": "./dist/reference.d.mts",
54
58
  "default": "./dist/reference.mjs"
55
59
  },
60
+ "./fault-attribution": {
61
+ "types": "./dist/fault-attribution.d.mts",
62
+ "default": "./dist/fault-attribution.mjs"
63
+ },
56
64
  "./context": {
57
65
  "types": "./dist/context.d.mts",
58
66
  "default": "./dist/context.mjs"