@cosmicdrift/kumiko-samples 0.403.0 → 0.404.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 +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/data-retention/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/feature-toggles/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +6 -2
- package/packages/bundled-features/src/form-draft/feature.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/retention-job.ts +1 -1
- package/packages/bundled-features/src/jobs/changes.json +6 -0
- package/packages/bundled-features/src/jobs/feature.ts +6 -7
- package/packages/bundled-features/src/user-data-rights/feature.ts +3 -3
- package/samples/recipes/lane-routing/README.md +1 -0
- package/samples/recipes/lane-routing/src/feature.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-samples",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.404.0",
|
|
4
4
|
"description": "Source trees of the Kumiko sample recipes, sample apps and bundled features in repo layout, for tooling such as few-shot corpus builds.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.404.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -77,7 +77,7 @@ export function createAuditFeature(): FeatureDefinition {
|
|
|
77
77
|
|
|
78
78
|
r.job({
|
|
79
79
|
name: "escape-hatch-retention",
|
|
80
|
-
trigger: {
|
|
80
|
+
trigger: { maintenance: true },
|
|
81
81
|
concurrency: "skip",
|
|
82
82
|
// pruneEvents needs a transaction-capable connection, which only unsafeRaw() hands a job.
|
|
83
83
|
escapeHatch: {
|
|
@@ -71,7 +71,7 @@ export function createDataRetentionFeature(): FeatureDefinition {
|
|
|
71
71
|
// retention rules are configured but never enforced.
|
|
72
72
|
r.job({
|
|
73
73
|
name: "retention-cleanup",
|
|
74
|
-
trigger: {
|
|
74
|
+
trigger: { maintenance: true },
|
|
75
75
|
perTenant: true,
|
|
76
76
|
concurrency: "skip",
|
|
77
77
|
escapeHatch: {
|
|
@@ -197,7 +197,7 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
|
|
|
197
197
|
if (attemptLogRetentionDays !== undefined) {
|
|
198
198
|
r.job({
|
|
199
199
|
name: DeliveryJobNames.attemptLogRetention,
|
|
200
|
-
trigger: {
|
|
200
|
+
trigger: { maintenance: true },
|
|
201
201
|
concurrency: "skip",
|
|
202
202
|
escapeHatch: { reason: ATTEMPT_LOG_RETENTION_ESCAPE_HATCH_REASON },
|
|
203
203
|
handler: async (_payload, ctx) => {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.404.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Feature-toggle Redis signal follows KUMIKO_REDIS_PREFIX",
|
|
6
|
+
"detail": "createRedisToggleSyncSignal takes an optional channelSubscribePattern (default from KUMIKO_REDIS_PREFIX) and ignores messages from other channels, which a prefix-wide subscription delivers."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.365.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createRedisPubSubSignal,
|
|
5
5
|
type RedisClientOptions,
|
|
6
6
|
redisChannelPrefixFromEnv,
|
|
7
|
+
redisNamespaceFromEnv,
|
|
7
8
|
} from "@cosmicdrift/kumiko-framework/redis";
|
|
8
9
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
9
10
|
import type { ToggleSyncSignal } from "./toggle-runtime.ts";
|
|
@@ -36,11 +37,12 @@ export function createRedisToggleSyncSignal(
|
|
|
36
37
|
redisUrl: string,
|
|
37
38
|
clientOptions?: RedisClientOptions,
|
|
38
39
|
channelPrefix: string = redisChannelPrefixFromEnv(),
|
|
40
|
+
channelSubscribePattern: string | undefined = redisNamespaceFromEnv().channelSubscribePattern,
|
|
39
41
|
): RedisToggleSyncSignal {
|
|
40
42
|
const toggleSyncChannel = `${channelPrefix}${TOGGLE_SYNC_CHANNEL_NAME}`;
|
|
41
43
|
const signal = createRedisPubSubSignal({
|
|
42
44
|
redisUrl,
|
|
43
|
-
channelPattern: toggleSyncChannel,
|
|
45
|
+
channelPattern: channelSubscribePattern ?? toggleSyncChannel,
|
|
44
46
|
label: "feature-toggles",
|
|
45
47
|
...(clientOptions ? { clientOptions } : {}),
|
|
46
48
|
});
|
|
@@ -50,7 +52,9 @@ export function createRedisToggleSyncSignal(
|
|
|
50
52
|
signal.publish(toggleSyncChannel, { featureName, enabled });
|
|
51
53
|
},
|
|
52
54
|
onMessage(listener) {
|
|
53
|
-
signal.onMessage((
|
|
55
|
+
signal.onMessage((channel, payload) => {
|
|
56
|
+
// skip: a wide subscribe pattern also delivers other features' channels
|
|
57
|
+
if (channel !== toggleSyncChannel) return;
|
|
54
58
|
if (!isTogglePayload(payload)) {
|
|
55
59
|
// biome-ignore lint/suspicious/noConsole: ops-visible fallback — no ctx.obs.log in a raw PubSubSignal handler.
|
|
56
60
|
console.error(
|
|
@@ -43,7 +43,7 @@ function registerFormDraft(r: FeatureRegistrar<typeof FORM_DRAFT_FEATURE_NAME>):
|
|
|
43
43
|
r.config({ keys: { retentionDays: formDraftRetentionDaysConfig } });
|
|
44
44
|
r.job({
|
|
45
45
|
name: "cleanup",
|
|
46
|
-
trigger: {
|
|
46
|
+
trigger: { maintenance: true },
|
|
47
47
|
concurrency: "skip",
|
|
48
48
|
escapeHatch: { reason: CLEANUP_ESCAPE_HATCH_REASON },
|
|
49
49
|
handler: (payload, ctx) =>
|
|
@@ -18,7 +18,7 @@ export function registerInboundMailRetentionJob(r: FeatureRegistrar<"inbound-mai
|
|
|
18
18
|
"inbound-mail retention runs raw deletes; every statement filters by this run's tenantId";
|
|
19
19
|
r.job({
|
|
20
20
|
name: "inbound-mail-retention",
|
|
21
|
-
trigger: {
|
|
21
|
+
trigger: { maintenance: true },
|
|
22
22
|
perTenant: true,
|
|
23
23
|
concurrency: "skip",
|
|
24
24
|
escapeHatch: { reason: INBOUND_MAIL_RETENTION_REASON },
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.404.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Cleanup jobs run once a day in the maintenance window",
|
|
6
|
+
"detail": "jobs (stale-run-sweep, hourly before, and retention-cleanup), user-data-rights (export and forget), data-retention, audit escape-hatch-retention, inbound-mail retention, delivery attempt-log-retention and form-draft cleanup use trigger { maintenance: true } instead of 03:00 to 03:30 and hourly crons. Without KUMIKO_MAINTENANCE_WINDOW they all run at 03:00 UTC. A stuck job run is now marked failed at most a day after it crosses the stale timeout."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.394.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -43,7 +43,7 @@ export type JobsFeatureOptions = {
|
|
|
43
43
|
// store_job_run_logs before the daily retention-cleanup job deletes it. Also the age after
|
|
44
44
|
// which a tenant-visible "queued" row counts as lost and is dropped by the stale-run sweep.
|
|
45
45
|
readonly retentionDays?: number;
|
|
46
|
-
// How long a run can sit at status "running" before the
|
|
46
|
+
// How long a run can sit at status "running" before the daily
|
|
47
47
|
// stale-run-sweep job marks it "failed" — a process kill mid-run
|
|
48
48
|
// never fires onJobComplete/onJobFailed, so nothing else ever revisits
|
|
49
49
|
// it). Must clear any legitimately long-running job (reindexEntity,
|
|
@@ -120,18 +120,17 @@ export function createJobsFeature(options: JobsFeatureOptions = {}): FeatureDefi
|
|
|
120
120
|
// stores — nothing else purges them.
|
|
121
121
|
r.job(
|
|
122
122
|
"retention-cleanup",
|
|
123
|
-
{ trigger: {
|
|
123
|
+
{ trigger: { maintenance: true }, concurrency: "skip" },
|
|
124
124
|
createRetentionCleanupJob(retentionDays),
|
|
125
125
|
);
|
|
126
126
|
|
|
127
127
|
// Sweeps store_job_runs for rows stuck at status "running" past the
|
|
128
|
-
// timeout.
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// doesn't matter.
|
|
128
|
+
// timeout. Runs in the maintenance window: the timeout is
|
|
129
|
+
// hours, so a daily pass finds a stuck run at most a day late and a sleeping
|
|
130
|
+
// app is not woken every hour for it.
|
|
132
131
|
r.job(
|
|
133
132
|
"stale-run-sweep",
|
|
134
|
-
{ trigger: {
|
|
133
|
+
{ trigger: { maintenance: true }, concurrency: "skip" },
|
|
135
134
|
createStaleRunSweepJob(staleRunTimeoutHours, retentionDays),
|
|
136
135
|
);
|
|
137
136
|
|
|
@@ -600,14 +600,14 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
600
600
|
type: "gauge",
|
|
601
601
|
unit: "seconds",
|
|
602
602
|
description:
|
|
603
|
-
"Unix timestamp of the last export storage-cleanup pass that completed. Alert on time() minus this value exceeding
|
|
603
|
+
"Unix timestamp of the last export storage-cleanup pass that completed. Alert on time() minus this value exceeding one day plus a buffer; the backlog-age gauge alone stays at its last value when the cron stops.",
|
|
604
604
|
});
|
|
605
605
|
|
|
606
606
|
// Worker for the asynchronous export pipeline, triggered by a cron job.
|
|
607
607
|
const RUN_EXPORT_JOBS_REASON = "processes pending export jobs across every tenant";
|
|
608
608
|
r.job({
|
|
609
609
|
name: "run-export-jobs",
|
|
610
|
-
trigger: {
|
|
610
|
+
trigger: { maintenance: true },
|
|
611
611
|
concurrency: "skip",
|
|
612
612
|
escapeHatch: { reason: RUN_EXPORT_JOBS_REASON },
|
|
613
613
|
handler: async (_payload, ctx) => {
|
|
@@ -705,7 +705,7 @@ export function createUserDataRightsFeature(opts: UserDataRightsOptions = {}): F
|
|
|
705
705
|
const RUN_FORGET_CLEANUP_REASON = "executes overdue Art.17 forget requests across every tenant";
|
|
706
706
|
r.job({
|
|
707
707
|
name: "run-forget-cleanup",
|
|
708
|
-
trigger: {
|
|
708
|
+
trigger: { maintenance: true },
|
|
709
709
|
concurrency: "skip",
|
|
710
710
|
escapeHatch: { reason: RUN_FORGET_CLEANUP_REASON },
|
|
711
711
|
handler: async (_payload, ctx) => {
|
|
@@ -5,6 +5,7 @@ Pin jobs to deploy lanes (`api` | `worker`) with event-triggered fan-out.
|
|
|
5
5
|
## What it shows
|
|
6
6
|
|
|
7
7
|
- `r.job({ runIn: "worker" })` after an HTTP write
|
|
8
|
+
- `trigger: { maintenance: true }` for a daily housekeeping job in the app's maintenance window (`KUMIKO_MAINTENANCE_WINDOW`)
|
|
8
9
|
- End-to-end verification via `createAllInOneEntrypoint`
|
|
9
10
|
|
|
10
11
|
## Source
|
|
@@ -23,6 +23,7 @@ import { z } from "zod";
|
|
|
23
23
|
// opaque to the framework.
|
|
24
24
|
export const renderedReceipts: Array<{ customerName: string; amount: number }> = [];
|
|
25
25
|
export const sentConfirmations: Array<{ customerName: string; amount: number }> = [];
|
|
26
|
+
export const pruneRuns: number[] = [];
|
|
26
27
|
|
|
27
28
|
// The entity declaration secures the qualified-name prefix "orders:write:order:create"
|
|
28
29
|
// for the write handler below and thereby the job trigger on this event.
|
|
@@ -100,5 +101,12 @@ export function createLaneRoutingFeature(): FeatureDefinition {
|
|
|
100
101
|
});
|
|
101
102
|
},
|
|
102
103
|
);
|
|
104
|
+
|
|
105
|
+
// Housekeeping: `maintenance` runs once a day in the app's maintenance window
|
|
106
|
+
// (KUMIKO_MAINTENANCE_WINDOW, UTC, default 03:00) instead of a cron time of its own, so a
|
|
107
|
+
// sleeping app wakes once a day for all its cleanup jobs.
|
|
108
|
+
r.job("prune-old-receipts", { trigger: { maintenance: true }, runIn: "worker" }, async () => {
|
|
109
|
+
pruneRuns.push(Date.now());
|
|
110
|
+
});
|
|
103
111
|
});
|
|
104
112
|
}
|