@takosjp/yurucommu-core 3.4.0 → 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/migrations/0022_inbound_dispatch_claims.sql +17 -0
- package/package.json +3 -2
- package/packages/api/package.json +1 -1
- package/packages/api/src/lib/api/notifications.ts +1 -0
- package/packages/api/src/lib/api/posts.ts +1 -0
- package/src/backend/index.ts +62 -12
- package/src/backend/lib/delivery/queue-batching.ts +53 -36
- package/src/backend/lib/delivery/queue-delivery.ts +3 -3
- package/src/backend/lib/delivery/queue.ts +13 -9
- package/src/backend/lib/delivery/types.ts +12 -0
- package/src/backend/lib/notification-push.ts +2 -2
- package/src/backend/lib/oauth-providers.ts +9 -0
- package/src/backend/lib/strip-image-metadata.ts +50 -30
- package/src/backend/middleware/bearer-auth.ts +24 -9
- package/src/backend/public.ts +38 -1
- package/src/backend/retention.ts +78 -0
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +4 -3
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +204 -5
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +78 -53
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +66 -2
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +35 -12
- package/src/backend/routes/activitypub/inbox-addressing.ts +236 -0
- package/src/backend/routes/activitypub/inbox-types.ts +8 -0
- package/src/backend/routes/activitypub/inbox.ts +410 -205
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/actors.ts +5 -5
- package/src/backend/routes/auth.ts +2 -1
- package/src/backend/routes/posts/post-helpers.ts +42 -23
- package/src/backend/routes/stories/routes.ts +5 -7
- package/src/backend/runtime/cloudflare.ts +63 -2
- package/src/backend/runtime/managed-relational.ts +197 -0
- package/src/backend/runtime/managed-runtime.ts +631 -0
- package/src/backend/runtime/queue.ts +40 -0
- package/src/backend/server.ts +15 -18
- package/src/backend/types.ts +9 -2
- package/src/db/d1-write.ts +270 -0
- package/src/db/index.ts +17 -0
- package/src/db/schema/federation.ts +19 -0
- package/src/db/schema/index.ts +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- Fence concurrent/retried ActivityPub inbox dispatches.
|
|
2
|
+
--
|
|
3
|
+
-- `activities.processed = 0` says an activity is retryable, but it does not say
|
|
4
|
+
-- whether another Worker is actively dispatching it. Keeping the lease in a
|
|
5
|
+
-- separate table preserves the public activities ledger shape and lets a
|
|
6
|
+
-- crashed owner expire without allowing an old owner to commit over a newer
|
|
7
|
+
-- retry.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS inbound_activity_claims (
|
|
10
|
+
activity_ap_id TEXT PRIMARY KEY,
|
|
11
|
+
processing_token TEXT,
|
|
12
|
+
lease_expires_at TEXT,
|
|
13
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE INDEX IF NOT EXISTS inbound_activity_claims_lease_idx
|
|
17
|
+
ON inbound_activity_claims(lease_expires_at);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takosjp/yurucommu-core",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.3",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"start": "bun src/backend/server.ts",
|
|
60
60
|
"dev": "bun src/backend/server.ts",
|
|
61
61
|
"dev:server": "bun src/backend/server.ts",
|
|
62
|
-
"check": "tsc --noEmit && bun run check:no-opentofu-artifacts",
|
|
62
|
+
"check": "bun run fmt:check && tsc --noEmit && bun run check:no-opentofu-artifacts && bun run test",
|
|
63
63
|
"check:no-opentofu-artifacts": "bun scripts/check-no-opentofu-artifacts.mjs",
|
|
64
64
|
"test": "bun run build:api && bun test test/ src/backend/ packages/api/src/ scripts/check-publish-version-discipline.test.ts scripts/publish-package-resumable.test.ts && bun run check:release-contents",
|
|
65
65
|
"test:backend": "bun test src/backend/",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"dependencies": {
|
|
83
83
|
"@aws-sdk/client-s3": "^3.971.0",
|
|
84
84
|
"@libsql/client": "^0.17.0",
|
|
85
|
+
"@takosjp/takosumi-contract": "^1.0.0",
|
|
85
86
|
"better-sqlite3": "^12.4.1",
|
|
86
87
|
"drizzle-orm": "^0.45.1",
|
|
87
88
|
"fdir": "^6.5.0",
|
|
@@ -39,6 +39,7 @@ export async function fetchNotifications(options?: {
|
|
|
39
39
|
|
|
40
40
|
export async function fetchUnreadCount(): Promise<number> {
|
|
41
41
|
const res = await apiFetch("/api/notifications/unread/count");
|
|
42
|
+
await assertOk(res, "Failed to load unread notification count");
|
|
42
43
|
const data = (await res.json()) as { count?: number };
|
|
43
44
|
return data.count || 0;
|
|
44
45
|
}
|
|
@@ -169,6 +169,7 @@ export async function fetchBookmarks(options?: {
|
|
|
169
169
|
if (options?.before) params.set("before", options.before);
|
|
170
170
|
const query = params.toString() ? `?${params}` : "";
|
|
171
171
|
const res = await apiFetch(`/api/bookmarks${query}`);
|
|
172
|
+
await assertOk(res, "Failed to load bookmarks");
|
|
172
173
|
const data = (await res.json()) as PostListResponse & {
|
|
173
174
|
next_cursor?: string | null;
|
|
174
175
|
has_more?: boolean;
|
package/src/backend/index.ts
CHANGED
|
@@ -4,8 +4,12 @@ import { NOTIFICATION_PUSHER_REGISTRATION_PATH } from "./lib/notification-pusher
|
|
|
4
4
|
import type { Env, EnvVars, Variables } from "./types.ts";
|
|
5
5
|
import { extractActorFromSession } from "./lib/session-actor.ts";
|
|
6
6
|
import { isBackendPath } from "./lib/backend-paths.ts";
|
|
7
|
-
import { wrapCloudflareBindings } from "./runtime/cloudflare.ts";
|
|
8
7
|
import {
|
|
8
|
+
wrapCloudflareBindings,
|
|
9
|
+
wrapCloudflareMessageBatch,
|
|
10
|
+
} from "./runtime/cloudflare.ts";
|
|
11
|
+
import {
|
|
12
|
+
getMobileOidcAudience,
|
|
9
13
|
getOidcClientCredentials,
|
|
10
14
|
getOidcIssuerUrl,
|
|
11
15
|
} from "./lib/oauth-providers.ts";
|
|
@@ -44,7 +48,16 @@ import { logger } from "./lib/logger.ts";
|
|
|
44
48
|
|
|
45
49
|
const log = logger.child({ component: "backend.index" });
|
|
46
50
|
let lastNotificationPushRecoverySweep = 0;
|
|
47
|
-
import type {
|
|
51
|
+
import type { IQueueBatch } from "./runtime/queue.ts";
|
|
52
|
+
import type {
|
|
53
|
+
D1Database,
|
|
54
|
+
Fetcher,
|
|
55
|
+
KVNamespace,
|
|
56
|
+
MessageBatch,
|
|
57
|
+
Queue,
|
|
58
|
+
R2Bucket,
|
|
59
|
+
ScheduledController,
|
|
60
|
+
} from "@cloudflare/workers-types";
|
|
48
61
|
import type {
|
|
49
62
|
DeliveryDlqMessageV1,
|
|
50
63
|
DeliveryQueueMessageV1,
|
|
@@ -54,6 +67,7 @@ import {
|
|
|
54
67
|
handleDeliveryQueueBatch,
|
|
55
68
|
} from "./lib/delivery/queue.ts";
|
|
56
69
|
import { enqueuePendingNotificationPushJobs } from "./lib/notification-push.ts";
|
|
70
|
+
import { runYurucommuRetention } from "./retention.ts";
|
|
57
71
|
|
|
58
72
|
type YurucommuApp = Hono<{ Bindings: Env; Variables: Variables }>;
|
|
59
73
|
|
|
@@ -74,6 +88,14 @@ export interface YurucommuBackendPluginV1 {
|
|
|
74
88
|
export interface CreateYurucommuBackendAppOptionsV1 {
|
|
75
89
|
plugins?: YurucommuBackendPluginV1[];
|
|
76
90
|
discovery?: YurucommuBackendDiscoveryOptionsV1;
|
|
91
|
+
/**
|
|
92
|
+
* Browser capture capabilities required by this product shell. Both default
|
|
93
|
+
* to denied; a product must opt in to each capability it actually uses.
|
|
94
|
+
*/
|
|
95
|
+
browserMedia?: {
|
|
96
|
+
camera?: boolean;
|
|
97
|
+
microphone?: boolean;
|
|
98
|
+
};
|
|
77
99
|
}
|
|
78
100
|
|
|
79
101
|
export interface YurucommuBackendDiscoveryClientV1 {
|
|
@@ -343,7 +365,7 @@ function mountReadinessRoutes(
|
|
|
343
365
|
) => {
|
|
344
366
|
const appUrl = normalizeOrigin(c.env.APP_URL, c.req.url);
|
|
345
367
|
const issuer = getOidcIssuerUrl(c.env) ?? appUrl;
|
|
346
|
-
const
|
|
368
|
+
const clientId = getMobileOidcAudience(c.env);
|
|
347
369
|
return c.json(
|
|
348
370
|
buildSocialServerDiscovery(appUrl, issuer, discovery, {
|
|
349
371
|
oidcClientId: getOidcIssuerUrl(c.env)
|
|
@@ -504,7 +526,12 @@ function applyBodyLimits(app: YurucommuApp): void {
|
|
|
504
526
|
});
|
|
505
527
|
}
|
|
506
528
|
|
|
507
|
-
function applyGlobalMiddleware(
|
|
529
|
+
function applyGlobalMiddleware(
|
|
530
|
+
app: YurucommuApp,
|
|
531
|
+
browserMedia: NonNullable<
|
|
532
|
+
CreateYurucommuBackendAppOptionsV1["browserMedia"]
|
|
533
|
+
> = {},
|
|
534
|
+
): void {
|
|
508
535
|
app.onError(createErrorMiddleware());
|
|
509
536
|
|
|
510
537
|
app.use("*", async (c, next) => {
|
|
@@ -562,11 +589,14 @@ function applyGlobalMiddleware(app: YurucommuApp): void {
|
|
|
562
589
|
setSecurityHeader("X-Content-Type-Options", "nosniff");
|
|
563
590
|
setSecurityHeader("X-Frame-Options", "DENY");
|
|
564
591
|
setSecurityHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
565
|
-
//
|
|
566
|
-
//
|
|
592
|
+
// Browser capture is product-specific and denied by default. Even when a
|
|
593
|
+
// capability is enabled, only this origin may use it; cross-origin frames
|
|
594
|
+
// and geolocation remain denied.
|
|
595
|
+
const camera = browserMedia.camera === true ? "(self)" : "()";
|
|
596
|
+
const microphone = browserMedia.microphone === true ? "(self)" : "()";
|
|
567
597
|
setSecurityHeader(
|
|
568
598
|
"Permissions-Policy",
|
|
569
|
-
|
|
599
|
+
`camera=${camera}, microphone=${microphone}, geolocation=()`,
|
|
570
600
|
);
|
|
571
601
|
// HSTS: once a client has reached this host over HTTPS, keep it on HTTPS
|
|
572
602
|
// (defeats SSL-strip / downgrade). Sent unconditionally — browsers ignore it
|
|
@@ -901,7 +931,7 @@ export function createYurucommuBackendApp(
|
|
|
901
931
|
// /healthz and /readyz stay reachable even when payload validation
|
|
902
932
|
// misbehaves.
|
|
903
933
|
applyBodyLimits(app);
|
|
904
|
-
applyGlobalMiddleware(app);
|
|
934
|
+
applyGlobalMiddleware(app, options.browserMedia);
|
|
905
935
|
for (const plugin of plugins) {
|
|
906
936
|
plugin.setup?.(pluginContext);
|
|
907
937
|
}
|
|
@@ -923,7 +953,7 @@ const app = createYurucommuBackendApp();
|
|
|
923
953
|
export const backendApp = app;
|
|
924
954
|
|
|
925
955
|
export async function handleYurucommuQueueBatch(
|
|
926
|
-
batch:
|
|
956
|
+
batch: IQueueBatch<DeliveryQueueMessageV1 | DeliveryDlqMessageV1>,
|
|
927
957
|
env: Env,
|
|
928
958
|
): Promise<void> {
|
|
929
959
|
const deliveryQueueName = env.DELIVERY_QUEUE_NAME ?? "yurucommu-delivery";
|
|
@@ -931,13 +961,13 @@ export async function handleYurucommuQueueBatch(
|
|
|
931
961
|
|
|
932
962
|
if (batch.queue === deliveryQueueName) {
|
|
933
963
|
return handleDeliveryQueueBatch(
|
|
934
|
-
batch as
|
|
964
|
+
batch as IQueueBatch<DeliveryQueueMessageV1>,
|
|
935
965
|
env,
|
|
936
966
|
);
|
|
937
967
|
}
|
|
938
968
|
if (batch.queue === deliveryDlqName) {
|
|
939
969
|
return handleDeliveryDlqBatch(
|
|
940
|
-
batch as
|
|
970
|
+
batch as IQueueBatch<DeliveryDlqMessageV1>,
|
|
941
971
|
env,
|
|
942
972
|
);
|
|
943
973
|
}
|
|
@@ -966,6 +996,12 @@ type WorkerBindings = EnvVars & {
|
|
|
966
996
|
REALTIME_STREAM?: DurableObjectNamespace;
|
|
967
997
|
};
|
|
968
998
|
|
|
999
|
+
function isMaterializedRuntimeEnv(
|
|
1000
|
+
bindings: WorkerBindings | Env,
|
|
1001
|
+
): bindings is Env {
|
|
1002
|
+
return "DB_INSTANCE" in bindings && !!bindings.DB_INSTANCE;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
969
1005
|
export default {
|
|
970
1006
|
async fetch(
|
|
971
1007
|
request: Request,
|
|
@@ -979,6 +1015,20 @@ export default {
|
|
|
979
1015
|
batch: MessageBatch<DeliveryQueueMessageV1 | DeliveryDlqMessageV1>,
|
|
980
1016
|
bindings: WorkerBindings,
|
|
981
1017
|
): Promise<void> {
|
|
982
|
-
return handleYurucommuQueueBatch(
|
|
1018
|
+
return handleYurucommuQueueBatch(
|
|
1019
|
+
wrapCloudflareMessageBatch(batch),
|
|
1020
|
+
wrapCloudflareBindings(bindings),
|
|
1021
|
+
);
|
|
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);
|
|
983
1033
|
},
|
|
984
1034
|
};
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* and batch dispatch of delivery messages.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Message, MessageBatch } from "@cloudflare/workers-types";
|
|
7
6
|
import type { Env } from "../../types.ts";
|
|
7
|
+
import type { IQueueMessage } from "../../runtime/queue.ts";
|
|
8
8
|
import type { Database } from "../../../db/index.ts";
|
|
9
9
|
import { and, eq, or, sql } from "drizzle-orm";
|
|
10
10
|
import {
|
|
@@ -117,12 +117,17 @@ export async function enqueueFollowerEndpointDeliveries(
|
|
|
117
117
|
baseUrl: string,
|
|
118
118
|
activityId: string,
|
|
119
119
|
followeeApId: string,
|
|
120
|
-
|
|
120
|
+
startCursor: string | null = null,
|
|
121
|
+
): Promise<{
|
|
122
|
+
processed: number;
|
|
123
|
+
capped: boolean;
|
|
124
|
+
nextCursor: string | null;
|
|
125
|
+
}> {
|
|
121
126
|
// Page through accepted followers with a keyset cursor instead of loading
|
|
122
127
|
// every row into memory at once. Each page is planned and dispatched in
|
|
123
128
|
// ≤100-message chunks before the next page is read, bounding both memory
|
|
124
129
|
// and per-call batch size.
|
|
125
|
-
let cursor: string | null =
|
|
130
|
+
let cursor: string | null = startCursor;
|
|
126
131
|
let processed = 0;
|
|
127
132
|
let capped = false;
|
|
128
133
|
|
|
@@ -184,7 +189,7 @@ export async function enqueueFollowerEndpointDeliveries(
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
return { processed, capped };
|
|
192
|
+
return { processed, capped, nextCursor: capped ? cursor : null };
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
/**
|
|
@@ -215,21 +220,27 @@ export async function snapshotAndEnqueueFollowerDeliveries(
|
|
|
215
220
|
return;
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
let cursor: string | null = null;
|
|
224
|
+
let processed = 0;
|
|
225
|
+
do {
|
|
226
|
+
const page = await enqueueFollowerEndpointDeliveries(
|
|
227
|
+
db,
|
|
228
|
+
queue,
|
|
229
|
+
env.APP_URL,
|
|
230
|
+
activityId,
|
|
231
|
+
followeeApId,
|
|
232
|
+
cursor,
|
|
233
|
+
);
|
|
234
|
+
processed += page.processed;
|
|
235
|
+
cursor = page.nextCursor;
|
|
236
|
+
} while (cursor !== null);
|
|
225
237
|
|
|
226
|
-
if (
|
|
227
|
-
log.
|
|
228
|
-
event: "delivery.fanout.
|
|
238
|
+
if (processed > FANOUT_MAX_FOLLOWERS) {
|
|
239
|
+
log.info("Follower snapshot continued across bounded pages", {
|
|
240
|
+
event: "delivery.fanout.snapshot_continued",
|
|
229
241
|
followee: followeeApId,
|
|
230
242
|
activityId,
|
|
231
243
|
processed,
|
|
232
|
-
max: FANOUT_MAX_FOLLOWERS,
|
|
233
244
|
});
|
|
234
245
|
}
|
|
235
246
|
}
|
|
@@ -238,32 +249,38 @@ export async function processFanoutFollowers(
|
|
|
238
249
|
db: Database,
|
|
239
250
|
env: Env,
|
|
240
251
|
msg: DeliveryFanoutFollowersMessageV1,
|
|
241
|
-
message:
|
|
252
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
242
253
|
): Promise<void> {
|
|
243
254
|
if (!requireQueue(env, "fanout", message)) return;
|
|
244
255
|
const queueEnv = env as QueueEnv;
|
|
245
256
|
|
|
246
|
-
const { processed, capped } =
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
257
|
+
const { processed, capped, nextCursor } =
|
|
258
|
+
await enqueueFollowerEndpointDeliveries(
|
|
259
|
+
db,
|
|
260
|
+
queueEnv.DELIVERY_QUEUE,
|
|
261
|
+
env.APP_URL,
|
|
262
|
+
msg.activityId,
|
|
263
|
+
msg.followeeApId,
|
|
264
|
+
msg.cursor ?? null,
|
|
265
|
+
);
|
|
253
266
|
|
|
254
|
-
if (capped) {
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
if (capped && nextCursor !== null) {
|
|
268
|
+
// Send the continuation before ACK. If this send fails, Cloudflare retries
|
|
269
|
+
// the current message; deterministic endpoint job ids make that safe.
|
|
270
|
+
await sendQueueMessage(env, {
|
|
271
|
+
version: DELIVERY_QUEUE_MESSAGE_VERSION,
|
|
272
|
+
type: "fanout_followers",
|
|
273
|
+
activityId: msg.activityId,
|
|
274
|
+
followeeApId: msg.followeeApId,
|
|
275
|
+
cursor: nextCursor,
|
|
276
|
+
scheduledAt: nowIso(),
|
|
277
|
+
});
|
|
278
|
+
log.info("Follower fanout continued from stable cursor", {
|
|
279
|
+
event: "delivery.fanout.continued",
|
|
263
280
|
followee: msg.followeeApId,
|
|
264
281
|
activityId: msg.activityId,
|
|
265
282
|
processed,
|
|
266
|
-
|
|
283
|
+
cursor: nextCursor,
|
|
267
284
|
});
|
|
268
285
|
}
|
|
269
286
|
|
|
@@ -288,7 +305,7 @@ export async function processFanoutCommunity(
|
|
|
288
305
|
db: Database,
|
|
289
306
|
env: Env,
|
|
290
307
|
msg: DeliveryFanoutCommunityMessageV1,
|
|
291
|
-
message:
|
|
308
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
292
309
|
): Promise<void> {
|
|
293
310
|
const baseUrl = env.APP_URL;
|
|
294
311
|
|
|
@@ -456,7 +473,7 @@ export async function processResolveActor(
|
|
|
456
473
|
db: Database,
|
|
457
474
|
env: Env,
|
|
458
475
|
msg: DeliveryResolveActorMessageV1,
|
|
459
|
-
message:
|
|
476
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
460
477
|
): Promise<void> {
|
|
461
478
|
if (!requireQueue(env, "resolve_actor", message)) return;
|
|
462
479
|
|
|
@@ -554,7 +571,7 @@ export async function processReconcileJob(
|
|
|
554
571
|
db: Database,
|
|
555
572
|
env: Env,
|
|
556
573
|
msg: DeliveryReconcileJobMessageV1,
|
|
557
|
-
message:
|
|
574
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
558
575
|
): Promise<void> {
|
|
559
576
|
if (!requireQueue(env, "reconcile", message)) return;
|
|
560
577
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Includes signing, circuit breaker checks, retry logic, and dead-letter handling.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { IQueueMessage } from "../../runtime/queue.ts";
|
|
7
7
|
import type { Env } from "../../types.ts";
|
|
8
8
|
import type { Database } from "../../../db/index.ts";
|
|
9
9
|
import { and, eq, lt, notInArray, or, sql } from "drizzle-orm";
|
|
@@ -186,7 +186,7 @@ async function failJob(
|
|
|
186
186
|
db: Database,
|
|
187
187
|
jobId: string,
|
|
188
188
|
error: string,
|
|
189
|
-
message:
|
|
189
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
190
190
|
): Promise<void> {
|
|
191
191
|
await db
|
|
192
192
|
.update(deliveryQueue)
|
|
@@ -286,7 +286,7 @@ export async function processDeliverEndpoint(
|
|
|
286
286
|
db: Database,
|
|
287
287
|
env: Env,
|
|
288
288
|
msg: DeliveryDeliverEndpointMessageV1,
|
|
289
|
-
message:
|
|
289
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
290
290
|
bulkhead: Bulkhead,
|
|
291
291
|
): Promise<void> {
|
|
292
292
|
if (!requireQueue(env, "deliver_endpoint", message)) return;
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
* and the batch handler that dispatches to sub-modules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Message, MessageBatch, Queue } from "@cloudflare/workers-types";
|
|
7
6
|
import type { Env } from "../../types.ts";
|
|
7
|
+
import type {
|
|
8
|
+
IQueueBatch,
|
|
9
|
+
IQueueMessage,
|
|
10
|
+
IQueueProducer,
|
|
11
|
+
} from "../../runtime/queue.ts";
|
|
8
12
|
import type { Database } from "../../../db/index.ts";
|
|
9
13
|
import { and, eq, notInArray, or, sql } from "drizzle-orm";
|
|
10
14
|
import { actorCache, deliveryQueue } from "../../../db/index.ts";
|
|
@@ -119,8 +123,8 @@ export function nowIso(): string {
|
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
export type QueueEnv = Env & {
|
|
122
|
-
DELIVERY_QUEUE:
|
|
123
|
-
DELIVERY_DLQ:
|
|
126
|
+
DELIVERY_QUEUE: IQueueProducer<DeliveryQueueMessageV1>;
|
|
127
|
+
DELIVERY_DLQ: IQueueProducer<DeliveryDlqMessageV1>;
|
|
124
128
|
};
|
|
125
129
|
|
|
126
130
|
function queueAvailable(env: Env): env is QueueEnv {
|
|
@@ -130,7 +134,7 @@ function queueAvailable(env: Env): env is QueueEnv {
|
|
|
130
134
|
export function requireQueue(
|
|
131
135
|
env: Env,
|
|
132
136
|
label: string,
|
|
133
|
-
message:
|
|
137
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
134
138
|
): env is QueueEnv {
|
|
135
139
|
if (queueAvailable(env)) return true;
|
|
136
140
|
log.warn("Missing DELIVERY_QUEUE/DELIVERY_DLQ bindings; dropping job", {
|
|
@@ -426,7 +430,7 @@ export async function enqueueFanoutToCommunity(
|
|
|
426
430
|
// ---------------------------------------------------------------------------
|
|
427
431
|
|
|
428
432
|
export async function handleDeliveryQueueBatch(
|
|
429
|
-
batch:
|
|
433
|
+
batch: IQueueBatch<DeliveryQueueMessageV1>,
|
|
430
434
|
env: Env,
|
|
431
435
|
): Promise<void> {
|
|
432
436
|
const db = env.DB_INSTANCE;
|
|
@@ -494,13 +498,13 @@ export async function handleDeliveryQueueBatch(
|
|
|
494
498
|
|
|
495
499
|
// Deliver endpoint messages with bulkhead+concurrency.
|
|
496
500
|
const deliveryMessages = batch.messages.filter(
|
|
497
|
-
(m:
|
|
501
|
+
(m: IQueueMessage<DeliveryQueueMessageV1>) =>
|
|
498
502
|
isDeliveryQueueMessageV1(m.body) && m.body.type === "deliver_endpoint",
|
|
499
|
-
) as Array<
|
|
503
|
+
) as Array<IQueueMessage<DeliveryQueueMessageV1>>;
|
|
500
504
|
await runWithConcurrency(
|
|
501
505
|
deliveryMessages,
|
|
502
506
|
BULKHEAD_GLOBAL_CONCURRENCY,
|
|
503
|
-
async (m:
|
|
507
|
+
async (m: IQueueMessage<DeliveryQueueMessageV1>) => {
|
|
504
508
|
try {
|
|
505
509
|
await processDeliverEndpoint(
|
|
506
510
|
db,
|
|
@@ -538,7 +542,7 @@ export async function handleDeliveryQueueBatch(
|
|
|
538
542
|
}
|
|
539
543
|
|
|
540
544
|
export async function handleDeliveryDlqBatch(
|
|
541
|
-
batch:
|
|
545
|
+
batch: IQueueBatch<DeliveryDlqMessageV1>,
|
|
542
546
|
env: Env,
|
|
543
547
|
): Promise<void> {
|
|
544
548
|
for (const message of batch.messages) {
|
|
@@ -5,6 +5,8 @@ export type DeliveryFanoutFollowersMessageV1 = {
|
|
|
5
5
|
type: "fanout_followers";
|
|
6
6
|
activityId: string;
|
|
7
7
|
followeeApId: string;
|
|
8
|
+
/** Stable keyset cursor used when a large graph spans queue invocations. */
|
|
9
|
+
cursor?: string;
|
|
8
10
|
scheduledAt: string; // ISO8601 UTC
|
|
9
11
|
};
|
|
10
12
|
|
|
@@ -24,6 +26,10 @@ export type DeliveryFanoutCommunityMessageV1 = {
|
|
|
24
26
|
// receive `activityId`. Absent for non-Create activities (edit/delete relay
|
|
25
27
|
// the activity directly).
|
|
26
28
|
announceActivityId?: string;
|
|
29
|
+
/** Current bounded fan-out phase; absent means start with local members. */
|
|
30
|
+
stage?: "local_members" | "remote_members" | "remote_followers";
|
|
31
|
+
/** Stable actor-id keyset cursor within `stage`. */
|
|
32
|
+
cursor?: string;
|
|
27
33
|
scheduledAt: string; // ISO8601 UTC
|
|
28
34
|
};
|
|
29
35
|
|
|
@@ -106,12 +112,18 @@ export function isDeliveryQueueMessageV1(
|
|
|
106
112
|
return (
|
|
107
113
|
typeof v.activityId === "string" &&
|
|
108
114
|
typeof v.followeeApId === "string" &&
|
|
115
|
+
(v.cursor === undefined || typeof v.cursor === "string") &&
|
|
109
116
|
typeof v.scheduledAt === "string"
|
|
110
117
|
);
|
|
111
118
|
case "fanout_community":
|
|
112
119
|
return (
|
|
113
120
|
typeof v.activityId === "string" &&
|
|
114
121
|
typeof v.communityApId === "string" &&
|
|
122
|
+
(v.stage === undefined ||
|
|
123
|
+
v.stage === "local_members" ||
|
|
124
|
+
v.stage === "remote_members" ||
|
|
125
|
+
v.stage === "remote_followers") &&
|
|
126
|
+
(v.cursor === undefined || typeof v.cursor === "string") &&
|
|
115
127
|
typeof v.scheduledAt === "string"
|
|
116
128
|
);
|
|
117
129
|
case "resolve_actor":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IQueueMessage } from "../runtime/queue.ts";
|
|
2
2
|
import { and, asc, eq, exists, inArray, lte, ne, sql } from "drizzle-orm";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -501,7 +501,7 @@ export async function purgeExpiredNotificationPushJobs(
|
|
|
501
501
|
export async function processNotificationPushJob(
|
|
502
502
|
env: Env,
|
|
503
503
|
body: DeliveryNotificationPushMessageV1,
|
|
504
|
-
message:
|
|
504
|
+
message: IQueueMessage<DeliveryQueueMessageV1>,
|
|
505
505
|
): Promise<void> {
|
|
506
506
|
const db = env.DB_INSTANCE;
|
|
507
507
|
let job = await db
|
|
@@ -61,6 +61,15 @@ export function getOidcClientCredentials(env: Env): {
|
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Audience advertised to native clients and accepted by the host exchange.
|
|
66
|
+
* A Capsule has one public OIDC client with multiple exact redirect URIs; web
|
|
67
|
+
* and native clients must therefore converge on this same client id.
|
|
68
|
+
*/
|
|
69
|
+
export function getMobileOidcAudience(env: Env): string {
|
|
70
|
+
return getOidcClientCredentials(env).clientId;
|
|
71
|
+
}
|
|
72
|
+
|
|
64
73
|
export function issuerEndpoint(issuer: string, path: string): string {
|
|
65
74
|
return `${issuer.replace(/\/$/, "")}${path}`;
|
|
66
75
|
}
|