@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
|
Binary file
|
|
@@ -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.
|
|
290
|
-
// has
|
|
291
|
-
//
|
|
292
|
-
// isolate
|
|
293
|
-
//
|
|
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 {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
fetchUserInfo,
|
|
8
8
|
getAuthConfig,
|
|
9
9
|
getClientCredentials,
|
|
10
|
+
getMobileOidcAudience,
|
|
10
11
|
getProvider,
|
|
11
12
|
} from "../lib/oauth-providers.ts";
|
|
12
13
|
import { verifyOidcIdToken } from "../lib/oidc-id-token.ts";
|
|
@@ -296,7 +297,7 @@ auth.post("/mobile/oidc", async (c) => {
|
|
|
296
297
|
}
|
|
297
298
|
|
|
298
299
|
try {
|
|
299
|
-
const
|
|
300
|
+
const clientId = getMobileOidcAudience(c.env);
|
|
300
301
|
const claims = await verifyOidcIdToken(idToken, {
|
|
301
302
|
issuer: provider.issuer,
|
|
302
303
|
clientId,
|
|
@@ -15,7 +15,10 @@ import {
|
|
|
15
15
|
communities,
|
|
16
16
|
communityMembers,
|
|
17
17
|
inbox as inboxTable,
|
|
18
|
+
insertMany,
|
|
18
19
|
objects,
|
|
20
|
+
runBatch,
|
|
21
|
+
type D1Statement,
|
|
19
22
|
} from "../../../db/index.ts";
|
|
20
23
|
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
21
24
|
import type { Database } from "../../../db/index.ts";
|
|
@@ -666,33 +669,49 @@ export async function processMentions(
|
|
|
666
669
|
}
|
|
667
670
|
|
|
668
671
|
if (activitiesToCreate.length > 0) {
|
|
672
|
+
// A mention notification is one invariant: its activity and inbox edge
|
|
673
|
+
// either both exist or neither does. Build D1-safe chunked INSERT
|
|
674
|
+
// statements, then commit each bounded page as one atomic batch. This
|
|
675
|
+
// avoids both the 100-bind ceiling and the old activity-without-inbox
|
|
676
|
+
// partial state when the second independent INSERT failed.
|
|
677
|
+
const MENTION_NOTIFICATION_PAGE_SIZE = 200;
|
|
669
678
|
try {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
679
|
+
for (
|
|
680
|
+
let offset = 0;
|
|
681
|
+
offset < activitiesToCreate.length;
|
|
682
|
+
offset += MENTION_NOTIFICATION_PAGE_SIZE
|
|
683
|
+
) {
|
|
684
|
+
const activityPage = activitiesToCreate.slice(
|
|
685
|
+
offset,
|
|
686
|
+
offset + MENTION_NOTIFICATION_PAGE_SIZE,
|
|
687
|
+
);
|
|
688
|
+
const inboxPage = inboxEntriesToCreate.slice(
|
|
689
|
+
offset,
|
|
690
|
+
offset + MENTION_NOTIFICATION_PAGE_SIZE,
|
|
691
|
+
);
|
|
692
|
+
const statements = [
|
|
693
|
+
...insertMany(db, activities, activityPage),
|
|
694
|
+
...insertMany(db, inboxTable, inboxPage),
|
|
695
|
+
];
|
|
696
|
+
await runBatch(db, statements as [D1Statement, ...D1Statement[]]);
|
|
697
|
+
}
|
|
686
698
|
} catch (e) {
|
|
687
|
-
log.error("Failed to persist mention
|
|
688
|
-
event: "posts.mention.
|
|
699
|
+
log.error("Failed to atomically persist mention notifications", {
|
|
700
|
+
event: "posts.mention.notification_persist_failed",
|
|
689
701
|
error: e,
|
|
690
702
|
});
|
|
691
|
-
mentionFailures.push(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
703
|
+
mentionFailures.push(
|
|
704
|
+
{
|
|
705
|
+
mention: "__batch__",
|
|
706
|
+
stage: "persist_activity",
|
|
707
|
+
reason: "mention_activity_persist_failed",
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
mention: "__batch__",
|
|
711
|
+
stage: "persist_inbox",
|
|
712
|
+
reason: "mention_inbox_persist_failed",
|
|
713
|
+
},
|
|
714
|
+
);
|
|
696
715
|
}
|
|
697
716
|
}
|
|
698
717
|
|
|
@@ -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
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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(
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
D1Database,
|
|
9
9
|
Fetcher,
|
|
10
10
|
KVNamespace,
|
|
11
|
+
MessageBatch,
|
|
12
|
+
Queue,
|
|
11
13
|
R2Bucket,
|
|
12
14
|
R2Object,
|
|
13
15
|
} from "@cloudflare/workers-types";
|
|
@@ -20,6 +22,13 @@ import type {
|
|
|
20
22
|
ObjectMetadata,
|
|
21
23
|
StorageObject,
|
|
22
24
|
} from "./types.ts";
|
|
25
|
+
import type {
|
|
26
|
+
IQueueBatch,
|
|
27
|
+
IQueueMessage,
|
|
28
|
+
IQueueProducer,
|
|
29
|
+
QueueBatchItem,
|
|
30
|
+
QueueSendOptions,
|
|
31
|
+
} from "./queue.ts";
|
|
23
32
|
|
|
24
33
|
/**
|
|
25
34
|
* Cloudflare R2 Storage Adapter
|
|
@@ -170,6 +179,46 @@ class CloudflareAssets implements IStaticAssets {
|
|
|
170
179
|
}
|
|
171
180
|
}
|
|
172
181
|
|
|
182
|
+
class CloudflareQueueProducer<T> implements IQueueProducer<T> {
|
|
183
|
+
constructor(private readonly queue: Queue<T>) {}
|
|
184
|
+
|
|
185
|
+
async send(body: T, options?: QueueSendOptions): Promise<void> {
|
|
186
|
+
await this.queue.send(body, options);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async sendBatch(
|
|
190
|
+
messages: readonly QueueBatchItem<T>[],
|
|
191
|
+
options?: QueueSendOptions,
|
|
192
|
+
): Promise<void> {
|
|
193
|
+
await this.queue.sendBatch([...messages], options);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function wrapCloudflareQueue<T>(queue: Queue<T>): IQueueProducer<T> {
|
|
198
|
+
return new CloudflareQueueProducer(queue);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function wrapCloudflareMessageBatch<T>(
|
|
202
|
+
batch: MessageBatch<T>,
|
|
203
|
+
): IQueueBatch<T> {
|
|
204
|
+
const messages: readonly IQueueMessage<T>[] = batch.messages.map(
|
|
205
|
+
(message) => ({
|
|
206
|
+
id: message.id,
|
|
207
|
+
timestamp: message.timestamp,
|
|
208
|
+
body: message.body,
|
|
209
|
+
attempts: message.attempts,
|
|
210
|
+
ack: () => message.ack(),
|
|
211
|
+
retry: (options) => message.retry(options),
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
queue: batch.queue,
|
|
216
|
+
messages,
|
|
217
|
+
ackAll: () => batch.ackAll(),
|
|
218
|
+
retryAll: (options) => batch.retryAll(options),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
173
222
|
/**
|
|
174
223
|
* Wrap a native Cloudflare Workers binding env into the app's runtime
|
|
175
224
|
* `Env` shape. The Hono app and all helper functions speak the runtime
|
|
@@ -182,21 +231,33 @@ export function wrapCloudflareBindings<
|
|
|
182
231
|
MEDIA?: R2Bucket;
|
|
183
232
|
KV: KVNamespace;
|
|
184
233
|
ASSETS?: Fetcher;
|
|
234
|
+
DELIVERY_QUEUE?: Queue<unknown>;
|
|
235
|
+
DELIVERY_DLQ?: Queue<unknown>;
|
|
185
236
|
},
|
|
186
237
|
>(
|
|
187
238
|
bindings: T,
|
|
188
|
-
): Omit<
|
|
239
|
+
): Omit<
|
|
240
|
+
T,
|
|
241
|
+
"DB" | "MEDIA" | "KV" | "ASSETS" | "DELIVERY_QUEUE" | "DELIVERY_DLQ"
|
|
242
|
+
> & {
|
|
189
243
|
DB_INSTANCE: ReturnType<typeof getDb>;
|
|
190
244
|
MEDIA?: IObjectStorage;
|
|
191
245
|
KV: IKeyValueStore;
|
|
192
246
|
ASSETS?: IStaticAssets;
|
|
247
|
+
DELIVERY_QUEUE?: IQueueProducer<unknown>;
|
|
248
|
+
DELIVERY_DLQ?: IQueueProducer<unknown>;
|
|
193
249
|
} {
|
|
194
|
-
const { DB, MEDIA, KV, ASSETS, ...rest } =
|
|
250
|
+
const { DB, MEDIA, KV, ASSETS, DELIVERY_QUEUE, DELIVERY_DLQ, ...rest } =
|
|
251
|
+
bindings;
|
|
195
252
|
return {
|
|
196
253
|
...rest,
|
|
197
254
|
DB_INSTANCE: getDb(DB),
|
|
198
255
|
MEDIA: MEDIA ? new CloudflareStorage(MEDIA) : undefined,
|
|
199
256
|
KV: new CloudflareKV(KV),
|
|
200
257
|
ASSETS: ASSETS ? new CloudflareAssets(ASSETS) : undefined,
|
|
258
|
+
DELIVERY_QUEUE: DELIVERY_QUEUE
|
|
259
|
+
? wrapCloudflareQueue(DELIVERY_QUEUE)
|
|
260
|
+
: undefined,
|
|
261
|
+
DELIVERY_DLQ: DELIVERY_DLQ ? wrapCloudflareQueue(DELIVERY_DLQ) : undefined,
|
|
201
262
|
};
|
|
202
263
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TAKOSUMI_MANAGED_RELATIONAL_RUNTIME_CONTRACT,
|
|
3
|
+
managedRelationalBatchGatewayRequest,
|
|
4
|
+
managedRelationalConnection,
|
|
5
|
+
parseManagedRelationalBatchResponse,
|
|
6
|
+
type ManagedRelationalMethod,
|
|
7
|
+
type ManagedRelationalParameter,
|
|
8
|
+
} from "@takosjp/takosumi-contract/managed-relational-runtime";
|
|
9
|
+
import { parseManagedRuntimeConnectionMaterialization } from "@takosjp/takosumi-contract/managed-runtime-connections";
|
|
10
|
+
import {
|
|
11
|
+
drizzle as drizzleProxy,
|
|
12
|
+
type AsyncBatchRemoteCallback,
|
|
13
|
+
type AsyncRemoteCallback,
|
|
14
|
+
} from "drizzle-orm/sqlite-proxy";
|
|
15
|
+
|
|
16
|
+
import * as schema from "../../db/schema.ts";
|
|
17
|
+
import { ManagedRuntimeGatewayError } from "./managed-runtime.ts";
|
|
18
|
+
import type { ManagedRuntimeGateway } from "./managed-runtime.ts";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_MAX_RELATIONAL_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
export interface ManagedRelationalDatabaseOptions {
|
|
23
|
+
readonly materialization: unknown;
|
|
24
|
+
readonly gateway: ManagedRuntimeGateway;
|
|
25
|
+
readonly alias: string;
|
|
26
|
+
readonly idempotencyKey?: () => string;
|
|
27
|
+
readonly maxResponseBytes?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Provider-neutral Drizzle adapter for a host-issued RelationalDatabase.
|
|
32
|
+
*
|
|
33
|
+
* Every callback is one bounded prepared statement. Drizzle `batch()` maps to
|
|
34
|
+
* one ordered-atomic host call; transaction-control and migration SQL are
|
|
35
|
+
* deliberately unavailable on this request path.
|
|
36
|
+
*/
|
|
37
|
+
export function createManagedRelationalDatabase(
|
|
38
|
+
options: ManagedRelationalDatabaseOptions,
|
|
39
|
+
) {
|
|
40
|
+
const materialization = parseManagedRuntimeConnectionMaterialization(
|
|
41
|
+
options.materialization,
|
|
42
|
+
);
|
|
43
|
+
const connection = managedRelationalConnection(
|
|
44
|
+
materialization,
|
|
45
|
+
options.alias,
|
|
46
|
+
);
|
|
47
|
+
const idempotencyKey =
|
|
48
|
+
options.idempotencyKey ??
|
|
49
|
+
(() => `yurucommu.relational:${crypto.randomUUID()}`);
|
|
50
|
+
const maxResponseBytes =
|
|
51
|
+
options.maxResponseBytes ?? DEFAULT_MAX_RELATIONAL_RESPONSE_BYTES;
|
|
52
|
+
if (
|
|
53
|
+
!Number.isSafeInteger(maxResponseBytes) ||
|
|
54
|
+
maxResponseBytes < 1 ||
|
|
55
|
+
maxResponseBytes > DEFAULT_MAX_RELATIONAL_RESPONSE_BYTES
|
|
56
|
+
) {
|
|
57
|
+
throw new TypeError("managed_relational_response_limit_invalid");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const execute = async (
|
|
61
|
+
statements: readonly {
|
|
62
|
+
readonly sql: string;
|
|
63
|
+
readonly params: readonly unknown[];
|
|
64
|
+
readonly method: ManagedRelationalMethod;
|
|
65
|
+
}[],
|
|
66
|
+
) => {
|
|
67
|
+
const canonical = statements.map((statement) => ({
|
|
68
|
+
sql: statement.sql,
|
|
69
|
+
params: statement.params.map(relationalParameter),
|
|
70
|
+
method: statement.method,
|
|
71
|
+
}));
|
|
72
|
+
const request = managedRelationalBatchGatewayRequest(connection.authority, {
|
|
73
|
+
statements: canonical,
|
|
74
|
+
idempotencyKey: idempotencyKey(),
|
|
75
|
+
});
|
|
76
|
+
const response = await boundedResponse(
|
|
77
|
+
await options.gateway.fetch(request),
|
|
78
|
+
maxResponseBytes,
|
|
79
|
+
);
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const value = (await response.json().catch(() => undefined)) as unknown;
|
|
82
|
+
const code =
|
|
83
|
+
isRecord(value) && typeof value.error === "string"
|
|
84
|
+
? value.error
|
|
85
|
+
: "managed_relational_request_failed";
|
|
86
|
+
throw new ManagedRuntimeGatewayError(
|
|
87
|
+
code,
|
|
88
|
+
response.status,
|
|
89
|
+
response.status === 429 || response.status >= 500,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const value = parseManagedRelationalBatchResponse(
|
|
93
|
+
await response.json(),
|
|
94
|
+
canonical.length,
|
|
95
|
+
);
|
|
96
|
+
return value.results;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const callback: AsyncRemoteCallback = async (sql, params, method) => {
|
|
100
|
+
const [result] = await execute([{ sql, params, method }]);
|
|
101
|
+
if (!result) throw new Error("managed_relational_result_missing");
|
|
102
|
+
return drizzleResult(result, method);
|
|
103
|
+
};
|
|
104
|
+
const batchCallback: AsyncBatchRemoteCallback = async (batch) => {
|
|
105
|
+
const results = await execute(batch);
|
|
106
|
+
return results.map((result, index) =>
|
|
107
|
+
drizzleResult(result, batch[index]!.method),
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return drizzleProxy(callback, batchCallback, { schema });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function drizzleResult(
|
|
115
|
+
result: Awaited<
|
|
116
|
+
ReturnType<typeof parseManagedRelationalBatchResponse>
|
|
117
|
+
>["results"][number],
|
|
118
|
+
method: ManagedRelationalMethod,
|
|
119
|
+
) {
|
|
120
|
+
return {
|
|
121
|
+
// sqlite-proxy exposes mutable `any[]` at this boundary even though the
|
|
122
|
+
// public runtime contract is intentionally immutable. Copy here so the
|
|
123
|
+
// provider-neutral contract never leaks a mutable result reference.
|
|
124
|
+
rows:
|
|
125
|
+
method === "get"
|
|
126
|
+
? [...(result.rows[0] ?? [])]
|
|
127
|
+
: result.rows.map((row) => [...row]),
|
|
128
|
+
meta: result.meta,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function relationalParameter(value: unknown): ManagedRelationalParameter {
|
|
133
|
+
if (value === null || typeof value === "string") return value;
|
|
134
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
135
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
136
|
+
throw new TypeError("managed_relational_parameter_unsupported");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function boundedResponse(
|
|
140
|
+
response: Response,
|
|
141
|
+
maxBytes: number,
|
|
142
|
+
): Promise<Response> {
|
|
143
|
+
const declaredLength = response.headers.get("content-length");
|
|
144
|
+
if (
|
|
145
|
+
declaredLength !== null &&
|
|
146
|
+
(!/^\d+$/u.test(declaredLength) || Number(declaredLength) > maxBytes)
|
|
147
|
+
) {
|
|
148
|
+
throw new ManagedRuntimeGatewayError(
|
|
149
|
+
"managed_relational_response_too_large",
|
|
150
|
+
502,
|
|
151
|
+
false,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
const reader = response.body?.getReader();
|
|
155
|
+
if (!reader) {
|
|
156
|
+
return new Response(null, {
|
|
157
|
+
status: response.status,
|
|
158
|
+
statusText: response.statusText,
|
|
159
|
+
headers: response.headers,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const chunks: Uint8Array[] = [];
|
|
163
|
+
let size = 0;
|
|
164
|
+
try {
|
|
165
|
+
while (true) {
|
|
166
|
+
const { done, value } = await reader.read();
|
|
167
|
+
if (done) break;
|
|
168
|
+
size += value.byteLength;
|
|
169
|
+
if (size > maxBytes) {
|
|
170
|
+
await reader.cancel("managed_relational_response_too_large");
|
|
171
|
+
throw new ManagedRuntimeGatewayError(
|
|
172
|
+
"managed_relational_response_too_large",
|
|
173
|
+
502,
|
|
174
|
+
false,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
chunks.push(value);
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
reader.releaseLock();
|
|
181
|
+
}
|
|
182
|
+
const body = new Uint8Array(size);
|
|
183
|
+
let offset = 0;
|
|
184
|
+
for (const chunk of chunks) {
|
|
185
|
+
body.set(chunk, offset);
|
|
186
|
+
offset += chunk.byteLength;
|
|
187
|
+
}
|
|
188
|
+
return new Response(body, {
|
|
189
|
+
status: response.status,
|
|
190
|
+
statusText: response.statusText,
|
|
191
|
+
headers: response.headers,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
196
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
197
|
+
}
|