@cosmicdrift/kumiko-bundled-features 0.350.0 → 0.352.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/audit/handlers/list.query.js +1 -1
- package/dist/auth-email-password/email-templates.js +1 -1
- package/dist/auth-email-password/handlers/invite-create.write.js +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +1 -1
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
- package/dist/auth-email-password/reset-token.d.ts +1 -1
- package/dist/auth-email-password/unlock-token.d.ts +1 -1
- package/dist/auth-email-password/verification-token.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-confirm.write.js +1 -1
- package/dist/auth-mfa/hmac-token-codec.d.ts +1 -1
- package/dist/auth-mfa/hmac-token-codec.js +1 -1
- package/dist/auth-mfa/mfa-challenge-token.d.ts +1 -1
- package/dist/auth-mfa/mfa-challenge-token.js +1 -1
- package/dist/auth-mfa/mfa-preauth-setup-token.d.ts +1 -1
- package/dist/auth-mfa/mfa-preauth-setup-token.js +1 -1
- package/dist/auth-mfa/mfa-setup-token.d.ts +1 -1
- package/dist/auth-mfa/mfa-setup-token.js +1 -1
- package/dist/billing-foundation/constants.d.ts +2 -2
- package/dist/billing-foundation/constants.js +3 -12
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +1 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -1
- package/dist/billing-foundation/feature.js +2 -9
- package/dist/billing-foundation/handlers/sync-subscription.write.js +3 -6
- package/dist/cap-counter/book-cap-usage.d.ts +5 -1
- package/dist/cap-counter/book-cap-usage.js +66 -45
- package/dist/cap-counter/cap-reservation.d.ts +12 -0
- package/dist/cap-counter/cap-reservation.js +135 -0
- package/dist/cap-counter/constants.d.ts +3 -0
- package/dist/cap-counter/constants.js +6 -0
- package/dist/cap-counter/enforce-cap.d.ts +1 -1
- package/dist/cap-counter/enforce-cap.js +1 -8
- package/dist/cap-counter/feature.js +7 -2
- package/dist/cap-counter/handlers/increment-rolling.write.d.ts +5 -0
- package/dist/cap-counter/handlers/increment-rolling.write.js +4 -0
- package/dist/cap-counter/index.d.ts +4 -2
- package/dist/cap-counter/index.js +3 -1
- package/dist/cap-counter/payload-field.d.ts +1 -0
- package/dist/cap-counter/payload-field.js +11 -0
- package/dist/cap-counter/reservation-chain.d.ts +2 -0
- package/dist/cap-counter/reservation-chain.js +44 -0
- package/dist/cap-counter/stock-cap-guard.d.ts +3 -0
- package/dist/cap-counter/tables.d.ts +3 -0
- package/dist/cap-counter/tables.js +36 -0
- package/dist/cap-counter/value-cap-guard.d.ts +18 -0
- package/dist/cap-counter/value-cap-guard.js +33 -0
- package/dist/cap-counter/with-cap-enforcement.d.ts +5 -7
- package/dist/cap-counter/with-cap-enforcement.js +35 -51
- package/dist/channel-discord/feature.js +1 -0
- package/dist/channel-slack/feature.js +1 -0
- package/dist/channel-teams/feature.js +1 -0
- package/dist/compliance-profiles/schema/profile-selection.d.ts +2 -0
- package/dist/delivery/attempt-log-retention.d.ts +9 -0
- package/dist/delivery/attempt-log-retention.js +23 -0
- package/dist/delivery/attempt-log.d.ts +1 -1
- package/dist/delivery/attempt-log.js +21 -2
- package/dist/delivery/constants.d.ts +1 -1
- package/dist/delivery/constants.js +1 -1
- package/dist/delivery/events.d.ts +1 -0
- package/dist/delivery/events.js +2 -0
- package/dist/delivery/feature.d.ts +1 -0
- package/dist/delivery/feature.js +39 -3
- package/dist/delivery/handlers/log.query.d.ts +1 -2
- package/dist/delivery/handlers/log.query.js +1 -1
- package/dist/delivery/index.d.ts +2 -1
- package/dist/delivery/index.js +2 -1
- package/dist/delivery/mask-recipient-address.d.ts +1 -0
- package/dist/delivery/mask-recipient-address.js +41 -0
- package/dist/delivery/public-names.d.ts +2 -0
- package/dist/delivery/public-names.js +3 -0
- package/dist/delivery/types.d.ts +1 -0
- package/dist/delivery/types.js +6 -2
- package/dist/feature-toggles/handlers/set.write.js +1 -1
- package/dist/form-draft/db/queries/cleanup.d.ts +1 -1
- package/dist/form-draft/db/queries/cleanup.js +1 -1
- package/dist/form-draft/db/queries/owned-file-refs.d.ts +1 -1
- package/dist/form-draft/lookup.d.ts +1 -1
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.js +1 -1
- package/dist/inbound-mail-foundation/oauth-state.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-state.js +1 -1
- package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
- package/dist/inbound-provider-imap/feature.js +1 -1
- package/dist/inbound-provider-imap/imap-client.js +1 -1
- package/dist/jobs/job-instant.d.ts +1 -1
- package/dist/jobs/job-instant.js +1 -1
- package/dist/ledger/entity.d.ts +3 -0
- package/dist/metrics/compute.d.ts +1 -1
- package/dist/metrics/feature.js +1 -1
- package/dist/metrics/types.d.ts +1 -1
- package/dist/metrics/window.d.ts +1 -1
- package/dist/metrics/window.js +1 -1
- package/dist/personal-access-tokens/expiry.js +1 -1
- package/dist/personal-access-tokens/handlers/create.write.js +1 -1
- package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
- package/dist/personal-access-tokens/revoke-for-user.js +1 -1
- package/dist/renderer-foundation/types.d.ts +1 -0
- package/dist/renderer-simple/feature.js +4 -6
- package/dist/renderer-simple/index.d.ts +1 -0
- package/dist/renderer-simple/mail-blocks.d.ts +21 -0
- package/dist/renderer-simple/mail-blocks.js +107 -0
- package/dist/renderer-simple/simple-renderer.js +21 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
- package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/session-callbacks.js +1 -1
- package/dist/shared/grace-period.d.ts +2 -1
- package/dist/shared/grace-period.js +3 -7
- package/dist/shared/row-bound-grant.d.ts +1 -1
- package/dist/shared/signed-token.d.ts +1 -1
- package/dist/shared/signed-token.js +1 -1
- package/dist/subscription-mollie/verify-webhook.js +1 -1
- package/dist/subscription-stripe/verify-webhook.js +1 -1
- package/dist/template-resolver/api.d.ts +1 -1
- package/dist/template-resolver/handlers/by-slug.query.d.ts +1 -1
- package/dist/template-resolver/handlers/by-tenant.query.d.ts +2 -2
- package/dist/template-resolver/handlers/collection-item.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-list.query.d.ts +1 -1
- package/dist/template-resolver/handlers/collection-shared.d.ts +2 -2
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -1
- package/dist/template-resolver/handlers/list.query.d.ts +1 -1
- package/dist/template-resolver/table.d.ts +9 -1
- package/dist/template-resolver/user-content-table.d.ts +4 -0
- package/dist/tenant/handlers/team-list.query.d.ts +1 -1
- package/dist/tenant/handlers/team-list.query.js +1 -1
- package/dist/tenant/invitation-table.d.ts +2 -0
- package/dist/tenant/invited-membership.d.ts +1 -1
- package/dist/tenant/invited-membership.js +1 -1
- package/dist/tenant/schema/tenant.d.ts +2 -0
- package/dist/tenant-handover/grant.d.ts +1 -1
- package/dist/tier-engine/index.d.ts +1 -0
- package/dist/tier-engine/index.js +1 -0
- package/dist/tier-engine/tier-option-gate.d.ts +32 -0
- package/dist/tier-engine/tier-option-gate.js +62 -0
- package/dist/user/schema/user.d.ts +2 -0
- package/dist/user-data-rights/db/queries/export-jobs.d.ts +1 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.d.ts +2 -2
- package/dist/user-data-rights/db/queries/forget-cleanup.js +2 -8
- package/dist/user-data-rights/deletion-token.d.ts +1 -1
- package/dist/user-data-rights/email-templates.js +1 -1
- package/dist/user-data-rights/handlers/list-download-attempts.query.js +1 -1
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/schema/export-job.d.ts +2 -0
- package/dist/user-data-rights-defaults/hooks/delivery-attempt.userdata-hook.js +25 -6
- package/dist/waitlist/entity.d.ts +2 -0
- package/dist/waitlist/handlers/invite.write.js +1 -1
- package/dist/waitlist/handlers/submit.write.js +1 -1
- package/package.json +10 -10
- package/src/cap-counter/changes.json +28 -0
- package/src/delivery/changes.json +12 -0
- package/src/derivatives-sharp/changes.json +6 -0
- package/src/legal-pages/changes.json +6 -0
- package/src/renderer-simple/changes.json +6 -0
- package/src/tier-engine/changes.json +6 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
17
17
|
import { access, defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
18
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
19
|
-
import { Temporal } from "temporal
|
|
19
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
20
20
|
import * as z from "zod";
|
|
21
21
|
import { AUDIT_SCOPE_VALUES } from "../constants.js";
|
|
22
22
|
import { resolveAuditScopeFilter } from "./resolve-audit-tenant.js";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// English ships here. Other languages register via @cosmicdrift/kumiko-locale-*
|
|
9
9
|
// (localeDe() / localeEs() call registerMailTranslations).
|
|
10
10
|
import { mailT, registerMailTranslations } from "@cosmicdrift/kumiko-framework/i18n";
|
|
11
|
-
import { Temporal } from "temporal
|
|
11
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
12
12
|
export const AUTH_MAIL_EN = {
|
|
13
13
|
"auth.mail.appNameDefault": "Account",
|
|
14
14
|
"auth.mail.reset.subject": "{app} — Reset your password",
|
|
@@ -20,7 +20,7 @@ import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
|
20
20
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
21
21
|
import { access, assignableAppRolesOf, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
22
22
|
import { InternalError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
23
|
-
import { Temporal } from "temporal
|
|
23
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
24
24
|
import * as z from "zod";
|
|
25
25
|
// kumiko-lint-ignore cross-feature-import invite-flow lebt in auth-email-password (Magic-Link-Pattern), DB-row-owner ist tenant-feature
|
|
26
26
|
import { INVITATION_STATUS, tenantInvitationEntity, tenantInvitationsTable, } from "../../tenant/invitation-table.js";
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { generateToken } from "@cosmicdrift/kumiko-framework/api";
|
|
24
24
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
25
25
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
26
|
-
import { Temporal } from "temporal
|
|
26
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
27
27
|
import * as z from "zod";
|
|
28
28
|
import { findSignupHandoverProvider } from "../../shared/index.js";
|
|
29
29
|
import { AUTH_SIGNUP_DEFAULT_TTL_MINUTES } from "../constants.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Temporal } from "temporal
|
|
1
|
+
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { type AuthUserRow } from "../auth-user-row.js";
|
|
4
4
|
import type { AuthMailContent, AuthMailLocale, RenderTokenContentArgs } from "../email-templates.js";
|
|
@@ -2,7 +2,7 @@ import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-fr
|
|
|
2
2
|
import { buildSessionRoles, createSystemUser, defineWriteHandler, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import { parseRoles } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
|
-
import { Temporal } from "temporal
|
|
5
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
6
6
|
import * as z from "zod";
|
|
7
7
|
import { burnToken, sessionLocaleField, sessionTimezoneField } from "../../shared/index.js";
|
|
8
8
|
import { USER_STATUS, UserQueries } from "../../user/index.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
-
import { Temporal } from "temporal
|
|
4
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
5
5
|
import * as z from "zod";
|
|
6
6
|
import { burnToken } from "../../shared/index.js";
|
|
7
7
|
import { base32Decode } from "../base32.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { Temporal } from "temporal
|
|
2
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
function hmacSign(input, secret) {
|
|
4
4
|
return createHmac("sha256", secret).update(input).digest("base64url");
|
|
5
5
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { Temporal } from "temporal
|
|
2
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
import { type HmacTokenVerifyResult } from "./hmac-token-codec.js";
|
|
4
4
|
export type MfaChallengePayload = {
|
|
5
5
|
readonly userId: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { Temporal } from "temporal
|
|
2
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
import { type HmacTokenVerifyResult } from "./hmac-token-codec.js";
|
|
4
4
|
export type MfaPreauthSetupPayload = {
|
|
5
5
|
readonly userId: string;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// signed-token.ts (userId-only payload) because setup needs to carry
|
|
11
11
|
// generated secret material, not just re-derive it from a lookup.
|
|
12
12
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
13
|
-
import { Temporal } from "temporal
|
|
13
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
14
14
|
function sign(input, secret) {
|
|
15
15
|
return createHmac("sha256", secret).update(input).digest("base64url");
|
|
16
16
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
|
-
import { Temporal
|
|
2
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
3
3
|
export declare const BILLING_FOUNDATION_FEATURE: "billing-foundation";
|
|
4
4
|
export declare const SUBSCRIPTION_PROVIDER_EXTENSION: "subscriptionProvider";
|
|
5
5
|
/** delivery notification type of the § 312f contract confirmation mail. */
|
|
@@ -119,7 +119,7 @@ export declare const BillingPlanActions: {
|
|
|
119
119
|
export type BillingPlanAction = (typeof BillingPlanActions)[keyof typeof BillingPlanActions];
|
|
120
120
|
export declare const TERMINAL_SUBSCRIPTION_STATUSES: ReadonlySet<string>;
|
|
121
121
|
export declare function isTerminalSubscriptionStatus(status: string): boolean;
|
|
122
|
-
export declare const STALE_INCOMPLETE_AFTER:
|
|
122
|
+
export declare const STALE_INCOMPLETE_AFTER: globalThis.Temporal.Duration;
|
|
123
123
|
/** Time-aware terminal check for the checkout gate: a canceled subscription
|
|
124
124
|
* is terminal (see `isTerminalSubscriptionStatus`), and so is an
|
|
125
125
|
* `incomplete` one once it's older than `STALE_INCOMPLETE_AFTER` — every
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/// <reference types="temporal-polyfill/global" preserve="true" />
|
|
2
2
|
// @runtime client
|
|
3
|
-
|
|
4
|
-
// module), so importing it here doesn't break browser bundles. Aliased —
|
|
5
|
-
// same reason as event-store.ts's own import: the un-aliased name would
|
|
6
|
-
// shadow the ambient global `Temporal` TYPE every `Temporal.Instant`
|
|
7
|
-
// annotation in this file (and its callers, e.g. StoredEvent.createdAt)
|
|
8
|
-
// resolves against, see #1438.
|
|
9
|
-
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
3
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
10
4
|
// Feature name
|
|
11
5
|
export const BILLING_FOUNDATION_FEATURE = "billing-foundation";
|
|
12
6
|
// Extension-point name fuer Provider-Plugins (subscription-stripe,
|
|
@@ -158,7 +152,7 @@ export function isTerminalSubscriptionStatus(status) {
|
|
|
158
152
|
// after roughly 23 hours; without a matching staleness cutoff on our side, a
|
|
159
153
|
// stale incomplete row would block that tenant from ever starting a new
|
|
160
154
|
// checkout, since the projection has no expiry-triggered event of its own.
|
|
161
|
-
export const STALE_INCOMPLETE_AFTER =
|
|
155
|
+
export const STALE_INCOMPLETE_AFTER = Temporal.Duration.from({ hours: 24 });
|
|
162
156
|
/** Time-aware terminal check for the checkout gate: a canceled subscription
|
|
163
157
|
* is terminal (see `isTerminalSubscriptionStatus`), and so is an
|
|
164
158
|
* `incomplete` one once it's older than `STALE_INCOMPLETE_AFTER` — every
|
|
@@ -170,10 +164,7 @@ export function isSubscriptionBlockingCheckout(sub, now) {
|
|
|
170
164
|
if (sub.status !== SubscriptionStatuses.incomplete)
|
|
171
165
|
return true;
|
|
172
166
|
const staleAt = sub.lastChangedAt.add(STALE_INCOMPLETE_AFTER);
|
|
173
|
-
|
|
174
|
-
// at runtime — row types resolve against ambient Temporal; polyfill Instant
|
|
175
|
-
// is a separate nominal type across the two .d.ts sources.
|
|
176
|
-
return (TemporalPolyfill.Instant.compare(now, staleAt) < 0);
|
|
167
|
+
return Temporal.Instant.compare(now, staleAt) < 0;
|
|
177
168
|
}
|
|
178
169
|
// A subscription in one of these statuses can be switched to a different
|
|
179
170
|
// plan tier via the provider's confirmation page (Stripe portal
|
|
@@ -50,10 +50,7 @@
|
|
|
50
50
|
// (z.B. für analytics: "wie viele Wechsel im Monat?"), kommt ein
|
|
51
51
|
// `subscription-provider-changed`-event-type später.
|
|
52
52
|
import { defineFeature, EXT_TENANT_DATA, } from "@cosmicdrift/kumiko-framework/engine";
|
|
53
|
-
|
|
54
|
-
// `Temporal` TYPE `ResolvedBillingFoundationOptions.now`'s return type
|
|
55
|
-
// resolves against, see event-store.ts's own import comment (#1438).
|
|
56
|
-
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
53
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
57
54
|
import { BILLING_FOUNDATION_FEATURE, SUBSCRIPTION_PROVIDER_EXTENSION, SubscriptionFoundationHandlers, } from "./constants.js";
|
|
58
55
|
import { registerConsumerProtection } from "./consumer-protection/register-consent.js";
|
|
59
56
|
import { paymentEntity, subscriptionEntity } from "./entities.js";
|
|
@@ -87,11 +84,7 @@ export function createBillingFoundationFeature(options = {}) {
|
|
|
87
84
|
// ever calls `Temporal.Now.instant` itself.
|
|
88
85
|
const widened = {
|
|
89
86
|
...options,
|
|
90
|
-
|
|
91
|
-
// at runtime — `now` is typed against the ambient Temporal global;
|
|
92
|
-
// TemporalPolyfill.Now.instant() returns the polyfill's own nominal
|
|
93
|
-
// Instant type across the two .d.ts sources.
|
|
94
|
-
now: options.now ?? (() => TemporalPolyfill.Now.instant()),
|
|
87
|
+
now: options.now ?? (() => Temporal.Now.instant()),
|
|
95
88
|
...(options.baseUrl !== undefined && {
|
|
96
89
|
baseUrl: options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl,
|
|
97
90
|
}),
|
|
@@ -14,23 +14,20 @@
|
|
|
14
14
|
// the same reason. A human SystemAdmin can still dispatch this directly.
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import { SYSTEM_ROLE } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
|
-
|
|
18
|
-
// `Temporal` TYPE `SubscriptionView.currentPeriodEnd`/`.cancelAt` resolve
|
|
19
|
-
// against, same reasoning as constants.ts's own import.
|
|
20
|
-
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
17
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
21
18
|
import * as z from "zod";
|
|
22
19
|
import { findProviderPlugin } from "../checkout-core.js";
|
|
23
20
|
import { isTerminalSubscriptionStatus, SubscriptionEventTypes } from "../constants.js";
|
|
24
21
|
import { getSubscriptionForTenant } from "../get-subscription-for-tenant.js";
|
|
25
22
|
import { appendSubscriptionEvent } from "./process-event.write.js";
|
|
26
23
|
export const syncSubscriptionSchema = z.object({}).strict();
|
|
27
|
-
// Parses both sides fresh via
|
|
24
|
+
// Parses both sides fresh via Temporal before comparing — tolerant of a
|
|
28
25
|
// provider snapshot's ISO string using a different (but equivalent)
|
|
29
26
|
// representation than `Temporal.Instant#toString()`'s canonical form
|
|
30
27
|
// (offset spelling, sub-second precision, ...), unlike a plain `===` on the
|
|
31
28
|
// raw strings.
|
|
32
29
|
function isoInstantsEqual(a, b) {
|
|
33
|
-
return
|
|
30
|
+
return Temporal.Instant.compare(Temporal.Instant.from(a), Temporal.Instant.from(b)) === 0;
|
|
34
31
|
}
|
|
35
32
|
// Deterministic per (snapshot-content, current stream head) — not per-call.
|
|
36
33
|
// Folding in `lastChangedAt` (not just the snapshot's own fields) closes a
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { type HandlerContext, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { type HandlerContext, type SessionUser, type TenantId, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
export declare function retryCounterWriteOnVersionConflict(writeAttempt: () => Promise<WriteResult>): Promise<WriteResult>;
|
|
4
|
+
export declare function retryOnStreamVersionConflict<T>(attempt: () => Promise<T>): Promise<T>;
|
|
3
5
|
export type BookCapUsageOptions = {
|
|
4
6
|
readonly capName: string;
|
|
5
7
|
readonly periodStartIso: string;
|
|
@@ -7,8 +9,10 @@ export type BookCapUsageOptions = {
|
|
|
7
9
|
readonly outsideTransaction?: boolean;
|
|
8
10
|
readonly guardCurrentValue?: (currentValue: number) => void;
|
|
9
11
|
};
|
|
12
|
+
export declare function requireOutsideTransactionDb(ctx: HandlerContext): TenantDb;
|
|
10
13
|
export declare function bookCapUsage(ctx: HandlerContext, options: BookCapUsageOptions): Promise<WriteResult>;
|
|
11
14
|
export declare function releaseCapUsage(ctx: HandlerContext, options: Omit<BookCapUsageOptions, "guardCurrentValue">): Promise<WriteResult>;
|
|
15
|
+
export declare function applyCapDeltaOn(db: TenantDb, user: SessionUser, options: BookCapUsageOptions, direction: "add" | "subtract"): Promise<WriteResult>;
|
|
12
16
|
export type MarkCapSoftWarnedOptions = {
|
|
13
17
|
readonly capName: string;
|
|
14
18
|
readonly periodStartIso: string;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// In-process cap booking for the caller's own tenant; no SystemAdmin dispatch needed.
|
|
2
2
|
import { runInOwnTransaction } from "@cosmicdrift/kumiko-framework/db";
|
|
3
3
|
import { createEntityExecutor, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
-
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
5
|
-
import { Temporal } from "temporal
|
|
4
|
+
import { eventsTable, VersionConflictError } from "@cosmicdrift/kumiko-framework/event-store";
|
|
5
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
6
6
|
import * as z from "zod";
|
|
7
7
|
import { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
|
|
8
|
-
import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, ROLLING_INCREMENTED_EVENT_QN } from "./constants.js";
|
|
8
|
+
import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, ROLLING_INCREMENTED_EVENT_QN, ROLLING_RELEASED_EVENT_QN, } from "./constants.js";
|
|
9
9
|
import { capCounterEntity } from "./entity.js";
|
|
10
10
|
const { table, executor } = createEntityExecutor("cap-counter", capCounterEntity);
|
|
11
11
|
// periodStartIso feeds the aggregate id and Temporal.Instant.from(); a non-instant string would
|
|
@@ -26,7 +26,7 @@ const BACKOFF_JITTER_MS_PER_ATTEMPT = 5;
|
|
|
26
26
|
function sleepMs(ms) {
|
|
27
27
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
28
|
}
|
|
29
|
-
async function retryCounterWriteOnVersionConflict(writeAttempt) {
|
|
29
|
+
export async function retryCounterWriteOnVersionConflict(writeAttempt) {
|
|
30
30
|
let result = await writeAttempt();
|
|
31
31
|
for (let attempt = 1; attempt < MAX_COUNTER_WRITE_ATTEMPTS && isLostCounterRace(result); attempt++) {
|
|
32
32
|
await sleepMs(Math.random() * attempt * BACKOFF_JITTER_MS_PER_ATTEMPT);
|
|
@@ -34,7 +34,21 @@ async function retryCounterWriteOnVersionConflict(writeAttempt) {
|
|
|
34
34
|
}
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
// Event-store appends signal a lost race by throwing; the whole attempt (read, guard, append) is repeated.
|
|
38
|
+
export async function retryOnStreamVersionConflict(attempt) {
|
|
39
|
+
for (let tries = 1;; tries++) {
|
|
40
|
+
try {
|
|
41
|
+
return await attempt();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!(error instanceof VersionConflictError) || tries >= MAX_COUNTER_WRITE_ATTEMPTS) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
await sleepMs(Math.random() * tries * BACKOFF_JITTER_MS_PER_ATTEMPT);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function requireOutsideTransactionDb(ctx) {
|
|
38
52
|
if (!ctx.dbOutsideTransaction) {
|
|
39
53
|
throw new Error("cap-counter.bookCapUsage: outsideTransaction requested but ctx.dbOutsideTransaction is undefined");
|
|
40
54
|
}
|
|
@@ -47,45 +61,46 @@ export async function bookCapUsage(ctx, options) {
|
|
|
47
61
|
export async function releaseCapUsage(ctx, options) {
|
|
48
62
|
return applyCapDelta(ctx, options, "subtract");
|
|
49
63
|
}
|
|
50
|
-
|
|
64
|
+
// One attempt against an already-open db handle; a lost race comes back as a version_conflict failure the caller retries.
|
|
65
|
+
export async function applyCapDeltaOn(db, user, options, direction) {
|
|
51
66
|
const parsed = capBookingSchema.parse(options);
|
|
52
|
-
const aggregateId = capCounterAggregateId(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return executor.create({
|
|
60
|
-
id: aggregateId,
|
|
61
|
-
capName: parsed.capName,
|
|
62
|
-
value: parsed.amount,
|
|
63
|
-
periodStart: Temporal.Instant.from(parsed.periodStartIso),
|
|
64
|
-
lastSoftWarnedAt: null,
|
|
65
|
-
}, ctx.user, db);
|
|
66
|
-
}
|
|
67
|
-
const currentRow = existing[0];
|
|
68
|
-
if (!currentRow) {
|
|
69
|
-
throw new Error("cap-counter.bookCapUsage: row vanished between length-check and read");
|
|
70
|
-
}
|
|
71
|
-
const currentValue = currentRow["value"]; // @cast-boundary db-row
|
|
72
|
-
const currentVersion = currentRow["version"]; // @cast-boundary db-row
|
|
73
|
-
options.guardCurrentValue?.(currentValue);
|
|
74
|
-
return executor.update({
|
|
67
|
+
const aggregateId = capCounterAggregateId(user.tenantId, parsed.capName, parsed.periodStartIso);
|
|
68
|
+
const existing = await db.selectMany(table, { id: aggregateId }, { limit: 1 });
|
|
69
|
+
if (existing.length === 0) {
|
|
70
|
+
options.guardCurrentValue?.(0);
|
|
71
|
+
if (direction === "subtract")
|
|
72
|
+
return { isSuccess: true, data: {} };
|
|
73
|
+
return executor.create({
|
|
75
74
|
id: aggregateId,
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
capName: parsed.capName,
|
|
76
|
+
value: parsed.amount,
|
|
77
|
+
periodStart: Temporal.Instant.from(parsed.periodStartIso),
|
|
78
|
+
lastSoftWarnedAt: null,
|
|
79
|
+
}, user, db);
|
|
80
|
+
}
|
|
81
|
+
const currentRow = existing[0];
|
|
82
|
+
if (!currentRow) {
|
|
83
|
+
throw new Error("cap-counter.bookCapUsage: row vanished between length-check and read");
|
|
83
84
|
}
|
|
85
|
+
const currentValue = currentRow["value"]; // @cast-boundary db-row
|
|
86
|
+
const currentVersion = currentRow["version"]; // @cast-boundary db-row
|
|
87
|
+
options.guardCurrentValue?.(currentValue);
|
|
88
|
+
return executor.update({
|
|
89
|
+
id: aggregateId,
|
|
90
|
+
version: currentVersion,
|
|
91
|
+
changes: {
|
|
92
|
+
value: direction === "add"
|
|
93
|
+
? currentValue + parsed.amount
|
|
94
|
+
: Math.max(0, currentValue - parsed.amount),
|
|
95
|
+
},
|
|
96
|
+
}, user, db);
|
|
97
|
+
}
|
|
98
|
+
async function applyCapDelta(ctx, options, direction) {
|
|
84
99
|
if (options.outsideTransaction) {
|
|
85
100
|
const outsideDb = requireOutsideTransactionDb(ctx);
|
|
86
|
-
return retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb,
|
|
101
|
+
return retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, (txDb) => applyCapDeltaOn(txDb, ctx.user, options, direction)));
|
|
87
102
|
}
|
|
88
|
-
return retryCounterWriteOnVersionConflict(() =>
|
|
103
|
+
return retryCounterWriteOnVersionConflict(() => applyCapDeltaOn(ctx.db, ctx.user, options, direction));
|
|
89
104
|
}
|
|
90
105
|
export async function markCapSoftWarned(ctx, options) {
|
|
91
106
|
const parsed = capPeriodSchema.parse(options);
|
|
@@ -117,27 +132,33 @@ export async function markCapSoftWarned(ctx, options) {
|
|
|
117
132
|
}
|
|
118
133
|
return retryCounterWriteOnVersionConflict(() => attemptMark(ctx.db));
|
|
119
134
|
}
|
|
120
|
-
|
|
121
|
-
// (e.g. a CapSpec.usage callback), not the enforce-and-throw path.
|
|
122
|
-
export async function readRollingCapUsage(db, tenantId, options) {
|
|
135
|
+
async function sumRollingEventAmounts(db, tenantId, options, type) {
|
|
123
136
|
const aggregateId = rollingCapAggregateId(tenantId, options.capName);
|
|
124
137
|
const cutoff = Temporal.Now.instant().subtract({ hours: options.windowDays * 24 });
|
|
125
138
|
const rows = await db.selectMany(eventsTable, {
|
|
126
139
|
tenantId,
|
|
127
140
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
128
141
|
aggregateId,
|
|
129
|
-
type
|
|
142
|
+
type,
|
|
130
143
|
createdAt: { gte: cutoff },
|
|
131
144
|
});
|
|
132
|
-
let
|
|
145
|
+
let total = 0;
|
|
133
146
|
for (const row of rows) {
|
|
134
147
|
// @cast-boundary engine-payload — events.payload is jsonb (typed as
|
|
135
148
|
// unknown by drizzle's $type<Record<string,unknown>>); narrowing the
|
|
136
149
|
// shape here mirrors enforceRollingCap's own read-side contract.
|
|
137
150
|
const payload = row["payload"];
|
|
138
151
|
if (typeof payload.amount === "number") {
|
|
139
|
-
|
|
152
|
+
total += payload.amount;
|
|
140
153
|
}
|
|
141
154
|
}
|
|
142
|
-
return
|
|
155
|
+
return total;
|
|
156
|
+
}
|
|
157
|
+
// Sums usage without throwing — for callers that only need the raw number
|
|
158
|
+
// (e.g. a CapSpec.usage callback), not the enforce-and-throw path.
|
|
159
|
+
// Incremented minus released amounts inside the window; a release whose increment already left the window cannot push it below 0.
|
|
160
|
+
export async function readRollingCapUsage(db, tenantId, options) {
|
|
161
|
+
const incremented = await sumRollingEventAmounts(db, tenantId, options, ROLLING_INCREMENTED_EVENT_QN);
|
|
162
|
+
const released = await sumRollingEventAmounts(db, tenantId, options, ROLLING_RELEASED_EVENT_QN);
|
|
163
|
+
return Math.max(0, incremented - released);
|
|
143
164
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { ReservationHandle } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
|
+
import { type BookCapUsageOptions, type ReadRollingCapUsageOptions } from "./book-cap-usage.js";
|
|
4
|
+
export type ReserveCalendarCapOptions = Required<Pick<BookCapUsageOptions, "capName" | "periodStartIso" | "amount">> & Pick<BookCapUsageOptions, "guardCurrentValue">;
|
|
5
|
+
export declare function reserveCalendarCap(ctx: HandlerContext, options: ReserveCalendarCapOptions): Promise<ReservationHandle>;
|
|
6
|
+
export type ReserveRollingCapOptions = ReadRollingCapUsageOptions & {
|
|
7
|
+
readonly amount: number;
|
|
8
|
+
readonly guardCurrentValue?: (currentValue: number) => void;
|
|
9
|
+
};
|
|
10
|
+
export declare function reserveRollingCap(ctx: HandlerContext, options: ReserveRollingCapOptions): Promise<ReservationHandle>;
|
|
11
|
+
export declare function releaseCapReservation(ctx: HandlerContext, id: string): Promise<void>;
|
|
12
|
+
export declare function releaseExpiredCapReservations(ctx: HandlerContext, capName: string): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { deleteMany, runInOwnTransaction } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import { appendEventInTenantDb, getStreamVersionInTenantDb, } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
|
+
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
|
+
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
6
|
+
import { rollingCapAggregateId } from "./aggregate-id.js";
|
|
7
|
+
import { applyCapDeltaOn, readRollingCapUsage, requireOutsideTransactionDb, retryCounterWriteOnVersionConflict, retryOnStreamVersionConflict, } from "./book-cap-usage.js";
|
|
8
|
+
import { CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CAP_RESERVATION_TTL_MINUTES, ROLLING_INCREMENTED_EVENT_QN, ROLLING_RELEASED_EVENT_QN, } from "./constants.js";
|
|
9
|
+
import { capReservationsTable } from "./tables.js";
|
|
10
|
+
const NOTHING_TO_RELEASE = { isSuccess: true, data: {} };
|
|
11
|
+
async function insertReservationRow(txDb, id, draft) {
|
|
12
|
+
const now = Temporal.Now.instant();
|
|
13
|
+
await txDb.insertOne(capReservationsTable, {
|
|
14
|
+
id,
|
|
15
|
+
capName: draft.capName,
|
|
16
|
+
kind: draft.kind,
|
|
17
|
+
periodStartIso: draft.periodStartIso ?? null,
|
|
18
|
+
amount: draft.amount,
|
|
19
|
+
expiresAt: now.add({ minutes: CAP_RESERVATION_TTL_MINUTES }),
|
|
20
|
+
createdAt: now,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function toHandle(ctx, id) {
|
|
24
|
+
return {
|
|
25
|
+
release: () => releaseCapReservation(ctx, id),
|
|
26
|
+
confirmInTransaction: (confirm) => confirmCapReservation(confirm, id),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// The counter increment and the reservation row commit together, so a booked amount never exists
|
|
30
|
+
// without a row that can give it back.
|
|
31
|
+
export async function reserveCalendarCap(ctx, options) {
|
|
32
|
+
const outsideDb = requireOutsideTransactionDb(ctx);
|
|
33
|
+
const id = generateId();
|
|
34
|
+
const reserved = await retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, async (txDb) => {
|
|
35
|
+
const booked = await applyCapDeltaOn(txDb, ctx.user, options, "add");
|
|
36
|
+
if (!booked.isSuccess)
|
|
37
|
+
return booked;
|
|
38
|
+
await insertReservationRow(txDb, id, {
|
|
39
|
+
capName: options.capName,
|
|
40
|
+
kind: "calendar",
|
|
41
|
+
periodStartIso: options.periodStartIso,
|
|
42
|
+
amount: options.amount,
|
|
43
|
+
});
|
|
44
|
+
return booked;
|
|
45
|
+
}));
|
|
46
|
+
if (!reserved.isSuccess)
|
|
47
|
+
throw reraiseAsKumikoError(reserved.error);
|
|
48
|
+
return toHandle(ctx, id);
|
|
49
|
+
}
|
|
50
|
+
function rollingEvent(user, capName, amount, expectedVersion, type) {
|
|
51
|
+
return {
|
|
52
|
+
aggregateId: rollingCapAggregateId(user.tenantId, capName),
|
|
53
|
+
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
54
|
+
expectedVersion,
|
|
55
|
+
type,
|
|
56
|
+
payload: { capName, amount },
|
|
57
|
+
metadata: { userId: user.id },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// The append is version-guarded: a booker that read the usage before another one appended loses the
|
|
61
|
+
// race and repeats the whole attempt, so the hard-cap check always sees the stream it appends to.
|
|
62
|
+
export async function reserveRollingCap(ctx, options) {
|
|
63
|
+
const outsideDb = requireOutsideTransactionDb(ctx);
|
|
64
|
+
const id = generateId();
|
|
65
|
+
const aggregateId = rollingCapAggregateId(ctx.user.tenantId, options.capName);
|
|
66
|
+
await retryOnStreamVersionConflict(() => runInOwnTransaction(outsideDb, async (txDb) => {
|
|
67
|
+
const version = await getStreamVersionInTenantDb(txDb, aggregateId);
|
|
68
|
+
const usage = await readRollingCapUsage(txDb, ctx.user.tenantId, options);
|
|
69
|
+
options.guardCurrentValue?.(usage);
|
|
70
|
+
await appendEventInTenantDb(txDb, rollingEvent(ctx.user, options.capName, options.amount, version, ROLLING_INCREMENTED_EVENT_QN));
|
|
71
|
+
await insertReservationRow(txDb, id, {
|
|
72
|
+
capName: options.capName,
|
|
73
|
+
kind: "rolling",
|
|
74
|
+
amount: options.amount,
|
|
75
|
+
});
|
|
76
|
+
return NOTHING_TO_RELEASE;
|
|
77
|
+
}));
|
|
78
|
+
return toHandle(ctx, id);
|
|
79
|
+
}
|
|
80
|
+
// Deleting the row takes its row lock inside the handler transaction, so a release started after a
|
|
81
|
+
// failed COMMIT waits for the real outcome and finds the row gone when the commit went through.
|
|
82
|
+
async function confirmCapReservation(confirm, id) {
|
|
83
|
+
await deleteMany(confirm.tx, capReservationsTable, { id, tenantId: confirm.tenantId });
|
|
84
|
+
}
|
|
85
|
+
async function giveBackCalendar(txDb, user, row) {
|
|
86
|
+
if (row.periodStartIso === null) {
|
|
87
|
+
throw new Error(`cap-counter: calendar reservation ${row.id} has no period to release against`);
|
|
88
|
+
}
|
|
89
|
+
return applyCapDeltaOn(txDb, user, { capName: row.capName, periodStartIso: row.periodStartIso, amount: row.amount }, "subtract");
|
|
90
|
+
}
|
|
91
|
+
async function giveBackRolling(txDb, user, row) {
|
|
92
|
+
const version = await getStreamVersionInTenantDb(txDb, rollingCapAggregateId(user.tenantId, row.capName));
|
|
93
|
+
await appendEventInTenantDb(txDb, rollingEvent(user, row.capName, row.amount, version, ROLLING_RELEASED_EVENT_QN));
|
|
94
|
+
return NOTHING_TO_RELEASE;
|
|
95
|
+
}
|
|
96
|
+
async function giveBackReservation(txDb, user, id) {
|
|
97
|
+
const row = await txDb.fetchOne(capReservationsTable, { id });
|
|
98
|
+
if (!row)
|
|
99
|
+
return NOTHING_TO_RELEASE;
|
|
100
|
+
// Waits for a confirm still holding the row; zero rows back means that confirm committed.
|
|
101
|
+
const claimed = await txDb.updateMany(capReservationsTable, { expiresAt: row.expiresAt }, { id });
|
|
102
|
+
if (claimed.length === 0)
|
|
103
|
+
return NOTHING_TO_RELEASE;
|
|
104
|
+
const given = row.kind === "rolling"
|
|
105
|
+
? await giveBackRolling(txDb, user, row)
|
|
106
|
+
: await giveBackCalendar(txDb, user, row);
|
|
107
|
+
if (!given.isSuccess)
|
|
108
|
+
return given;
|
|
109
|
+
await txDb.deleteMany(capReservationsTable, { id });
|
|
110
|
+
return given;
|
|
111
|
+
}
|
|
112
|
+
export async function releaseCapReservation(ctx, id) {
|
|
113
|
+
const outsideDb = requireOutsideTransactionDb(ctx);
|
|
114
|
+
const released = await retryOnStreamVersionConflict(() => retryCounterWriteOnVersionConflict(() => runInOwnTransaction(outsideDb, (txDb) => giveBackReservation(txDb, ctx.user, id))));
|
|
115
|
+
if (!released.isSuccess)
|
|
116
|
+
throw reraiseAsKumikoError(released.error);
|
|
117
|
+
}
|
|
118
|
+
// A failed release leaves its row behind; giving it back once the TTL passed bounds how long that
|
|
119
|
+
// capacity stays counted.
|
|
120
|
+
export async function releaseExpiredCapReservations(ctx, capName) {
|
|
121
|
+
const outsideDb = requireOutsideTransactionDb(ctx);
|
|
122
|
+
const expired = await outsideDb.selectMany(capReservationsTable, {
|
|
123
|
+
tenantId: ctx.user.tenantId,
|
|
124
|
+
capName,
|
|
125
|
+
expiresAt: { lt: Temporal.Now.instant() },
|
|
126
|
+
});
|
|
127
|
+
for (const { id } of expired) {
|
|
128
|
+
try {
|
|
129
|
+
await releaseCapReservation(ctx, id);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
ctx.log?.error("releasing an expired cap reservation failed", { id, capName, error });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -3,6 +3,9 @@ export declare const CAP_COUNTER_LIST_SCREEN_ID: "cap-list";
|
|
|
3
3
|
export declare const CAP_COUNTER_ROLLING_AGGREGATE_TYPE: "cap-counter-rolling";
|
|
4
4
|
export declare const ROLLING_INCREMENTED_EVENT_SHORT: "rolling-incremented";
|
|
5
5
|
export declare const ROLLING_INCREMENTED_EVENT_QN: "cap-counter:event:rolling-incremented";
|
|
6
|
+
export declare const ROLLING_RELEASED_EVENT_SHORT: "rolling-released";
|
|
7
|
+
export declare const ROLLING_RELEASED_EVENT_QN: "cap-counter:event:rolling-released";
|
|
8
|
+
export declare const CAP_RESERVATION_TTL_MINUTES = 60;
|
|
6
9
|
export declare const CapCounterHandlers: {
|
|
7
10
|
readonly increment: "cap-counter:write:increment";
|
|
8
11
|
readonly incrementRolling: "cap-counter:write:increment-rolling";
|