@takosjp/yurucommu-core 3.4.1 → 3.4.3

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": "@takosjp/yurucommu-core",
3
- "version": "3.4.1",
3
+ "version": "3.4.3",
4
4
  "license": "AGPL-3.0-only",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takosjp/yurucommu-api",
3
- "version": "3.4.0",
3
+ "version": "3.4.3",
4
4
  "description": "Typed client SDK and public API contract for yurucommu-server clients.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -56,6 +56,7 @@ import type {
56
56
  MessageBatch,
57
57
  Queue,
58
58
  R2Bucket,
59
+ ScheduledController,
59
60
  } from "@cloudflare/workers-types";
60
61
  import type {
61
62
  DeliveryDlqMessageV1,
@@ -66,6 +67,7 @@ import {
66
67
  handleDeliveryQueueBatch,
67
68
  } from "./lib/delivery/queue.ts";
68
69
  import { enqueuePendingNotificationPushJobs } from "./lib/notification-push.ts";
70
+ import { runYurucommuRetention } from "./retention.ts";
69
71
 
70
72
  type YurucommuApp = Hono<{ Bindings: Env; Variables: Variables }>;
71
73
 
@@ -994,6 +996,12 @@ type WorkerBindings = EnvVars & {
994
996
  REALTIME_STREAM?: DurableObjectNamespace;
995
997
  };
996
998
 
999
+ function isMaterializedRuntimeEnv(
1000
+ bindings: WorkerBindings | Env,
1001
+ ): bindings is Env {
1002
+ return "DB_INSTANCE" in bindings && !!bindings.DB_INSTANCE;
1003
+ }
1004
+
997
1005
  export default {
998
1006
  async fetch(
999
1007
  request: Request,
@@ -1012,4 +1020,15 @@ export default {
1012
1020
  wrapCloudflareBindings(bindings),
1013
1021
  );
1014
1022
  },
1023
+
1024
+ async scheduled(
1025
+ _controller: ScheduledController,
1026
+ bindings: WorkerBindings | Env,
1027
+ _ctx: ExecutionContext,
1028
+ ): Promise<void> {
1029
+ const env = isMaterializedRuntimeEnv(bindings)
1030
+ ? bindings
1031
+ : wrapCloudflareBindings(bindings);
1032
+ await runYurucommuRetention(env);
1033
+ },
1015
1034
  };
@@ -9,6 +9,12 @@ export {
9
9
  type YurucommuBackendDiscoveryOptionsV1,
10
10
  type YurucommuBackendPluginV1,
11
11
  } from "./index.ts";
12
+ export {
13
+ runYurucommuRetention,
14
+ YurucommuRetentionError,
15
+ type YurucommuRetentionResult,
16
+ type YurucommuRetentionStep,
17
+ } from "./retention.ts";
12
18
  export { default } from "./index.ts";
13
19
  export { default as app } from "./index.ts";
14
20
  export { type Database, getDb, getDbSQLite } from "../db/index.ts";
@@ -19,7 +25,10 @@ export {
19
25
  } from "./runtime/cloudflare.ts";
20
26
  export {
21
27
  ManagedRuntimeGatewayError,
28
+ createManagedRuntimeKeyValueStore,
29
+ createManagedRuntimeObjectStorage,
22
30
  createManagedRuntimeQueueProducer,
31
+ type ManagedRuntimeDataAdapterOptions,
23
32
  type ManagedRuntimeGateway,
24
33
  type ManagedRuntimeQueueProducerOptions,
25
34
  } from "./runtime/managed-runtime.ts";
@@ -27,6 +36,13 @@ export {
27
36
  createManagedRelationalDatabase,
28
37
  type ManagedRelationalDatabaseOptions,
29
38
  } from "./runtime/managed-relational.ts";
39
+ export type {
40
+ IKeyValueStore,
41
+ IObjectStorage,
42
+ ListObjectsResult,
43
+ ObjectMetadata,
44
+ StorageObject,
45
+ } from "./runtime/types.ts";
30
46
  export type {
31
47
  IQueueBatch,
32
48
  IQueueMessage,
@@ -0,0 +1,78 @@
1
+ import type { Env } from "./types.ts";
2
+ import { enqueuePendingNotificationPushJobs } from "./lib/notification-push.ts";
3
+ import { reapDrainedTombstones } from "./routes/actors.ts";
4
+ import { cleanupExpiredStories } from "./routes/stories/query-helpers.ts";
5
+
6
+ export type YurucommuRetentionStep =
7
+ "expired_stories" | "drained_tombstones" | "notification_push";
8
+
9
+ export interface YurucommuRetentionResult {
10
+ readonly expiredStories: number;
11
+ readonly reapedTombstones: number;
12
+ readonly enqueuedNotificationPushJobs: number;
13
+ }
14
+
15
+ /**
16
+ * Identifies the exact bounded retention step that failed. Scheduled callers
17
+ * must reject the invocation instead of treating a partial or skipped sweep as
18
+ * success; the original database/runtime error remains available as `cause`.
19
+ */
20
+ export class YurucommuRetentionError extends Error {
21
+ constructor(
22
+ readonly step: YurucommuRetentionStep,
23
+ cause: unknown,
24
+ ) {
25
+ super(`yurucommu retention failed at ${step}`, { cause });
26
+ this.name = "YurucommuRetentionError";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Run one bounded retention pass against the already-materialized runtime.
32
+ *
33
+ * This deliberately reuses the same race-safe cleanup paths as request/queue
34
+ * handling:
35
+ * - Story expiry uses the canonical object cascade and purges its media last.
36
+ * - Tombstones remain until every Delete delivery has drained.
37
+ * - Notification push performs bounded pusher/job retention, stale-job
38
+ * recovery, and enqueues due durable outbox rows when a queue is available.
39
+ *
40
+ * Steps are awaited sequentially because D1 is the shared authority. Any
41
+ * failure rejects with its exact step; callers must retry the cron invocation
42
+ * rather than silently acknowledging incomplete retention.
43
+ */
44
+ export async function runYurucommuRetention(
45
+ env: Env,
46
+ ): Promise<YurucommuRetentionResult> {
47
+ if (!env?.DB_INSTANCE) {
48
+ throw new TypeError("Yurucommu retention requires DB_INSTANCE");
49
+ }
50
+
51
+ const expiredStories = await retentionStep("expired_stories", () =>
52
+ cleanupExpiredStories(env.DB_INSTANCE, env.MEDIA),
53
+ );
54
+ const reapedTombstones = await retentionStep("drained_tombstones", () =>
55
+ reapDrainedTombstones(env.DB_INSTANCE),
56
+ );
57
+ const enqueuedNotificationPushJobs = await retentionStep(
58
+ "notification_push",
59
+ () => enqueuePendingNotificationPushJobs(env),
60
+ );
61
+
62
+ return {
63
+ expiredStories,
64
+ reapedTombstones,
65
+ enqueuedNotificationPushJobs,
66
+ };
67
+ }
68
+
69
+ async function retentionStep<T>(
70
+ step: YurucommuRetentionStep,
71
+ run: () => Promise<T>,
72
+ ): Promise<T> {
73
+ try {
74
+ return await run();
75
+ } catch (cause) {
76
+ throw new YurucommuRetentionError(step, cause);
77
+ }
78
+ }
@@ -286,11 +286,11 @@ export async function cancelTombstoneDelete(
286
286
  return deleteActivityIds.length;
287
287
  }
288
288
 
289
- // Best-effort, opportunistic tombstone reaping on the read path. This Worker
290
- // has no `scheduled` handler, so (mirroring maybeCleanupExpiredStories) the
291
- // sweep is triggered probabilistically and guarded so at most one runs per
292
- // isolate at a time. Tombstones are already excluded from every serving query,
293
- // so a missed sweep only delays storage/key-material reclamation.
289
+ // Best-effort, opportunistic tombstone reaping on the read path. The public
290
+ // Worker has a scheduled retention handler too; this remains as a fallback for
291
+ // self-hosted runtimes without cron. It is guarded so at most one pass runs per
292
+ // isolate. Tombstones are already excluded from every serving query, so a
293
+ // missed sweep only delays storage/key-material reclamation.
294
294
  let tombstoneReapInFlight = false;
295
295
 
296
296
  export function maybeReapDrainedTombstones(db: Database): void {
@@ -62,13 +62,11 @@ const stories = new Hono<{ Bindings: Env; Variables: Variables }>();
62
62
 
63
63
  // Best-effort, opportunistic retention of expired stories.
64
64
  //
65
- // This is NOT a substitute for a scheduled job: this Worker has no `scheduled`
66
- // handler / cron trigger, so expiry cleanup is triggered probabilistically on
67
- // the read path. Expired stories are already excluded from every read query
68
- // (the feed/single-story handlers filter on `endTime`), so the only impact of
69
- // a missed sweep is storage growth, not stale data leaking to users. The guard
70
- // below ensures at most one sweep runs at a time per isolate, so a burst of
71
- // feed requests cannot kick off several concurrent full-table delete sweeps.
65
+ // The public Worker also exposes a scheduled retention handler. Keep this
66
+ // probabilistic read-path pass as a self-hosting fallback for runtimes that do
67
+ // not configure a cron trigger. Expired stories are already excluded from every
68
+ // read query, so a missed sweep affects storage only. The guard below ensures
69
+ // at most one fallback pass runs at a time per isolate.
72
70
  let expiredStoryCleanupInFlight = false;
73
71
 
74
72
  function maybeCleanupExpiredStories(