@cosmicdrift/kumiko-bundled-features 0.26.0 → 0.27.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { enforceStockCap } from "../enforce-cap";
3
+
4
+ describe("enforceStockCap", () => {
5
+ test("hardSlot: Grenze ist exakt limit (kein Buffer)", () => {
6
+ const at = (current: number) => enforceStockCap({ current, limit: 5, profile: "hardSlot" });
7
+ expect(at(0).state).toBe("ok");
8
+ expect(at(4).state).toBe("ok");
9
+ // Tenant hat 5 → die Anlage der 6. wird geblockt (current=5 >= 5).
10
+ expect(at(5).state).toBe("exceeded");
11
+ expect(at(6).state).toBe("exceeded");
12
+ });
13
+
14
+ test("storage: 5% Buffer über dem limit", () => {
15
+ const at = (current: number) => enforceStockCap({ current, limit: 100, profile: "storage" });
16
+ expect(at(104).state).toBe("ok"); // 104 < 105 (=100×1.05)
17
+ expect(at(105).state).toBe("exceeded");
18
+ });
19
+
20
+ test("burstable: 20% Buffer", () => {
21
+ const at = (current: number) => enforceStockCap({ current, limit: 10, profile: "burstable" });
22
+ expect(at(11).state).toBe("ok"); // 11 < 12 (=10×1.2)
23
+ expect(at(12).state).toBe("exceeded");
24
+ });
25
+
26
+ test("limit 0 = keine Allowance → jede Anlage exceeded", () => {
27
+ expect(enforceStockCap({ current: 0, limit: 0, profile: "hardSlot" }).state).toBe("exceeded");
28
+ });
29
+
30
+ test("Result trägt current + limit für die Caller-Fehlermeldung", () => {
31
+ expect(enforceStockCap({ current: 7, limit: 5, profile: "hardSlot" })).toEqual({
32
+ state: "exceeded",
33
+ current: 7,
34
+ limit: 5,
35
+ });
36
+ });
37
+ });
@@ -394,3 +394,36 @@ export async function enforceRollingCapAndMaybeNotify(
394
394
 
395
395
  return result;
396
396
  }
397
+
398
+ // =============================================================================
399
+ // Stock-Cap (Bestand) — live-gezählte Kardinalität gegen Tier-Limit
400
+ // =============================================================================
401
+
402
+ export type StockCapResult =
403
+ | { readonly state: "ok"; readonly current: number; readonly limit: number }
404
+ | { readonly state: "exceeded"; readonly current: number; readonly limit: number };
405
+
406
+ /**
407
+ * Stock-Cap: prüft eine vom Caller LIVE gezählte Kardinalität (z.B. Anzahl
408
+ * existierender Components eines Tenants) gegen ein Tier-Limit.
409
+ *
410
+ * Anders als {@link enforceCap}/{@link enforceRollingCap} gibt es KEINEN
411
+ * gespeicherten Counter: der Caller zählt die Projektion selbst
412
+ * (`count(*) WHERE tenant_id = …`) und übergibt `current`. Das ist drift-frei
413
+ * (ein Delete gibt den Slot sofort frei), braucht keine Counter-Tabelle und
414
+ * kein Increment/Decrement-Bookkeeping. Misst einen Bestand, keinen Fluss.
415
+ *
416
+ * Reine Funktion — wirft NICHT und mappt KEINEN HTTP-Status. Ein erreichtes
417
+ * Stock-Limit heißt „Upgrade nötig", nicht „retry later" (429): der Caller
418
+ * entscheidet die Reaktion, typisch ein app-eigener 422/`upgrade_required`
419
+ * mit i18n. Mit `hardSlot` (soft=hard=1.0) ist die Grenze exakt `limit`.
420
+ */
421
+ export function enforceStockCap(options: {
422
+ readonly current: number;
423
+ readonly limit: number;
424
+ readonly profile: CapToleranceProfileName;
425
+ }): StockCapResult {
426
+ const hardThreshold = options.limit * CAP_TOLERANCES[options.profile].hard;
427
+ const state = options.current >= hardThreshold ? "exceeded" : "ok";
428
+ return { state, current: options.current, limit: options.limit };
429
+ }
@@ -20,7 +20,9 @@ export {
20
20
  enforceCapAndMaybeNotify,
21
21
  enforceRollingCap,
22
22
  enforceRollingCapAndMaybeNotify,
23
+ enforceStockCap,
23
24
  type SoftHitNotifier,
25
+ type StockCapResult,
24
26
  } from "./enforce-cap";
25
27
  export { capCounterEntity } from "./entity";
26
28
  export { capCounterFeature } from "./feature";