@cosmicdrift/kumiko-bundled-features 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.
@@ -43,7 +43,7 @@ export function createAuditFeature() {
43
43
  }));
44
44
  r.job({
45
45
  name: "escape-hatch-retention",
46
- trigger: { cron: "30 3 * * *" },
46
+ trigger: { maintenance: true },
47
47
  concurrency: "skip",
48
48
  // pruneEvents needs a transaction-capable connection, which only unsafeRaw() hands a job.
49
49
  escapeHatch: {
@@ -41,7 +41,7 @@ export function createDataRetentionFeature() {
41
41
  // retention rules are configured but never enforced.
42
42
  r.job({
43
43
  name: "retention-cleanup",
44
- trigger: { cron: "0 3 * * *" },
44
+ trigger: { maintenance: true },
45
45
  perTenant: true,
46
46
  concurrency: "skip",
47
47
  escapeHatch: {
@@ -125,7 +125,7 @@ export function createDeliveryFeature(options) {
125
125
  if (attemptLogRetentionDays !== undefined) {
126
126
  r.job({
127
127
  name: DeliveryJobNames.attemptLogRetention,
128
- trigger: { cron: "15 3 * * *" },
128
+ trigger: { maintenance: true },
129
129
  concurrency: "skip",
130
130
  escapeHatch: { reason: ATTEMPT_LOG_RETENTION_ESCAPE_HATCH_REASON },
131
131
  handler: async (_payload, ctx) => {
@@ -3,5 +3,5 @@ import type { ToggleSyncSignal } from "./toggle-runtime.ts";
3
3
  export type RedisToggleSyncSignal = ToggleSyncSignal & {
4
4
  close(): Promise<void>;
5
5
  };
6
- export declare function createRedisToggleSyncSignal(redisUrl: string, clientOptions?: RedisClientOptions, channelPrefix?: string): RedisToggleSyncSignal;
6
+ export declare function createRedisToggleSyncSignal(redisUrl: string, clientOptions?: RedisClientOptions, channelPrefix?: string, channelSubscribePattern?: string | undefined): RedisToggleSyncSignal;
7
7
  export declare function createCacheSyncToggleSignal(bus: CacheSyncBus): ToggleSyncSignal;
@@ -1,4 +1,4 @@
1
- import { CACHE_SYNC_TOPICS, createRedisPubSubSignal, redisChannelPrefixFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
1
+ import { CACHE_SYNC_TOPICS, createRedisPubSubSignal, redisChannelPrefixFromEnv, redisNamespaceFromEnv, } from "@cosmicdrift/kumiko-framework/redis";
2
2
  import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
3
3
  // Single fixed channel — unlike the SSE broker there's no per-tenant/
4
4
  // per-user variance to multiplex, every toggle flip is global.
@@ -13,11 +13,11 @@ function isTogglePayload(value) {
13
13
  // the shared cursor's toggle-set event would ever learn about a flip.
14
14
  // Standalone Redis transport for callers that want their own connections;
15
15
  // createFeatureToggleRuntime builds a cache-sync-bus signal by default.
16
- export function createRedisToggleSyncSignal(redisUrl, clientOptions, channelPrefix = redisChannelPrefixFromEnv()) {
16
+ export function createRedisToggleSyncSignal(redisUrl, clientOptions, channelPrefix = redisChannelPrefixFromEnv(), channelSubscribePattern = redisNamespaceFromEnv().channelSubscribePattern) {
17
17
  const toggleSyncChannel = `${channelPrefix}${TOGGLE_SYNC_CHANNEL_NAME}`;
18
18
  const signal = createRedisPubSubSignal({
19
19
  redisUrl,
20
- channelPattern: toggleSyncChannel,
20
+ channelPattern: channelSubscribePattern ?? toggleSyncChannel,
21
21
  label: "feature-toggles",
22
22
  ...(clientOptions ? { clientOptions } : {}),
23
23
  });
@@ -26,7 +26,10 @@ export function createRedisToggleSyncSignal(redisUrl, clientOptions, channelPref
26
26
  signal.publish(toggleSyncChannel, { featureName, enabled });
27
27
  },
28
28
  onMessage(listener) {
29
- signal.onMessage((_channel, payload) => {
29
+ signal.onMessage((channel, payload) => {
30
+ // skip: a wide subscribe pattern also delivers other features' channels
31
+ if (channel !== toggleSyncChannel)
32
+ return;
30
33
  if (!isTogglePayload(payload)) {
31
34
  // biome-ignore lint/suspicious/noConsole: ops-visible fallback — no ctx.obs.log in a raw PubSubSignal handler.
32
35
  console.error(`[kumiko:feature-toggles] dropping malformed cache-sync message on "${toggleSyncChannel}"`);
@@ -34,7 +34,7 @@ function registerFormDraft(r) {
34
34
  r.config({ keys: { retentionDays: formDraftRetentionDaysConfig } });
35
35
  r.job({
36
36
  name: "cleanup",
37
- trigger: { cron: "0 3 * * *" },
37
+ trigger: { maintenance: true },
38
38
  concurrency: "skip",
39
39
  escapeHatch: { reason: CLEANUP_ESCAPE_HATCH_REASON },
40
40
  handler: (payload, ctx) => cleanupDraftsJob(payload, ctx, ctx.db.unsafeRaw()),
@@ -10,7 +10,7 @@ export function registerInboundMailRetentionJob(r) {
10
10
  const INBOUND_MAIL_RETENTION_REASON = "inbound-mail retention runs raw deletes; every statement filters by this run's tenantId";
11
11
  r.job({
12
12
  name: "inbound-mail-retention",
13
- trigger: { cron: "0 3 * * *" },
13
+ trigger: { maintenance: true },
14
14
  perTenant: true,
15
15
  concurrency: "skip",
16
16
  escapeHatch: { reason: INBOUND_MAIL_RETENTION_REASON },
@@ -65,13 +65,12 @@ export function createJobsFeature(options = {}) {
65
65
  }, reindexEntityJob);
66
66
  // store_job_runs/store_job_run_logs are direct-write, unbounded-growth
67
67
  // stores — nothing else purges them.
68
- r.job("retention-cleanup", { trigger: { cron: "0 3 * * *" }, concurrency: "skip" }, createRetentionCleanupJob(retentionDays));
68
+ r.job("retention-cleanup", { trigger: { maintenance: true }, concurrency: "skip" }, createRetentionCleanupJob(retentionDays));
69
69
  // Sweeps store_job_runs for rows stuck at status "running" past the
70
- // timeout. Hourly matches the timeout's own granularity —
71
- // frequent enough to catch a stuck run soon after it crosses the
72
- // threshold, cheap enough that an (almost always empty) result set
73
- // doesn't matter.
74
- r.job("stale-run-sweep", { trigger: { cron: "0 * * * *" }, concurrency: "skip" }, createStaleRunSweepJob(staleRunTimeoutHours, retentionDays));
70
+ // timeout. Runs in the maintenance window: the timeout is
71
+ // hours, so a daily pass finds a stuck run at most a day late and a sleeping
72
+ // app is not woken every hour for it.
73
+ r.job("stale-run-sweep", { trigger: { maintenance: true }, concurrency: "skip" }, createStaleRunSweepJob(staleRunTimeoutHours, retentionDays));
75
74
  const handlers = {
76
75
  trigger: r.writeHandler(triggerWrite),
77
76
  retry: r.writeHandler(retryWrite),
@@ -416,13 +416,13 @@ export function createUserDataRightsFeature(opts = {}) {
416
416
  r.metric(EXPORT_CLEANUP_LAST_RUN_TIMESTAMP_METRIC, {
417
417
  type: "gauge",
418
418
  unit: "seconds",
419
- description: "Unix timestamp of the last export storage-cleanup pass that completed. Alert on time() minus this value exceeding the cron interval plus a buffer; the backlog-age gauge alone stays at its last value when the cron stops.",
419
+ description: "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.",
420
420
  });
421
421
  // Worker for the asynchronous export pipeline, triggered by a cron job.
422
422
  const RUN_EXPORT_JOBS_REASON = "processes pending export jobs across every tenant";
423
423
  r.job({
424
424
  name: "run-export-jobs",
425
- trigger: { cron: "0 3 * * *" },
425
+ trigger: { maintenance: true },
426
426
  concurrency: "skip",
427
427
  escapeHatch: { reason: RUN_EXPORT_JOBS_REASON },
428
428
  handler: async (_payload, ctx) => {
@@ -509,7 +509,7 @@ export function createUserDataRightsFeature(opts = {}) {
509
509
  const RUN_FORGET_CLEANUP_REASON = "executes overdue Art.17 forget requests across every tenant";
510
510
  r.job({
511
511
  name: "run-forget-cleanup",
512
- trigger: { cron: "0 3 * * *" },
512
+ trigger: { maintenance: true },
513
513
  concurrency: "skip",
514
514
  escapeHatch: { reason: RUN_FORGET_CLEANUP_REASON },
515
515
  handler: async (_payload, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.403.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>",
@@ -570,9 +570,9 @@
570
570
  }
571
571
  },
572
572
  "dependencies": {
573
- "@cosmicdrift/kumiko-framework": "0.403.0",
574
- "@cosmicdrift/kumiko-renderer": "0.403.0",
575
- "@cosmicdrift/kumiko-types": "0.403.0",
573
+ "@cosmicdrift/kumiko-framework": "0.404.0",
574
+ "@cosmicdrift/kumiko-renderer": "0.404.0",
575
+ "@cosmicdrift/kumiko-types": "0.404.0",
576
576
  "@mollie/api-client": "^4.5.0",
577
577
  "@node-rs/argon2": "^2.0.2",
578
578
  "@tiptap/core": "^3.31.3",
@@ -1166,17 +1166,17 @@
1166
1166
  ],
1167
1167
  "devDependencies": {
1168
1168
  "@testing-library/user-event": "^14.6.8",
1169
- "@cosmicdrift/kumiko-locale-de": "0.403.0",
1170
- "@cosmicdrift/kumiko-locale-es": "0.403.0",
1171
- "@cosmicdrift/kumiko-locale-fr": "0.403.0",
1172
- "@cosmicdrift/kumiko-locale-it": "0.403.0",
1173
- "@cosmicdrift/kumiko-renderer-mail-html": "0.403.0",
1169
+ "@cosmicdrift/kumiko-locale-de": "0.404.0",
1170
+ "@cosmicdrift/kumiko-locale-es": "0.404.0",
1171
+ "@cosmicdrift/kumiko-locale-fr": "0.404.0",
1172
+ "@cosmicdrift/kumiko-locale-it": "0.404.0",
1173
+ "@cosmicdrift/kumiko-renderer-mail-html": "0.404.0",
1174
1174
  "jsqr": "^1.4.0",
1175
1175
  "unpdf": "^1.8.1",
1176
- "@cosmicdrift/kumiko-testing": "0.403.0"
1176
+ "@cosmicdrift/kumiko-testing": "0.404.0"
1177
1177
  },
1178
1178
  "peerDependencies": {
1179
- "@cosmicdrift/kumiko-testing": "0.403.0"
1179
+ "@cosmicdrift/kumiko-testing": "0.404.0"
1180
1180
  },
1181
1181
  "peerDependenciesMeta": {
1182
1182
  "@cosmicdrift/kumiko-testing": {
@@ -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",
@@ -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",