@helipod/runtime-cloudflare-shard 0.1.0
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/LICENSE +11 -0
- package/README.md +120 -0
- package/dist/index.d.ts +266 -0
- package/dist/index.js +390 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Helipod Commercial License (Enterprise)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Helipod.
|
|
4
|
+
|
|
5
|
+
This directory (ee/) and everything under it is NOT covered by the license of the
|
|
6
|
+
rest of this repository. It is source-available for reference and, at present, free
|
|
7
|
+
to use in production (no license key is required yet; future releases will gate
|
|
8
|
+
paid entitlements behind a commercial license key). It may not be copied,
|
|
9
|
+
modified, or distributed as part of a product or service that competes with
|
|
10
|
+
Helipod. A commercial license activating paid entitlements will govern future
|
|
11
|
+
releases of this directory; code in ee/ never converts to an open-source license.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# @helipod/runtime-cloudflare-shard (Enterprise)
|
|
2
|
+
|
|
3
|
+
> Licensed under the **Helipod Commercial License** — see [`ee/LICENSE`](../../LICENSE). This is a
|
|
4
|
+
> paid-tier (scale-out) package. Single-node Cloudflare self-host stays free forever via the
|
|
5
|
+
> [`@helipod/runtime-cloudflare`](../../../packages/runtime-cloudflare) package.
|
|
6
|
+
|
|
7
|
+
**Slice 6, Milestone 1 — multi-shard write scale-out on Cloudflare Durable Objects.** `.shardBy(key)`
|
|
8
|
+
→ one Durable Object per shard key. N distinct keys ⇒ N distinct single-threaded DOs ⇒ **N× the
|
|
9
|
+
single-DO ~200–500 writes/s ceiling AND N× the 10 GB/DO storage ceiling**.
|
|
10
|
+
|
|
11
|
+
This is the Cloudflare-native analog of [`ee/fleet`](../fleet) (the portable Postgres/object-store
|
|
12
|
+
multi-node scale-out) — a **sibling, not a consumer**: it uses Cloudflare's native
|
|
13
|
+
`namespace.getByName(shardKey)` addressing instead of leases/forwarders, and has **no `ee/fleet`
|
|
14
|
+
dependency**.
|
|
15
|
+
|
|
16
|
+
## The routing model
|
|
17
|
+
|
|
18
|
+
A **stateless Worker** (`createShardWorkerHandler`) resolves each request to the ONE owning shard-DO
|
|
19
|
+
and forwards to it. Each shard-DO is an **unmodified** `HelipodDurableObject` from the free
|
|
20
|
+
`@helipod/runtime-cloudflare` package (M1 reuses Slice 3 verbatim — no engine change). The DO **name
|
|
21
|
+
is the shard key**: there is no shard map, no coordinator, no reshard (a new key just addresses a new
|
|
22
|
+
DO forever).
|
|
23
|
+
|
|
24
|
+
A request's shard key is sourced, in precedence order:
|
|
25
|
+
|
|
26
|
+
1. **Explicit envelope** — `X-Helipod-Shard: <value>` header or `?shard=<value>` query param. The
|
|
27
|
+
primary mechanism, and the only one that works for a WebSocket upgrade (a shard-scoped socket:
|
|
28
|
+
`wss://…/api/sync?shard=<roomId>`) and for a query (queries declare no `shardBy`).
|
|
29
|
+
2. **Derived** — for `POST /api/run`, a mutation declaring `shardBy` has its key extracted from the
|
|
30
|
+
args exactly as the engine's executor does.
|
|
31
|
+
3. **Default** — no key ⇒ the `"default"` DO (unsharded tables + no-`shardBy` mutations), byte-identical
|
|
32
|
+
to a single-shard Slice-3 deploy.
|
|
33
|
+
|
|
34
|
+
Two routing modes: **`"key"`** (default — one DO per distinct value, collision-safe name encoding) or
|
|
35
|
+
**`"hash"`** (fixed-N jump-consistent-hash, byte-identical `ShardId` to the portable path).
|
|
36
|
+
|
|
37
|
+
The licensing switch is the app's Worker entry: a single-shard app `export default
|
|
38
|
+
createWorkerHandler(...)` (free package); a multi-shard app `export default createShardWorkerHandler(...)`
|
|
39
|
+
(this package). Nothing in the free package imports this one.
|
|
40
|
+
|
|
41
|
+
## Geographic placement — this is why sharding scales *geographically*
|
|
42
|
+
|
|
43
|
+
A Durable Object is **single-homed**: it is pinned to **one** data center at creation and **never
|
|
44
|
+
moves**. By default it lands near whoever **first** `get()`s it. `get(id, { locationHint })` overrides
|
|
45
|
+
that placement — but **only the first `get()` for a given DO is honored** (the DO is pinned
|
|
46
|
+
thereafter; see the [data-location reference](https://developers.cloudflare.com/durable-objects/reference/data-location/)).
|
|
47
|
+
|
|
48
|
+
This is *why* multi-shard is the unit of geographic scale-out: each shard-DO is a distinct object, so
|
|
49
|
+
**each can be placed near its own audience** — `roomTokyo` in `apac-ne`, `roomBerlin` in `weur` — at the
|
|
50
|
+
same time, with no shared bottleneck between them. The router derives that hint per request, from the
|
|
51
|
+
envelope, in precedence order (each **stable per shard key** where possible, so the one `get()` that
|
|
52
|
+
counts is deterministic):
|
|
53
|
+
|
|
54
|
+
1. **Explicit** — `?region=<hint>` param or `X-Helipod-Region: <hint>` header. App-controlled and
|
|
55
|
+
fully deterministic (mirrors `?shard=`). An invalid explicit hint is a hard `INVALID_REGION_HINT`
|
|
56
|
+
400 at the edge — never passed to `get()`, because a bad hint would mis-place the DO **permanently**.
|
|
57
|
+
2. **Region-prefixed key (opt-in)** — with `regionPrefixedKeys: true`, a shard-key value of the form
|
|
58
|
+
`"<hint>:<rest>"` (e.g. `"enam:room123"`) derives its hint from the prefix. Off by default (no app is
|
|
59
|
+
forced into a key format); the **full** key value still names the DO — the prefix is read for
|
|
60
|
+
placement only.
|
|
61
|
+
3. **Auto from origin** — `request.cf.continent` mapped to the nearest hint, placing a **new** shard
|
|
62
|
+
near the user who first creates it. This is *also* Cloudflare's own default, so it is a made-explicit
|
|
63
|
+
convenience, not a new guarantee — and it is **first-requester-wins** (not stable across requesters).
|
|
64
|
+
4. **Default** — no hint. The router forwards with **no options bag** — byte-identical to the pre-hint
|
|
65
|
+
behavior.
|
|
66
|
+
|
|
67
|
+
Valid hints are the 11 Cloudflare region codes (`wnam enam sam weur eeur apac apac-ne apac-se oc afr
|
|
68
|
+
me`); jurisdictions (`eu`/`fedramp`) are a **separate** mechanism, not a `locationHint` value.
|
|
69
|
+
|
|
70
|
+
**Honest boundary:** the **reactive/write path always routes to the DO's home** — a subscriber and a
|
|
71
|
+
committer for `roomTokyo` both reach the Tokyo-placed DO wherever *they* are. Placement optimizes for
|
|
72
|
+
the **audience of a shard**, so place a shard near where most of its traffic originates. Routing itself
|
|
73
|
+
is a stateless, **O(1)** name derivation (no shard map, no coordinator, no lock) — so it stays constant
|
|
74
|
+
regardless of how many shards exist. True cross-region latency improvement can only be *measured* from a
|
|
75
|
+
distributed load test against a real deploy (see [`rig/README.md`](./rig/README.md)); the real-workerd
|
|
76
|
+
suite proves the hint is threaded and the routing is O(1), not the latency delta.
|
|
77
|
+
|
|
78
|
+
## Non-goals (M1 — enforced with a typed error, never silently broken)
|
|
79
|
+
|
|
80
|
+
- A **reactive** query/mutation spanning **multiple shards** → rejected with
|
|
81
|
+
`CROSS_SHARD_UNSUPPORTED` (HTTP 400). A shard-scoped reactive subscription lives on exactly one DO,
|
|
82
|
+
so Slice 3's in-DO G1/G4 ordering carries over unchanged; a cross-shard reactive query would re-open
|
|
83
|
+
distributed cross-shard invalidation — deferred by construction.
|
|
84
|
+
- **Cross-shard global-unique** → Milestone 2 (`.global()`/D1). Uniqueness **within one shard key** is
|
|
85
|
+
free (a per-DO local index).
|
|
86
|
+
- A `.shardBy` mutation whose args omit the shard key → rejected with `SHARD_KEY_REQUIRED`.
|
|
87
|
+
- **`.global()`/D1** → Milestone 2 (not yet built).
|
|
88
|
+
|
|
89
|
+
## Cross-shard `fanOut` reads (Milestone 2d — shipped)
|
|
90
|
+
|
|
91
|
+
A **non-reactive, one-shot** cross-shard read: `POST /api/run?fanout=1 { path, args }` (or
|
|
92
|
+
`X-Helipod-Fanout: true`) fans the SAME request out to every shard-DO, concatenates each shard's
|
|
93
|
+
`{ value: [...] }` array, and returns one merged `{ value }` response. It requires a **fixed shard
|
|
94
|
+
count** — routing mode `"hash"` (`shardIdList(numShards)` is the enumerable shard set); mode `"key"`
|
|
95
|
+
(one DO per arbitrary key, no directory) has no way to enumerate "every shard", so it's rejected
|
|
96
|
+
`FANOUT_REQUIRES_FIXED_SHARDS`. Also rejected: `fanOut` + an explicit `?shard=` key
|
|
97
|
+
(`FANOUT_WITH_SHARD_KEY` — target one shard OR fan out, never both), `fanOut` on the WebSocket
|
|
98
|
+
`/api/sync` upgrade (`FANOUT_NOT_SUBSCRIBABLE` — fanOut is one-shot, not reactive), and `fanOut` of a
|
|
99
|
+
`.shardBy` mutation (fanOut is a read). A shard that throws, times out, responds non-200, or returns a
|
|
100
|
+
non-array `value` is **failures-as-data**: recorded in a `partial.failedShards` entry rather than
|
|
101
|
+
failing the whole request, so a slow/broken shard degrades the read instead of blocking it. v1 is a
|
|
102
|
+
plain concatenation (no ordered k-way merge, no global result limit — each shard already applies its
|
|
103
|
+
own query's `.take(n)`); the fan-out pool is bounded (`FANOUT_CONCURRENCY`) with a per-shard timeout.
|
|
104
|
+
|
|
105
|
+
## Proof
|
|
106
|
+
|
|
107
|
+
- **Node unit suite** (`bun run test`): routing, canonicalization, `fanOut` routing/worker guards
|
|
108
|
+
(scripted fake namespace), and the one-way licensing gate.
|
|
109
|
+
- **Real-workerd suite** (`bun run test:workers`, via `@cloudflare/vitest-pool-workers`): chains TWO
|
|
110
|
+
projects — `vitest.workers.config.ts` (mode `"key"` fixture, `test-workers/*.worker.test.ts`): the
|
|
111
|
+
full Worker→shard-DO path on genuine Durable Objects — distinct keys → distinct DOs, shard isolation,
|
|
112
|
+
shard-scoped reactive push + isolation, `fanOut` rejection (`FANOUT_REQUIRES_FIXED_SHARDS`, no fixed
|
|
113
|
+
shard set), independent commits; and `vitest.workers.hash.config.ts` (a SEPARATE mode-`"hash"`,
|
|
114
|
+
`numShards: 4` fixture — `test-workers/test-worker-hash.ts` / `wrangler.hash.jsonc` —
|
|
115
|
+
`fanOut` needs an enumerable shard set the mode-`"key"` fixture structurally doesn't have):
|
|
116
|
+
`test-workers/fanout.worker.test.ts` proves the union-across-shards concat, both rejection guards, and
|
|
117
|
+
a GENUINE single-shard failure (one shard-DO's own query handler throws for real, not a mocked
|
|
118
|
+
namespace) degrading to `partial.failedShards` — all on real Durable Objects.
|
|
119
|
+
- **Deploy rig** (`rig/`): the human-run real-Cloudflare multi-shard E2E (this worktree has no CF
|
|
120
|
+
login) — see [`rig/README.md`](./rig/README.md).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { WorkerHandler } from '@helipod/runtime-cloudflare';
|
|
2
|
+
export { DEFAULT_SHARD_NAME, DurableObjectAppConfig, HelipodDurableObject } from '@helipod/runtime-cloudflare';
|
|
3
|
+
import { LoadedProject } from '@helipod/cli/project';
|
|
4
|
+
|
|
5
|
+
/** The DO name for unsharded tables / no-`shardBy` requests — byte-identical to Slice 3's single DO. */
|
|
6
|
+
declare const DEFAULT_SHARD_DO_NAME: string;
|
|
7
|
+
type ShardRoutingMode = "key" | "hash";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve a shard-key value to its owning DO name.
|
|
10
|
+
*
|
|
11
|
+
* @param value the shard-key value extracted from the request (a scalar the engine can index).
|
|
12
|
+
* @param mode routing mode; "key" (default) or "hash".
|
|
13
|
+
* @param numShards for mode "hash" only: the fixed shard count N (≥ 1). Ignored in mode "key".
|
|
14
|
+
*/
|
|
15
|
+
declare function shardDoName(value: unknown, mode?: ShardRoutingMode, numShards?: number): string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Typed routing rejections (M1). The router never silently serves partial/wrong data — a request it
|
|
19
|
+
* cannot pin to exactly ONE owning shard-DO is rejected LOUDLY with a coded 400, the same discipline
|
|
20
|
+
* the engine's kernel shard guards use (`ForbiddenOperationError` names the table/field/fix). These
|
|
21
|
+
* codes cross the wire as `{ error: { code, message } }` so a client can branch on `code`.
|
|
22
|
+
*/
|
|
23
|
+
/** A `.shardBy` mutation reached the router without its shard-key value in the args. Mirrors the
|
|
24
|
+
* engine kernel's "declares a shard but does not carry it" guard, enforced at the edge so a sharded
|
|
25
|
+
* write is never mis-routed to the `"default"` DO (where it would land on the wrong data). */
|
|
26
|
+
declare const SHARD_KEY_REQUIRED = "SHARD_KEY_REQUIRED";
|
|
27
|
+
/** A request explicitly asked to span multiple shard-DOs (a fan-out flag, or a multi-valued shard
|
|
28
|
+
* key). M1 supports only shard-SCOPED operations — a cross-shard query/mutation is a hard non-goal
|
|
29
|
+
* (§2.1: reactive cross-shard is refused; a non-reactive `fanOut` read lands in M2). Rejected here
|
|
30
|
+
* rather than fanned out and merged, so no caller ever receives a silently-partial result. */
|
|
31
|
+
declare const CROSS_SHARD_UNSUPPORTED = "CROSS_SHARD_UNSUPPORTED";
|
|
32
|
+
/** An EXPLICIT region hint (`?region=` / `X-Helipod-Region`) was supplied but is not one of
|
|
33
|
+
* Cloudflare's 11 `locationHint` region codes. Rejected LOUDLY at the edge rather than passed to
|
|
34
|
+
* `get(id, { locationHint })`: a bad hint would place the shard-DO in an unintended data center
|
|
35
|
+
* PERMANENTLY (a DO is single-homed and pinned on its first `get()`), so we refuse it up front and
|
|
36
|
+
* name the fix, exactly the same discipline the shard guards use. */
|
|
37
|
+
declare const INVALID_REGION_HINT = "INVALID_REGION_HINT";
|
|
38
|
+
/** A `fanOut` request hit a deployment with no enumerable shard set — routing mode "key" (the
|
|
39
|
+
* default) addresses a new DO per key value with no directory, and Cloudflare has no DO-listing API,
|
|
40
|
+
* so "all shards" is undefined. fanOut requires a fixed-shard-count deployment (mode "hash"). */
|
|
41
|
+
declare const FANOUT_REQUIRES_FIXED_SHARDS = "FANOUT_REQUIRES_FIXED_SHARDS";
|
|
42
|
+
/** A request asked to `fanOut` AND named a shard key. You either target one shard (a shard key) OR
|
|
43
|
+
* fan out across all shards, never both. */
|
|
44
|
+
declare const FANOUT_WITH_SHARD_KEY = "FANOUT_WITH_SHARD_KEY";
|
|
45
|
+
/** A `fanOut` was requested on the WebSocket `/api/sync` upgrade. fanOut is a non-reactive one-shot
|
|
46
|
+
* read (fanning invalidation across every shard reactively is out of scope) — it works only over
|
|
47
|
+
* the one-shot `POST /api/run` path. */
|
|
48
|
+
declare const FANOUT_NOT_SUBSCRIBABLE = "FANOUT_NOT_SUBSCRIBABLE";
|
|
49
|
+
/** A `fanOut` request's target is not a resolved `query` — a mutation (sharded or not), an action,
|
|
50
|
+
* or a function whose type could not be classified at all (no `loaded` module set at resolve time,
|
|
51
|
+
* an unparseable/non-`POST /api/run` request, or an unknown `path`). fanOut is READ-ONLY: fanning a
|
|
52
|
+
* mutation or action out to every shard-DO would multiply ONE logical write into N commits/side
|
|
53
|
+
* effects, one per shard. An unclassifiable target fails CLOSED under this same code — a request
|
|
54
|
+
* that cannot be PROVEN to name a query is never fanned out on the assumption that it might be one. */
|
|
55
|
+
declare const FANOUT_NOT_A_QUERY = "FANOUT_NOT_A_QUERY";
|
|
56
|
+
type ShardRoutingErrorCode = typeof SHARD_KEY_REQUIRED | typeof CROSS_SHARD_UNSUPPORTED | typeof INVALID_REGION_HINT | typeof FANOUT_REQUIRES_FIXED_SHARDS | typeof FANOUT_WITH_SHARD_KEY | typeof FANOUT_NOT_SUBSCRIBABLE | typeof FANOUT_NOT_A_QUERY;
|
|
57
|
+
interface ShardRoutingErrorBody {
|
|
58
|
+
error: {
|
|
59
|
+
code: ShardRoutingErrorCode;
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
declare function routingError(code: ShardRoutingErrorCode, message: string): ShardRoutingErrorBody;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The stateless shard-routing decision (M1) — pure, host-agnostic, unit-testable under plain Node.
|
|
67
|
+
* Given a request, decide the ONE owning shard-DO name (or a typed rejection). The Worker
|
|
68
|
+
* (`worker.ts`) wraps this and forwards to `env[binding].get(idFromName(name))`.
|
|
69
|
+
*
|
|
70
|
+
* How a request's shard key is sourced, in precedence order (spec §1.2 — the shard key rides the
|
|
71
|
+
* ENVELOPE, Lunora's model, so routing is a pure Worker concern and the DO never parses a row to learn
|
|
72
|
+
* its own name):
|
|
73
|
+
*
|
|
74
|
+
* 1. **Explicit** — an `X-Helipod-Shard: <value>` header or a `?shard=<value>` query param. This
|
|
75
|
+
* is the primary mechanism and the ONLY one that works for a WebSocket upgrade (which has no
|
|
76
|
+
* readable body) and for a query (which declares no `shardBy`). A room/tenant/user socket opens
|
|
77
|
+
* `…/api/sync?shard=<roomId>`; every op on it runs on that room's DO.
|
|
78
|
+
* 2. **Derived** — for `POST /api/run { path, args }`, if no explicit key is present, look the
|
|
79
|
+
* function up in the bundled `loaded` project and, if it is a mutation declaring `shardBy`,
|
|
80
|
+
* extract the shard-key value from `args` EXACTLY as the executor does
|
|
81
|
+
* (`packages/executor/src/executor.ts` — a string names an arg, a function is a resolver). This
|
|
82
|
+
* reuses the engine's own convention verbatim, so the edge and the kernel agree on the key.
|
|
83
|
+
* 3. **Default** — no key ⇒ the `"default"` DO (unsharded tables + no-`shardBy` mutations), exactly
|
|
84
|
+
* Slice 3.
|
|
85
|
+
*
|
|
86
|
+
* Cross-shard is REFUSED, never fanned out (M1 non-goal, §2.1): an explicit fan-out flag
|
|
87
|
+
* (`X-Helipod-Fanout: true` / `?fanout=1`) or a multi-valued shard key (a comma) is a typed
|
|
88
|
+
* `CROSS_SHARD_UNSUPPORTED` 400. A `.shardBy` mutation whose args omit the key is `SHARD_KEY_REQUIRED`.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
interface ShardRoutingOptions {
|
|
92
|
+
/** Routing mode: "key" (A, default — one DO per key) or "hash" (B — fixed-N jump-hash). §1.1. */
|
|
93
|
+
mode?: ShardRoutingMode;
|
|
94
|
+
/** For mode "hash": the fixed shard count N (≥ 1). Ignored in mode "key". */
|
|
95
|
+
numShards?: number;
|
|
96
|
+
/**
|
|
97
|
+
* The bundled app, used ONLY to DERIVE a mutation's shard key from `POST /api/run` args (source #2).
|
|
98
|
+
* Optional: omit it and routing falls back to the explicit envelope key + default (still correct;
|
|
99
|
+
* derivation is a convenience so a client need not know each mutation's shard-key field name).
|
|
100
|
+
*/
|
|
101
|
+
loaded?: LoadedProject;
|
|
102
|
+
/**
|
|
103
|
+
* Opt-in placement source (b): read a `"<hint>:<rest>"` region prefix off the shard-key value to
|
|
104
|
+
* derive the DO's `locationHint` (e.g. `"enam:room123"` → `enam`). OFF by default — an app that does
|
|
105
|
+
* not want a key format is never forced into one. The prefix is only READ for placement; the full
|
|
106
|
+
* key value still names the DO.
|
|
107
|
+
*/
|
|
108
|
+
regionPrefixedKeys?: boolean;
|
|
109
|
+
}
|
|
110
|
+
type ShardResolution = {
|
|
111
|
+
kind: "shard";
|
|
112
|
+
name: string;
|
|
113
|
+
/** The geographic `locationHint` to place a NEWLY-created shard-DO near its audience (source
|
|
114
|
+
* a/b/c). Absent ⇒ the router forwards with NO options bag (byte-identical to the pre-hint
|
|
115
|
+
* behavior; Cloudflare places the DO near the first requester). */
|
|
116
|
+
locationHint?: string;
|
|
117
|
+
} | {
|
|
118
|
+
kind: "fanout";
|
|
119
|
+
/** Every shard-DO name to fan this request out to (mode "hash" only — the enumerable
|
|
120
|
+
* `shardIdList(numShards)`, e.g. `["default", "s1", "s2", …]`). */
|
|
121
|
+
shardIds: string[];
|
|
122
|
+
} | {
|
|
123
|
+
kind: "error";
|
|
124
|
+
status: number;
|
|
125
|
+
body: ShardRoutingErrorBody;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the owning shard-DO name for a request (or a typed rejection). Reads the request body only
|
|
129
|
+
* when needed (a `POST /api/run` with no explicit key + a `loaded` project); callers MUST forward the
|
|
130
|
+
* ORIGINAL request, never the clone this reads.
|
|
131
|
+
*/
|
|
132
|
+
declare function resolveShard(request: Request, opts?: ShardRoutingOptions): Promise<ShardResolution>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The multi-shard stateless Worker router (M1). The DO-native analog of `ee/fleet` for the portable
|
|
136
|
+
* path — but a SIBLING, not a consumer: Cloudflare's native `namespace.getByName(shardKey)` addressing
|
|
137
|
+
* replaces the lease/forwarder mechanism entirely. This handler holds NO state; it resolves the ONE
|
|
138
|
+
* owning shard-DO name from the request envelope (`resolveShard`) and forwards the ORIGINAL request to
|
|
139
|
+
* it. Each shard-DO is an UNMODIFIED `HelipodDurableObject` from the FREE `@helipod/runtime-cloudflare`
|
|
140
|
+
* package (reused verbatim — N distinct keys ⇒ N distinct DOs ⇒ N× the single-DO write ceiling AND
|
|
141
|
+
* N×10 GB storage). No engine change: a shard-DO differs from Slice 3 in nothing.
|
|
142
|
+
*
|
|
143
|
+
* Licensing (locked): this package DEPENDS ON the free single-shard host and reuses its DO class;
|
|
144
|
+
* nothing in `@helipod/runtime-cloudflare` imports back into here. The single-shard vs multi-shard
|
|
145
|
+
* choice is made at the APP'S Worker entry (which handler it `export default`s), never by a runtime
|
|
146
|
+
* gate — so a free single-node deploy never statically links this ee code.
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build the multi-shard Worker `fetch` handler. `bindingName` is the shard-DO `durable_objects`
|
|
151
|
+
* binding in `wrangler.jsonc` (e.g. `"HELIPOD_DO"`); `opts` selects the routing mode and — for the
|
|
152
|
+
* derive-from-args convenience — the bundled `loaded` app.
|
|
153
|
+
*/
|
|
154
|
+
declare function createShardWorkerHandler(bindingName: string, opts?: ShardRoutingOptions): WorkerHandler;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Map a Cloudflare `request.cf.continent` code (`AF`/`AN`/`AS`/`EU`/`NA`/`OC`/`SA`) to the nearest
|
|
158
|
+
* `locationHint`. Continent granularity is deliberately coarse (source (c) is a convenience, not a
|
|
159
|
+
* precision tool — an app that needs finer placement uses the explicit or prefixed sources). Antarctica
|
|
160
|
+
* (`AN`) is intentionally absent ⇒ no hint (there is no sensible nearby DO region). North America maps
|
|
161
|
+
* to `enam` and Europe to `weur` as the documented continent-level defaults; reach for `?region=wnam`/
|
|
162
|
+
* `eeur`/`apac-ne`/… when a finer placement matters.
|
|
163
|
+
*/
|
|
164
|
+
declare const CONTINENT_TO_HINT: Readonly<Record<string, string>>;
|
|
165
|
+
/** Resolve a continent code to a hint, or `undefined` if it is unknown/unmapped. */
|
|
166
|
+
declare function continentToHint(continent: string | undefined | null): string | undefined;
|
|
167
|
+
/** The `cf` object workerd attaches to an inbound Request (only the fields we read, structurally). */
|
|
168
|
+
interface RequestCfLike {
|
|
169
|
+
continent?: string;
|
|
170
|
+
}
|
|
171
|
+
/** The outcome of hint derivation: a (possibly absent) valid hint, or a typed rejection for an
|
|
172
|
+
* explicit-but-invalid region. `hint: undefined` is the DEFAULT case — the caller passes no options
|
|
173
|
+
* bag, byte-identical to the pre-hint behavior. */
|
|
174
|
+
type HintDerivation = {
|
|
175
|
+
ok: true;
|
|
176
|
+
hint: string | undefined;
|
|
177
|
+
} | {
|
|
178
|
+
ok: false;
|
|
179
|
+
invalidRegion: string;
|
|
180
|
+
};
|
|
181
|
+
interface DeriveLocationHintInput {
|
|
182
|
+
url: URL;
|
|
183
|
+
headers: Headers;
|
|
184
|
+
/** `request.cf` if present (workerd populates it; `undefined` under Node/tests). */
|
|
185
|
+
cf?: RequestCfLike;
|
|
186
|
+
/** The resolved shard-key VALUE, used ONLY by the opt-in region-prefix source (b). */
|
|
187
|
+
shardKeyValue?: unknown;
|
|
188
|
+
/** Enable source (b): read a `"<hint>:<rest>"` prefix off the shard-key value. Off by default. */
|
|
189
|
+
regionPrefixedKeys?: boolean;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Derive the placement hint for a request, in precedence order (a) → (d). Returns `{ ok: true, hint }`
|
|
193
|
+
* (hint possibly `undefined`) or `{ ok: false, invalidRegion }` when an EXPLICIT region hint is invalid.
|
|
194
|
+
*/
|
|
195
|
+
declare function deriveLocationHint(input: DeriveLocationHintInput): HintDerivation;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Pure generator for a MULTI-SHARD Cloudflare Worker/DO entrypoint — the ee twin of the free host's
|
|
199
|
+
* `generateWorkerEntrySource` (`packages/runtime-cloudflare/src/worker-entry.ts`). Same static-import
|
|
200
|
+
* discipline (a Worker bundle can no more dir-scan `convex/` than a `bun build --compile` binary can),
|
|
201
|
+
* and it emits the SAME concrete DO class — a plain `HelipodDurableObject` subclass from the FREE
|
|
202
|
+
* package, unmodified (M1: a shard-DO IS Slice 3). The ONLY difference from the single-shard entry is
|
|
203
|
+
* the `export default`: `createShardWorkerHandler(...)` (this ee package) instead of
|
|
204
|
+
* `createWorkerHandler(...)` (the free package).
|
|
205
|
+
*
|
|
206
|
+
* This is the licensing switch made concrete: an app's Worker entry chooses single- vs multi-shard by
|
|
207
|
+
* WHICH handler it default-exports. The free host never imports this generator.
|
|
208
|
+
*
|
|
209
|
+
* File storage (`r2BindingName`) is wired the same way the free generator wires it: since every
|
|
210
|
+
* shard-DO is that SAME unmodified `HelipodDurableObject`, its `appConfig()` already knows how to use
|
|
211
|
+
* a `blobStore` — this generator just needs to construct one from the R2 binding and thread it through,
|
|
212
|
+
* identically for every shard.
|
|
213
|
+
*/
|
|
214
|
+
interface ShardWorkerEntryInputs {
|
|
215
|
+
moduleImports: Array<{
|
|
216
|
+
key: string;
|
|
217
|
+
absPath: string;
|
|
218
|
+
}>;
|
|
219
|
+
schemaAbsPath: string;
|
|
220
|
+
configAbsPath: string | null;
|
|
221
|
+
/** The shard-DO `durable_objects` binding name (e.g. `"HELIPOD_DO"`). */
|
|
222
|
+
bindingName: string;
|
|
223
|
+
/** The exported DO class name the `wrangler.jsonc` migration references (e.g. `"HelipodDO"`). */
|
|
224
|
+
doClassName: string;
|
|
225
|
+
/** Routing mode: "key" (default) or "hash". */
|
|
226
|
+
mode?: "key" | "hash";
|
|
227
|
+
/** For mode "hash": the fixed shard count N. */
|
|
228
|
+
numShards?: number;
|
|
229
|
+
/** Opt-in geographic placement source (b): derive a shard-DO's `locationHint` from a `"<hint>:<rest>"`
|
|
230
|
+
* prefix on the shard-key value. Off by default. */
|
|
231
|
+
regionPrefixedKeys?: boolean;
|
|
232
|
+
/** The env var the admin key is read from at DO boot (default `HELIPOD_ADMIN_KEY`). */
|
|
233
|
+
adminKeyEnvVar?: string;
|
|
234
|
+
/** Override the free host package import specifier (the in-repo fixture uses this). */
|
|
235
|
+
hostPackage?: string;
|
|
236
|
+
/** Override the ee shard package import specifier (the in-repo fixture uses this). */
|
|
237
|
+
shardPackage?: string;
|
|
238
|
+
/**
|
|
239
|
+
* The `r2_buckets` binding name in `wrangler.jsonc` (e.g. `"STORAGE_BUCKET"`). Set to enable file
|
|
240
|
+
* storage on the shard-DO: the generated `appConfig` constructs an `R2BlobStore` over
|
|
241
|
+
* `env[r2BindingName]` and threads it as `blobStore`, so `ctx.storage` + the `/api/storage/*` routes
|
|
242
|
+
* work on every shard-DO — same mechanism as the free single-shard host
|
|
243
|
+
* (`generateWorkerEntrySource`'s `r2BindingName`, `packages/runtime-cloudflare/src/worker-entry.ts`),
|
|
244
|
+
* since each shard-DO is that SAME unmodified `HelipodDurableObject` class. Absent → no R2 import,
|
|
245
|
+
* `blobStore` omitted, file storage inert (byte-less deploy, unchanged).
|
|
246
|
+
*/
|
|
247
|
+
r2BindingName?: string;
|
|
248
|
+
}
|
|
249
|
+
declare function generateShardWorkerEntrySource(inp: ShardWorkerEntryInputs): string;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* `@helipod/runtime-cloudflare-shard` — the MULTI-SHARD Cloudflare Durable Object front (Slice 6,
|
|
253
|
+
* Milestone 1: `.shardBy(key)` write scale-out). A paid-tier (ee/) SIBLING of `ee/fleet`: this is the
|
|
254
|
+
* Cloudflare-native analog of the portable multi-node scale-out. It routes each request to the owning
|
|
255
|
+
* shard-DO by the shard key (CF's `getByName(shardKey)` model — the DO NAME is the shard key, no shard
|
|
256
|
+
* map, no coordinator), and each shard-DO is an UNMODIFIED `HelipodDurableObject` from the FREE
|
|
257
|
+
* `@helipod/runtime-cloudflare` package. N distinct keys ⇒ N distinct DOs ⇒ N× write throughput +
|
|
258
|
+
* N×10 GB storage. No engine change: M1 is pure routing over independent Slice-3 hosts.
|
|
259
|
+
*
|
|
260
|
+
* NON-GOALS (M1, enforced not silently broken): a reactive query/mutation spanning MULTIPLE shards, and
|
|
261
|
+
* cross-shard global-unique — both rejected with a typed `CROSS_SHARD_UNSUPPORTED`. `.global()`/D1 and
|
|
262
|
+
* the opt-in non-reactive fan-out read are Milestone 2.
|
|
263
|
+
*/
|
|
264
|
+
declare const RUNTIME_CLOUDFLARE_SHARD_VERSION = "0.0.0";
|
|
265
|
+
|
|
266
|
+
export { CONTINENT_TO_HINT, CROSS_SHARD_UNSUPPORTED, DEFAULT_SHARD_DO_NAME, type DeriveLocationHintInput, type HintDerivation, INVALID_REGION_HINT, RUNTIME_CLOUDFLARE_SHARD_VERSION, SHARD_KEY_REQUIRED, type ShardResolution, type ShardRoutingErrorBody, type ShardRoutingErrorCode, type ShardRoutingMode, type ShardRoutingOptions, type ShardWorkerEntryInputs, continentToHint, createShardWorkerHandler, deriveLocationHint, generateShardWorkerEntrySource, resolveShard, routingError, shardDoName };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// src/worker.ts
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SHARD_NAME
|
|
4
|
+
} from "@helipod/runtime-cloudflare";
|
|
5
|
+
|
|
6
|
+
// src/canonical.ts
|
|
7
|
+
import { shardIdForKeyValue, DEFAULT_SHARD } from "@helipod/id-codec";
|
|
8
|
+
import { encodeIndexKey } from "@helipod/index-key-codec";
|
|
9
|
+
var DEFAULT_SHARD_DO_NAME = DEFAULT_SHARD;
|
|
10
|
+
function toHex(bytes) {
|
|
11
|
+
let s = "";
|
|
12
|
+
for (let i = 0; i < bytes.length; i++) s += bytes[i].toString(16).padStart(2, "0");
|
|
13
|
+
return s;
|
|
14
|
+
}
|
|
15
|
+
function shardDoName(value, mode = "key", numShards = 1) {
|
|
16
|
+
if (mode === "hash") {
|
|
17
|
+
return shardIdForKeyValue(value, Math.max(1, numShards));
|
|
18
|
+
}
|
|
19
|
+
const bytes = encodeIndexKey([value]);
|
|
20
|
+
return `s.${toHex(bytes)}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/errors.ts
|
|
24
|
+
var SHARD_KEY_REQUIRED = "SHARD_KEY_REQUIRED";
|
|
25
|
+
var CROSS_SHARD_UNSUPPORTED = "CROSS_SHARD_UNSUPPORTED";
|
|
26
|
+
var INVALID_REGION_HINT = "INVALID_REGION_HINT";
|
|
27
|
+
var FANOUT_REQUIRES_FIXED_SHARDS = "FANOUT_REQUIRES_FIXED_SHARDS";
|
|
28
|
+
var FANOUT_WITH_SHARD_KEY = "FANOUT_WITH_SHARD_KEY";
|
|
29
|
+
var FANOUT_NOT_SUBSCRIBABLE = "FANOUT_NOT_SUBSCRIBABLE";
|
|
30
|
+
var FANOUT_NOT_A_QUERY = "FANOUT_NOT_A_QUERY";
|
|
31
|
+
function routingError(code, message) {
|
|
32
|
+
return { error: { code, message } };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/location.ts
|
|
36
|
+
import { isValidLocationHint } from "@helipod/runtime-cloudflare";
|
|
37
|
+
var REGION_HEADER = "x-helipod-region";
|
|
38
|
+
var REGION_PARAM = "region";
|
|
39
|
+
var CONTINENT_TO_HINT = {
|
|
40
|
+
AF: "afr",
|
|
41
|
+
AS: "apac",
|
|
42
|
+
EU: "weur",
|
|
43
|
+
NA: "enam",
|
|
44
|
+
OC: "oc",
|
|
45
|
+
SA: "sam"
|
|
46
|
+
};
|
|
47
|
+
function continentToHint(continent) {
|
|
48
|
+
if (!continent) return void 0;
|
|
49
|
+
return CONTINENT_TO_HINT[continent.toUpperCase()];
|
|
50
|
+
}
|
|
51
|
+
function explicitRegion(url, headers) {
|
|
52
|
+
const h = headers.get(REGION_HEADER);
|
|
53
|
+
if (h && h.length > 0) return h;
|
|
54
|
+
const q = url.searchParams.get(REGION_PARAM);
|
|
55
|
+
if (q && q.length > 0) return q;
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function regionFromKeyPrefix(shardKeyValue) {
|
|
59
|
+
if (typeof shardKeyValue !== "string") return void 0;
|
|
60
|
+
const i = shardKeyValue.indexOf(":");
|
|
61
|
+
if (i <= 0) return void 0;
|
|
62
|
+
const prefix = shardKeyValue.slice(0, i);
|
|
63
|
+
return isValidLocationHint(prefix) ? prefix : void 0;
|
|
64
|
+
}
|
|
65
|
+
function deriveLocationHint(input) {
|
|
66
|
+
const explicit = explicitRegion(input.url, input.headers);
|
|
67
|
+
if (explicit !== null) {
|
|
68
|
+
if (!isValidLocationHint(explicit)) return { ok: false, invalidRegion: explicit };
|
|
69
|
+
return { ok: true, hint: explicit };
|
|
70
|
+
}
|
|
71
|
+
if (input.regionPrefixedKeys) {
|
|
72
|
+
const prefixed = regionFromKeyPrefix(input.shardKeyValue);
|
|
73
|
+
if (prefixed) return { ok: true, hint: prefixed };
|
|
74
|
+
}
|
|
75
|
+
const origin = continentToHint(input.cf?.continent);
|
|
76
|
+
if (origin) return { ok: true, hint: origin };
|
|
77
|
+
return { ok: true, hint: void 0 };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/route.ts
|
|
81
|
+
import { LOCATION_HINTS } from "@helipod/runtime-cloudflare";
|
|
82
|
+
import { shardIdList } from "@helipod/id-codec";
|
|
83
|
+
var SHARD_HEADER = "x-helipod-shard";
|
|
84
|
+
var FANOUT_HEADER = "x-helipod-fanout";
|
|
85
|
+
function isFanoutRequested(url, headers) {
|
|
86
|
+
const h = headers.get(FANOUT_HEADER);
|
|
87
|
+
if (h && (h === "true" || h === "1")) return true;
|
|
88
|
+
const q = url.searchParams.get("fanout");
|
|
89
|
+
return q === "true" || q === "1";
|
|
90
|
+
}
|
|
91
|
+
function explicitShardKey(url, headers) {
|
|
92
|
+
const h = headers.get(SHARD_HEADER);
|
|
93
|
+
if (h && h.length > 0) return h;
|
|
94
|
+
const q = url.searchParams.get("shard");
|
|
95
|
+
if (q && q.length > 0) return q;
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
function splitFunctionPath(path) {
|
|
99
|
+
const i = path.lastIndexOf(":");
|
|
100
|
+
if (i <= 0 || i === path.length - 1) return null;
|
|
101
|
+
return [path.slice(0, i), path.slice(i + 1)];
|
|
102
|
+
}
|
|
103
|
+
function deriveShardFromRun(loaded, path, args) {
|
|
104
|
+
const split = splitFunctionPath(path);
|
|
105
|
+
if (!split) return { sharded: false, hasValue: false };
|
|
106
|
+
const [modulePath, name] = split;
|
|
107
|
+
const fn = loaded.modules[modulePath]?.[name];
|
|
108
|
+
if (!fn || fn.type !== "mutation" || fn.shardBy === void 0) return { sharded: false, hasValue: false };
|
|
109
|
+
const shardBy = fn.shardBy;
|
|
110
|
+
let value;
|
|
111
|
+
if (typeof shardBy === "string") {
|
|
112
|
+
value = args?.[shardBy];
|
|
113
|
+
} else if (typeof shardBy === "function") {
|
|
114
|
+
try {
|
|
115
|
+
value = shardBy(args);
|
|
116
|
+
} catch {
|
|
117
|
+
value = void 0;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { sharded: true, value, hasValue: value !== void 0 && value !== null };
|
|
121
|
+
}
|
|
122
|
+
function classifyFunctionType(loaded, path) {
|
|
123
|
+
const split = splitFunctionPath(path);
|
|
124
|
+
if (!split) return void 0;
|
|
125
|
+
const [modulePath, name] = split;
|
|
126
|
+
const fn = loaded.modules[modulePath]?.[name];
|
|
127
|
+
return typeof fn?.type === "string" ? fn.type : void 0;
|
|
128
|
+
}
|
|
129
|
+
async function resolveShard(request, opts = {}) {
|
|
130
|
+
const mode = opts.mode ?? "key";
|
|
131
|
+
const numShards = opts.numShards ?? 1;
|
|
132
|
+
const url = new URL(request.url);
|
|
133
|
+
const headers = request.headers;
|
|
134
|
+
const cf = request.cf;
|
|
135
|
+
const finish = (name, shardKeyValue) => {
|
|
136
|
+
const derived = deriveLocationHint({
|
|
137
|
+
url,
|
|
138
|
+
headers,
|
|
139
|
+
cf,
|
|
140
|
+
shardKeyValue,
|
|
141
|
+
regionPrefixedKeys: opts.regionPrefixedKeys
|
|
142
|
+
});
|
|
143
|
+
if (!derived.ok) {
|
|
144
|
+
return {
|
|
145
|
+
kind: "error",
|
|
146
|
+
status: 400,
|
|
147
|
+
body: routingError(
|
|
148
|
+
INVALID_REGION_HINT,
|
|
149
|
+
`region hint "${derived.invalidRegion}" is not a valid Durable Object location hint. Use one of: ${LOCATION_HINTS.join(", ")}.`
|
|
150
|
+
)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return derived.hint !== void 0 ? { kind: "shard", name, locationHint: derived.hint } : { kind: "shard", name };
|
|
154
|
+
};
|
|
155
|
+
if (isFanoutRequested(url, headers)) {
|
|
156
|
+
if (mode !== "hash") {
|
|
157
|
+
return {
|
|
158
|
+
kind: "error",
|
|
159
|
+
status: 400,
|
|
160
|
+
body: routingError(
|
|
161
|
+
FANOUT_REQUIRES_FIXED_SHARDS,
|
|
162
|
+
'fanOut requires a fixed-shard-count deployment (routing mode "hash"); this deployment addresses a Durable Object per key value with no enumerable shard set.'
|
|
163
|
+
)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (explicitShardKey(url, headers) !== null) {
|
|
167
|
+
return {
|
|
168
|
+
kind: "error",
|
|
169
|
+
status: 400,
|
|
170
|
+
body: routingError(
|
|
171
|
+
FANOUT_WITH_SHARD_KEY,
|
|
172
|
+
"a request may either fan out across all shards or name one shard key, not both."
|
|
173
|
+
)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (url.pathname === "/api/sync" || headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
177
|
+
return {
|
|
178
|
+
kind: "error",
|
|
179
|
+
status: 400,
|
|
180
|
+
body: routingError(
|
|
181
|
+
FANOUT_NOT_SUBSCRIBABLE,
|
|
182
|
+
"fanOut is a non-reactive one-shot read and cannot be used on a WebSocket (/api/sync) subscription."
|
|
183
|
+
)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
let fanoutTargetType;
|
|
187
|
+
let fanoutPath;
|
|
188
|
+
if (opts.loaded && request.method === "POST" && url.pathname === "/api/run") {
|
|
189
|
+
let body;
|
|
190
|
+
try {
|
|
191
|
+
body = await request.clone().json();
|
|
192
|
+
} catch {
|
|
193
|
+
body = void 0;
|
|
194
|
+
}
|
|
195
|
+
if (body && typeof body.path === "string") {
|
|
196
|
+
fanoutPath = body.path;
|
|
197
|
+
fanoutTargetType = classifyFunctionType(opts.loaded, body.path);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (fanoutTargetType !== "query") {
|
|
201
|
+
return {
|
|
202
|
+
kind: "error",
|
|
203
|
+
status: 400,
|
|
204
|
+
body: routingError(
|
|
205
|
+
FANOUT_NOT_A_QUERY,
|
|
206
|
+
fanoutPath !== void 0 ? `"${fanoutPath}" is not a query (fanOut is read-only; only queries may fan out across shards).` : "fanOut requires a POST /api/run request naming a resolvable query function; the target's type could not be determined."
|
|
207
|
+
)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { kind: "fanout", shardIds: shardIdList(numShards) };
|
|
211
|
+
}
|
|
212
|
+
const explicit = explicitShardKey(url, headers);
|
|
213
|
+
if (explicit !== null) {
|
|
214
|
+
if (explicit.includes(",")) {
|
|
215
|
+
return {
|
|
216
|
+
kind: "error",
|
|
217
|
+
status: 400,
|
|
218
|
+
body: routingError(
|
|
219
|
+
CROSS_SHARD_UNSUPPORTED,
|
|
220
|
+
`a request may name exactly one shard key; received a multi-valued key ("${explicit}"). M1 supports only shard-scoped operations.`
|
|
221
|
+
)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return finish(shardDoName(explicit, mode, numShards), explicit);
|
|
225
|
+
}
|
|
226
|
+
if (opts.loaded && request.method === "POST" && url.pathname === "/api/run") {
|
|
227
|
+
let body;
|
|
228
|
+
try {
|
|
229
|
+
body = await request.clone().json();
|
|
230
|
+
} catch {
|
|
231
|
+
body = void 0;
|
|
232
|
+
}
|
|
233
|
+
if (body && typeof body.path === "string") {
|
|
234
|
+
const derived = deriveShardFromRun(opts.loaded, body.path, body.args);
|
|
235
|
+
if (derived.sharded) {
|
|
236
|
+
if (!derived.hasValue) {
|
|
237
|
+
return {
|
|
238
|
+
kind: "error",
|
|
239
|
+
status: 400,
|
|
240
|
+
body: routingError(
|
|
241
|
+
SHARD_KEY_REQUIRED,
|
|
242
|
+
`mutation "${body.path}" is sharded but its arguments do not carry the shard key. Include the shard-key field in the arguments, or pass it as an \`X-Helipod-Shard\` header.`
|
|
243
|
+
)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return finish(shardDoName(derived.value, mode, numShards), derived.value);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return finish(DEFAULT_SHARD_DO_NAME, void 0);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/worker.ts
|
|
254
|
+
function createShardWorkerHandler(bindingName, opts = {}) {
|
|
255
|
+
return {
|
|
256
|
+
async fetch(request, env) {
|
|
257
|
+
const ns = env[bindingName];
|
|
258
|
+
if (!ns || typeof ns.idFromName !== "function") {
|
|
259
|
+
return json(500, {
|
|
260
|
+
error: `helipod: Durable Object binding "${bindingName}" is not configured in wrangler.jsonc`
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
const resolution = await resolveShard(request, opts);
|
|
264
|
+
if (resolution.kind === "error") {
|
|
265
|
+
return json(resolution.status, resolution.body);
|
|
266
|
+
}
|
|
267
|
+
if (resolution.kind === "fanout") {
|
|
268
|
+
return fanOut(ns, resolution.shardIds, request);
|
|
269
|
+
}
|
|
270
|
+
const id = ns.idFromName(resolution.name);
|
|
271
|
+
const stub = resolution.locationHint ? ns.get(id, { locationHint: resolution.locationHint }) : ns.get(id);
|
|
272
|
+
return stub.fetch(request);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function json(status, value) {
|
|
277
|
+
return new Response(JSON.stringify(value), {
|
|
278
|
+
status,
|
|
279
|
+
headers: { "content-type": "application/json" }
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
var FANOUT_CONCURRENCY = 8;
|
|
283
|
+
var FANOUT_SHARD_TIMEOUT_MS = 1e4;
|
|
284
|
+
async function fanOut(ns, shardIds, request) {
|
|
285
|
+
const value = [];
|
|
286
|
+
const failedShards = [];
|
|
287
|
+
let cursor = 0;
|
|
288
|
+
async function worker() {
|
|
289
|
+
for (; ; ) {
|
|
290
|
+
const i = cursor++;
|
|
291
|
+
if (i >= shardIds.length) return;
|
|
292
|
+
const shardId = shardIds[i];
|
|
293
|
+
try {
|
|
294
|
+
const stub = ns.get(ns.idFromName(shardId));
|
|
295
|
+
const res = await withTimeout(stub.fetch(request.clone()), FANOUT_SHARD_TIMEOUT_MS);
|
|
296
|
+
if (!res.ok) {
|
|
297
|
+
failedShards.push({ shardId, error: `shard responded ${res.status}` });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const body = await res.json();
|
|
301
|
+
if (!Array.isArray(body.value)) {
|
|
302
|
+
failedShards.push({
|
|
303
|
+
shardId,
|
|
304
|
+
error: "shard result is not an array (fanOut requires a query returning a list)"
|
|
305
|
+
});
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
value.push(...body.value);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
failedShards.push({ shardId, error: e instanceof Error ? e.message : String(e) });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
await Promise.all(Array.from({ length: Math.min(FANOUT_CONCURRENCY, shardIds.length) }, () => worker()));
|
|
315
|
+
const allShardsFailed = shardIds.length > 0 && failedShards.length === shardIds.length;
|
|
316
|
+
return json(allShardsFailed ? 502 : 200, failedShards.length ? { value, partial: { failedShards } } : { value });
|
|
317
|
+
}
|
|
318
|
+
function withTimeout(p, ms) {
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const t = setTimeout(() => reject(new Error(`shard fetch timed out after ${ms}ms`)), ms);
|
|
321
|
+
p.then(
|
|
322
|
+
(v) => {
|
|
323
|
+
clearTimeout(t);
|
|
324
|
+
resolve(v);
|
|
325
|
+
},
|
|
326
|
+
(e) => {
|
|
327
|
+
clearTimeout(t);
|
|
328
|
+
reject(e);
|
|
329
|
+
}
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/worker-entry.ts
|
|
335
|
+
function generateShardWorkerEntrySource(inp) {
|
|
336
|
+
const host = inp.hostPackage ?? "@helipod/runtime-cloudflare";
|
|
337
|
+
const shard = inp.shardPackage ?? "@helipod/runtime-cloudflare-shard";
|
|
338
|
+
const adminEnv = inp.adminKeyEnvVar ?? "HELIPOD_ADMIN_KEY";
|
|
339
|
+
const mode = inp.mode ?? "key";
|
|
340
|
+
const r2 = inp.r2BindingName;
|
|
341
|
+
const L = ["// AUTO-GENERATED by `@helipod/runtime-cloudflare-shard` worker-entry \u2014 do not edit."];
|
|
342
|
+
inp.moduleImports.forEach((m, i) => L.push(`import * as m${i} from ${JSON.stringify(m.absPath)};`));
|
|
343
|
+
L.push(`import schema from ${JSON.stringify(inp.schemaAbsPath)};`);
|
|
344
|
+
if (inp.configAbsPath) L.push(`import * as __config from ${JSON.stringify(inp.configAbsPath)};`);
|
|
345
|
+
L.push(`import { HelipodDurableObject } from ${JSON.stringify(host)};`);
|
|
346
|
+
L.push(`import { createShardWorkerHandler } from ${JSON.stringify(shard)};`);
|
|
347
|
+
if (r2) L.push(`import { R2BlobStore } from "@helipod/blobstore-r2";`);
|
|
348
|
+
L.push("");
|
|
349
|
+
const modEntries = inp.moduleImports.map((m, i) => `${JSON.stringify(m.key)}: m${i}`).join(", ");
|
|
350
|
+
L.push(`const loaded = { schema: (schema && schema.default) ?? schema, modules: { ${modEntries} } };`);
|
|
351
|
+
L.push(inp.configAbsPath ? `const components = (__config.default ?? __config).components ?? [];` : `const components = [];`);
|
|
352
|
+
L.push("");
|
|
353
|
+
L.push(`export class ${inp.doClassName} extends HelipodDurableObject {`);
|
|
354
|
+
L.push(` appConfig(env) {`);
|
|
355
|
+
if (r2) {
|
|
356
|
+
L.push(` const __bucket = env && env[${JSON.stringify(r2)}];`);
|
|
357
|
+
L.push(` return { loaded, components, adminKey: (env && env[${JSON.stringify(adminEnv)}]) ?? "", ...(__bucket ? { blobStore: new R2BlobStore({ bucket: __bucket }) } : {}) };`);
|
|
358
|
+
} else {
|
|
359
|
+
L.push(` return { loaded, components, adminKey: (env && env[${JSON.stringify(adminEnv)}]) ?? "" };`);
|
|
360
|
+
}
|
|
361
|
+
L.push(` }`);
|
|
362
|
+
L.push(`}`);
|
|
363
|
+
L.push("");
|
|
364
|
+
const rpk = inp.regionPrefixedKeys ? ", regionPrefixedKeys: true" : "";
|
|
365
|
+
const routeOpts = mode === "hash" ? `{ mode: "hash", numShards: ${JSON.stringify(inp.numShards ?? 1)}, loaded${rpk} }` : `{ mode: "key", loaded${rpk} }`;
|
|
366
|
+
L.push(`export default createShardWorkerHandler(${JSON.stringify(inp.bindingName)}, ${routeOpts});`);
|
|
367
|
+
return L.join("\n") + "\n";
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/index.ts
|
|
371
|
+
import { HelipodDurableObject } from "@helipod/runtime-cloudflare";
|
|
372
|
+
var RUNTIME_CLOUDFLARE_SHARD_VERSION = "0.0.0";
|
|
373
|
+
export {
|
|
374
|
+
CONTINENT_TO_HINT,
|
|
375
|
+
CROSS_SHARD_UNSUPPORTED,
|
|
376
|
+
DEFAULT_SHARD_DO_NAME,
|
|
377
|
+
DEFAULT_SHARD_NAME,
|
|
378
|
+
HelipodDurableObject,
|
|
379
|
+
INVALID_REGION_HINT,
|
|
380
|
+
RUNTIME_CLOUDFLARE_SHARD_VERSION,
|
|
381
|
+
SHARD_KEY_REQUIRED,
|
|
382
|
+
continentToHint,
|
|
383
|
+
createShardWorkerHandler,
|
|
384
|
+
deriveLocationHint,
|
|
385
|
+
generateShardWorkerEntrySource,
|
|
386
|
+
resolveShard,
|
|
387
|
+
routingError,
|
|
388
|
+
shardDoName
|
|
389
|
+
};
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/worker.ts","../src/canonical.ts","../src/errors.ts","../src/location.ts","../src/route.ts","../src/worker-entry.ts","../src/index.ts"],"sourcesContent":["/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * The multi-shard stateless Worker router (M1). The DO-native analog of `ee/fleet` for the portable\n * path — but a SIBLING, not a consumer: Cloudflare's native `namespace.getByName(shardKey)` addressing\n * replaces the lease/forwarder mechanism entirely. This handler holds NO state; it resolves the ONE\n * owning shard-DO name from the request envelope (`resolveShard`) and forwards the ORIGINAL request to\n * it. Each shard-DO is an UNMODIFIED `HelipodDurableObject` from the FREE `@helipod/runtime-cloudflare`\n * package (reused verbatim — N distinct keys ⇒ N distinct DOs ⇒ N× the single-DO write ceiling AND\n * N×10 GB storage). No engine change: a shard-DO differs from Slice 3 in nothing.\n *\n * Licensing (locked): this package DEPENDS ON the free single-shard host and reuses its DO class;\n * nothing in `@helipod/runtime-cloudflare` imports back into here. The single-shard vs multi-shard\n * choice is made at the APP'S Worker entry (which handler it `export default`s), never by a runtime\n * gate — so a free single-node deploy never statically links this ee code.\n */\nimport {\n DEFAULT_SHARD_NAME,\n type WorkerHandler,\n type DurableObjectNamespaceLike,\n} from \"@helipod/runtime-cloudflare\";\nimport { resolveShard, type ShardRoutingOptions } from \"./route\";\n\n// Re-export the Slice-3 default name so an app/rig references one symbol. `getByName(name)` (CF's\n// sugar, changelog 2025-08-21) is exactly `get(idFromName(name))`; we use the `idFromName`+`get`\n// spelling the free host already proves in workerd, and which the `DurableObjectNamespaceLike` seam\n// declares. They resolve the SAME globally-unique object for a name.\nexport { DEFAULT_SHARD_NAME };\n\n/**\n * Build the multi-shard Worker `fetch` handler. `bindingName` is the shard-DO `durable_objects`\n * binding in `wrangler.jsonc` (e.g. `\"HELIPOD_DO\"`); `opts` selects the routing mode and — for the\n * derive-from-args convenience — the bundled `loaded` app.\n */\nexport function createShardWorkerHandler(bindingName: string, opts: ShardRoutingOptions = {}): WorkerHandler {\n return {\n async fetch(request: Request, env: Record<string, unknown>): Promise<Response> {\n const ns = env[bindingName] as DurableObjectNamespaceLike | undefined;\n if (!ns || typeof ns.idFromName !== \"function\") {\n return json(500, {\n error: `helipod: Durable Object binding \"${bindingName}\" is not configured in wrangler.jsonc`,\n });\n }\n const resolution = await resolveShard(request, opts);\n if (resolution.kind === \"error\") {\n return json(resolution.status, resolution.body);\n }\n if (resolution.kind === \"fanout\") {\n return fanOut(ns, resolution.shardIds, request);\n }\n const id = ns.idFromName(resolution.name);\n // Place a NEWLY-created shard-DO near its audience (source a/b/c). Only the FIRST `get()` for this\n // id honors the hint — a DO is single-homed and pinned thereafter — so the router derives the SAME\n // hint per key where possible (explicit/prefix are stable; cf-origin is first-requester-wins). No\n // hint ⇒ `get(id)` with no options bag: byte-identical to the pre-hint forward.\n const stub = resolution.locationHint\n ? ns.get(id, { locationHint: resolution.locationHint })\n : ns.get(id);\n return stub.fetch(request);\n },\n };\n}\n\nfunction json(status: number, value: unknown): Response {\n return new Response(JSON.stringify(value), {\n status,\n headers: { \"content-type\": \"application/json\" },\n });\n}\n\n/** Bounded-concurrency in-flight cap on the fan-out pool (§ below) — a fixed, deliberately small\n * ceiling so a wide `numShards` deployment can't open hundreds of simultaneous subrequests from one\n * Worker invocation. */\nconst FANOUT_CONCURRENCY = 8;\n/** Per-shard fetch timeout — a single slow/hung shard-DO must not hang the whole fan-out; it is\n * recorded as a failed shard instead (failures-as-data, never a thrown fan-out failure). */\nconst FANOUT_SHARD_TIMEOUT_MS = 10_000;\n\n/**\n * Fan `request` out to every DO named in `shardIds`, concatenate each shard's `{ value: [...] }`\n * array, and return ONE merged `Response`. A shard that throws, times out, responds non-200, or\n * returns a non-array `value` becomes a `failedShard` entry — failures-as-data, never a thrown\n * fan-out failure — so a slow or broken shard degrades the result instead of failing the whole read.\n *\n * Concurrency is bounded by a simple index-cursor worker pool (`FANOUT_CONCURRENCY` workers each\n * pulling the next `shardIds` index) rather than `Promise.all`-ing every shard at once, so the number\n * of simultaneously in-flight subrequests is capped regardless of `shardIds.length`.\n *\n * v1 concat: no global result limit is applied here. Each shard already applies its own query's\n * `.take(n)` inside its own engine (the fanned-out request is byte-identical per shard), so the\n * concatenation is naturally capped at `shardIds.length * n`. A caller that needs a hard GLOBAL cap\n * slices the returned `value` client-side; threading a `?fanoutLimit=` router param is left for a\n * later slice if this proves insufficient in practice.\n *\n * Status (I1): a PARTIAL failure (some shards ok, some failed) stays 200 with `value`/`partial` —\n * failures-as-data is the intended contract for a degraded-but-usable result. But when EVERY shard\n * failed (e.g. an invalid bearer token 401s every shard, or the whole cluster is down), `value` would\n * otherwise be `[]` with status 200 — indistinguishable from a genuinely empty table to a caller that\n * doesn't inspect `partial`. A TOTAL failure instead returns 502, with `partial.failedShards` still in\n * the body so the caller can see which shards failed and why.\n */\nasync function fanOut(ns: DurableObjectNamespaceLike, shardIds: string[], request: Request): Promise<Response> {\n const value: unknown[] = [];\n const failedShards: Array<{ shardId: string; error: string }> = [];\n\n let cursor = 0;\n async function worker(): Promise<void> {\n for (;;) {\n const i = cursor++;\n if (i >= shardIds.length) return;\n const shardId = shardIds[i]!;\n try {\n const stub = ns.get(ns.idFromName(shardId));\n const res = await withTimeout(stub.fetch(request.clone()), FANOUT_SHARD_TIMEOUT_MS);\n if (!res.ok) {\n failedShards.push({ shardId, error: `shard responded ${res.status}` });\n continue;\n }\n const body = (await res.json()) as { value?: unknown };\n if (!Array.isArray(body.value)) {\n failedShards.push({\n shardId,\n error: \"shard result is not an array (fanOut requires a query returning a list)\",\n });\n continue;\n }\n value.push(...body.value);\n } catch (e) {\n failedShards.push({ shardId, error: e instanceof Error ? e.message : String(e) });\n }\n }\n }\n\n await Promise.all(Array.from({ length: Math.min(FANOUT_CONCURRENCY, shardIds.length) }, () => worker()));\n\n // Total failure (every shard failed) must not look like a successful empty read — see the I1 note\n // above. `shardIds.length > 0` guards a degenerate empty shard set from spuriously counting as\n // \"all failed\" (0 === 0).\n const allShardsFailed = shardIds.length > 0 && failedShards.length === shardIds.length;\n return json(allShardsFailed ? 502 : 200, failedShards.length ? { value, partial: { failedShards } } : { value });\n}\n\nfunction withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {\n return new Promise((resolve, reject) => {\n const t = setTimeout(() => reject(new Error(`shard fetch timed out after ${ms}ms`)), ms);\n p.then(\n (v) => {\n clearTimeout(t);\n resolve(v);\n },\n (e) => {\n clearTimeout(t);\n reject(e);\n },\n );\n });\n}\n","/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * Map a shard-key VALUE to the Durable Object NAME that owns its shard. Two routing modes (spec §1.1),\n * both deterministic and both reusing the engine's OWN canonicalization so a key routes identically\n * however it arrives (a raw `1` number vs the string `\"1\"` route to distinct DOs, exactly as they land\n * in distinct shards on the portable path):\n *\n * - **mode \"key\" (A, default)** — one DO per distinct shard-key value. `getByName(canonicalName)`.\n * No fixed shard count, no reshard, no shard map: a new key just addresses a new DO forever. This\n * is Lunora's model and the CF-idiomatic default (§1.1, §5.2). The name is a collision-safe encoding\n * of the value (index-key bytes → hex), never the raw value, so no app key can ever alias the\n * reserved `\"default\"` DO or smuggle control characters into a DO name.\n *\n * - **mode \"hash\" (B)** — fixed-N jump-consistent-hash, reusing `shardIdForKeyValue` from\n * `@helipod/id-codec` VERBATIM. The DO name is the SAME `ShardId` string the portable path uses\n * (`\"default\"`/`\"s1\"`/…), so an app's key→shard identity is byte-identical across the portable and\n * DO hosts (important for a future migration tool, §7 #7). Cost: growing N needs an offline reshard.\n *\n * The DO name is a pure function of the value + mode; the Worker holds no shard map.\n */\nimport { shardIdForKeyValue, DEFAULT_SHARD } from \"@helipod/id-codec\";\nimport { encodeIndexKey, type IndexableValue } from \"@helipod/index-key-codec\";\n\n/** The DO name for unsharded tables / no-`shardBy` requests — byte-identical to Slice 3's single DO. */\nexport const DEFAULT_SHARD_DO_NAME = DEFAULT_SHARD; // \"default\"\n\nexport type ShardRoutingMode = \"key\" | \"hash\";\n\nfunction toHex(bytes: Uint8Array): string {\n let s = \"\";\n for (let i = 0; i < bytes.length; i++) s += bytes[i]!.toString(16).padStart(2, \"0\");\n return s;\n}\n\n/**\n * Resolve a shard-key value to its owning DO name.\n *\n * @param value the shard-key value extracted from the request (a scalar the engine can index).\n * @param mode routing mode; \"key\" (default) or \"hash\".\n * @param numShards for mode \"hash\" only: the fixed shard count N (≥ 1). Ignored in mode \"key\".\n */\nexport function shardDoName(value: unknown, mode: ShardRoutingMode = \"key\", numShards = 1): string {\n if (mode === \"hash\") {\n // Byte-identical to the portable path: returns \"default\" | \"s1\" | … .\n return shardIdForKeyValue(value, Math.max(1, numShards));\n }\n // mode \"key\": collision-safe per-value name. `s.` prefix guarantees it can NEVER equal \"default\".\n const bytes = encodeIndexKey([value as IndexableValue]);\n return `s.${toHex(bytes)}`;\n}\n","/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * Typed routing rejections (M1). The router never silently serves partial/wrong data — a request it\n * cannot pin to exactly ONE owning shard-DO is rejected LOUDLY with a coded 400, the same discipline\n * the engine's kernel shard guards use (`ForbiddenOperationError` names the table/field/fix). These\n * codes cross the wire as `{ error: { code, message } }` so a client can branch on `code`.\n */\n\n/** A `.shardBy` mutation reached the router without its shard-key value in the args. Mirrors the\n * engine kernel's \"declares a shard but does not carry it\" guard, enforced at the edge so a sharded\n * write is never mis-routed to the `\"default\"` DO (where it would land on the wrong data). */\nexport const SHARD_KEY_REQUIRED = \"SHARD_KEY_REQUIRED\";\n\n/** A request explicitly asked to span multiple shard-DOs (a fan-out flag, or a multi-valued shard\n * key). M1 supports only shard-SCOPED operations — a cross-shard query/mutation is a hard non-goal\n * (§2.1: reactive cross-shard is refused; a non-reactive `fanOut` read lands in M2). Rejected here\n * rather than fanned out and merged, so no caller ever receives a silently-partial result. */\nexport const CROSS_SHARD_UNSUPPORTED = \"CROSS_SHARD_UNSUPPORTED\";\n\n/** An EXPLICIT region hint (`?region=` / `X-Helipod-Region`) was supplied but is not one of\n * Cloudflare's 11 `locationHint` region codes. Rejected LOUDLY at the edge rather than passed to\n * `get(id, { locationHint })`: a bad hint would place the shard-DO in an unintended data center\n * PERMANENTLY (a DO is single-homed and pinned on its first `get()`), so we refuse it up front and\n * name the fix, exactly the same discipline the shard guards use. */\nexport const INVALID_REGION_HINT = \"INVALID_REGION_HINT\";\n\n/** A `fanOut` request hit a deployment with no enumerable shard set — routing mode \"key\" (the\n * default) addresses a new DO per key value with no directory, and Cloudflare has no DO-listing API,\n * so \"all shards\" is undefined. fanOut requires a fixed-shard-count deployment (mode \"hash\"). */\nexport const FANOUT_REQUIRES_FIXED_SHARDS = \"FANOUT_REQUIRES_FIXED_SHARDS\";\n\n/** A request asked to `fanOut` AND named a shard key. You either target one shard (a shard key) OR\n * fan out across all shards, never both. */\nexport const FANOUT_WITH_SHARD_KEY = \"FANOUT_WITH_SHARD_KEY\";\n\n/** A `fanOut` was requested on the WebSocket `/api/sync` upgrade. fanOut is a non-reactive one-shot\n * read (fanning invalidation across every shard reactively is out of scope) — it works only over\n * the one-shot `POST /api/run` path. */\nexport const FANOUT_NOT_SUBSCRIBABLE = \"FANOUT_NOT_SUBSCRIBABLE\";\n\n/** A `fanOut` request's target is not a resolved `query` — a mutation (sharded or not), an action,\n * or a function whose type could not be classified at all (no `loaded` module set at resolve time,\n * an unparseable/non-`POST /api/run` request, or an unknown `path`). fanOut is READ-ONLY: fanning a\n * mutation or action out to every shard-DO would multiply ONE logical write into N commits/side\n * effects, one per shard. An unclassifiable target fails CLOSED under this same code — a request\n * that cannot be PROVEN to name a query is never fanned out on the assumption that it might be one. */\nexport const FANOUT_NOT_A_QUERY = \"FANOUT_NOT_A_QUERY\";\n\nexport type ShardRoutingErrorCode =\n | typeof SHARD_KEY_REQUIRED\n | typeof CROSS_SHARD_UNSUPPORTED\n | typeof INVALID_REGION_HINT\n | typeof FANOUT_REQUIRES_FIXED_SHARDS\n | typeof FANOUT_WITH_SHARD_KEY\n | typeof FANOUT_NOT_SUBSCRIBABLE\n | typeof FANOUT_NOT_A_QUERY;\n\nexport interface ShardRoutingErrorBody {\n error: { code: ShardRoutingErrorCode; message: string };\n}\n\nexport function routingError(code: ShardRoutingErrorCode, message: string): ShardRoutingErrorBody {\n return { error: { code, message } };\n}\n","/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * Per-shard geographic PLACEMENT derivation (M1 locality). A Durable Object is single-homed — pinned\n * to ONE data center at creation and never moved — and by default lands near whoever FIRST `get()`s it.\n * `get(id, { locationHint })` overrides that, but ONLY the first `get()` for a given DO is honored.\n * This is precisely WHY sharding unlocks geographic scale-out: each shard-DO is a distinct object, so\n * each can be placed near ITS OWN audience. The router derives that hint, per request, from the\n * envelope — the same \"the envelope is the source of truth\" model the shard key itself rides.\n *\n * Precedence (spec-locked; each must be STABLE per shard key so the first `get()` — the only one that\n * counts — is deterministic wherever possible):\n *\n * (a) EXPLICIT — a `?region=<hint>` param or an `X-Helipod-Region: <hint>` header. App-controlled\n * and fully deterministic; mirrors how the shard key itself is taken from `?shard=`. An invalid\n * explicit hint is a hard error (the caller asked for a specific placement and got it wrong).\n * (b) REGION-PREFIXED KEY (opt-in) — if enabled and the shard-key VALUE is a string of the form\n * `\"<hint>:<rest>\"` whose prefix is a known hint (e.g. `\"enam:room123\"`), derive the hint from\n * the prefix. Deterministic per key. OFF by default so an app that doesn't want a key format\n * isn't forced into one; the full key value is still what names the DO (the prefix is only read).\n * (c) AUTO from origin — `request.cf.continent` mapped to the nearest hint. This places a NEW shard\n * near the user who first creates it. That is ALSO Cloudflare's own default (place near the first\n * requester), so this is a made-explicit convenience, not a new guarantee — and it is\n * \"first-requester-wins\", NOT stable across requesters, which is acceptable and documented.\n * (d) DEFAULT — no hint. `get(id)` with no options bag: byte-identical to the pre-hint behavior.\n */\nimport { isValidLocationHint } from \"@helipod/runtime-cloudflare\";\n\n/** The explicit-region envelope sources, mirroring the shard-key `?shard=` / `X-Helipod-Shard` pair. */\nconst REGION_HEADER = \"x-helipod-region\";\nconst REGION_PARAM = \"region\";\n\n/**\n * Map a Cloudflare `request.cf.continent` code (`AF`/`AN`/`AS`/`EU`/`NA`/`OC`/`SA`) to the nearest\n * `locationHint`. Continent granularity is deliberately coarse (source (c) is a convenience, not a\n * precision tool — an app that needs finer placement uses the explicit or prefixed sources). Antarctica\n * (`AN`) is intentionally absent ⇒ no hint (there is no sensible nearby DO region). North America maps\n * to `enam` and Europe to `weur` as the documented continent-level defaults; reach for `?region=wnam`/\n * `eeur`/`apac-ne`/… when a finer placement matters.\n */\nexport const CONTINENT_TO_HINT: Readonly<Record<string, string>> = {\n AF: \"afr\",\n AS: \"apac\",\n EU: \"weur\",\n NA: \"enam\",\n OC: \"oc\",\n SA: \"sam\",\n};\n\n/** Resolve a continent code to a hint, or `undefined` if it is unknown/unmapped. */\nexport function continentToHint(continent: string | undefined | null): string | undefined {\n if (!continent) return undefined;\n return CONTINENT_TO_HINT[continent.toUpperCase()];\n}\n\n/** The `cf` object workerd attaches to an inbound Request (only the fields we read, structurally). */\ninterface RequestCfLike {\n continent?: string;\n}\n\n/** The outcome of hint derivation: a (possibly absent) valid hint, or a typed rejection for an\n * explicit-but-invalid region. `hint: undefined` is the DEFAULT case — the caller passes no options\n * bag, byte-identical to the pre-hint behavior. */\nexport type HintDerivation =\n | { ok: true; hint: string | undefined }\n | { ok: false; invalidRegion: string };\n\nexport interface DeriveLocationHintInput {\n url: URL;\n headers: Headers;\n /** `request.cf` if present (workerd populates it; `undefined` under Node/tests). */\n cf?: RequestCfLike;\n /** The resolved shard-key VALUE, used ONLY by the opt-in region-prefix source (b). */\n shardKeyValue?: unknown;\n /** Enable source (b): read a `\"<hint>:<rest>\"` prefix off the shard-key value. Off by default. */\n regionPrefixedKeys?: boolean;\n}\n\n/** Read the explicit `?region=` / `X-Helipod-Region` hint, if any (header wins, mirroring the\n * shard-key precedence). Empty string counts as \"not provided\". */\nfunction explicitRegion(url: URL, headers: Headers): string | null {\n const h = headers.get(REGION_HEADER);\n if (h && h.length > 0) return h;\n const q = url.searchParams.get(REGION_PARAM);\n if (q && q.length > 0) return q;\n return null;\n}\n\n/** Extract a `\"<hint>:<rest>\"` region prefix from a string shard-key value, iff the prefix is a known\n * hint. Returns the hint or `undefined`. Never throws; a non-string value yields `undefined`. */\nfunction regionFromKeyPrefix(shardKeyValue: unknown): string | undefined {\n if (typeof shardKeyValue !== \"string\") return undefined;\n const i = shardKeyValue.indexOf(\":\");\n if (i <= 0) return undefined;\n const prefix = shardKeyValue.slice(0, i);\n return isValidLocationHint(prefix) ? prefix : undefined;\n}\n\n/**\n * Derive the placement hint for a request, in precedence order (a) → (d). Returns `{ ok: true, hint }`\n * (hint possibly `undefined`) or `{ ok: false, invalidRegion }` when an EXPLICIT region hint is invalid.\n */\nexport function deriveLocationHint(input: DeriveLocationHintInput): HintDerivation {\n // (a) Explicit — validated; a wrong explicit hint is a hard error, never silently dropped.\n const explicit = explicitRegion(input.url, input.headers);\n if (explicit !== null) {\n if (!isValidLocationHint(explicit)) return { ok: false, invalidRegion: explicit };\n return { ok: true, hint: explicit };\n }\n\n // (b) Region-prefixed key (opt-in).\n if (input.regionPrefixedKeys) {\n const prefixed = regionFromKeyPrefix(input.shardKeyValue);\n if (prefixed) return { ok: true, hint: prefixed };\n }\n\n // (c) Auto from origin — first-requester-wins, matches CF's own default.\n const origin = continentToHint(input.cf?.continent);\n if (origin) return { ok: true, hint: origin };\n\n // (d) Default — no hint.\n return { ok: true, hint: undefined };\n}\n","/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * The stateless shard-routing decision (M1) — pure, host-agnostic, unit-testable under plain Node.\n * Given a request, decide the ONE owning shard-DO name (or a typed rejection). The Worker\n * (`worker.ts`) wraps this and forwards to `env[binding].get(idFromName(name))`.\n *\n * How a request's shard key is sourced, in precedence order (spec §1.2 — the shard key rides the\n * ENVELOPE, Lunora's model, so routing is a pure Worker concern and the DO never parses a row to learn\n * its own name):\n *\n * 1. **Explicit** — an `X-Helipod-Shard: <value>` header or a `?shard=<value>` query param. This\n * is the primary mechanism and the ONLY one that works for a WebSocket upgrade (which has no\n * readable body) and for a query (which declares no `shardBy`). A room/tenant/user socket opens\n * `…/api/sync?shard=<roomId>`; every op on it runs on that room's DO.\n * 2. **Derived** — for `POST /api/run { path, args }`, if no explicit key is present, look the\n * function up in the bundled `loaded` project and, if it is a mutation declaring `shardBy`,\n * extract the shard-key value from `args` EXACTLY as the executor does\n * (`packages/executor/src/executor.ts` — a string names an arg, a function is a resolver). This\n * reuses the engine's own convention verbatim, so the edge and the kernel agree on the key.\n * 3. **Default** — no key ⇒ the `\"default\"` DO (unsharded tables + no-`shardBy` mutations), exactly\n * Slice 3.\n *\n * Cross-shard is REFUSED, never fanned out (M1 non-goal, §2.1): an explicit fan-out flag\n * (`X-Helipod-Fanout: true` / `?fanout=1`) or a multi-valued shard key (a comma) is a typed\n * `CROSS_SHARD_UNSUPPORTED` 400. A `.shardBy` mutation whose args omit the key is `SHARD_KEY_REQUIRED`.\n */\nimport type { LoadedProject } from \"@helipod/cli/project\";\nimport { shardDoName, DEFAULT_SHARD_DO_NAME, type ShardRoutingMode } from \"./canonical\";\nimport {\n SHARD_KEY_REQUIRED,\n CROSS_SHARD_UNSUPPORTED,\n INVALID_REGION_HINT,\n FANOUT_REQUIRES_FIXED_SHARDS,\n FANOUT_WITH_SHARD_KEY,\n FANOUT_NOT_SUBSCRIBABLE,\n FANOUT_NOT_A_QUERY,\n routingError,\n type ShardRoutingErrorBody,\n} from \"./errors\";\nimport { deriveLocationHint } from \"./location\";\nimport { LOCATION_HINTS } from \"@helipod/runtime-cloudflare\";\nimport { shardIdList } from \"@helipod/id-codec\";\n\nexport interface ShardRoutingOptions {\n /** Routing mode: \"key\" (A, default — one DO per key) or \"hash\" (B — fixed-N jump-hash). §1.1. */\n mode?: ShardRoutingMode;\n /** For mode \"hash\": the fixed shard count N (≥ 1). Ignored in mode \"key\". */\n numShards?: number;\n /**\n * The bundled app, used ONLY to DERIVE a mutation's shard key from `POST /api/run` args (source #2).\n * Optional: omit it and routing falls back to the explicit envelope key + default (still correct;\n * derivation is a convenience so a client need not know each mutation's shard-key field name).\n */\n loaded?: LoadedProject;\n /**\n * Opt-in placement source (b): read a `\"<hint>:<rest>\"` region prefix off the shard-key value to\n * derive the DO's `locationHint` (e.g. `\"enam:room123\"` → `enam`). OFF by default — an app that does\n * not want a key format is never forced into one. The prefix is only READ for placement; the full\n * key value still names the DO.\n */\n regionPrefixedKeys?: boolean;\n}\n\nexport type ShardResolution =\n | {\n kind: \"shard\";\n name: string;\n /** The geographic `locationHint` to place a NEWLY-created shard-DO near its audience (source\n * a/b/c). Absent ⇒ the router forwards with NO options bag (byte-identical to the pre-hint\n * behavior; Cloudflare places the DO near the first requester). */\n locationHint?: string;\n }\n | {\n kind: \"fanout\";\n /** Every shard-DO name to fan this request out to (mode \"hash\" only — the enumerable\n * `shardIdList(numShards)`, e.g. `[\"default\", \"s1\", \"s2\", …]`). */\n shardIds: string[];\n }\n | { kind: \"error\"; status: number; body: ShardRoutingErrorBody };\n\nconst SHARD_HEADER = \"x-helipod-shard\";\nconst FANOUT_HEADER = \"x-helipod-fanout\";\n\n/** Truthy fan-out signal: header `true`/`1`, or `?fanout=true`/`1`. */\nfunction isFanoutRequested(url: URL, headers: Headers): boolean {\n const h = headers.get(FANOUT_HEADER);\n if (h && (h === \"true\" || h === \"1\")) return true;\n const q = url.searchParams.get(\"fanout\");\n return q === \"true\" || q === \"1\";\n}\n\n/** The explicit envelope shard key, if any (header wins over query param). Empty string counts as\n * \"not provided\" so a stray `?shard=` never routes an unsharded request off the default DO. */\nfunction explicitShardKey(url: URL, headers: Headers): string | null {\n const h = headers.get(SHARD_HEADER);\n if (h && h.length > 0) return h;\n const q = url.searchParams.get(\"shard\");\n if (q && q.length > 0) return q;\n return null;\n}\n\n/** Split a `path:name` function path into its `[modulePath, name]`, honoring nested module paths\n * (`foo/bar:baz`). Returns null if the path has no `:` separator. */\nfunction splitFunctionPath(path: string): [string, string] | null {\n const i = path.lastIndexOf(\":\");\n if (i <= 0 || i === path.length - 1) return null;\n return [path.slice(0, i), path.slice(i + 1)];\n}\n\ninterface DerivedShard {\n /** The function declared `shardBy` (a \"sharded mutation\"). */\n sharded: boolean;\n /** The shard-key value, present only when `sharded` and the args carried it. */\n value?: unknown;\n hasValue: boolean;\n}\n\n/** Mirror the executor's shard resolution (`executor.ts` run()): a mutation with a `shardBy` string\n * reads the named arg; a `shardBy` resolver function is called with the args. Anything else (a query,\n * a no-`shardBy` mutation, an unknown path) is not a sharded op. Never throws — a resolver that throws\n * is treated as \"no value\" so the caller surfaces `SHARD_KEY_REQUIRED`, not a 500. */\nfunction deriveShardFromRun(loaded: LoadedProject, path: string, args: unknown): DerivedShard {\n const split = splitFunctionPath(path);\n if (!split) return { sharded: false, hasValue: false };\n const [modulePath, name] = split;\n const fn = loaded.modules[modulePath]?.[name] as\n | { type?: unknown; shardBy?: unknown }\n | undefined;\n if (!fn || fn.type !== \"mutation\" || fn.shardBy === undefined) return { sharded: false, hasValue: false };\n const shardBy = fn.shardBy;\n let value: unknown;\n if (typeof shardBy === \"string\") {\n value = (args as Record<string, unknown> | null | undefined)?.[shardBy];\n } else if (typeof shardBy === \"function\") {\n try {\n value = (shardBy as (a: unknown) => unknown)(args);\n } catch {\n value = undefined;\n }\n }\n return { sharded: true, value, hasValue: value !== undefined && value !== null };\n}\n\n/** Classify a `path:name` function's declared `type` (`\"query\"` / `\"mutation\"` / `\"action\"` / …) from\n * the loaded module set — the SAME lookup `deriveShardFromRun` uses (split the path, index into\n * `loaded.modules`), reused here so fanOut's read-only guard agrees with the shard-key deriver about\n * what a \"sharded mutation\" even is. Returns `undefined` when the type cannot be determined (a\n * malformed path or an unknown function) — callers MUST treat `undefined` as \"not a query\" (fail\n * closed), never as \"assume query\". */\nfunction classifyFunctionType(loaded: LoadedProject, path: string): string | undefined {\n const split = splitFunctionPath(path);\n if (!split) return undefined;\n const [modulePath, name] = split;\n const fn = loaded.modules[modulePath]?.[name] as { type?: unknown } | undefined;\n return typeof fn?.type === \"string\" ? fn.type : undefined;\n}\n\n/**\n * Resolve the owning shard-DO name for a request (or a typed rejection). Reads the request body only\n * when needed (a `POST /api/run` with no explicit key + a `loaded` project); callers MUST forward the\n * ORIGINAL request, never the clone this reads.\n */\nexport async function resolveShard(request: Request, opts: ShardRoutingOptions = {}): Promise<ShardResolution> {\n const mode = opts.mode ?? \"key\";\n const numShards = opts.numShards ?? 1;\n const url = new URL(request.url);\n const headers = request.headers;\n const cf = (request as { cf?: { continent?: string } }).cf;\n\n /** Attach the placement hint (sources a/b/c) to a resolved shard name, or surface an invalid explicit\n * region as a typed 400. `shardKeyValue` feeds the opt-in region-prefix source; `undefined` on the\n * default DO. Kept as a single tail so every shard branch derives the hint identically. */\n const finish = (name: string, shardKeyValue: unknown): ShardResolution => {\n const derived = deriveLocationHint({\n url,\n headers,\n cf,\n shardKeyValue,\n regionPrefixedKeys: opts.regionPrefixedKeys,\n });\n if (!derived.ok) {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n INVALID_REGION_HINT,\n `region hint \"${derived.invalidRegion}\" is not a valid Durable Object location hint. ` +\n `Use one of: ${LOCATION_HINTS.join(\", \")}.`,\n ),\n };\n }\n return derived.hint !== undefined ? { kind: \"shard\", name, locationHint: derived.hint } : { kind: \"shard\", name };\n };\n\n // Fan-out (M2d): a non-reactive one-shot read across every shard of a fixed-shard-count (mode\n // \"hash\") deployment. Guarded up front, before the explicit/derived/default branches below, since\n // fan-out addresses ALL shards rather than resolving to exactly one.\n if (isFanoutRequested(url, headers)) {\n // fanOut needs an enumerable shard set — only mode \"hash\" (a fixed numShards) has one.\n if (mode !== \"hash\") {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n FANOUT_REQUIRES_FIXED_SHARDS,\n \"fanOut requires a fixed-shard-count deployment (routing mode \\\"hash\\\"); this deployment addresses a Durable Object per key value with no enumerable shard set.\",\n ),\n };\n }\n // fanOut + a shard key is contradictory — target one shard OR fan out all, not both.\n if (explicitShardKey(url, headers) !== null) {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n FANOUT_WITH_SHARD_KEY,\n \"a request may either fan out across all shards or name one shard key, not both.\",\n ),\n };\n }\n // fanOut is non-reactive — a WebSocket subscribe can't fan out.\n if (url.pathname === \"/api/sync\" || headers.get(\"upgrade\")?.toLowerCase() === \"websocket\") {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n FANOUT_NOT_SUBSCRIBABLE,\n \"fanOut is a non-reactive one-shot read and cannot be used on a WebSocket (/api/sync) subscription.\",\n ),\n };\n }\n // fanOut is READ-ONLY (C1): reject anything that isn't a resolved `query` — a plain mutation, a\n // `.shardBy` sharded mutation, or an action would otherwise fall through here and get fanned out\n // to every shard-DO, each of which RUNS AND COMMITS it via the unmodified DO — turning one\n // logical write into N. Classify the target the same way `deriveShardFromRun` looks up the\n // function; a target whose type can't be PROVEN to be \"query\" (no `loaded`, an unparseable body,\n // a request shape other than `POST /api/run`, or an unknown path) fails CLOSED under the same\n // code — never fanned out on the assumption that it might be a query.\n let fanoutTargetType: string | undefined;\n let fanoutPath: string | undefined;\n if (opts.loaded && request.method === \"POST\" && url.pathname === \"/api/run\") {\n let body: { path?: unknown; args?: unknown } | undefined;\n try {\n body = (await request.clone().json()) as { path?: unknown; args?: unknown };\n } catch {\n body = undefined;\n }\n if (body && typeof body.path === \"string\") {\n fanoutPath = body.path;\n fanoutTargetType = classifyFunctionType(opts.loaded, body.path);\n }\n }\n if (fanoutTargetType !== \"query\") {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n FANOUT_NOT_A_QUERY,\n fanoutPath !== undefined\n ? `\"${fanoutPath}\" is not a query (fanOut is read-only; only queries may fan out across shards).`\n : \"fanOut requires a POST /api/run request naming a resolvable query function; the target's type could not be determined.\",\n ),\n };\n }\n return { kind: \"fanout\", shardIds: shardIdList(numShards) };\n }\n\n // (1) Explicit envelope key.\n const explicit = explicitShardKey(url, headers);\n if (explicit !== null) {\n // A comma-separated key is an attempt to address several shards at once — refuse it.\n if (explicit.includes(\",\")) {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n CROSS_SHARD_UNSUPPORTED,\n `a request may name exactly one shard key; received a multi-valued key (\"${explicit}\"). ` +\n \"M1 supports only shard-scoped operations.\",\n ),\n };\n }\n return finish(shardDoName(explicit, mode, numShards), explicit);\n }\n\n // (2) Derive from a POST /api/run body when we have the bundled app to consult.\n if (opts.loaded && request.method === \"POST\" && url.pathname === \"/api/run\") {\n let body: { path?: unknown; args?: unknown } | undefined;\n try {\n body = (await request.clone().json()) as { path?: unknown; args?: unknown };\n } catch {\n body = undefined; // let the DO produce its own parse error; route to default.\n }\n if (body && typeof body.path === \"string\") {\n const derived = deriveShardFromRun(opts.loaded, body.path, body.args);\n if (derived.sharded) {\n if (!derived.hasValue) {\n return {\n kind: \"error\",\n status: 400,\n body: routingError(\n SHARD_KEY_REQUIRED,\n `mutation \"${body.path}\" is sharded but its arguments do not carry the shard key. ` +\n \"Include the shard-key field in the arguments, or pass it as an `X-Helipod-Shard` header.\",\n ),\n };\n }\n return finish(shardDoName(derived.value, mode, numShards), derived.value);\n }\n }\n }\n\n // (3) Default DO — unsharded / no-`shardBy` / non-run requests.\n return finish(DEFAULT_SHARD_DO_NAME, undefined);\n}\n","/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * Pure generator for a MULTI-SHARD Cloudflare Worker/DO entrypoint — the ee twin of the free host's\n * `generateWorkerEntrySource` (`packages/runtime-cloudflare/src/worker-entry.ts`). Same static-import\n * discipline (a Worker bundle can no more dir-scan `convex/` than a `bun build --compile` binary can),\n * and it emits the SAME concrete DO class — a plain `HelipodDurableObject` subclass from the FREE\n * package, unmodified (M1: a shard-DO IS Slice 3). The ONLY difference from the single-shard entry is\n * the `export default`: `createShardWorkerHandler(...)` (this ee package) instead of\n * `createWorkerHandler(...)` (the free package).\n *\n * This is the licensing switch made concrete: an app's Worker entry chooses single- vs multi-shard by\n * WHICH handler it default-exports. The free host never imports this generator.\n *\n * File storage (`r2BindingName`) is wired the same way the free generator wires it: since every\n * shard-DO is that SAME unmodified `HelipodDurableObject`, its `appConfig()` already knows how to use\n * a `blobStore` — this generator just needs to construct one from the R2 binding and thread it through,\n * identically for every shard.\n */\nexport interface ShardWorkerEntryInputs {\n moduleImports: Array<{ key: string; absPath: string }>;\n schemaAbsPath: string;\n configAbsPath: string | null;\n /** The shard-DO `durable_objects` binding name (e.g. `\"HELIPOD_DO\"`). */\n bindingName: string;\n /** The exported DO class name the `wrangler.jsonc` migration references (e.g. `\"HelipodDO\"`). */\n doClassName: string;\n /** Routing mode: \"key\" (default) or \"hash\". */\n mode?: \"key\" | \"hash\";\n /** For mode \"hash\": the fixed shard count N. */\n numShards?: number;\n /** Opt-in geographic placement source (b): derive a shard-DO's `locationHint` from a `\"<hint>:<rest>\"`\n * prefix on the shard-key value. Off by default. */\n regionPrefixedKeys?: boolean;\n /** The env var the admin key is read from at DO boot (default `HELIPOD_ADMIN_KEY`). */\n adminKeyEnvVar?: string;\n /** Override the free host package import specifier (the in-repo fixture uses this). */\n hostPackage?: string;\n /** Override the ee shard package import specifier (the in-repo fixture uses this). */\n shardPackage?: string;\n /**\n * The `r2_buckets` binding name in `wrangler.jsonc` (e.g. `\"STORAGE_BUCKET\"`). Set to enable file\n * storage on the shard-DO: the generated `appConfig` constructs an `R2BlobStore` over\n * `env[r2BindingName]` and threads it as `blobStore`, so `ctx.storage` + the `/api/storage/*` routes\n * work on every shard-DO — same mechanism as the free single-shard host\n * (`generateWorkerEntrySource`'s `r2BindingName`, `packages/runtime-cloudflare/src/worker-entry.ts`),\n * since each shard-DO is that SAME unmodified `HelipodDurableObject` class. Absent → no R2 import,\n * `blobStore` omitted, file storage inert (byte-less deploy, unchanged).\n */\n r2BindingName?: string;\n}\n\nexport function generateShardWorkerEntrySource(inp: ShardWorkerEntryInputs): string {\n const host = inp.hostPackage ?? \"@helipod/runtime-cloudflare\";\n const shard = inp.shardPackage ?? \"@helipod/runtime-cloudflare-shard\";\n const adminEnv = inp.adminKeyEnvVar ?? \"HELIPOD_ADMIN_KEY\";\n const mode = inp.mode ?? \"key\";\n const r2 = inp.r2BindingName;\n const L: string[] = [\"// AUTO-GENERATED by `@helipod/runtime-cloudflare-shard` worker-entry — do not edit.\"];\n inp.moduleImports.forEach((m, i) => L.push(`import * as m${i} from ${JSON.stringify(m.absPath)};`));\n L.push(`import schema from ${JSON.stringify(inp.schemaAbsPath)};`);\n if (inp.configAbsPath) L.push(`import * as __config from ${JSON.stringify(inp.configAbsPath)};`);\n L.push(`import { HelipodDurableObject } from ${JSON.stringify(host)};`);\n L.push(`import { createShardWorkerHandler } from ${JSON.stringify(shard)};`);\n // R2 blob store — imported only when a bucket binding is configured, so a byte-less deploy pulls in\n // no `@helipod/blobstore-r2` at all (mirrors the free single-shard generator verbatim).\n if (r2) L.push(`import { R2BlobStore } from \"@helipod/blobstore-r2\";`);\n L.push(\"\");\n const modEntries = inp.moduleImports.map((m, i) => `${JSON.stringify(m.key)}: m${i}`).join(\", \");\n L.push(`const loaded = { schema: (schema && schema.default) ?? schema, modules: { ${modEntries} } };`);\n L.push(inp.configAbsPath ? `const components = (__config.default ?? __config).components ?? [];` : `const components = [];`);\n L.push(\"\");\n L.push(`export class ${inp.doClassName} extends HelipodDurableObject {`);\n L.push(` appConfig(env) {`);\n if (r2) {\n // `env[r2BindingName]` is the R2 bucket binding, shared across every shard-DO instance; guard so a\n // missing binding degrades to byte-less rather than throwing at boot.\n L.push(` const __bucket = env && env[${JSON.stringify(r2)}];`);\n L.push(` return { loaded, components, adminKey: (env && env[${JSON.stringify(adminEnv)}]) ?? \"\", ...(__bucket ? { blobStore: new R2BlobStore({ bucket: __bucket }) } : {}) };`);\n } else {\n L.push(` return { loaded, components, adminKey: (env && env[${JSON.stringify(adminEnv)}]) ?? \"\" };`);\n }\n L.push(` }`);\n L.push(`}`);\n L.push(\"\");\n // `loaded` is passed to the router so a POST /api/run can DERIVE a mutation's shard key from its args.\n // `regionPrefixedKeys` (opt-in) enables the `\"<hint>:<rest>\"`-prefix placement source.\n const rpk = inp.regionPrefixedKeys ? \", regionPrefixedKeys: true\" : \"\";\n const routeOpts = mode === \"hash\"\n ? `{ mode: \"hash\", numShards: ${JSON.stringify(inp.numShards ?? 1)}, loaded${rpk} }`\n : `{ mode: \"key\", loaded${rpk} }`;\n L.push(`export default createShardWorkerHandler(${JSON.stringify(inp.bindingName)}, ${routeOpts});`);\n return L.join(\"\\n\") + \"\\n\";\n}\n","/* Helipod Enterprise. Licensed under the Helipod Commercial License — see ee/LICENSE. */\n\n/**\n * `@helipod/runtime-cloudflare-shard` — the MULTI-SHARD Cloudflare Durable Object front (Slice 6,\n * Milestone 1: `.shardBy(key)` write scale-out). A paid-tier (ee/) SIBLING of `ee/fleet`: this is the\n * Cloudflare-native analog of the portable multi-node scale-out. It routes each request to the owning\n * shard-DO by the shard key (CF's `getByName(shardKey)` model — the DO NAME is the shard key, no shard\n * map, no coordinator), and each shard-DO is an UNMODIFIED `HelipodDurableObject` from the FREE\n * `@helipod/runtime-cloudflare` package. N distinct keys ⇒ N distinct DOs ⇒ N× write throughput +\n * N×10 GB storage. No engine change: M1 is pure routing over independent Slice-3 hosts.\n *\n * NON-GOALS (M1, enforced not silently broken): a reactive query/mutation spanning MULTIPLE shards, and\n * cross-shard global-unique — both rejected with a typed `CROSS_SHARD_UNSUPPORTED`. `.global()`/D1 and\n * the opt-in non-reactive fan-out read are Milestone 2.\n */\nexport const RUNTIME_CLOUDFLARE_SHARD_VERSION = \"0.0.0\";\n\nexport { createShardWorkerHandler, DEFAULT_SHARD_NAME } from \"./worker\";\nexport { resolveShard, type ShardRoutingOptions, type ShardResolution } from \"./route\";\nexport { shardDoName, DEFAULT_SHARD_DO_NAME, type ShardRoutingMode } from \"./canonical\";\nexport {\n SHARD_KEY_REQUIRED,\n CROSS_SHARD_UNSUPPORTED,\n INVALID_REGION_HINT,\n routingError,\n type ShardRoutingErrorCode,\n type ShardRoutingErrorBody,\n} from \"./errors\";\nexport {\n deriveLocationHint,\n continentToHint,\n CONTINENT_TO_HINT,\n type HintDerivation,\n type DeriveLocationHintInput,\n} from \"./location\";\nexport { generateShardWorkerEntrySource, type ShardWorkerEntryInputs } from \"./worker-entry\";\n\n// Re-export the FREE DO host class so an app imports ONE package at its Worker entry. This is a plain\n// re-export of the free class — the multi-shard host is the SAME DO, addressed N times by the router.\nexport { HelipodDurableObject, type DurableObjectAppConfig } from \"@helipod/runtime-cloudflare\";\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@helipod/runtime-cloudflare-shard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:workers": "vitest run --config vitest.workers.config.ts && vitest run --config vitest.workers.hash.config.ts",
|
|
24
|
+
"typecheck": "tsc --noEmit --project tsconfig.json",
|
|
25
|
+
"clean": "rm -rf dist .turbo"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@helipod/runtime-cloudflare": "0.1.0",
|
|
29
|
+
"@helipod/id-codec": "0.1.0",
|
|
30
|
+
"@helipod/index-key-codec": "0.1.0",
|
|
31
|
+
"@helipod/cli": "0.1.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@cloudflare/vitest-pool-workers": "0.5.40",
|
|
35
|
+
"@cloudflare/workers-types": "^4.20241127.0",
|
|
36
|
+
"@helipod/docstore-d1": "0.1.0",
|
|
37
|
+
"@helipod/executor": "0.1.0",
|
|
38
|
+
"@helipod/values": "0.1.0",
|
|
39
|
+
"@types/node": "^22.10.5",
|
|
40
|
+
"tsup": "^8.3.5",
|
|
41
|
+
"typescript": "^5.7.2",
|
|
42
|
+
"vitest": "^2.1.8"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/helipod-sh/helipod.git",
|
|
50
|
+
"directory": "ee/packages/runtime-cloudflare-shard"
|
|
51
|
+
}
|
|
52
|
+
}
|