@takosjp/yurucommu-core 3.4.3 → 3.4.4
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/0023_delivery_resolution_outbox.sql +33 -0
- package/migrations/0024_delivery_fanout_outbox.sql +35 -0
- package/migrations/0025_delivery_endpoint_terminal_retention.sql +6 -0
- package/migrations/0026_remote_actor_fetch_failures.sql +29 -0
- package/migrations/0027_remote_actor_tombstones.sql +12 -0
- package/migrations/0028_remote_actor_delivery_fence.sql +30 -0
- package/migrations/0029_delivery_endpoint_recipients.sql +47 -0
- package/package.json +2 -1
- package/packages/api/package.json +1 -1
- package/packages/api/src/lib/api/communities.ts +2 -0
- package/packages/api/src/lib/api/fetch.ts +69 -13
- package/packages/api/src/lib/api/normalize.ts +32 -10
- package/packages/api/src/lib/api/notifications.ts +4 -1
- package/packages/api/src/lib/rtc-client.ts +127 -29
- package/src/backend/federation-helpers.ts +1 -0
- package/src/backend/index.ts +16 -3
- package/src/backend/lib/account-migration.ts +340 -0
- package/src/backend/lib/activity-delete-cascade.ts +193 -0
- package/src/backend/lib/activitypub-actor-cache.ts +615 -48
- package/src/backend/lib/activitypub-actor-identity-sql.ts +179 -0
- package/src/backend/lib/activitypub-actor-identity.ts +39 -0
- package/src/backend/lib/activitypub-validators.ts +62 -4
- package/src/backend/lib/ap-ids.ts +36 -6
- package/src/backend/lib/ap-verify.ts +7 -17
- package/src/backend/lib/blocklist-purge.ts +676 -42
- package/src/backend/lib/blocklist.ts +278 -39
- package/src/backend/lib/community-visibility.ts +47 -33
- package/src/backend/lib/delivery/fanout-outbox.ts +336 -0
- package/src/backend/lib/delivery/planner.ts +16 -12
- package/src/backend/lib/delivery/queue-batching.ts +389 -145
- package/src/backend/lib/delivery/queue-delivery.ts +47 -25
- package/src/backend/lib/delivery/queue.ts +479 -73
- package/src/backend/lib/delivery/resolution-outbox.ts +456 -0
- package/src/backend/lib/delivery/types.ts +31 -7
- package/src/backend/lib/feed-exclude.ts +40 -28
- package/src/backend/lib/follow-edge-mutations.ts +217 -0
- package/src/backend/lib/notification-eligibility.ts +15 -9
- package/src/backend/lib/notification-push.ts +2 -2
- package/src/backend/lib/oidc-id-token.ts +66 -0
- package/src/backend/lib/personal-actor-moderation.ts +239 -0
- package/src/backend/lib/post-visibility.ts +79 -16
- package/src/backend/lib/remote-activity-id.ts +61 -0
- package/src/backend/lib/unread-counts.ts +3 -0
- package/src/backend/retention.ts +38 -1
- package/src/backend/routes/account-teardown.ts +422 -156
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
- package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
- package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
- package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
- package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
- package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
- package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
- package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
- package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
- package/src/backend/routes/activitypub/inbox-types.ts +14 -2
- package/src/backend/routes/activitypub/inbox.ts +81 -105
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub.ts +6 -5
- package/src/backend/routes/actors-helpers.ts +34 -8
- package/src/backend/routes/actors.ts +328 -164
- package/src/backend/routes/auth-helpers.ts +57 -9
- package/src/backend/routes/auth.ts +10 -2
- package/src/backend/routes/communities/membership-invites.ts +4 -1
- package/src/backend/routes/communities/membership-members.ts +201 -73
- package/src/backend/routes/communities/membership-requests.ts +141 -76
- package/src/backend/routes/communities/membership-shared.ts +228 -21
- package/src/backend/routes/communities/messages.ts +9 -2
- package/src/backend/routes/communities/routes.ts +48 -13
- package/src/backend/routes/dm/contacts.ts +29 -41
- package/src/backend/routes/dm/conversations-helpers.ts +9 -1
- package/src/backend/routes/dm/messages.ts +36 -42
- package/src/backend/routes/dm/read-archive.ts +4 -2
- package/src/backend/routes/dm/requests.ts +62 -54
- package/src/backend/routes/follow-helpers.ts +200 -60
- package/src/backend/routes/follow.ts +146 -140
- package/src/backend/routes/media.ts +20 -94
- package/src/backend/routes/moderation.ts +72 -4
- package/src/backend/routes/notes.ts +3 -4
- package/src/backend/routes/notifications.ts +209 -108
- package/src/backend/routes/posts/delete-cascade.ts +253 -89
- package/src/backend/routes/posts/federation.ts +373 -0
- package/src/backend/routes/posts/interactions.ts +160 -184
- package/src/backend/routes/posts/like-mutation.ts +240 -0
- package/src/backend/routes/posts/post-helpers.ts +112 -162
- package/src/backend/routes/posts/queries.ts +150 -54
- package/src/backend/routes/posts/routes.ts +169 -329
- package/src/backend/routes/posts/transformers.ts +61 -6
- package/src/backend/routes/recommendations.ts +37 -44
- package/src/backend/routes/rtc/index.ts +26 -6
- package/src/backend/routes/search.ts +39 -55
- package/src/backend/routes/stories/interactions.ts +22 -6
- package/src/backend/routes/stories/query-helpers.ts +28 -41
- package/src/backend/routes/stories/routes.ts +125 -108
- package/src/backend/routes/takos-tools/dm.ts +17 -17
- package/src/backend/routes/takos-tools/posts.ts +189 -106
- package/src/backend/routes/takos-tools/search.ts +13 -4
- package/src/backend/routes/takos-tools/timeline.ts +35 -27
- package/src/backend/routes/takos-tools-response.ts +6 -2
- package/src/backend/routes/timeline.ts +5 -5
- package/src/backend/runtime/call-signaling-do.ts +35 -4
- package/src/backend/runtime/one-time-ticket.ts +116 -0
- package/src/backend/runtime/realtime-stream-do.ts +5 -61
- package/src/backend/runtime/signaling-hub.ts +36 -3
- package/src/backend/server.ts +95 -80
- package/src/db/d1-write.ts +67 -43
- package/src/db/schema/federation.ts +42 -1
- package/src/db/schema/messaging.ts +110 -2
- package/src/db/schema.ts +1 -1
package/src/db/d1-write.ts
CHANGED
|
@@ -88,31 +88,35 @@ function requireBatchable(db: Database, context: string): BatchableDb {
|
|
|
88
88
|
return candidate as BatchableDb;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
* Drizzle builds ONE column list for the whole `values([...])` call (the union
|
|
95
|
-
* of the keys present across the rows) and binds a parameter per column per
|
|
96
|
-
* row, so the budget must be divided by that union — not by the keys of the
|
|
97
|
-
* first row.
|
|
98
|
-
*/
|
|
99
|
-
function paramsPerRow(
|
|
91
|
+
/** Reject row keys that do not belong to the target table. */
|
|
92
|
+
function assertKnownRowKeys(
|
|
100
93
|
table: SQLiteTable,
|
|
101
94
|
rows: readonly Record<string, unknown>[],
|
|
102
|
-
):
|
|
103
|
-
const keys = new Set<string>();
|
|
104
|
-
for (const row of rows) {
|
|
105
|
-
for (const key of Object.keys(row)) keys.add(key);
|
|
106
|
-
}
|
|
95
|
+
): void {
|
|
107
96
|
const known = new Set(Object.keys(getTableColumns(table)));
|
|
108
|
-
for (const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
97
|
+
for (const row of rows) {
|
|
98
|
+
for (const key of Object.keys(row)) {
|
|
99
|
+
if (!known.has(key)) {
|
|
100
|
+
throw new D1WriteShapeError(
|
|
101
|
+
`insertMany: row key "${key}" is not a column of the target table`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
113
104
|
}
|
|
114
105
|
}
|
|
115
|
-
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Count the exact parameters Drizzle compiled into an insert statement. */
|
|
109
|
+
function boundParameterCount(statement: D1Statement): number {
|
|
110
|
+
const candidate = statement as unknown as {
|
|
111
|
+
toSQL(): { readonly params: readonly unknown[] };
|
|
112
|
+
};
|
|
113
|
+
if (typeof candidate.toSQL !== "function") {
|
|
114
|
+
throw new D1WriteShapeError(
|
|
115
|
+
"insertMany: the active insert builder exposes no toSQL(); the D1 " +
|
|
116
|
+
"parameter budget cannot be proven before execution",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return candidate.toSQL().params.length;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
122
|
/**
|
|
@@ -132,35 +136,47 @@ export function insertMany<TTable extends SQLiteTable>(
|
|
|
132
136
|
): readonly D1Statement[] {
|
|
133
137
|
if (rows.length === 0) return [];
|
|
134
138
|
|
|
135
|
-
|
|
136
|
-
table,
|
|
137
|
-
rows as readonly Record<string, unknown>[],
|
|
138
|
-
);
|
|
139
|
-
if (perRow === 0) {
|
|
140
|
-
throw new D1WriteShapeError("insertMany: rows bind no columns");
|
|
141
|
-
}
|
|
142
|
-
if (perRow > D1_SAFE_PARAM_BUDGET) {
|
|
143
|
-
throw new D1WriteShapeError(
|
|
144
|
-
`insertMany: a single row binds ${perRow} parameters, over the D1 ` +
|
|
145
|
-
`budget of ${D1_SAFE_PARAM_BUDGET}; no chunking can make this fit`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
139
|
+
assertKnownRowKeys(table, rows as readonly Record<string, unknown>[]);
|
|
148
140
|
|
|
149
|
-
const
|
|
150
|
-
const statements: D1Statement[] = [];
|
|
151
|
-
for (let offset = 0; offset < rows.length; offset += chunkSize) {
|
|
152
|
-
const chunk = rows.slice(offset, offset + chunkSize);
|
|
153
|
-
// Drizzle's insert builder is generic over the table; the concrete row type
|
|
154
|
-
// is already checked by the public signature, so the builder-local widening
|
|
155
|
-
// here is the narrowest cast that keeps the call site typed.
|
|
141
|
+
const buildStatement = (chunk: readonly TTable["$inferInsert"][]) => {
|
|
156
142
|
const builder = db
|
|
157
143
|
.insert(table)
|
|
158
144
|
.values(chunk as TTable["$inferInsert"][]) as unknown as D1Statement & {
|
|
159
145
|
onConflictDoNothing(): D1Statement;
|
|
160
146
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
147
|
+
return opts?.conflict === "ignore"
|
|
148
|
+
? builder.onConflictDoNothing()
|
|
149
|
+
: builder;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const statements: D1Statement[] = [];
|
|
153
|
+
for (let offset = 0; offset < rows.length;) {
|
|
154
|
+
// Start at no more than the parameter budget in rows, then compile and
|
|
155
|
+
// shrink. Inspecting the actual query is essential: Drizzle fills omitted
|
|
156
|
+
// `$defaultFn`, primitive `.default(...)`, and on-update defaults at compile
|
|
157
|
+
// time, each of which can add a bound parameter that is absent from the
|
|
158
|
+
// caller's row keys. The former key-count heuristic under-budgeted those
|
|
159
|
+
// hidden values and emitted production-invalid 105/120-parameter inserts.
|
|
160
|
+
let chunkSize = Math.min(D1_SAFE_PARAM_BUDGET, rows.length - offset);
|
|
161
|
+
let statement: D1Statement;
|
|
162
|
+
while (true) {
|
|
163
|
+
statement = buildStatement(rows.slice(offset, offset + chunkSize));
|
|
164
|
+
const parameters = boundParameterCount(statement);
|
|
165
|
+
if (parameters <= D1_SAFE_PARAM_BUDGET) break;
|
|
166
|
+
if (chunkSize === 1) {
|
|
167
|
+
throw new D1WriteShapeError(
|
|
168
|
+
`insertMany: a single row compiles to ${parameters} parameters, ` +
|
|
169
|
+
`over the D1 budget of ${D1_SAFE_PARAM_BUDGET}; no chunking can ` +
|
|
170
|
+
`make this fit`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
const scaled = Math.floor(
|
|
174
|
+
(chunkSize * D1_SAFE_PARAM_BUDGET) / parameters,
|
|
175
|
+
);
|
|
176
|
+
chunkSize = Math.max(1, Math.min(chunkSize - 1, scaled));
|
|
177
|
+
}
|
|
178
|
+
statements.push(statement);
|
|
179
|
+
offset += chunkSize;
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
if (statements.length > D1_MAX_BATCH_STATEMENTS) {
|
|
@@ -215,6 +231,14 @@ export async function runBatch(
|
|
|
215
231
|
db: Database,
|
|
216
232
|
statements: readonly [D1Statement, ...D1Statement[]],
|
|
217
233
|
): Promise<void> {
|
|
234
|
+
if (statements.length > D1_MAX_BATCH_STATEMENTS) {
|
|
235
|
+
throw new D1BatchTooLargeError(
|
|
236
|
+
`runBatch: ${statements.length} statements exceed the atomic batch cap ` +
|
|
237
|
+
`of ${D1_MAX_BATCH_STATEMENTS}. Do not split a logical mutation across ` +
|
|
238
|
+
`batches; make the writes set-based or use the generational-replace ` +
|
|
239
|
+
`pattern in docs/quality/d1-write.md §3.`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
218
242
|
await requireBatchable(db, "runBatch").batch(statements);
|
|
219
243
|
}
|
|
220
244
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { index, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
2
|
import { nowIsoUtc } from "./date-utils.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -17,3 +17,44 @@ export const inboundActivityClaims = sqliteTable(
|
|
|
17
17
|
},
|
|
18
18
|
(t) => [index("inbound_activity_claims_lease_idx").on(t.leaseExpiresAt)],
|
|
19
19
|
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Durable negative-cache/backoff state for a remote actor document fetch.
|
|
23
|
+
*
|
|
24
|
+
* This is deliberately separate from actor_cache: a failed fetch is not an
|
|
25
|
+
* actor and must never leak into actor search, signature verification, or
|
|
26
|
+
* delivery planning as if it were a partially valid cached identity.
|
|
27
|
+
*/
|
|
28
|
+
export const remoteActorFetchFailures = sqliteTable(
|
|
29
|
+
"remote_actor_fetch_failures",
|
|
30
|
+
{
|
|
31
|
+
actorApId: text("actor_ap_id").primaryKey(),
|
|
32
|
+
kind: text("kind").notNull(),
|
|
33
|
+
reason: text("reason").notNull(),
|
|
34
|
+
httpStatus: integer("http_status"),
|
|
35
|
+
failureCount: integer("failure_count").notNull().default(1),
|
|
36
|
+
retryAt: text("retry_at"),
|
|
37
|
+
processingToken: text("processing_token"),
|
|
38
|
+
leaseExpiresAt: text("lease_expires_at"),
|
|
39
|
+
createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
|
|
40
|
+
updatedAt: text("updated_at").notNull().$defaultFn(nowIsoUtc),
|
|
41
|
+
},
|
|
42
|
+
(t) => [
|
|
43
|
+
index("remote_actor_fetch_failures_retry_idx").on(t.retryAt),
|
|
44
|
+
index("remote_actor_fetch_failures_lease_idx").on(t.leaseExpiresAt),
|
|
45
|
+
index("remote_actor_fetch_failures_updated_idx").on(t.updatedAt),
|
|
46
|
+
],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Durable lifecycle authority established by a verified self-Delete(Actor).
|
|
51
|
+
*
|
|
52
|
+
* This is deliberately separate from actor_cache and fetch-failure backoff:
|
|
53
|
+
* cache data is disposable, a remote fetch can recover, but a signed deletion
|
|
54
|
+
* must fence late/concurrent cache writers and all subsequent inbound traffic.
|
|
55
|
+
*/
|
|
56
|
+
export const remoteActorTombstones = sqliteTable("remote_actor_tombstones", {
|
|
57
|
+
actorApId: text("actor_ap_id").primaryKey(),
|
|
58
|
+
deleteActivityApId: text("delete_activity_ap_id").notNull(),
|
|
59
|
+
deletedAt: text("deleted_at").notNull().$defaultFn(nowIsoUtc),
|
|
60
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Messaging/DM tables: activities, inbox, deliveryQueue,
|
|
3
|
-
*
|
|
2
|
+
* Messaging/DM tables: activities, inbox, deliveryQueue,
|
|
3
|
+
* deliveryEndpointRecipients, deliveryCircuit, notificationArchived, dmTyping,
|
|
4
|
+
* dmReadStatus, dmArchivedConversations, mediaUploads
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import {
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
primaryKey,
|
|
10
11
|
sqliteTable,
|
|
11
12
|
text,
|
|
13
|
+
uniqueIndex,
|
|
12
14
|
} from "drizzle-orm/sqlite-core";
|
|
13
15
|
import { nowIso, nowIsoUtc } from "./date-utils.ts";
|
|
14
16
|
|
|
@@ -61,12 +63,118 @@ export const deliveryQueue = sqliteTable(
|
|
|
61
63
|
nextAttemptAt: text("next_attempt_at").notNull().$defaultFn(nowIso),
|
|
62
64
|
deliveredAt: text("delivered_at"),
|
|
63
65
|
error: text("error"),
|
|
66
|
+
recipientAttributionComplete: integer("recipient_attribution_complete")
|
|
67
|
+
.notNull()
|
|
68
|
+
.default(0),
|
|
64
69
|
createdAt: text("created_at").notNull().$defaultFn(nowIso),
|
|
65
70
|
},
|
|
66
71
|
(t) => [
|
|
67
72
|
index("delivery_queue_activity_idx").on(t.activityApId),
|
|
68
73
|
index("delivery_queue_next_attempt_idx").on(t.nextAttemptAt),
|
|
69
74
|
index("delivery_queue_status_next_idx").on(t.status, t.nextAttemptAt),
|
|
75
|
+
index("delivery_queue_terminal_retention_idx").on(t.status, t.createdAt),
|
|
76
|
+
],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Recipient attribution for endpoint-aggregated jobs created after migration
|
|
81
|
+
* 0029. Legacy delivery_queue rows deliberately remain unattributed rather
|
|
82
|
+
* than pretending an incomplete reconstruction is authoritative.
|
|
83
|
+
*/
|
|
84
|
+
export const deliveryEndpointRecipients = sqliteTable(
|
|
85
|
+
"delivery_endpoint_recipients",
|
|
86
|
+
{
|
|
87
|
+
deliveryJobId: text("delivery_job_id").notNull(),
|
|
88
|
+
recipientActorApId: text("recipient_actor_ap_id").notNull(),
|
|
89
|
+
createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
|
|
90
|
+
},
|
|
91
|
+
(t) => [
|
|
92
|
+
primaryKey({ columns: [t.deliveryJobId, t.recipientActorApId] }),
|
|
93
|
+
index("delivery_endpoint_recipients_actor_idx").on(
|
|
94
|
+
t.recipientActorApId,
|
|
95
|
+
t.deliveryJobId,
|
|
96
|
+
),
|
|
97
|
+
],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// DELIVERY_RESOLUTIONS
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Durable first-hop outbox for an outbound Activity whose recipient endpoint
|
|
106
|
+
* is not yet known. Queue messages are only wakeups; this row remains the
|
|
107
|
+
* retry authority until actor resolution creates a deliveryQueue endpoint job.
|
|
108
|
+
*/
|
|
109
|
+
export const deliveryResolutions = sqliteTable(
|
|
110
|
+
"delivery_resolutions",
|
|
111
|
+
{
|
|
112
|
+
id: text("id").primaryKey(),
|
|
113
|
+
activityApId: text("activity_ap_id").notNull(),
|
|
114
|
+
recipientActorApId: text("recipient_actor_ap_id").notNull(),
|
|
115
|
+
status: text("status").notNull().default("pending"),
|
|
116
|
+
processingToken: text("processing_token"),
|
|
117
|
+
attempts: integer("attempts").notNull().default(0),
|
|
118
|
+
nextAttemptAt: text("next_attempt_at").notNull().$defaultFn(nowIsoUtc),
|
|
119
|
+
lastError: text("last_error"),
|
|
120
|
+
createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
|
|
121
|
+
updatedAt: text("updated_at")
|
|
122
|
+
.notNull()
|
|
123
|
+
.$defaultFn(nowIsoUtc)
|
|
124
|
+
.$onUpdateFn(nowIsoUtc),
|
|
125
|
+
resolvedAt: text("resolved_at"),
|
|
126
|
+
},
|
|
127
|
+
(t) => [
|
|
128
|
+
uniqueIndex("delivery_resolutions_activity_actor_idx").on(
|
|
129
|
+
t.activityApId,
|
|
130
|
+
t.recipientActorApId,
|
|
131
|
+
),
|
|
132
|
+
index("delivery_resolutions_status_next_idx").on(t.status, t.nextAttemptAt),
|
|
133
|
+
index("delivery_resolutions_terminal_retention_idx").on(
|
|
134
|
+
t.status,
|
|
135
|
+
t.updatedAt,
|
|
136
|
+
),
|
|
137
|
+
index("delivery_resolutions_activity_idx").on(t.activityApId),
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// DELIVERY_FANOUTS
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Durable first-hop outbox for follower/community fanout. Queue messages only
|
|
147
|
+
* wake the planner; the row remains authoritative until the final page has
|
|
148
|
+
* completed so a failed producer RPC or Bun process restart can replay it.
|
|
149
|
+
*/
|
|
150
|
+
export const deliveryFanouts = sqliteTable(
|
|
151
|
+
"delivery_fanouts",
|
|
152
|
+
{
|
|
153
|
+
id: text("id").primaryKey(),
|
|
154
|
+
activityApId: text("activity_ap_id").notNull(),
|
|
155
|
+
kind: text("kind").notNull(),
|
|
156
|
+
targetApId: text("target_ap_id").notNull(),
|
|
157
|
+
announceActivityApId: text("announce_activity_ap_id"),
|
|
158
|
+
status: text("status").notNull().default("pending"),
|
|
159
|
+
publications: integer("publications").notNull().default(0),
|
|
160
|
+
lastError: text("last_error"),
|
|
161
|
+
createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
|
|
162
|
+
updatedAt: text("updated_at")
|
|
163
|
+
.notNull()
|
|
164
|
+
.$defaultFn(nowIsoUtc)
|
|
165
|
+
.$onUpdateFn(nowIsoUtc),
|
|
166
|
+
completedAt: text("completed_at"),
|
|
167
|
+
},
|
|
168
|
+
(t) => [
|
|
169
|
+
uniqueIndex("delivery_fanouts_intent_idx").on(
|
|
170
|
+
t.activityApId,
|
|
171
|
+
t.kind,
|
|
172
|
+
t.targetApId,
|
|
173
|
+
t.announceActivityApId,
|
|
174
|
+
),
|
|
175
|
+
index("delivery_fanouts_status_created_idx").on(t.status, t.createdAt),
|
|
176
|
+
index("delivery_fanouts_terminal_retention_idx").on(t.status, t.updatedAt),
|
|
177
|
+
index("delivery_fanouts_activity_idx").on(t.activityApId),
|
|
70
178
|
],
|
|
71
179
|
);
|
|
72
180
|
|
package/src/db/schema.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - schema/social.ts: follows, blocks, mutes
|
|
9
9
|
* - schema/communities.ts: communities, communityMembers, communityJoinRequests, communityInvites
|
|
10
10
|
* - schema/stories.ts: storyViews, storyVotes, storyShares
|
|
11
|
-
* - schema/messaging.ts: activities, deliveryQueue, deliveryCircuit, inbox, notificationArchived, dm*, dmCommunityReadStatus, mediaUploads
|
|
11
|
+
* - schema/messaging.ts: activities, deliveryQueue, deliveryEndpointRecipients, deliveryResolutions, deliveryFanouts, deliveryCircuit, inbox, notificationArchived, dm*, dmCommunityReadStatus, mediaUploads
|
|
12
12
|
* - schema/relations.ts: all Drizzle relation definitions
|
|
13
13
|
*/
|
|
14
14
|
|