@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.
|
|
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
|
+
}
|
package/src/cap-counter/index.ts
CHANGED
|
@@ -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";
|