@takosjp/yurucommu-core 4.0.0 → 4.1.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/README.en.md +3 -0
- package/README.md +2 -0
- package/package.json +3 -2
- package/packages/api/package.json +1 -1
- package/src/backend/index.ts +44 -16
- package/src/backend/public.ts +60 -0
- package/src/backend/routes/media.ts +4 -0
- package/src/backend/runtime/bun.ts +1 -1
- package/src/backend/runtime/edge-facades.ts +353 -0
- package/src/backend/runtime/edge-kv.ts +204 -0
- package/src/backend/runtime/edge-objects.ts +124 -0
- package/src/backend/runtime/edge-queue.ts +135 -0
- package/src/backend/runtime/edge-sql.ts +207 -0
- package/src/backend/runtime/lane.ts +309 -0
- package/src/backend/runtime/managed-relational.ts +70 -23
- package/src/backend/runtime/node-paths.ts +79 -0
- package/src/backend/runtime/shared.ts +9 -62
- package/src/backend/runtime/sqlite-proxy-rows.ts +309 -0
- package/src/backend/server.ts +5 -0
- package/src/backend/types.ts +9 -0
- package/src/db/index.ts +2 -1
package/README.en.md
CHANGED
|
@@ -87,6 +87,9 @@ Takos core. See [`AGENTS.md`](AGENTS.md) for the full product boundary.
|
|
|
87
87
|
|
|
88
88
|
## Documentation
|
|
89
89
|
|
|
90
|
+
- [Runtime lanes](docs/design/runtime-lanes.md) — how the bindings differ between
|
|
91
|
+
a raw-Cloudflare deployment and one on a host that projects the portable
|
|
92
|
+
facades, and how a deployment declares which it is
|
|
90
93
|
- [Deployment guide](https://yurucommu.com/help/deployment.html)
|
|
91
94
|
- [Getting started](https://yurucommu.com/help/getting-started.html)
|
|
92
95
|
- [Help site](https://yurucommu.com/help/)
|
package/README.md
CHANGED
|
@@ -86,6 +86,8 @@ Yurucommu は ActivityPub 連合・コンテンツ配送・ユーザー identity
|
|
|
86
86
|
|
|
87
87
|
## ドキュメント
|
|
88
88
|
|
|
89
|
+
- [Runtime lanes](docs/design/runtime-lanes.md) — raw Cloudflare binding と
|
|
90
|
+
portable facade で binding の形が変わる点と、その宣言方法
|
|
89
91
|
- [Deployment guide](https://yurucommu.com/help/deployment.html)
|
|
90
92
|
- [Getting started](https://yurucommu.com/help/getting-started.html)
|
|
91
93
|
- [Help site](https://yurucommu.com/help/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takosjp/yurucommu-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -59,8 +59,9 @@
|
|
|
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": "bun run fmt:check && tsc --noEmit && bun run check:no-opentofu-artifacts && bun run test",
|
|
62
|
+
"check": "bun run fmt:check && tsc --noEmit && bun run check:no-opentofu-artifacts && bun run check:worker-bundle && bun run test",
|
|
63
63
|
"check:no-opentofu-artifacts": "bun scripts/check-no-opentofu-artifacts.mjs",
|
|
64
|
+
"check:worker-bundle": "bun scripts/check-worker-bundle-portable.mjs",
|
|
64
65
|
"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
66
|
"test:backend": "bun test src/backend/",
|
|
66
67
|
"build": "bun run build:api",
|
package/src/backend/index.ts
CHANGED
|
@@ -5,9 +5,12 @@ 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
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
resolveRuntimeLane,
|
|
9
|
+
wrapRuntimeBindings,
|
|
10
|
+
wrapRuntimeMessageBatch,
|
|
11
|
+
type PortableWorkerBindings,
|
|
12
|
+
} from "./runtime/lane.ts";
|
|
13
|
+
import type { EdgeQueueBatch } from "./runtime/edge-facades.ts";
|
|
11
14
|
import {
|
|
12
15
|
getMobileOidcAudience,
|
|
13
16
|
getOidcClientCredentials,
|
|
@@ -992,15 +995,17 @@ export async function handleYurucommuQueueBatch(
|
|
|
992
995
|
batch.ackAll();
|
|
993
996
|
}
|
|
994
997
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
998
|
+
/**
|
|
999
|
+
* Bindings that are not the runtime ports, and so pass through whichever lane
|
|
1000
|
+
* wrapper runs. Durable Objects live here: Takoform's Worker Version form has
|
|
1001
|
+
* no Durable Object binding, so on the portable lane both are simply unbound
|
|
1002
|
+
* and the routes that need them answer 503, exactly as on a Cloudflare
|
|
1003
|
+
* deployment that did not declare them.
|
|
1004
|
+
*/
|
|
1005
|
+
type PassthroughBindings = EnvVars & {
|
|
999
1006
|
ASSETS?: Fetcher;
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
// Signaling hub Durable Object namespace (call feature). wrapCloudflareBindings
|
|
1003
|
-
// spreads it through untouched (it is not DB/MEDIA/KV/ASSETS) so app code and
|
|
1007
|
+
// Signaling hub Durable Object namespace (call feature). The lane wrappers
|
|
1008
|
+
// spread it through untouched (it is not DB/MEDIA/KV/ASSETS) so app code and
|
|
1004
1009
|
// the rtc routes read it as c.env.CALL_SIGNALING.
|
|
1005
1010
|
CALL_SIGNALING?: DurableObjectNamespace;
|
|
1006
1011
|
// Per-user realtime event stream Durable Object namespace. Same pass-through
|
|
@@ -1009,6 +1014,26 @@ type WorkerBindings = EnvVars & {
|
|
|
1009
1014
|
REALTIME_STREAM?: DurableObjectNamespace;
|
|
1010
1015
|
};
|
|
1011
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* What this Worker may be handed.
|
|
1019
|
+
*
|
|
1020
|
+
* Raw Cloudflare bindings, or the portable facades a wrapper host projects.
|
|
1021
|
+
* `wrapRuntimeBindings` decides which by reading the deployment's declared
|
|
1022
|
+
* `YURUCOMMU_RUNTIME_LANE` and proving it against the bindings that actually
|
|
1023
|
+
* arrived; see runtime/lane.ts.
|
|
1024
|
+
*/
|
|
1025
|
+
type WorkerBindings = PassthroughBindings &
|
|
1026
|
+
(
|
|
1027
|
+
| {
|
|
1028
|
+
DB: D1Database;
|
|
1029
|
+
MEDIA?: R2Bucket;
|
|
1030
|
+
KV: KVNamespace;
|
|
1031
|
+
DELIVERY_QUEUE?: Queue<DeliveryQueueMessageV1>;
|
|
1032
|
+
DELIVERY_DLQ?: Queue<DeliveryDlqMessageV1>;
|
|
1033
|
+
}
|
|
1034
|
+
| PortableWorkerBindings
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1012
1037
|
function isMaterializedRuntimeEnv(
|
|
1013
1038
|
bindings: WorkerBindings | Env,
|
|
1014
1039
|
): bindings is Env {
|
|
@@ -1021,16 +1046,19 @@ export default {
|
|
|
1021
1046
|
bindings: WorkerBindings,
|
|
1022
1047
|
ctx: ExecutionContext,
|
|
1023
1048
|
): Promise<Response> {
|
|
1024
|
-
return app.fetch(request,
|
|
1049
|
+
return app.fetch(request, wrapRuntimeBindings(bindings), ctx);
|
|
1025
1050
|
},
|
|
1026
1051
|
|
|
1027
1052
|
async queue(
|
|
1028
|
-
batch:
|
|
1053
|
+
batch:
|
|
1054
|
+
| MessageBatch<DeliveryQueueMessageV1 | DeliveryDlqMessageV1>
|
|
1055
|
+
| EdgeQueueBatch,
|
|
1029
1056
|
bindings: WorkerBindings,
|
|
1030
1057
|
): Promise<void> {
|
|
1058
|
+
const lane = resolveRuntimeLane(bindings.YURUCOMMU_RUNTIME_LANE);
|
|
1031
1059
|
return handleYurucommuQueueBatch(
|
|
1032
|
-
|
|
1033
|
-
|
|
1060
|
+
wrapRuntimeMessageBatch(batch, lane),
|
|
1061
|
+
wrapRuntimeBindings(bindings),
|
|
1034
1062
|
);
|
|
1035
1063
|
},
|
|
1036
1064
|
|
|
@@ -1041,7 +1069,7 @@ export default {
|
|
|
1041
1069
|
): Promise<void> {
|
|
1042
1070
|
const env = isMaterializedRuntimeEnv(bindings)
|
|
1043
1071
|
? bindings
|
|
1044
|
-
:
|
|
1072
|
+
: wrapRuntimeBindings(bindings);
|
|
1045
1073
|
await runYurucommuRetention(env);
|
|
1046
1074
|
},
|
|
1047
1075
|
};
|
package/src/backend/public.ts
CHANGED
|
@@ -36,6 +36,66 @@ export {
|
|
|
36
36
|
createManagedRelationalDatabase,
|
|
37
37
|
type ManagedRelationalDatabaseOptions,
|
|
38
38
|
} from "./runtime/managed-relational.ts";
|
|
39
|
+
// The portable lane: the binding facades a wrapper host projects, and the lane
|
|
40
|
+
// selector that proves a deployment's declared lane against the bindings that
|
|
41
|
+
// actually arrived.
|
|
42
|
+
export {
|
|
43
|
+
DEFAULT_RUNTIME_LANE,
|
|
44
|
+
RUNTIME_LANE_VAR,
|
|
45
|
+
RUNTIME_LANES,
|
|
46
|
+
RuntimeLaneError,
|
|
47
|
+
assertRuntimeLaneBindings,
|
|
48
|
+
resolveRuntimeLane,
|
|
49
|
+
type CloudflareWorkerBindings,
|
|
50
|
+
type PortableWorkerBindings,
|
|
51
|
+
type RuntimeLane,
|
|
52
|
+
wrapPortableBindings,
|
|
53
|
+
wrapRuntimeBindings,
|
|
54
|
+
wrapRuntimeMessageBatch,
|
|
55
|
+
} from "./runtime/lane.ts";
|
|
56
|
+
export {
|
|
57
|
+
EDGE_KV_MAX_EXPIRATION_TTL_SECONDS,
|
|
58
|
+
EDGE_KV_MIN_EXPIRATION_TTL_SECONDS,
|
|
59
|
+
isEdgeObjectsBinding,
|
|
60
|
+
isEdgeQueueBatch,
|
|
61
|
+
isEdgeSqlBinding,
|
|
62
|
+
isNativeD1Database,
|
|
63
|
+
isNativeR2Bucket,
|
|
64
|
+
type EdgeKvBinding,
|
|
65
|
+
type EdgeObjectsBinding,
|
|
66
|
+
type EdgeQueueBatch,
|
|
67
|
+
type EdgeQueueBinding,
|
|
68
|
+
type EdgeSqlBinding,
|
|
69
|
+
type EdgeSqlResult,
|
|
70
|
+
type EdgeSqlValue,
|
|
71
|
+
} from "./runtime/edge-facades.ts";
|
|
72
|
+
export {
|
|
73
|
+
EdgeKeyValueOptionError,
|
|
74
|
+
EdgeKeyValueStore,
|
|
75
|
+
EdgeKeyValueValueError,
|
|
76
|
+
wrapEdgeKv,
|
|
77
|
+
} from "./runtime/edge-kv.ts";
|
|
78
|
+
export {
|
|
79
|
+
EdgeSqlShapeError,
|
|
80
|
+
createEdgeSqlDatabase,
|
|
81
|
+
} from "./runtime/edge-sql.ts";
|
|
82
|
+
export {
|
|
83
|
+
ProxyColumnMismatchError,
|
|
84
|
+
positionalRow,
|
|
85
|
+
rewriteProjection,
|
|
86
|
+
type ProjectedStatement,
|
|
87
|
+
type RewrittenStatement,
|
|
88
|
+
} from "./runtime/sqlite-proxy-rows.ts";
|
|
89
|
+
export {
|
|
90
|
+
EdgeQueueShapeError,
|
|
91
|
+
wrapEdgeMessageBatch,
|
|
92
|
+
wrapEdgeQueue,
|
|
93
|
+
} from "./runtime/edge-queue.ts";
|
|
94
|
+
export {
|
|
95
|
+
EdgeObjectStorage,
|
|
96
|
+
EdgeObjectsShapeError,
|
|
97
|
+
wrapEdgeObjects,
|
|
98
|
+
} from "./runtime/edge-objects.ts";
|
|
39
99
|
export type {
|
|
40
100
|
IKeyValueStore,
|
|
41
101
|
ObjectStore,
|
|
@@ -234,6 +234,10 @@ media.post("/upload", async (c) => {
|
|
|
234
234
|
// transcode pipeline, and buffering a 40MB video would pressure the Worker
|
|
235
235
|
// memory budget.
|
|
236
236
|
if (isVideo) {
|
|
237
|
+
// The Blob is handed over whole rather than as a bare stream: an
|
|
238
|
+
// ObjectStore adapter reads `File.size` from it, so the portable
|
|
239
|
+
// `edge.objects` lane can declare the length while streaming instead of
|
|
240
|
+
// buffering the whole video in the Worker to discover it.
|
|
237
241
|
await media.put(r2Key, file, {
|
|
238
242
|
contentType,
|
|
239
243
|
});
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
assertPathChainWithinBasePath,
|
|
25
25
|
isPathWithinBasePath,
|
|
26
26
|
resolvePathWithinBasePath,
|
|
27
|
-
} from "./
|
|
27
|
+
} from "./node-paths.ts";
|
|
28
28
|
import { MemoryKV } from "./memory-kv.ts";
|
|
29
29
|
import { isBackendPath } from "../lib/backend-paths.ts";
|
|
30
30
|
import { loadBunSqlite } from "./compat-bun/types.ts";
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The portable binding facades a Takoserver-hosted Worker receives.
|
|
3
|
+
*
|
|
4
|
+
* A Worker Version published through Takoform onto a Takoserver Host does not
|
|
5
|
+
* get Cloudflare's native `KVNamespace` / `D1Database` / `Queue` / `R2Bucket`
|
|
6
|
+
* objects. The Host's generated entrypoint replaces `env` with an object whose
|
|
7
|
+
* bindings are the exact facades named by the Interface the Version declared —
|
|
8
|
+
* `edge.kv@1.0.0`, `edge.sql@1.0.0`, `edge.queue@1.0.0`, `edge.objects@1.0.0`.
|
|
9
|
+
* The managed Cloudflare backend and the self-host backend project the SAME
|
|
10
|
+
* facade: same methods, same option keys, same error names. Takoserver's
|
|
11
|
+
* ADR 0005 states this explicitly for object storage, and its self-host wrapper
|
|
12
|
+
* repeats it for all four.
|
|
13
|
+
*
|
|
14
|
+
* This module is a TYPE MIRROR of that contract plus the structural probes the
|
|
15
|
+
* lane selector uses. It deliberately contains no behaviour: the adapters that
|
|
16
|
+
* map a facade onto this repo's runtime ports live in `edge-kv.ts`,
|
|
17
|
+
* `edge-sql.ts`, `edge-queue.ts`, and `edge-objects.ts`.
|
|
18
|
+
*
|
|
19
|
+
* Source of truth (read, do not re-derive from memory):
|
|
20
|
+
* takoserver `src/providers/cloudflare-managed-worker-wrapper.ts`
|
|
21
|
+
* — `projectEnv`, `createKvAdapter`, `createSqlAdapter`,
|
|
22
|
+
* `createQueueAdapter`, `createEdgeObjectsR2Adapter`
|
|
23
|
+
* takoserver `src/providers/selfhost-worker-wrapper.ts`
|
|
24
|
+
* — `projectEnv`, `createKvAdapter`, `createSqlAdapter`,
|
|
25
|
+
* `createQueueAdapter`, `createObjectsAdapter`
|
|
26
|
+
*
|
|
27
|
+
* Every method rejects with an `Error` whose `name` is the portable error code
|
|
28
|
+
* (`invalid_key`, `invalid_value`, `value_too_large`, `metadata_too_large`,
|
|
29
|
+
* `invalid_cursor`, `invalid_argument`, `sql_error`, `numeric_out_of_range`,
|
|
30
|
+
* `busy`, `not_found`, `precondition_failed`, `range_not_satisfiable`,
|
|
31
|
+
* `invalid_body`, `message_too_large`, `batch_too_large`, `invalid_part`,
|
|
32
|
+
* `already_settled`, `backend_unavailable`). The adapters let those propagate
|
|
33
|
+
* unchanged so a caller sees the Host's own vocabulary.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Limits the facades enforce. Mirrored so the adapters can fail before the
|
|
37
|
+
* round-trip instead of surfacing an opaque `invalid_*` from the Host. */
|
|
38
|
+
export const EDGE_KV_MAX_KEY_BYTES = 467;
|
|
39
|
+
export const EDGE_KV_MAX_VALUE_BYTES = 26214400;
|
|
40
|
+
/** `expirationTtlSeconds` is rejected outside this range by both backends. */
|
|
41
|
+
export const EDGE_KV_MIN_EXPIRATION_TTL_SECONDS = 60;
|
|
42
|
+
export const EDGE_KV_MAX_EXPIRATION_TTL_SECONDS = 315360000;
|
|
43
|
+
export const EDGE_KV_MAX_LIST_LIMIT = 1000;
|
|
44
|
+
export const EDGE_SQL_MAX_STATEMENTS = 100;
|
|
45
|
+
export const EDGE_SQL_MAX_PARAMETERS = 100;
|
|
46
|
+
export const EDGE_SQL_MAX_ROWS = 10000;
|
|
47
|
+
export const EDGE_SQL_MAX_COLUMNS = 100;
|
|
48
|
+
export const EDGE_QUEUE_MAX_MESSAGES = 100;
|
|
49
|
+
|
|
50
|
+
/** A byte string on the wire. The facades never hand out raw `Uint8Array`. */
|
|
51
|
+
export interface EdgeEncodedBytes {
|
|
52
|
+
readonly encoding: "base64";
|
|
53
|
+
readonly data: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Exactly what `edge.sql` accepts as a bound parameter and returns in a row. */
|
|
57
|
+
export type EdgeSqlValue = null | number | string | EdgeEncodedBytes;
|
|
58
|
+
|
|
59
|
+
export interface EdgeSqlStatement {
|
|
60
|
+
readonly sql: string;
|
|
61
|
+
readonly params?: readonly EdgeSqlValue[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One statement's result. `rows` are RECORDS keyed by result-column name, not
|
|
66
|
+
* positional arrays — the single most consequential difference from D1, and the
|
|
67
|
+
* reason the lane has to rewrite the projection list (`sqlite-proxy-rows.ts`)
|
|
68
|
+
* before it can hand anything to Drizzle.
|
|
69
|
+
*/
|
|
70
|
+
export interface EdgeSqlResult {
|
|
71
|
+
readonly rows: readonly Readonly<Record<string, EdgeSqlValue>>[];
|
|
72
|
+
readonly rowsWritten: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `edge.sql@1.0.0`. */
|
|
76
|
+
export interface EdgeSqlBinding {
|
|
77
|
+
execute(
|
|
78
|
+
sql: string,
|
|
79
|
+
params?: readonly EdgeSqlValue[],
|
|
80
|
+
): Promise<EdgeSqlResult>;
|
|
81
|
+
/** `execute` restricted to statements that write nothing. */
|
|
82
|
+
query(sql: string, params?: readonly EdgeSqlValue[]): Promise<EdgeSqlResult>;
|
|
83
|
+
/** All-or-none. 1..100 statements, ordered, one Host round trip. */
|
|
84
|
+
transaction(
|
|
85
|
+
statements: readonly EdgeSqlStatement[],
|
|
86
|
+
): Promise<readonly EdgeSqlResult[]>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface EdgeKvPutOptions {
|
|
90
|
+
readonly expirationTtlSeconds?: number;
|
|
91
|
+
/** String values only; the Host projects a record of strings. */
|
|
92
|
+
readonly metadata?: Record<string, string>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface EdgeKvListOptions {
|
|
96
|
+
readonly prefix?: string;
|
|
97
|
+
readonly cursor?: string;
|
|
98
|
+
readonly limit?: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A listed key carries its NAME ONLY. Neither backend returns the expiration or
|
|
103
|
+
* the metadata it stored, so `IKeyValueStore.list` reports those as absent on
|
|
104
|
+
* this lane.
|
|
105
|
+
*/
|
|
106
|
+
export interface EdgeKvListResult {
|
|
107
|
+
readonly keys: readonly { readonly name: string }[];
|
|
108
|
+
readonly listComplete: boolean;
|
|
109
|
+
readonly cursor?: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `edge.kv@1.0.0`. Values are always bytes; there is no `type` option. */
|
|
113
|
+
export interface EdgeKvBinding {
|
|
114
|
+
get(key: string): Promise<ArrayBuffer | null>;
|
|
115
|
+
getWithMetadata(key: string): Promise<{
|
|
116
|
+
readonly value: ArrayBuffer;
|
|
117
|
+
readonly metadata?: Record<string, string>;
|
|
118
|
+
} | null>;
|
|
119
|
+
put(
|
|
120
|
+
key: string,
|
|
121
|
+
value: string | ArrayBuffer | ArrayBufferView,
|
|
122
|
+
options?: EdgeKvPutOptions,
|
|
123
|
+
): Promise<void>;
|
|
124
|
+
delete(key: string): Promise<void>;
|
|
125
|
+
list(options?: EdgeKvListOptions): Promise<EdgeKvListResult>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface EdgeQueueSendOptions {
|
|
129
|
+
readonly delaySeconds?: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface EdgeQueueBatchItem {
|
|
133
|
+
readonly body: string | ArrayBuffer | ArrayBufferView;
|
|
134
|
+
readonly delaySeconds?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `edge.queue@1.0.0` producer. Bodies are BYTES — there is no structured-clone
|
|
139
|
+
* path, so a JavaScript object has to be serialized by the caller. `send`
|
|
140
|
+
* returns the Host's acceptance id, which is not a provider dedupe id.
|
|
141
|
+
*/
|
|
142
|
+
export interface EdgeQueueBinding {
|
|
143
|
+
send(
|
|
144
|
+
body: string | ArrayBuffer | ArrayBufferView,
|
|
145
|
+
options?: EdgeQueueSendOptions,
|
|
146
|
+
): Promise<string>;
|
|
147
|
+
sendBatch(
|
|
148
|
+
messages: readonly EdgeQueueBatchItem[],
|
|
149
|
+
): Promise<readonly string[]>;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** One message as the Host hands it to a declared `queue` handler. */
|
|
153
|
+
export interface EdgeQueueMessage {
|
|
154
|
+
readonly id: string;
|
|
155
|
+
readonly timestampMillis: number;
|
|
156
|
+
readonly attempts: number;
|
|
157
|
+
readonly body: EdgeEncodedBytes;
|
|
158
|
+
acknowledge(): void;
|
|
159
|
+
/** `delaySeconds`, when given, must be >= 1. */
|
|
160
|
+
retry(options?: { readonly delaySeconds?: number }): void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface EdgeQueueBatch {
|
|
164
|
+
readonly batchId: string;
|
|
165
|
+
readonly queue: string;
|
|
166
|
+
readonly messages: readonly EdgeQueueMessage[];
|
|
167
|
+
acknowledgeAll(): void;
|
|
168
|
+
retryAll(options?: { readonly delaySeconds?: number }): void;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface EdgeObjectMetadata {
|
|
172
|
+
readonly etag: string;
|
|
173
|
+
readonly size: number;
|
|
174
|
+
readonly contentType?: string;
|
|
175
|
+
readonly uploadedAtMillis?: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface EdgeObjectBody extends EdgeObjectMetadata {
|
|
179
|
+
readonly body: ReadableStream;
|
|
180
|
+
readonly partial: boolean;
|
|
181
|
+
readonly range?: { readonly offset: number; readonly length: number };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface EdgeObjectListResult {
|
|
185
|
+
readonly objects: readonly (EdgeObjectMetadata & { readonly key: string })[];
|
|
186
|
+
readonly prefixes: readonly string[];
|
|
187
|
+
readonly truncated: boolean;
|
|
188
|
+
readonly cursor?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* `edge.objects@1.0.0`. Note the fixed arities — the Host counts
|
|
193
|
+
* `arguments.length`, so `get(key)` with one argument is a type error and the
|
|
194
|
+
* adapter must pass `undefined` explicitly. There is no `customMetadata`, and a
|
|
195
|
+
* streaming `put` requires `contentLength`.
|
|
196
|
+
*/
|
|
197
|
+
export interface EdgeObjectsBinding {
|
|
198
|
+
head(key: string): Promise<EdgeObjectMetadata | null>;
|
|
199
|
+
get(
|
|
200
|
+
key: string,
|
|
201
|
+
options:
|
|
202
|
+
undefined | { readonly range?: { offset: number; length?: number } },
|
|
203
|
+
): Promise<EdgeObjectBody | null>;
|
|
204
|
+
put(
|
|
205
|
+
key: string,
|
|
206
|
+
body: string | ArrayBuffer | ArrayBufferView | ReadableStream,
|
|
207
|
+
options:
|
|
208
|
+
| undefined
|
|
209
|
+
| {
|
|
210
|
+
readonly contentLength?: number;
|
|
211
|
+
readonly contentType?: string;
|
|
212
|
+
},
|
|
213
|
+
): Promise<{ readonly etag: string; readonly size: number }>;
|
|
214
|
+
delete(key: string): Promise<void>;
|
|
215
|
+
list(
|
|
216
|
+
options:
|
|
217
|
+
| undefined
|
|
218
|
+
| {
|
|
219
|
+
readonly prefix?: string;
|
|
220
|
+
readonly delimiter?: string;
|
|
221
|
+
readonly cursor?: string;
|
|
222
|
+
readonly limit?: number;
|
|
223
|
+
},
|
|
224
|
+
): Promise<EdgeObjectListResult>;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function hasMethods(value: unknown, names: readonly string[]): boolean {
|
|
228
|
+
if (typeof value !== "object" || value === null) return false;
|
|
229
|
+
const record = value as Record<string, unknown>;
|
|
230
|
+
for (const name of names) {
|
|
231
|
+
if (typeof record[name] !== "function") return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Structural probes.
|
|
238
|
+
*
|
|
239
|
+
* Only ONE binding can be told apart by shape, and the difference matters:
|
|
240
|
+
*
|
|
241
|
+
* decisive `DB` — `execute`/`query`/`transaction` (facade) against
|
|
242
|
+
* `prepare`/`batch` (D1). Disjoint method sets.
|
|
243
|
+
* decisive a queue *batch* — `acknowledgeAll` (facade) against `ackAll`
|
|
244
|
+
* (Cloudflare `MessageBatch`).
|
|
245
|
+
* AMBIGUOUS `KV` — `edge.kv` and `KVNamespace` expose the same five
|
|
246
|
+
* method names.
|
|
247
|
+
* AMBIGUOUS a queue *producer* — both are `send`/`sendBatch`.
|
|
248
|
+
* AMBIGUOUS `MEDIA` — `edge.objects@1.0.0` is R2's method set by design,
|
|
249
|
+
* multipart helpers included. See below.
|
|
250
|
+
*
|
|
251
|
+
* That is why the lane is a DECLARED variable rather than something sniffed:
|
|
252
|
+
* three of the five bindings cannot be identified at all. The declaration is
|
|
253
|
+
* then cross-checked against the decisive bindings, so a Worker whose var and
|
|
254
|
+
* whose bindings disagree refuses to start instead of calling
|
|
255
|
+
* `kv.get(key, {type})` on a facade that would silently treat the options
|
|
256
|
+
* object as nothing.
|
|
257
|
+
*/
|
|
258
|
+
export function isEdgeSqlBinding(value: unknown): value is EdgeSqlBinding {
|
|
259
|
+
return (
|
|
260
|
+
hasMethods(value, ["execute", "query", "transaction"]) &&
|
|
261
|
+
typeof (value as Record<string, unknown>).prepare !== "function"
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Cloudflare's `D1Database` is the `prepare`/`batch`/`exec` shape. */
|
|
266
|
+
export function isNativeD1Database(value: unknown): boolean {
|
|
267
|
+
return (
|
|
268
|
+
hasMethods(value, ["prepare", "batch"]) &&
|
|
269
|
+
typeof (value as Record<string, unknown>).execute !== "function"
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function isEdgeQueueBatch(value: unknown): value is EdgeQueueBatch {
|
|
274
|
+
return (
|
|
275
|
+
hasMethods(value, ["acknowledgeAll", "retryAll"]) &&
|
|
276
|
+
Array.isArray((value as Record<string, unknown>).messages)
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A bucket-shaped binding with `edge.objects@1.0.0`'s call signatures.
|
|
282
|
+
*
|
|
283
|
+
* NOT DECISIVE, and never a lane test. The facade is R2's method set on
|
|
284
|
+
* purpose — `head`, `get`, `put`, `delete`, `list`, and the four multipart
|
|
285
|
+
* calls — so that an app written against R2 ports over unchanged. A real
|
|
286
|
+
* `R2Bucket` therefore answers `true` here too. Use it to check that SOMETHING
|
|
287
|
+
* bucket-shaped arrived, never to decide which host projected it: that is what
|
|
288
|
+
* `YURUCOMMU_RUNTIME_LANE` is for.
|
|
289
|
+
*
|
|
290
|
+
* The arity check is the facade's own contract rather than a discriminator:
|
|
291
|
+
* the Host counts `arguments.length`, so `get` takes its options slot even when
|
|
292
|
+
* that slot is `undefined`.
|
|
293
|
+
*/
|
|
294
|
+
export function isEdgeObjectsBinding(
|
|
295
|
+
value: unknown,
|
|
296
|
+
): value is EdgeObjectsBinding {
|
|
297
|
+
return (
|
|
298
|
+
hasMethods(value, ["head", "get", "put", "delete", "list"]) &&
|
|
299
|
+
(value as { get: (...args: unknown[]) => unknown }).get.length === 2
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Cloudflare's `R2Bucket` — and, unavoidably, the `edge.objects` facade.
|
|
305
|
+
*
|
|
306
|
+
* A method-name test cannot separate the two, because Takoserver's facade
|
|
307
|
+
* carries `createMultipartUpload` as well (`selfhost-worker-wrapper.ts`
|
|
308
|
+
* `createObjectsAdapter`). 4.1.0 used this function to refuse the portable
|
|
309
|
+
* lane and so refused every self-hosted deployment. Keep it for describing a
|
|
310
|
+
* binding; do not let it decide a lane.
|
|
311
|
+
*/
|
|
312
|
+
export function isNativeR2Bucket(value: unknown): boolean {
|
|
313
|
+
return hasMethods(value, [
|
|
314
|
+
"head",
|
|
315
|
+
"get",
|
|
316
|
+
"put",
|
|
317
|
+
"delete",
|
|
318
|
+
"list",
|
|
319
|
+
"createMultipartUpload",
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Decode one `{encoding:"base64"}` value into bytes. */
|
|
324
|
+
export function decodeEdgeBytes(value: EdgeEncodedBytes): Uint8Array {
|
|
325
|
+
const binary = atob(value.data);
|
|
326
|
+
const bytes = new Uint8Array(binary.length);
|
|
327
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
328
|
+
bytes[index] = binary.charCodeAt(index);
|
|
329
|
+
}
|
|
330
|
+
return bytes;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Encode bytes into the facade's wire value. */
|
|
334
|
+
export function encodeEdgeBytes(bytes: Uint8Array): EdgeEncodedBytes {
|
|
335
|
+
let binary = "";
|
|
336
|
+
// Chunked so a large blob does not blow the argument limit of `apply`.
|
|
337
|
+
const CHUNK = 0x8000;
|
|
338
|
+
for (let index = 0; index < bytes.length; index += CHUNK) {
|
|
339
|
+
binary += String.fromCharCode(
|
|
340
|
+
...bytes.subarray(index, Math.min(index + CHUNK, bytes.length)),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return { encoding: "base64", data: btoa(binary) };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function isEdgeEncodedBytes(value: unknown): value is EdgeEncodedBytes {
|
|
347
|
+
return (
|
|
348
|
+
typeof value === "object" &&
|
|
349
|
+
value !== null &&
|
|
350
|
+
(value as EdgeEncodedBytes).encoding === "base64" &&
|
|
351
|
+
typeof (value as EdgeEncodedBytes).data === "string"
|
|
352
|
+
);
|
|
353
|
+
}
|