@takosjp/yurucommu-core 3.4.0 → 3.4.1
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/src/lib/api/notifications.ts +1 -0
- package/packages/api/src/lib/api/posts.ts +1 -0
- package/src/backend/index.ts +43 -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 +22 -1
- 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/auth.ts +2 -1
- package/src/backend/routes/posts/post-helpers.ts +42 -23
- 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.1",
|
|
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,15 @@ 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
|
+
} from "@cloudflare/workers-types";
|
|
48
60
|
import type {
|
|
49
61
|
DeliveryDlqMessageV1,
|
|
50
62
|
DeliveryQueueMessageV1,
|
|
@@ -74,6 +86,14 @@ export interface YurucommuBackendPluginV1 {
|
|
|
74
86
|
export interface CreateYurucommuBackendAppOptionsV1 {
|
|
75
87
|
plugins?: YurucommuBackendPluginV1[];
|
|
76
88
|
discovery?: YurucommuBackendDiscoveryOptionsV1;
|
|
89
|
+
/**
|
|
90
|
+
* Browser capture capabilities required by this product shell. Both default
|
|
91
|
+
* to denied; a product must opt in to each capability it actually uses.
|
|
92
|
+
*/
|
|
93
|
+
browserMedia?: {
|
|
94
|
+
camera?: boolean;
|
|
95
|
+
microphone?: boolean;
|
|
96
|
+
};
|
|
77
97
|
}
|
|
78
98
|
|
|
79
99
|
export interface YurucommuBackendDiscoveryClientV1 {
|
|
@@ -343,7 +363,7 @@ function mountReadinessRoutes(
|
|
|
343
363
|
) => {
|
|
344
364
|
const appUrl = normalizeOrigin(c.env.APP_URL, c.req.url);
|
|
345
365
|
const issuer = getOidcIssuerUrl(c.env) ?? appUrl;
|
|
346
|
-
const
|
|
366
|
+
const clientId = getMobileOidcAudience(c.env);
|
|
347
367
|
return c.json(
|
|
348
368
|
buildSocialServerDiscovery(appUrl, issuer, discovery, {
|
|
349
369
|
oidcClientId: getOidcIssuerUrl(c.env)
|
|
@@ -504,7 +524,12 @@ function applyBodyLimits(app: YurucommuApp): void {
|
|
|
504
524
|
});
|
|
505
525
|
}
|
|
506
526
|
|
|
507
|
-
function applyGlobalMiddleware(
|
|
527
|
+
function applyGlobalMiddleware(
|
|
528
|
+
app: YurucommuApp,
|
|
529
|
+
browserMedia: NonNullable<
|
|
530
|
+
CreateYurucommuBackendAppOptionsV1["browserMedia"]
|
|
531
|
+
> = {},
|
|
532
|
+
): void {
|
|
508
533
|
app.onError(createErrorMiddleware());
|
|
509
534
|
|
|
510
535
|
app.use("*", async (c, next) => {
|
|
@@ -562,11 +587,14 @@ function applyGlobalMiddleware(app: YurucommuApp): void {
|
|
|
562
587
|
setSecurityHeader("X-Content-Type-Options", "nosniff");
|
|
563
588
|
setSecurityHeader("X-Frame-Options", "DENY");
|
|
564
589
|
setSecurityHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
565
|
-
//
|
|
566
|
-
//
|
|
590
|
+
// Browser capture is product-specific and denied by default. Even when a
|
|
591
|
+
// capability is enabled, only this origin may use it; cross-origin frames
|
|
592
|
+
// and geolocation remain denied.
|
|
593
|
+
const camera = browserMedia.camera === true ? "(self)" : "()";
|
|
594
|
+
const microphone = browserMedia.microphone === true ? "(self)" : "()";
|
|
567
595
|
setSecurityHeader(
|
|
568
596
|
"Permissions-Policy",
|
|
569
|
-
|
|
597
|
+
`camera=${camera}, microphone=${microphone}, geolocation=()`,
|
|
570
598
|
);
|
|
571
599
|
// HSTS: once a client has reached this host over HTTPS, keep it on HTTPS
|
|
572
600
|
// (defeats SSL-strip / downgrade). Sent unconditionally — browsers ignore it
|
|
@@ -901,7 +929,7 @@ export function createYurucommuBackendApp(
|
|
|
901
929
|
// /healthz and /readyz stay reachable even when payload validation
|
|
902
930
|
// misbehaves.
|
|
903
931
|
applyBodyLimits(app);
|
|
904
|
-
applyGlobalMiddleware(app);
|
|
932
|
+
applyGlobalMiddleware(app, options.browserMedia);
|
|
905
933
|
for (const plugin of plugins) {
|
|
906
934
|
plugin.setup?.(pluginContext);
|
|
907
935
|
}
|
|
@@ -923,7 +951,7 @@ const app = createYurucommuBackendApp();
|
|
|
923
951
|
export const backendApp = app;
|
|
924
952
|
|
|
925
953
|
export async function handleYurucommuQueueBatch(
|
|
926
|
-
batch:
|
|
954
|
+
batch: IQueueBatch<DeliveryQueueMessageV1 | DeliveryDlqMessageV1>,
|
|
927
955
|
env: Env,
|
|
928
956
|
): Promise<void> {
|
|
929
957
|
const deliveryQueueName = env.DELIVERY_QUEUE_NAME ?? "yurucommu-delivery";
|
|
@@ -931,13 +959,13 @@ export async function handleYurucommuQueueBatch(
|
|
|
931
959
|
|
|
932
960
|
if (batch.queue === deliveryQueueName) {
|
|
933
961
|
return handleDeliveryQueueBatch(
|
|
934
|
-
batch as
|
|
962
|
+
batch as IQueueBatch<DeliveryQueueMessageV1>,
|
|
935
963
|
env,
|
|
936
964
|
);
|
|
937
965
|
}
|
|
938
966
|
if (batch.queue === deliveryDlqName) {
|
|
939
967
|
return handleDeliveryDlqBatch(
|
|
940
|
-
batch as
|
|
968
|
+
batch as IQueueBatch<DeliveryDlqMessageV1>,
|
|
941
969
|
env,
|
|
942
970
|
);
|
|
943
971
|
}
|
|
@@ -979,6 +1007,9 @@ export default {
|
|
|
979
1007
|
batch: MessageBatch<DeliveryQueueMessageV1 | DeliveryDlqMessageV1>,
|
|
980
1008
|
bindings: WorkerBindings,
|
|
981
1009
|
): Promise<void> {
|
|
982
|
-
return handleYurucommuQueueBatch(
|
|
1010
|
+
return handleYurucommuQueueBatch(
|
|
1011
|
+
wrapCloudflareMessageBatch(batch),
|
|
1012
|
+
wrapCloudflareBindings(bindings),
|
|
1013
|
+
);
|
|
983
1014
|
},
|
|
984
1015
|
};
|
|
@@ -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
|
}
|