@thomasfosterau/legation-alchemy-cloudflare 0.0.0-stage → 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/CHANGELOG.md +172 -0
- package/LICENSE +21 -0
- package/README.md +222 -2
- package/dist/index.d.ts +329 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +613 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/package.json +58 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @thomasfosterau/legation-alchemy-cloudflare
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 604ffd3: New package `@thomasfosterau/legation-alchemy-cloudflare` (ADR-0034): the reference deployment of ADR-0022's topology on Cloudflare, built on the Effect-native services of Alchemy's Cloudflare provider. It replaces `@thomasfosterau/legation-cloudflare`, which was never published.
|
|
8
|
+
|
|
9
|
+
- `Room.make(config)` is the per-user room's Alchemy Durable Object implementation: the sync server over stateless HTTP push/pull/read, CVRs in the object's storage (`DurableCvrStore`), a `FanoutBus` over the topic shards, and hibernatable sockets (`Cloudflare.upgrade`) that carry only contentless pokes. `backend`, `policy`, `identity` and `queries` are effects built in the object's per-instance phase; `shards` is the app's shard class. Every shard call, room poke and lease registration is bounded by `publishTimeout`.
|
|
10
|
+
- `FanoutShard.make()` is the topic shard's implementation, with a typed RPC surface (`register`, `publish`): a shard answers its leased rooms and the publishing room pokes each once, over the `poke` RPC, with every topic it matched.
|
|
11
|
+
- The app declares the classes (`class LegationRoom extends Cloudflare.DurableObject<LegationRoom, Room.RoomObject>()("LegationRoom") {}`) and binds the implementations (`LegationRoom.make(Room.make(...))`, `LegationShard.make(FanoutShard.make())`).
|
|
12
|
+
- `Worker.routeSync` routes an Alchemy Worker's `/sync` requests to the caller's room through the namespace's stub, answering `Option.none()` outside the base path. A room answers a route by the path's last segment.
|
|
13
|
+
- `D1Executor.make(db)` is `SqlBackend`'s executor over `Cloudflare.D1.QueryDatabase`'s client; D1 has no interactive transactions, so it reports `transactional: false` and does not meet Backend guarantee 1 (#342, #353).
|
|
14
|
+
- `RoomTransport`, `BindingTransport` and `EdgeInvalidation` are unchanged from the package this one replaces.
|
|
15
|
+
|
|
16
|
+
Peers: `alchemy` and `@alchemy.run/cloudflare` at exactly 2.0.0-beta.85, and `effect ^4.1.0`, which Alchemy requires. `@cloudflare/workers-types` is not a peer.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Updated dependencies [2a1f63c]
|
|
21
|
+
- Updated dependencies [ef59fa7]
|
|
22
|
+
- Updated dependencies [56a2e51]
|
|
23
|
+
- Updated dependencies [d09dc65]
|
|
24
|
+
- Updated dependencies [db2366a]
|
|
25
|
+
- Updated dependencies [97db489]
|
|
26
|
+
- Updated dependencies [cc67999]
|
|
27
|
+
- Updated dependencies [a94df19]
|
|
28
|
+
- Updated dependencies [d76b04d]
|
|
29
|
+
- Updated dependencies [6f53fb1]
|
|
30
|
+
- Updated dependencies [d675d2e]
|
|
31
|
+
- Updated dependencies [1743c2b]
|
|
32
|
+
- Updated dependencies [1c56914]
|
|
33
|
+
- Updated dependencies [615e792]
|
|
34
|
+
- Updated dependencies [c8d9afc]
|
|
35
|
+
- Updated dependencies [4709f41]
|
|
36
|
+
- Updated dependencies [f0348b4]
|
|
37
|
+
- Updated dependencies [3e29e1b]
|
|
38
|
+
- Updated dependencies [a053736]
|
|
39
|
+
- Updated dependencies [8a8adfc]
|
|
40
|
+
- Updated dependencies [8995115]
|
|
41
|
+
- Updated dependencies [8995115]
|
|
42
|
+
- Updated dependencies [87fa7a5]
|
|
43
|
+
- Updated dependencies [822001c]
|
|
44
|
+
- Updated dependencies [4c59860]
|
|
45
|
+
- Updated dependencies [961e604]
|
|
46
|
+
- Updated dependencies [5937efe]
|
|
47
|
+
- Updated dependencies [08ac9cd]
|
|
48
|
+
- Updated dependencies [1fc3725]
|
|
49
|
+
- Updated dependencies [8995115]
|
|
50
|
+
- Updated dependencies [8a8adfc]
|
|
51
|
+
- Updated dependencies [025fabb]
|
|
52
|
+
- Updated dependencies [0b8fccb]
|
|
53
|
+
- Updated dependencies [e7369e1]
|
|
54
|
+
- Updated dependencies [8995115]
|
|
55
|
+
- Updated dependencies [c856d66]
|
|
56
|
+
- Updated dependencies [01a8c64]
|
|
57
|
+
- Updated dependencies [3362c37]
|
|
58
|
+
- Updated dependencies [62dcb1c]
|
|
59
|
+
- Updated dependencies [be4be06]
|
|
60
|
+
- Updated dependencies [ae18cb3]
|
|
61
|
+
- Updated dependencies [be0a44d]
|
|
62
|
+
- Updated dependencies [cc2f5b7]
|
|
63
|
+
- Updated dependencies [e818dc4]
|
|
64
|
+
- Updated dependencies [d1b01a4]
|
|
65
|
+
- Updated dependencies [5496ddf]
|
|
66
|
+
- Updated dependencies [19eb4b0]
|
|
67
|
+
- Updated dependencies [a874acb]
|
|
68
|
+
- Updated dependencies [c5d3484]
|
|
69
|
+
- Updated dependencies [38c3147]
|
|
70
|
+
- Updated dependencies [e9363c0]
|
|
71
|
+
- Updated dependencies [fed4634]
|
|
72
|
+
- Updated dependencies [fcc52eb]
|
|
73
|
+
- Updated dependencies [454e75c]
|
|
74
|
+
- Updated dependencies [2003cd6]
|
|
75
|
+
- Updated dependencies [52c614f]
|
|
76
|
+
- Updated dependencies [94d6327]
|
|
77
|
+
- Updated dependencies [91f07db]
|
|
78
|
+
- Updated dependencies [6e9e119]
|
|
79
|
+
- Updated dependencies [6c2c802]
|
|
80
|
+
- Updated dependencies [ebcc6aa]
|
|
81
|
+
- Updated dependencies [a1b95d4]
|
|
82
|
+
- Updated dependencies [c105fbf]
|
|
83
|
+
- Updated dependencies [e1a028a]
|
|
84
|
+
- Updated dependencies [dfb8759]
|
|
85
|
+
- Updated dependencies [9260efb]
|
|
86
|
+
- Updated dependencies [0644af5]
|
|
87
|
+
- Updated dependencies [4b05d55]
|
|
88
|
+
- Updated dependencies
|
|
89
|
+
- Updated dependencies [82911cc]
|
|
90
|
+
- Updated dependencies [9c9f2b0]
|
|
91
|
+
- Updated dependencies [7b21d11]
|
|
92
|
+
- Updated dependencies [6f54de8]
|
|
93
|
+
- Updated dependencies [ab1fb4e]
|
|
94
|
+
- Updated dependencies [792d8f5]
|
|
95
|
+
- Updated dependencies [a3b2ee4]
|
|
96
|
+
- Updated dependencies [eb9555a]
|
|
97
|
+
- Updated dependencies [c0ab703]
|
|
98
|
+
- Updated dependencies [8c65627]
|
|
99
|
+
- Updated dependencies [26ad3f3]
|
|
100
|
+
- Updated dependencies [d7cafd2]
|
|
101
|
+
- Updated dependencies [cb0a33d]
|
|
102
|
+
- Updated dependencies [912d95d]
|
|
103
|
+
- Updated dependencies [7290d2b]
|
|
104
|
+
- Updated dependencies [41c33e3]
|
|
105
|
+
- Updated dependencies [a77261c]
|
|
106
|
+
- Updated dependencies [c47786c]
|
|
107
|
+
- Updated dependencies [501b12c]
|
|
108
|
+
- Updated dependencies [7f1efac]
|
|
109
|
+
- Updated dependencies [a04918b]
|
|
110
|
+
- Updated dependencies [cf0b26c]
|
|
111
|
+
- Updated dependencies [ddb1fda]
|
|
112
|
+
- Updated dependencies [f54f568]
|
|
113
|
+
- Updated dependencies [fc880dc]
|
|
114
|
+
- Updated dependencies [9cbd160]
|
|
115
|
+
- Updated dependencies [0bebedf]
|
|
116
|
+
- Updated dependencies [bc4c599]
|
|
117
|
+
- Updated dependencies [20a6e18]
|
|
118
|
+
- Updated dependencies [b4d2b80]
|
|
119
|
+
- Updated dependencies [2201b4c]
|
|
120
|
+
- Updated dependencies [0cdefe7]
|
|
121
|
+
- Updated dependencies [66f3b5b]
|
|
122
|
+
- Updated dependencies [7e11e61]
|
|
123
|
+
- Updated dependencies [6175e38]
|
|
124
|
+
- Updated dependencies [caf4761]
|
|
125
|
+
- Updated dependencies [821b10d]
|
|
126
|
+
- Updated dependencies [9e74a83]
|
|
127
|
+
- Updated dependencies [49fd066]
|
|
128
|
+
- Updated dependencies [06405ed]
|
|
129
|
+
- Updated dependencies [e7f4871]
|
|
130
|
+
- Updated dependencies [916d91b]
|
|
131
|
+
- Updated dependencies [47f4e15]
|
|
132
|
+
- Updated dependencies [f829146]
|
|
133
|
+
- Updated dependencies [13658a7]
|
|
134
|
+
- Updated dependencies [31cfb2c]
|
|
135
|
+
- Updated dependencies [a1cea1c]
|
|
136
|
+
- Updated dependencies [7e94abc]
|
|
137
|
+
- Updated dependencies [4fbd0b7]
|
|
138
|
+
- Updated dependencies [9d2f21b]
|
|
139
|
+
- Updated dependencies [3016170]
|
|
140
|
+
- Updated dependencies [f4a1a60]
|
|
141
|
+
- Updated dependencies [507ccb1]
|
|
142
|
+
- Updated dependencies [c671ccf]
|
|
143
|
+
- Updated dependencies [16889f0]
|
|
144
|
+
- Updated dependencies [fb2a992]
|
|
145
|
+
- Updated dependencies [22f320d]
|
|
146
|
+
- Updated dependencies [652ae7b]
|
|
147
|
+
- Updated dependencies [83bcf76]
|
|
148
|
+
- Updated dependencies [20e57d3]
|
|
149
|
+
- Updated dependencies [f010749]
|
|
150
|
+
- Updated dependencies [8d02113]
|
|
151
|
+
- Updated dependencies [bf9054d]
|
|
152
|
+
- Updated dependencies [e40c73a]
|
|
153
|
+
- Updated dependencies [ca38874]
|
|
154
|
+
- Updated dependencies [27e1040]
|
|
155
|
+
- Updated dependencies [e68eb3e]
|
|
156
|
+
- Updated dependencies [985c4d5]
|
|
157
|
+
- Updated dependencies [3be5abc]
|
|
158
|
+
- Updated dependencies [97db489]
|
|
159
|
+
- Updated dependencies [4a23603]
|
|
160
|
+
- Updated dependencies [f441941]
|
|
161
|
+
- Updated dependencies [a01f43b]
|
|
162
|
+
- Updated dependencies [9a6426b]
|
|
163
|
+
- Updated dependencies [97cc5f9]
|
|
164
|
+
- Updated dependencies [fc880dc]
|
|
165
|
+
- Updated dependencies [e34bc24]
|
|
166
|
+
- Updated dependencies [0470e1c]
|
|
167
|
+
- Updated dependencies [439f449]
|
|
168
|
+
- Updated dependencies [3a88118]
|
|
169
|
+
- Updated dependencies [1b8b120]
|
|
170
|
+
- Updated dependencies [cf48723]
|
|
171
|
+
- Updated dependencies [b4489c5]
|
|
172
|
+
- @thomasfosterau/legation@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Foster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,223 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @thomasfosterau/legation-alchemy-cloudflare
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The reference deployment of the [`@thomasfosterau/legation`](../effect) sync engine on
|
|
4
|
+
Cloudflare — per-user Durable Object rooms, topic-sharded fan-out, and a D1
|
|
5
|
+
backend — following ADR-0022 (topology) and ADR-0023 (topic-derived fan-out),
|
|
6
|
+
built on the Effect-native services of [Alchemy](https://alchemy.run)'s
|
|
7
|
+
Cloudflare provider, `@alchemy.run/cloudflare` (ADR-0034). The Durable Objects
|
|
8
|
+
are Alchemy Durable Objects, their state, storage and sockets are the provider's
|
|
9
|
+
`Cloudflare.DurableObjectState`, and the D1 executor runs through its
|
|
10
|
+
`Cloudflare.D1.QueryDatabase` client.
|
|
11
|
+
|
|
12
|
+
- **`Room`** — the per-user Durable Object (`LegationRoom.make(Room.make(...))`).
|
|
13
|
+
It runs the sync `Server` for one user: push/pull/read over stateless HTTP
|
|
14
|
+
(each request wakes the object, is answered, and lets it hibernate), CVRs in
|
|
15
|
+
the object's transactional storage (`DurableCvrStore`, co-located with the
|
|
16
|
+
user), a `FanoutBus` over the topic shards, and hibernatable WebSockets
|
|
17
|
+
(`Cloudflare.upgrade`) that carry only contentless poke frames — so hibernated
|
|
18
|
+
sockets cost nothing between writes. Each shard call, room poke and lease
|
|
19
|
+
registration is bounded by `publishTimeout` (default 5 seconds): a hung shard
|
|
20
|
+
is dropped with its poke, so it cannot hold a push open, and a lost poke only
|
|
21
|
+
delays the next pull.
|
|
22
|
+
- **`FanoutShard`** — one Durable Object per topic (`getByName(topic)` is the
|
|
23
|
+
shard function), with a typed RPC surface (`register`, `publish`). Rooms lease
|
|
24
|
+
their topics on a heartbeat; a publish answers every live room, and the
|
|
25
|
+
publishing room pokes them (one `poke` RPC per room, carrying every topic it
|
|
26
|
+
matched). Expired leases age out lazily, so a crashed room needs no
|
|
27
|
+
distributed cleanup.
|
|
28
|
+
- **`Worker` (`routeSync`)** — the stateless entry, for an Alchemy Worker's
|
|
29
|
+
`fetch`. It claims only the sync base path (default `/sync`), resolves the
|
|
30
|
+
caller's identity to their room, forwards the request to the room's stub
|
|
31
|
+
(WebSocket upgrades included), and answers `Option.none()` for everything
|
|
32
|
+
else, so it composes inside any existing handler.
|
|
33
|
+
- **`D1Executor`** — a `SqlBackend` executor over the `QueryDatabaseClient` of
|
|
34
|
+
`Cloudflare.D1.QueryDatabase`. D1 has no interactive transactions, so a
|
|
35
|
+
mutation's writes and its mutation-id bump commit separately, and D1 isolates
|
|
36
|
+
no handler's reads from another client group's writes: SqlBackend on D1 does
|
|
37
|
+
not meet Backend guarantee 1 (#342, #353). For apps that need it, use Durable
|
|
38
|
+
Object storage or Postgres via Hyperdrive; on D1, prefer single-statement,
|
|
39
|
+
idempotent handlers.
|
|
40
|
+
- **`RoomTransport`** — the browser-side `ClientTransport`: push/pull/read as
|
|
41
|
+
HTTP POSTs plus one reconnecting poke socket. Sync stays correct over HTTP
|
|
42
|
+
alone — a dead socket only delays pokes, and the pull interval is the safety
|
|
43
|
+
net.
|
|
44
|
+
- **`BindingTransport`** — the same protocol over a **service binding**
|
|
45
|
+
(`env.API.fetch`), for the two-Worker shape where the Worker rendering the
|
|
46
|
+
page is not the Worker holding the backend. It forwards the inbound cookie
|
|
47
|
+
and origin, so the bound Worker resolves the same caller — and therefore the
|
|
48
|
+
same room — that the browser will reach on its own first pull.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install @thomasfosterau/legation-alchemy-cloudflare @thomasfosterau/legation effect \
|
|
54
|
+
alchemy@2.0.0-beta.85 @alchemy.run/cloudflare@2.0.0-beta.85
|
|
55
|
+
npm install --save-dev @cloudflare/workers-types
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`alchemy` and `@alchemy.run/cloudflare` are peers at exactly 2.0.0-beta.85 (each
|
|
59
|
+
`@alchemy.run/*` release pins `alchemy` exactly). `@cloudflare/workers-types` is
|
|
60
|
+
not a peer of this package: the provider's own declarations import it, so a
|
|
61
|
+
Worker project type-checks with it installed.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
The app declares its two Durable Object classes and binds this package's
|
|
66
|
+
implementations to them; the Worker hosting them provides both layers.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// sync.ts
|
|
70
|
+
import * as Cloudflare from "@alchemy.run/cloudflare";
|
|
71
|
+
import * as Effect from "effect/Effect";
|
|
72
|
+
import { Authorization } from "@thomasfosterau/legation";
|
|
73
|
+
import { SqlBackend } from "@thomasfosterau/legation/sql";
|
|
74
|
+
import { D1Executor, FanoutShard, Room } from "@thomasfosterau/legation-alchemy-cloudflare";
|
|
75
|
+
|
|
76
|
+
export class LegationShard extends Cloudflare.DurableObject<
|
|
77
|
+
LegationShard,
|
|
78
|
+
FanoutShard.ShardObject
|
|
79
|
+
>()("LegationShard") {}
|
|
80
|
+
|
|
81
|
+
export class LegationRoom extends Cloudflare.DurableObject<LegationRoom, Room.RoomObject>()(
|
|
82
|
+
"LegationRoom"
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
export const LegationShardLive = LegationShard.make(FanoutShard.make());
|
|
86
|
+
|
|
87
|
+
export const LegationRoomLive = LegationRoom.make(
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
// Bindings resolve at init; the room builds its server per instance.
|
|
90
|
+
const db = yield* Cloudflare.D1.QueryDatabase(AppDatabase);
|
|
91
|
+
return yield* Room.make({
|
|
92
|
+
nodes,
|
|
93
|
+
backend: Effect.map(D1Executor.make(db), (executor) => SqlBackend.make({ executor, nodes })),
|
|
94
|
+
policy: Effect.succeed(Authorization.policy({/* … */})),
|
|
95
|
+
shards: LegationShard,
|
|
96
|
+
});
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// worker.ts — the Worker that hosts both classes and routes /sync to the rooms
|
|
103
|
+
import * as Effect from "effect/Effect";
|
|
104
|
+
import * as Layer from "effect/Layer";
|
|
105
|
+
import * as Option from "effect/Option";
|
|
106
|
+
import { Worker } from "@thomasfosterau/legation-alchemy-cloudflare";
|
|
107
|
+
|
|
108
|
+
export default Api.make(
|
|
109
|
+
Effect.gen(function* () {
|
|
110
|
+
const rooms = yield* LegationRoom;
|
|
111
|
+
return {
|
|
112
|
+
fetch: Effect.gen(function* () {
|
|
113
|
+
const routed = yield* Worker.routeSync({ rooms, roomIdOf: resolveSession });
|
|
114
|
+
return Option.isSome(routed) ? routed.value : yield* app;
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}).pipe(Effect.provide(Layer.mergeAll(LegationRoomLive, LegationShardLive)))
|
|
118
|
+
);
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`Room.make`'s `backend`, `policy`, `identity` and `queries` are effects run in
|
|
122
|
+
the room's per-instance phase, where the object's state and Alchemy's
|
|
123
|
+
`RuntimeContext` are available, so a backend over D1 (or any other binding)
|
|
124
|
+
is built there. A request reaches the room with its base path intact, and the
|
|
125
|
+
room answers by the path's last segment (`/sync/push` is the `push` route).
|
|
126
|
+
|
|
127
|
+
## What Alchemy does not cover
|
|
128
|
+
|
|
129
|
+
- **Interactive transactions on D1.** The provider's client has `batch()`
|
|
130
|
+
(statements in one implicit transaction) but nothing interactive, so the
|
|
131
|
+
executor reports `transactional: false`, as before.
|
|
132
|
+
- **Aborting a shard or room call.** Alchemy's RPC stubs take no abort signal.
|
|
133
|
+
A call past `publishTimeout` is abandoned, not aborted: the room stops
|
|
134
|
+
waiting for it and workerd cancels it with the event that made it.
|
|
135
|
+
- **The `/socket` upgrade in tests.** `Cloudflare.upgrade` needs workerd's
|
|
136
|
+
`WebSocketPair` and a 101 response, which Node lacks; the tests accept a fake
|
|
137
|
+
socket directly.
|
|
138
|
+
|
|
139
|
+
## Effect version
|
|
140
|
+
|
|
141
|
+
Peers `effect ^4.1.0`, the floor Alchemy requires (`alchemy` and
|
|
142
|
+
`@alchemy.run/cloudflare` 2.0.0-beta.85 peer `effect ^4.1.0`). Tested against
|
|
143
|
+
`4.1.2` (the version CI pins) and `4.1.0` (the floor; CI runs this package's
|
|
144
|
+
tests against it on every PR); install one copy of `effect` in your app
|
|
145
|
+
(ADR-0003). The rest of legation peers `effect ^4.0.0`.
|
|
146
|
+
|
|
147
|
+
## SSR from a separate Worker
|
|
148
|
+
|
|
149
|
+
A binding-only API Worker (`workersDev: false`, no custom domain) is reachable
|
|
150
|
+
only through the front door's service binding, so an SSR render cannot hold the
|
|
151
|
+
sync server in-process. `Hydration.snapshot` takes a `ClientTransport`; give it
|
|
152
|
+
a `BindingTransport` and the render pulls over the binding:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// +page.server.ts — the effect an Effect-based `load` runs
|
|
156
|
+
import { BindingTransport } from "@thomasfosterau/legation-alchemy-cloudflare";
|
|
157
|
+
import { loadSnapshot } from "@thomasfosterau/legation-svelte/kit";
|
|
158
|
+
|
|
159
|
+
const pageSnapshot = ({ cookies, request, platform }) =>
|
|
160
|
+
loadSnapshot(cookies, {
|
|
161
|
+
nodes,
|
|
162
|
+
reads: [{ collection: "products" }],
|
|
163
|
+
transport: BindingTransport.make({
|
|
164
|
+
binding: platform.env.API,
|
|
165
|
+
request, // the document request: its cookie and origin are forwarded
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The render happens under the _browser's_ client group id, so the snapshot's
|
|
171
|
+
pull cookie continues that group's CVR conversation and the browser's first
|
|
172
|
+
pull is a delta rather than a reset. That only holds while the hop preserves
|
|
173
|
+
the caller's identity — drop the cookie and the snapshot still carries the
|
|
174
|
+
right rows while its cookie belongs to another room's CVR, which is a silent
|
|
175
|
+
reset on every first paint. `binding-snapshot.test.ts` pins both sides of that.
|
|
176
|
+
|
|
177
|
+
## Evicting server-side caches on a write
|
|
178
|
+
|
|
179
|
+
`EdgeInvalidation` turns the hints a committed write publishes into evictions
|
|
180
|
+
of the caches an app keeps on Cloudflare (#206): `kv` deletes the
|
|
181
|
+
`{collection}:{id}` entry of every touched row from a KV read-through cache, and
|
|
182
|
+
`cachePurge` purges the Workers cache by tag, with the same tag spelling the
|
|
183
|
+
browser's Cache API invalidation uses (`collection:{name}`,
|
|
184
|
+
`collection:{name}:{id}`, from `HttpCache` in `@thomasfosterau/legation/internal`).
|
|
185
|
+
The eviction is no longer an obligation of each mutation: it subscribes to the
|
|
186
|
+
bus (`InvalidationSink.run`).
|
|
187
|
+
|
|
188
|
+
**The cache lives in a different Worker than the write.** `cache.purge({ tags })`
|
|
189
|
+
purges only the calling Worker's cache. In the usual shape the front door (the
|
|
190
|
+
SvelteKit Worker serving every page) holds the cache, while writes land in the
|
|
191
|
+
API Worker behind its service binding. So the API Worker forwards what each
|
|
192
|
+
write touched, and the front door runs the sinks:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
// API Worker, where the server and its bus run (a Durable Object room, say)
|
|
196
|
+
yield * InvalidationSink.run(EdgeInvalidation.forward(env.FRONT_DOOR));
|
|
197
|
+
|
|
198
|
+
// Front door Worker: an RPC entrypoint, reachable only through the binding
|
|
199
|
+
export class Invalidation extends WorkerEntrypoint<Env> {
|
|
200
|
+
invalidate(payload: string): Promise<void> {
|
|
201
|
+
const sink = InvalidationSink.all([
|
|
202
|
+
EdgeInvalidation.kv(this.env.RESOURCES),
|
|
203
|
+
EdgeInvalidation.cachePurge(this.ctx.cache),
|
|
204
|
+
]);
|
|
205
|
+
return Effect.runPromise(EdgeInvalidation.receive(sink)(payload));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Expose the receiving end as an RPC method, not as a route on the front door's
|
|
211
|
+
`fetch`: a `fetch` route is reachable from the internet, and anyone could then
|
|
212
|
+
purge the cache.
|
|
213
|
+
|
|
214
|
+
Every eviction is fire-and-forget. A failed KV delete, a failed forward, and a
|
|
215
|
+
purge that rejects or resolves `{ success: false }` (which is how `cache.purge`
|
|
216
|
+
reports a Cloudflare-side failure, so a `catch` alone misses it) are logged;
|
|
217
|
+
none of them fails or delays the write, which committed before its hint was
|
|
218
|
+
published. The cache's TTL is the backstop for a lost eviction. A large write is
|
|
219
|
+
purged in calls of at most `maxTagsPerPurge` tags (default 30).
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT
|