@thomasfosterau/legation-browser 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 +842 -0
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/index.d.ts +656 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1109 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/package.json +57 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
# @thomasfosterau/legation-browser
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d356db4: Flush the journal after the tab has closed (#213, #246). `BrowserConnectivity`
|
|
8
|
+
flushes on reconnect and return-to-visible, but only while a page is open;
|
|
9
|
+
`BackgroundSync` covers the rest by answering a Background Sync `sync` event
|
|
10
|
+
in the service worker.
|
|
11
|
+
|
|
12
|
+
- **`BackgroundSync.listen`** installs the worker's `sync` listener. Call it at
|
|
13
|
+
the top level of the worker script: a worker woken for `sync` dispatches the
|
|
14
|
+
event straight after evaluating the script, so the listener holds the event
|
|
15
|
+
open (up to `readyTimeout`) until a host attaches, then flushes. Its
|
|
16
|
+
`identity` option is **required** (see below).
|
|
17
|
+
- **`ServiceWorkerHost.start({ backgroundSync })`** attaches the host's engine
|
|
18
|
+
to the listener for as long as it serves, and registers the tag only when
|
|
19
|
+
the journal cannot drain now: a mutation journaled while the worker is
|
|
20
|
+
offline (`navigator.onLine === false`, or the `offline` option), or a push
|
|
21
|
+
that failed terminally (`syncFailures`). Never on an online write — online,
|
|
22
|
+
Chromium fires a fresh registration's `sync` at once, which would push each
|
|
23
|
+
write on its own and bypass `pushDebounce` and paced compaction.
|
|
24
|
+
`registerOnJournal: false` turns host-side registration off.
|
|
25
|
+
- **`BackgroundSync.register`** is the page-side registration helper. It never
|
|
26
|
+
fails: `"Registered"`, `"Unsupported"` (no `SyncManager` — Safari, Firefox)
|
|
27
|
+
or `"Refused"`, logged at debug level only. Absent Background Sync, the
|
|
28
|
+
journal flushes on the next connectivity signal.
|
|
29
|
+
- **`BackgroundSync.flush`** is the flush itself: `ClientEngine.push`, not a
|
|
30
|
+
replay loop, so exactly-once comes from the backend's ledger and a refused
|
|
31
|
+
write is surfaced as `rejected` rather than discarded. A transport failure
|
|
32
|
+
rejects the event so the browser reschedules it. `"Flushed"` means the push
|
|
33
|
+
completed (possibly with nothing pending).
|
|
34
|
+
|
|
35
|
+
Only the library's own tag (`BackgroundSync.defaultTag`, or `tag`) is
|
|
36
|
+
answered. The identity boundary (#212) applies: quarantined entries are
|
|
37
|
+
unreachable from push, and `identity` — the fingerprint of whoever is signed
|
|
38
|
+
in now, as the app resolves it in the worker — is required, so a flush skips
|
|
39
|
+
(`"IdentityChanged"`) whenever the store is not stamped for that identity,
|
|
40
|
+
which catches a tab closed before `Client.reset` ran. An app with no sign-in
|
|
41
|
+
passes `BackgroundSync.identityUnchecked` to opt out explicitly.
|
|
42
|
+
|
|
43
|
+
`ServiceWorkerHost`'s docs no longer say Background Sync registration stays
|
|
44
|
+
app code. Periodic Background Sync is still app code, deliberately.
|
|
45
|
+
|
|
46
|
+
- a94df19: Bound every in-process message hub so a stalled or slow consumer can no longer
|
|
47
|
+
grow a queue without limit (#44). Because pull is the correctness path, each hub
|
|
48
|
+
sheds load under pressure without threatening convergence:
|
|
49
|
+
|
|
50
|
+
- **Poke hub** (`Transport.makePokeHub`): now `sliding(1)` per client group with
|
|
51
|
+
replay 1. A poke storm collapses to one queued poke per group — an outstanding
|
|
52
|
+
poke absorbs the rest — and a late/reconnecting subscriber still sees the most
|
|
53
|
+
recent poke.
|
|
54
|
+
- **Invalidation bus** (`InvalidationBus.makeInProcess` and the memory broker):
|
|
55
|
+
now bounded with a `dropping` strategy at a configurable capacity. A dropped
|
|
56
|
+
hint only delays the next pull (ADR-0009); drops are counted in
|
|
57
|
+
`legation_hints_dropped_total`. New `makeInProcessWith` / `layerInProcessWith`
|
|
58
|
+
/ `makeMemoryBrokerWith` take a `capacity` option.
|
|
59
|
+
- **Engine change hub** (`ClientEngine`): now bounded `dropping` at a
|
|
60
|
+
configurable capacity (`ClientEngineOptions.changeHubCapacity`, default 1024).
|
|
61
|
+
Each view-change carries a monotonic sequence number; a consumer that sees a
|
|
62
|
+
seq gap collapses it into a full `Rebuild` that subsumes the dropped `Patch`es,
|
|
63
|
+
so the view stays correct under pressure and behaves identically under none.
|
|
64
|
+
Collapses are counted in `legation_change_collapses_total`.
|
|
65
|
+
- **Mirror change hub** (`TabMirror`): now bounded `dropping` at a configurable
|
|
66
|
+
capacity (`TabMirrorOptions.changeHubCapacity`, default 1024). A seq gap makes
|
|
67
|
+
the observer re-read its subset from the mirror's `Ref` (a full refresh,
|
|
68
|
+
lossless because the `Ref` holds the latest rows), counted in
|
|
69
|
+
`legation_mirror_refreshes_total`.
|
|
70
|
+
|
|
71
|
+
A `Patch` is never silently dropped: it is only ever shed behind a seq gap that
|
|
72
|
+
forces a `Rebuild` before the view is next read.
|
|
73
|
+
|
|
74
|
+
- 2ff573c: `BrowserConnectivity.withBrowserConnectivity` now reports connectivity from a
|
|
75
|
+
service worker. A worker has no `window`, so it gets no `online`/`offline`
|
|
76
|
+
events; there the wrapper probes `navigator.onLine` on a timer and emits a
|
|
77
|
+
status change when it flips. When the navigator goes back online, `statusChanges`
|
|
78
|
+
emits `connected` once, even if the transport still reports `disconnected`, so
|
|
79
|
+
the sync loop's push and pull run at once for a worker-hosted engine instead of
|
|
80
|
+
waiting on the push retry backoff (#328). `status` still reads the worse of the
|
|
81
|
+
two signals. Where a `window` exists the `online`/`offline` listeners are
|
|
82
|
+
unchanged, but the reconnect edge applies there too: an `online` event emits
|
|
83
|
+
`connected` once whatever the transport reports. `statusChanges` is therefore
|
|
84
|
+
an edge trigger for the sync loop, not a mirror of `status`; a connection label
|
|
85
|
+
should read `status`.
|
|
86
|
+
|
|
87
|
+
The new optional `pollInterval` option sets the probe interval (default
|
|
88
|
+
`"5 seconds"`); it bounds how long a reconnect takes to reach the sync loop.
|
|
89
|
+
|
|
90
|
+
Auto-push still does not consult connectivity status: a push attempted while
|
|
91
|
+
the status is `disconnected` goes out anyway, because `navigator.onLine` can be
|
|
92
|
+
wrong and a failed push already backs off. This is documented on the module.
|
|
93
|
+
|
|
94
|
+
- 5496ddf: Bump the pinned Effect version from `4.0.0-beta.84` to `4.0.0-beta.107` across
|
|
95
|
+
all packages (catalog and exact `peerDependencies`), and move
|
|
96
|
+
`@thomasfosterau/legation-jsonapi` and the commerce example from
|
|
97
|
+
`@thomasfosterau/effect-jsonapi@^0.6.0` to `^0.12.0` (#152).
|
|
98
|
+
|
|
99
|
+
**Why beta.107.** effect-jsonapi 0.12 requires `effect >= 4.0.0-beta.104` and
|
|
100
|
+
develops against `4.0.0-beta.107` exactly, so that is the version its typings
|
|
101
|
+
are known to line up with. The later `4.0.0-rc.*` releases were not attempted
|
|
102
|
+
in this change: the exact-pin policy (ADR-0003) prefers the version the
|
|
103
|
+
adopted library is built against, and every `effect/unstable/*` module this
|
|
104
|
+
repo uses (`httpapi`, `http`, `rpc`, `reactivity`, `persistence`, `sql`)
|
|
105
|
+
compiles and tests clean on beta.107.
|
|
106
|
+
|
|
107
|
+
**Effect API drift absorbed (no behavioural change):**
|
|
108
|
+
|
|
109
|
+
- `Schedule.either` / `Schedule.both` are gone; the default retry policy is now
|
|
110
|
+
`Schedule.max([Schedule.min([exponential, spaced]), recurs(5)])`, which is
|
|
111
|
+
the same policy (`min` recurs while any schedule recurs at the shortest
|
|
112
|
+
delay, `max` recurs while all recur at the longest delay).
|
|
113
|
+
- `Schema.TaggedErrorClass` is now `Schema.TaggedError` (the wire
|
|
114
|
+
`SyncRpcError` is unchanged on the wire).
|
|
115
|
+
- `SchemaRepresentation.fromASTs` is now `SchemaRepresentation.toRepresentations`
|
|
116
|
+
(spec export output is unchanged).
|
|
117
|
+
- `HttpApiGroup.Any` is gone and `HttpApi`'s group parameter is now
|
|
118
|
+
invariant, so `JsonApiSync.layer`, `JsonApiSync.inspect` and
|
|
119
|
+
`Convention.reflectResources` are generic over the concrete `HttpApi`
|
|
120
|
+
instead of accepting a widened alias. The exported `JsonApiSync.JsonApi`
|
|
121
|
+
alias is now `HttpApi.Constraint` (identifier and groups erased); pass your
|
|
122
|
+
concrete api to the functions rather than annotating it with the alias.
|
|
123
|
+
|
|
124
|
+
**effect-jsonapi 0.6 → 0.12 at the ADR-0016 boundary.** `Document`, `Query`,
|
|
125
|
+
`Resource`, `Relationship` and `Endpoint` lost no exports; `Query` gained
|
|
126
|
+
`IncludeOption`/`IncludePathsOf`/`OffsetPageStruct`/`bracketPageKeys` and
|
|
127
|
+
`Resource` gained `IncludeDepth`/`IncludePathsTo`. `schema.ts`, `types.ts` and
|
|
128
|
+
`CollectionLoader.ts` compile unchanged. One typing regression was noticed in
|
|
129
|
+
0.12: endpoints with `:id` params (`Endpoint.get`/`delete`/`update`) declare
|
|
130
|
+
their params as `Schema.Struct<Schema.Struct.Fields & { id }>`, which leaks
|
|
131
|
+
`unknown` into the requirements of the corresponding `HttpApiClient` calls.
|
|
132
|
+
The commerce example narrows this with a small `run` helper in
|
|
133
|
+
`server/client.ts`; nothing in the published packages is affected.
|
|
134
|
+
|
|
135
|
+
The bump is `minor` rather than `patch` because the exact `effect` peer pin
|
|
136
|
+
means every consumer must move `effect` in the same change, and the
|
|
137
|
+
`@thomasfosterau/legation-jsonapi` type signatures above changed shape.
|
|
138
|
+
|
|
139
|
+
- 19eb4b0: Bump the pinned Effect version from `4.0.0-beta.65` to `4.0.0-beta.75` across all packages.
|
|
140
|
+
|
|
141
|
+
`@thomasfosterau/legation-browser` now builds its IndexedDB cache store on
|
|
142
|
+
`@effect/platform-browser` (`BrowserKeyValueStore.layerIndexedDb` + core's
|
|
143
|
+
`KeyValueStoreAdapter`) instead of a hand-rolled IndexedDB wrapper.
|
|
144
|
+
`@effect/platform-browser` is a new peer dependency of the browser package, and
|
|
145
|
+
`IndexedDbCacheStore.make` is now a scoped resource (requires `Scope`); the
|
|
146
|
+
`storeName` option was removed (the underlying object store is managed by
|
|
147
|
+
platform-browser).
|
|
148
|
+
|
|
149
|
+
- 38c3147: Effect-idiomatic API refactor:
|
|
150
|
+
|
|
151
|
+
- Protocol messages (`ClientMessage`, `ServerMessage`, `PushResult`, `WireKeyFilter`), cache
|
|
152
|
+
events (`CacheEvent`), and outbox events (`OutboxEvent`) are now `Data.TaggedEnum`s with
|
|
153
|
+
generated constructors, `$is` guards, and exhaustive `$match` matchers
|
|
154
|
+
(e.g. `Protocol.ClientMessage.Subscribe({...})`). The types are structurally unchanged, so
|
|
155
|
+
existing object-literal construction still compiles.
|
|
156
|
+
- `SyncCacheDefaults` `Context.Reference` for ambient cache timing defaults
|
|
157
|
+
(staleTime / timeToLive / staleWhileRevalidate).
|
|
158
|
+
- The cache's in-flight dedupe map uses `MutableHashMap`; background stale-while-revalidate
|
|
159
|
+
refresh failures are now logged instead of silently swallowed.
|
|
160
|
+
- The WebSocket transports validate incoming frames with `ProtocolSchema` codecs instead of
|
|
161
|
+
unchecked `JSON.parse` casts, and bridge socket events into Effect without `Effect.runFork`
|
|
162
|
+
inside callbacks.
|
|
163
|
+
- All tests use `@effect/vitest` (`it.effect` / `it.live` / `it.layer`, `TestClock`).
|
|
164
|
+
|
|
165
|
+
- fed4634: Peer the Effect v4 release-candidate line through an open floor rather than
|
|
166
|
+
an exact pin (#222), so an app can install `@thomasfosterau/legation-*` beside
|
|
167
|
+
`@thomasfosterau/effect-auth` and `@thomasfosterau/effect-jsonapi` and resolve
|
|
168
|
+
a single copy of `effect`. The published floor is `>=4.0.0-rc.118` (see
|
|
169
|
+
`effect-rc118-floor.md`), on `effect` and on the `@effect/platform-browser` /
|
|
170
|
+
`@effect/platform-node` peers.
|
|
171
|
+
|
|
172
|
+
**`@thomasfosterau/legation-jsonapi`**: a repeated shorthand filter key
|
|
173
|
+
(`?filter[name]=a&filter[name]=b`) is read as the comma list it spells
|
|
174
|
+
(`filter[name]=a,b`, an `In` predicate) rather than rejected with a 400,
|
|
175
|
+
because `@thomasfosterau/effect-jsonapi` accepts the repeated-key spelling at
|
|
176
|
+
every list-valued position.
|
|
177
|
+
|
|
178
|
+
The packages run in browsers and workers and deliberately declare no `engines`
|
|
179
|
+
(ADR-0003).
|
|
180
|
+
|
|
181
|
+
- fcc52eb: Target Effect `4.0.0-rc.118`: every package peers `effect >=4.0.0-rc.118`
|
|
182
|
+
(#261), and the `@effect/platform-browser` / `@effect/platform-node` peers use
|
|
183
|
+
the same floor. Install `effect` and every `@effect/*` companion at
|
|
184
|
+
`4.0.0-rc.118` or later together.
|
|
185
|
+
|
|
186
|
+
The packages import Effect's modules at their rc.118 paths (`effect/rpc`,
|
|
187
|
+
`effect/http`, `effect/http-api`, …); rc.118 removed the `effect/unstable/*`
|
|
188
|
+
export paths, so the packages don't resolve beside an earlier `effect`.
|
|
189
|
+
|
|
190
|
+
**`@thomasfosterau/legation-jsonapi`** depends on `@thomasfosterau/effect-jsonapi@^0.16.1`,
|
|
191
|
+
the first release that peers `effect >=4.0.0-rc.118`.
|
|
192
|
+
|
|
193
|
+
**`@thomasfosterau/legation-svelte`** depends on `@thomasfosterau/effect-svelte@^0.4.0`, for
|
|
194
|
+
the same reason.
|
|
195
|
+
|
|
196
|
+
- 2003cd6: Built-in multi-tab leader election for the no-service-worker degraded
|
|
197
|
+
topology (#119): `@thomasfosterau/legation-browser`'s `ElectedHost.run` wraps
|
|
198
|
+
`navigator.locks` (held for the tab's whole lifetime, no voluntary
|
|
199
|
+
step-down) around `ServiceWorkerHost.run`'s main-thread host composition —
|
|
200
|
+
`ClientEngine` + durable store + transports + `BroadcastMirrorChannel`
|
|
201
|
+
serving while elected, a `Client.connect` mirror over the same channel
|
|
202
|
+
while not. It hands back one `Client` that keeps working unchanged across
|
|
203
|
+
the mirror→host promotion (a switching `ClientView` swaps the underlying
|
|
204
|
+
tier, replaying explicit subscriptions and re-hydrating live
|
|
205
|
+
`observe`/`related` streams) and a `role: Stream<"host" | "mirror">` for
|
|
206
|
+
devtools/debug UI. A promoted tab boots against the SAME durable store —
|
|
207
|
+
same journal, rows, and cookie — so the CVR conversation resumes rather
|
|
208
|
+
than restarting. Environments without `navigator.locks` get a documented
|
|
209
|
+
fallback (becomes host immediately), not a polyfill.
|
|
210
|
+
|
|
211
|
+
`@thomasfosterau/legation`'s `Mirror` now fails any request still in flight when
|
|
212
|
+
the host it was talking to is gone — either because the mirror's own scope
|
|
213
|
+
closed, or because a second `HostReady` announces the host restarted — with
|
|
214
|
+
a clear `TransportError` instead of leaving the caller's `mutate` /
|
|
215
|
+
`mutateWithAck` / server-fallthrough `read` call hanging forever. A
|
|
216
|
+
mutation already journaled by the old host before it died is never lost:
|
|
217
|
+
it's durable in the shared store and the next host's sync loop picks it up
|
|
218
|
+
regardless of what this caller was told.
|
|
219
|
+
|
|
220
|
+
- c105fbf: Drive HTTP-cache invalidation from collection changes (#49).
|
|
221
|
+
|
|
222
|
+
- **`PullCompleted` enrichment** (`@thomasfosterau/legation`): the `PullCompleted`
|
|
223
|
+
`EngineEvent` now carries `collections` — the distinct collections the pulled
|
|
224
|
+
patch touched (a `Clear` stands for every collection the client currently
|
|
225
|
+
holds, since it resets the whole view). The field is always present. This lets
|
|
226
|
+
a collection-scoped reaction fire even on a client with an empty journal (the
|
|
227
|
+
common case, where a pull emits a `Patch` change and no `ViewRebuilt`).
|
|
228
|
+
|
|
229
|
+
- **`HttpCacheInvalidation`** (`@thomasfosterau/legation-browser`): `invalidateOnChanges` forks
|
|
230
|
+
a scoped fibre that evicts every cached response tagged for a changed
|
|
231
|
+
collection via the store's `invalidateTags`; a store error is logged and
|
|
232
|
+
swallowed, never fatal. `collectionChanges` derives the change feed from an
|
|
233
|
+
engine's `engineEvents` (mapping `PullCompleted.collections` and scoped
|
|
234
|
+
`ViewRebuilt.collections`, dropping empties). Wire it in a service worker by
|
|
235
|
+
forking `invalidateOnChanges` alongside `ServiceWorkerHost.run` (recipe in the
|
|
236
|
+
host's doc comment).
|
|
237
|
+
|
|
238
|
+
The tag convention is `collection:{name}` (the default `tagFor`): a response
|
|
239
|
+
tagged `collection:todos` is evicted whenever `todos` changes. This is a
|
|
240
|
+
collection-level granularity ceiling — every response tagged for a changed
|
|
241
|
+
collection is evicted; a per-row `collection:{name}:{id}` convention could
|
|
242
|
+
follow once patch-op row ids are plumbed through to the change feed (not
|
|
243
|
+
implemented here).
|
|
244
|
+
|
|
245
|
+
- e1a028a: Per-row HTTP-cache tags (#207): row-granular cache invalidation, closing the
|
|
246
|
+
collection-level ceiling noted in `.changeset/http-cache-invalidation.md`.
|
|
247
|
+
|
|
248
|
+
- **Change feed carries row ids** (`@thomasfosterau/legation`): the `PullCompleted`
|
|
249
|
+
`EngineEvent` now also carries `rows` — the `(collection, id)` pairs the
|
|
250
|
+
pulled patch's `Put`/`Del` ops touched (a `Clear` contributes to
|
|
251
|
+
`collections` only, since it has no finite row list). Always present,
|
|
252
|
+
possibly empty.
|
|
253
|
+
|
|
254
|
+
- **Fine tag convention** (`@thomasfosterau/legation-browser`): `HttpCacheInvalidation` gains
|
|
255
|
+
`defaultFineTagFor` — `collection:{name}:{id}`, one level finer than the
|
|
256
|
+
existing `collection:{name}` (`defaultTagFor`). `collectionChanges` now
|
|
257
|
+
yields `Touched` (`{ collections, rows }`) and `invalidateOnChanges` evicts
|
|
258
|
+
the union of both tag sets on every change. The coarse eviction is kept
|
|
259
|
+
deliberately even though every touched row's collection is already implied:
|
|
260
|
+
an aggregate document can be staled by a row it does not yet contain, which
|
|
261
|
+
has no fine tag to match.
|
|
262
|
+
|
|
263
|
+
- **Response tagging + budget** (`@thomasfosterau/legation-browser`): `tagsFor(collection,
|
|
264
|
+
rowIds)` builds a response's tags — coarse plus one fine tag per row, up to
|
|
265
|
+
`FINE_TAG_ROW_BUDGET` (43, derived in a comment against the real ceiling on
|
|
266
|
+
the `x-legation-cache-meta` header these tags ride on). Beyond the budget, a
|
|
267
|
+
response is tagged coarse-only with `budgetExceeded: true` on the result.
|
|
268
|
+
`tagsForObserved` pairs the same computation with a logged warning and the
|
|
269
|
+
new `legation_cache_tag_budget_exceeded_total` counter
|
|
270
|
+
(`Telemetry.cacheTagBudgetExceededTotal`), so the fallback is observable
|
|
271
|
+
rather than silent.
|
|
272
|
+
|
|
273
|
+
Both granularities share one tag spelling across the client-side
|
|
274
|
+
invalidation path and response tagging, so the edge/KV invalidation sink
|
|
275
|
+
(#206) can reuse it unchanged.
|
|
276
|
+
|
|
277
|
+
- Scope the durable replica to a resolved identity (#212), closing the gap where
|
|
278
|
+
adopting legation would make a multi-tenant app on a shared device _less_ safe
|
|
279
|
+
than what it replaced. The invariant, now in `CONTEXT.md` beside the HTTP-cache
|
|
280
|
+
section: **a durable replica belongs to exactly one resolved identity, and an
|
|
281
|
+
identity change is a reset, not a rebase.**
|
|
282
|
+
|
|
283
|
+
Identity is resolved server-side per pull, which is what makes the auth filter
|
|
284
|
+
trustworthy — but the replica the authorised rows land in outlives the session
|
|
285
|
+
that authorised them. A signs out, B signs in, and B's Client boots onto A's
|
|
286
|
+
rows because they are the freshest thing on the device and nothing recorded
|
|
287
|
+
which identity authorised them. A's unpushed journal made it worse: durable,
|
|
288
|
+
causally ordered, and about to go out under B's cookies.
|
|
289
|
+
|
|
290
|
+
- **`Identity` (`@thomasfosterau/legation`, new)**: `Fingerprint`, the opaque
|
|
291
|
+
app-supplied token a replica is stamped with — a session-subject hash, **never
|
|
292
|
+
PII and never a credential**, as the type's own docs say. `anonymous` is the
|
|
293
|
+
signed-out principal; `unstamped` is a replica that has never been scoped, and
|
|
294
|
+
equals nothing. `transitionOf` is the whole rule, in one place so boot and
|
|
295
|
+
runtime cannot disagree: `Unchanged`, `Upgrade` (anonymous → signed in, the
|
|
296
|
+
one benign transition), or `Reset`.
|
|
297
|
+
|
|
298
|
+
- **`ClientStore` identity boundary**: a new `identity` option stamps the
|
|
299
|
+
fingerprint and compares it on open. A mismatch quarantines the unpushed
|
|
300
|
+
journal under the **superseded** fingerprint, then drops confirmed rows and
|
|
301
|
+
the pull cookie. New surface: `getIdentity` / `setIdentity`,
|
|
302
|
+
`quarantinePending` / `quarantinedMutations` / `quarantines` /
|
|
303
|
+
`releaseQuarantine` / `discardQuarantine`, and `clearCookie`. A boundary that
|
|
304
|
+
cannot be established **seals** the store — every operation fails loudly,
|
|
305
|
+
nothing is served, nothing is destroyed, and the next open retries — rather
|
|
306
|
+
than reporting a boundary that isn't there.
|
|
307
|
+
|
|
308
|
+
- **Quarantine is its own keyspace**, which is the security property rather than
|
|
309
|
+
an implementation detail: `pendingMutations` reads the journal and `push`
|
|
310
|
+
reads `pendingMutations`, so a quarantined entry is structurally unreachable
|
|
311
|
+
from any push path — a background flush included — under any identity but the
|
|
312
|
+
one it is keyed by. Mutation ids are preserved rather than renumbered, so an
|
|
313
|
+
entry that was in flight at reset is at worst acknowledged and skipped by the
|
|
314
|
+
backend's exactly-once ledger, never applied twice. Only
|
|
315
|
+
`discardQuarantine` destroys one: no reset and no boot does.
|
|
316
|
+
|
|
317
|
+
- **`Client.reset(fingerprint)`** applies the same rule at runtime, dropping the
|
|
318
|
+
Client's window ledger along with the rows so coverage cannot answer the new
|
|
319
|
+
principal from the old replica. `Client.make` takes an `identity` and adopts
|
|
320
|
+
it before the sync loop starts. `identity`, `quarantines`,
|
|
321
|
+
`quarantinedMutations` and `discardQuarantine` round out the surface; a
|
|
322
|
+
Client built with `connect` says the host owns the replica rather than faking
|
|
323
|
+
a reset.
|
|
324
|
+
|
|
325
|
+
- **The sync client id is scoped to the principal** while the boundary is on
|
|
326
|
+
(derived from the configured `clientId` plus the fingerprint). Mutation ids
|
|
327
|
+
are a per-client sequence the backend requires to be contiguous, so a second
|
|
328
|
+
principal continuing the first's numbering would push a gap and restarting it
|
|
329
|
+
would collide with ids already applied; scoping gives each principal its own
|
|
330
|
+
sequence, and gives a returning one back exactly the sequence they left.
|
|
331
|
+
|
|
332
|
+
- **`IndexedDbClientStore` (`@thomasfosterau/legation-browser`)** takes an `identity`, and
|
|
333
|
+
`layerNamespaced` offers the per-identity replica as the alternative to
|
|
334
|
+
resetting — namespace for a device with a few regular users who switch back
|
|
335
|
+
and forth, reset where sign-ins are one-off or unbounded.
|
|
336
|
+
|
|
337
|
+
Opt-in: with no `identity` configured, nothing is stamped, nothing is compared,
|
|
338
|
+
and behaviour is unchanged. Turning it on resets an unstamped replica once,
|
|
339
|
+
which is the safe direction — an unstamped replica is exactly the state this
|
|
340
|
+
boundary exists to distrust.
|
|
341
|
+
|
|
342
|
+
- a3b2ee4: The JSON:API façade (ADR-0029, issue #166): a fetch handler that answers a
|
|
343
|
+
covered JSON:API `GET` from the replica and stays out of the way otherwise.
|
|
344
|
+
|
|
345
|
+
- `JsonApiFacade.fetchHandler({ nodes, api, view, coverageOf, cookie?, resources?, baseUrl?, prefix?, online? })`
|
|
346
|
+
in `@thomasfosterau/legation-jsonapi` builds the façade from the app's `HttpApi`
|
|
347
|
+
(`Convention.reflectResources` gives the `list` / `getOne` routes it
|
|
348
|
+
recognises; those whose type is no node are left to the server), the
|
|
349
|
+
node graph, the replica's view and a coverage source. It presumes the
|
|
350
|
+
api's read endpoints are declared with the graph's codec
|
|
351
|
+
(`JsonApiQuery.querySchema` / `singleRowSchema`) and served by the
|
|
352
|
+
derived handlers, which is what lets a parse failure be answered locally.
|
|
353
|
+
`matches(request)` says whether a request is its business — a `GET` whose
|
|
354
|
+
`Accept` names `application/vnd.api+json` on a reflected route under
|
|
355
|
+
`baseUrl` — and `handle(request)` answers: a covered request gets `200`
|
|
356
|
+
with the document the one writer produces (byte-equal in content to the
|
|
357
|
+
derived REST handler's for the same declarations, the optimistic overlay
|
|
358
|
+
visible as `Client.query` sees it), `Cache-Control: no-store` and
|
|
359
|
+
`x-legation-served-at` set to the replica's CVR cookie (the store's,
|
|
360
|
+
empty until the first pull); a URL the grammar rejects gets the codec's
|
|
361
|
+
`400` document (`RestHandlers.queryErrorDocument`), except an `include`
|
|
362
|
+
the graph does not declare, which the server rules on; an uncovered
|
|
363
|
+
request while offline gets the `503` `offline` error document of
|
|
364
|
+
ADR-0029 §4 (`handler.offline`, with the cookie, also for a forward that
|
|
365
|
+
rejected at the transport). Everything else is `undefined` for the caller
|
|
366
|
+
to forward unchanged: a non-candidate, an uncovered request while online,
|
|
367
|
+
a collection no bound resource declares, and a single-resource request
|
|
368
|
+
whose row the replica does not hold (the 404 is the endpoint's own).
|
|
369
|
+
Coverage is every read `ReplicaDocuments.toCover` names, each judged by
|
|
370
|
+
`coverageOf` — `client.coverageOf`, or the host's registrations below.
|
|
371
|
+
The module lives in `@thomasfosterau/legation-jsonapi` by ADR-0006: it needs this
|
|
372
|
+
package's codec, writer and route reflection plus the standard
|
|
373
|
+
`Request` / `Response` globals, and `navigator.onLine` is an option with a
|
|
374
|
+
guarded default, so `@thomasfosterau/legation-browser` stays free of effect-jsonapi.
|
|
375
|
+
- `EngineHost.run` returns a `Host` whose `registrations` lists what the
|
|
376
|
+
host holds on its mirrors' behalf — root windows, relational registrations
|
|
377
|
+
with their edge selects, both halves of a `SubscribeUrl` with its canonical
|
|
378
|
+
URL while a party holds it by URL — as `Coverage.Subscription`s: the
|
|
379
|
+
ledger a façade's coverage check scans in a worker, where no `Client`'s
|
|
380
|
+
own ledger sees the tabs' pins. A registration is listed once its engine
|
|
381
|
+
registration exists (a relational one once its reconcile loop has
|
|
382
|
+
started; one naming an edge the registry does not declare registers
|
|
383
|
+
nothing) and dropped when the last mirror releases it or is swept.
|
|
384
|
+
`Coverage.verdictOf` and
|
|
385
|
+
`Coverage.coverageOf(nodes, subscriptions)` give the same `Verdict` as
|
|
386
|
+
`Client.coverageOf` over any such ledger; `Client` uses the former.
|
|
387
|
+
- `ServiceWorkerHost.start` serves exactly as `run` does and returns the
|
|
388
|
+
`Host` — the engine and its registrations — for the worker to build the
|
|
389
|
+
façade from; `run` is `start` then serve forever.
|
|
390
|
+
- The commerce worker composes façade, then its unchanged HTTP cache, then
|
|
391
|
+
the network (`examples/commerce/src/service-worker.ts`,
|
|
392
|
+
`src/lib/facade.ts`, `src/lib/host.ts`); the api's shape moves to
|
|
393
|
+
`server/api.ts` so the worker reflects the routes the server serves; and
|
|
394
|
+
the replica test runs the façade over the same seed: every URL the
|
|
395
|
+
shell's pins cover is answered with the served document's bytes, and the
|
|
396
|
+
rest go to the network. `Convention.segmentsOf` and `Convention.isParam`
|
|
397
|
+
are exported.
|
|
398
|
+
|
|
399
|
+
- 3538a81: Depend on the engine core under its new name, `@thomasfosterau/legation` (formerly `@thomasfosterau/legation-effect`), and its engine seams at `@thomasfosterau/legation/internal`. Install `@thomasfosterau/legation` alongside these packages and import the core from it.
|
|
400
|
+
- 501b12c: The mirror seam sheds its browser-tab coupling: the party served by
|
|
401
|
+
`EngineHost` is any client context — a browser tab, an Electron window,
|
|
402
|
+
an extension, a CLI instance — so the vocabulary now says what the
|
|
403
|
+
contract means. `TabMirror` is `Mirror` (module, namespace, and type),
|
|
404
|
+
`tabId` is `mirrorId` (wire field included), `TabMessage` is
|
|
405
|
+
`MirrorMessage` (`decodeTabMessage` → `decodeMirrorMessage`), the host's
|
|
406
|
+
liveness option `tabTimeout` is `mirrorTimeout`, and
|
|
407
|
+
`MirrorChannel.makeLoopbackPair` returns `{ host, mirror }`. Clean
|
|
408
|
+
break, no aliases; `@thomasfosterau/legation-browser` re-exports the new names.
|
|
409
|
+
- 7f1efac: Named queries and auto-subscribing relational reads cross the mirror
|
|
410
|
+
protocol (ADR-0021 follow-up). `ClientView` gains
|
|
411
|
+
`subscribeQuery`/`unsubscribeQuery`/`queryNamed`/`observeNamed` and
|
|
412
|
+
`related` — both hosts satisfy the widened seam (`ClientViewError` now
|
|
413
|
+
includes `QueryError`). The tab↔host protocol adds
|
|
414
|
+
`SubscribeNamed`/`UnsubscribeNamed` (the host engine owns the server
|
|
415
|
+
round-trip) and `SubscribeRelated`/`UnsubscribeRelated`, where the host runs
|
|
416
|
+
the reconcile loop AND the join and the tab receives resolved rows
|
|
417
|
+
(`RelatedUpdate`) — data work stays off the main thread. `EngineHost` takes
|
|
418
|
+
an optional `registry` for relational resolution and refcounts all
|
|
419
|
+
registration kinds per tab. A new `HostReady` announcement makes
|
|
420
|
+
registrations survive host restarts: a starting host broadcasts it and
|
|
421
|
+
every mirror replays the registrations it holds — covering killed service
|
|
422
|
+
workers and tabs that raced the worker's boot. On a mirror, observing IS
|
|
423
|
+
subscribing: `TabMirror.observe`/`observeNamed`/`related` auto-register
|
|
424
|
+
their subject with the host for the stream's lifetime through a per-key
|
|
425
|
+
interest ledger (a mirror only carries keys some tab registered, and the
|
|
426
|
+
host refcounts per tab, so the ledger posts `Subscribe` on first interest
|
|
427
|
+
and `Unsubscribe` only when the last observer ends and no explicit
|
|
428
|
+
`subscribe` pins the key). In `@thomasfosterau/legation-svelte`, `namedQuery`,
|
|
429
|
+
`useNamedQuery`, and `useRelated({ subscribe: true })` now work over
|
|
430
|
+
mirror-only runtimes (the internal engine-lookup fallback is removed).
|
|
431
|
+
- cf0b26c: The mirror seam moves to core, where ADR-0006's dependency rule places it:
|
|
432
|
+
`MirrorProtocol`, the `MirrorChannel` interface + loopback pair,
|
|
433
|
+
`EngineHost`, and `TabMirror` are exported from `@thomasfosterau/legation` — none
|
|
434
|
+
of them needs a browser global, so Electron windows, extensions, and CLI
|
|
435
|
+
processes can host or consume mirrors without `@thomasfosterau/legation-browser`. The
|
|
436
|
+
browser package keeps what is actually browser-coupled — the new
|
|
437
|
+
`BroadcastMirrorChannel.make(name)` binding (formerly
|
|
438
|
+
`MirrorChannel.makeBroadcastChannel`) and `ServiceWorkerHost` — and
|
|
439
|
+
re-exports the moved modules for compatibility.
|
|
440
|
+
- f54f568: Per-mutation outcome surface (#36). Callers can now await a mutation's
|
|
441
|
+
authoritative backend outcome — applied, or permanently rejected with a reason
|
|
442
|
+
(#35) — rather than only its journal ack.
|
|
443
|
+
|
|
444
|
+
- `ClientEngine.mutateWithAck` returns a `MutationHandle` whose `acknowledged`
|
|
445
|
+
effect resolves with the `MutationOutcome` once the backend settles the
|
|
446
|
+
mutation via a push or pull acknowledgement. `ClientEngine.mutationEvents`
|
|
447
|
+
streams the lifecycle (`journaled`, then `acknowledged` or `rejected`). Plain
|
|
448
|
+
`mutate`/`mutateEncoded` are unchanged and register no deferred; pending
|
|
449
|
+
acknowledgements are interrupted on engine scope close, so they never hang.
|
|
450
|
+
- Svelte gains `useMutationWithAck` (over an additive
|
|
451
|
+
`EngineAtoms.mutationWithAck` atom), returning a promise of the outcome.
|
|
452
|
+
- The tab mirror forwards the outcome: `MirrorProtocol` gains an additive
|
|
453
|
+
`MutateSettled` host→tab message, `EngineHost` awaits the backend verdict on a
|
|
454
|
+
scoped fibre and posts it, and `TabMirror.mutateWithAck` awaits it (the
|
|
455
|
+
existing journal-ack `mutate` is unchanged).
|
|
456
|
+
|
|
457
|
+
- 06405ed: A covered read is answered from the replica only once the windows its rows link to have been pulled (#426). A window is pulled once a pull that started after it was held has applied, or once an already-pulled window covers it. Each window is marked on its own, so a new edge window does not reset the windows already pulled. A read needs the edge windows its root rows link to, and an unpulled window of a sibling key does not uncover a read that links only to other keys. A relational registration whose root is pulled but whose edge windows are not yet held covers nothing. Before its windows are pulled, `Client.query` falls through to the server (offline, it answers locally, as for any uncovered read), and the JSON:API façade forwards the request online and answers `503` offline.
|
|
458
|
+
|
|
459
|
+
The pull state is recomputed, not persisted: after a restart each window starts unpulled until a pull lands. `ClientEngine` gains a read-only `pullCounts`; `ClientView.pulled(key)` answers with a per-window summary (`Coverage.Pulled`); `Coverage` gains `pulledFor` and `windowKey`; the mirror protocol gains a host message, `PulledRegistrations`, which an older mirror ignores (it is an addition, and replaces no message). A host now pulls promptly after a plain subscribe, as it already did for named queries.
|
|
460
|
+
|
|
461
|
+
- 9d2f21b: Rename the project to **Legation**. Every package moves to the `@legation`
|
|
462
|
+
scope: the engine is now `@legation/effect` (formerly
|
|
463
|
+
`@thomasfosterau/effect-sync-core`), and the adapters/bindings are
|
|
464
|
+
`@legation/browser`, `@legation/node`, `@legation/jsonapi`, `@legation/sql`,
|
|
465
|
+
and `@legation/svelte` (formerly `@thomasfosterau/effect-sync-*`).
|
|
466
|
+
|
|
467
|
+
Runtime identifiers follow the brand, which is **breaking for persisted local
|
|
468
|
+
state and dashboards**:
|
|
469
|
+
|
|
470
|
+
- `Context.Service` identifiers: `@thomasfosterau/effect-sync-core/*` →
|
|
471
|
+
`@legation/effect/*` (and per-package equivalents)
|
|
472
|
+
- Client-store key prefix: `effect-sync/` → `legation/`; CVR store prefix:
|
|
473
|
+
`effect-sync/cvr/` → `legation/cvr/`. Existing IndexedDB/KV state under the
|
|
474
|
+
old prefix is orphaned — clients rehydrate via a full pull; export any
|
|
475
|
+
pending journal before upgrading offline clients.
|
|
476
|
+
- Metrics: `effect_sync_*` → `legation_*`; spans: `effect-sync.*` →
|
|
477
|
+
`legation.*`
|
|
478
|
+
- Default BroadcastChannel name: `effect-sync` → `legation`; Redis hint
|
|
479
|
+
stream: `effect-sync:hints` → `legation:hints`; cache metadata header:
|
|
480
|
+
`x-effect-sync-cache-meta` → `x-legation-cache-meta`
|
|
481
|
+
|
|
482
|
+
- c189ae7: Add a runtime-specific adapter package:
|
|
483
|
+
|
|
484
|
+
- `@thomasfosterau/legation-browser` — WebSocket client transport (`ClientTransport` over the browser `WebSocket` global) and IndexedDB-backed `CacheStore` persistence for offline-first apps.
|
|
485
|
+
|
|
486
|
+
- 16889f0: Rename every package from the `@legation/*` scope to
|
|
487
|
+
`@thomasfosterau/legation-*` (#303): `@legation/effect` is now
|
|
488
|
+
`@thomasfosterau/legation`, and likewise for `browser`, `cloudflare`,
|
|
489
|
+
`devtools`, `jsonapi`, `node`, `sql` and `svelte`. Subpath entries keep their
|
|
490
|
+
paths (`@thomasfosterau/legation/internal`,
|
|
491
|
+
`@thomasfosterau/legation-svelte/kit`,
|
|
492
|
+
`@thomasfosterau/legation-devtools/svelte`). Update imports and dependency
|
|
493
|
+
names accordingly; no `@legation/*` name was ever published to npm.
|
|
494
|
+
|
|
495
|
+
`Context.Service` identifiers and `Symbol.for` keys follow the package name
|
|
496
|
+
(`@legation/effect/Client` → `@thomasfosterau/legation/Client`, and
|
|
497
|
+
per-package equivalents). The reserved identity sentinels
|
|
498
|
+
(`@legation/anonymous`, `@legation/unstamped`), the `legation/` client-store
|
|
499
|
+
prefixes, metric and span names are unchanged.
|
|
500
|
+
|
|
501
|
+
- 9a6426b: Sync-loop scheduling (#33). `ClientEngine` grows a configurable scheduler on
|
|
502
|
+
`runSync`, driven by `Effect.Schedule` and the existing `statusChanges` seam:
|
|
503
|
+
|
|
504
|
+
- `syncLoop.pushDebounce` auto-pushes after `mutate`/`mutateEncoded`, debounced
|
|
505
|
+
so a burst of mutations coalesces into a single push.
|
|
506
|
+
- `syncLoop.pullInterval` pulls periodically as the correctness backstop, so a
|
|
507
|
+
client converges over pull alone even without pokes.
|
|
508
|
+
- The baseline triggers (pull on poke, push+pull on reconnect) are retained,
|
|
509
|
+
and every scheduling fibre is scoped — interrupted when the engine scope
|
|
510
|
+
closes.
|
|
511
|
+
|
|
512
|
+
`@thomasfosterau/legation-browser` adds `BrowserConnectivity.withBrowserConnectivity`, which
|
|
513
|
+
layers `navigator.onLine` + `online`/`offline` + `visibilitychange` onto any
|
|
514
|
+
`ClientTransport` so `statusChanges` becomes truthful for carriers (like the RPC
|
|
515
|
+
transport) that otherwise report themselves permanently connected, and a
|
|
516
|
+
return-to-visible surfaces a poke.
|
|
517
|
+
|
|
518
|
+
`@thomasfosterau/legation-jsonapi` `JsonApiSync` now configures the core loop (`pullInterval`,
|
|
519
|
+
`pushDebounce` options, 5-second default interval) instead of running its own
|
|
520
|
+
`Effect.forever` poll; `JsonApiSync.start` just runs `runSync`.
|
|
521
|
+
|
|
522
|
+
- e34bc24: Connection-status realism at the transport seam: `Transport.withInferredStatus`
|
|
523
|
+
derives real connectivity from observed push/pull behaviour ("connecting"
|
|
524
|
+
until the first call settles, "connected" on success, "disconnected" on a
|
|
525
|
+
`TransportError`) — wrapping the default RPC carrier, which reports itself
|
|
526
|
+
permanently "connected". `Transport.withFallback(primary, fallback)` layers a
|
|
527
|
+
fallback carrier (e.g. plain HTTP under WebSockets) with a sideline cooldown
|
|
528
|
+
(`restoreAfter`, default 30s), an optional per-call primary time budget
|
|
529
|
+
(`primaryTimeout` — a hanging primary is interrupted, sidelined, and the call
|
|
530
|
+
falls to the fallback instead of stalling sync), merged pokes that survive a
|
|
531
|
+
failing primary poke stream, inferred status ("connected" while either
|
|
532
|
+
carrier serves), and version rejections propagating without falling back.
|
|
533
|
+
`BrowserConnectivity.withBrowserConnectivity` now combines `navigator.onLine`
|
|
534
|
+
with the wrapped transport's own status (worst of the two wins) instead of
|
|
535
|
+
replacing it.
|
|
536
|
+
- 1b8b120: URL-keyed subscriptions over the mirror channel (ADR-0029 section 2, issue #159).
|
|
537
|
+
|
|
538
|
+
- `Client.subscribe` / `Client.unsubscribe` accept a JSON:API URL as well as a `Query.Read`. A Client built with the new `urls` option (`Client.UrlReads`: `parse(url)` and `canonical(read)`) parses the URL, plans the read and canonicalises it, so `subscribe(url)` and `subscribe(read)` of one query — and any non-canonical spelling of the URL (parameter order, a repeated `include` key, an API prefix, a `fields[]` projection) — share one ledger entry keyed by the canonical URL beside the subset hash. A URL the bridge refuses (an unknown collection, an unparsable filter, an undeclared edge) fails with a `QueryError` naming the offending parameter and registers nothing; a Client built without `urls` fails a URL call with a `QueryError`, never a defect. `Client.subscriptions` lists the canonical URLs of the explicit pins for devtools, and `Client.coverageOf` names the covering subscription's canonical URL (`Verdict.url`) while a pin holds it. `Client.make`, `Client.connect` and `Client.fromView` take `urls`; `@thomasfosterau/legation` itself does not know the URL grammar (ADR-0006), so `@thomasfosterau/legation-jsonapi` builds the seam from the node graph: `JsonApiQuery.urlReads(nodes)`.
|
|
539
|
+
- An explicit pin now registers its edge windows. `Client.subscribe(read)` with `include` holds the foreign windows the read's edges select — `ClientView.pinEdges`, a new member of the view: `Related.trackEdges` (the relational read's reconcile loop without its join) over an owned engine, the relational registration with the host over a connected one — until the last pin releases, and records the edge selects on the ledger entry, so a read with `include` is covered by a pin that declared the edge (ADR-0029 rule 4). The commerce shell's `myCartItems` pin (`include: { variant: true }`) now syncs its edge window. Release stays symmetric with the pin count; the ledger update and the registration it decides are one critical section.
|
|
540
|
+
- A relational registration no longer waits on the network: `Related.relatedWith` (behind `observe` with `include`, `related` and now pins) runs its pulls in the background, one at a time with one queued, so a stalled transport delays rows and never blocks the caller; a failed pull only delays rows (an offline `Client.make` no longer dies).
|
|
541
|
+
- Engine subscriptions are counted where two registrations can hold one window: `ClientView.fromEngine` and `EngineHost` register through `Subscriptions.counted(engine)` — idempotent, so a tab hosting for others (`ElectedHost`) counts once for both fronts — and a relational read's own root subscription ending beside an explicit pin, or one relational read's foreign window beside another's, no longer drops the window from the engine's set-based subscription list.
|
|
542
|
+
- The mirror protocol gains `SubscribeUrl` / `UnsubscribeUrl` (`{ mirrorId, url }`): a page with no `Client` posts them on the channel, and an `EngineHost` built with `nodes` and `urls` parses, plans and registers the URL's root and edge windows under that `mirrorId` — a thin adapter over the existing `Subscribe` and `SubscribeRelated`, refcounted and swept like any mirror's registrations. `EngineHost`'s `registry` defaults to `nodes.collections`. `ServiceWorkerHost.run` and `ElectedHost.run` forward `nodes` / `urls`; `ElectedHost`'s Client takes `urls` too.
|
|
543
|
+
- `Query.tryPlan(nodes, read)` is `Query.plan` with its `QueryError` as a typed failure.
|
|
544
|
+
|
|
545
|
+
- cf48723: Web Push as a poke carrier, to converge a client with no open tab (#214). `PushPoke.sink` (on `@thomasfosterau/legation/internal`) is an `InvalidationSink` that sends a contentless marker, `PushPoke.payload`, to the push subscriptions the app's registry names for a change, through the app's own sender: the library holds no credential and stores no subscription. The coalescing window and the per-target budget are required options with no default. In `@thomasfosterau/legation-browser`, `PushPoke.listen` is the worker's `push` listener: the library's push registers the Background Sync tag by default, or pulls with `action: "pull"`, only when the replica is stamped for whoever is signed in now, and every other push is left to the app's own handler. `ServiceWorkerHost.start` takes it as `pushPoke`.
|
|
546
|
+
|
|
547
|
+
### Patch Changes
|
|
548
|
+
|
|
549
|
+
- ef59fa7: A durable pull now writes once, and an interrupted pull is never recorded. The
|
|
550
|
+
key-value seam gains an optional `AtomicKeyValue` service on
|
|
551
|
+
`@thomasfosterau/legation/internal`: an all-or-nothing `apply` over
|
|
552
|
+
`set` and `remove` operations. When a host supplies it, `ClientStore` commits a
|
|
553
|
+
pull's rows, cookie, journal trim and unpulled-tail release as one batch, with
|
|
554
|
+
no release record, and a push acknowledgement's park and trim as one batch. A
|
|
555
|
+
store without the service keeps the release-record path from #387, unchanged,
|
|
556
|
+
and an open still finishes a leftover record either way.
|
|
557
|
+
|
|
558
|
+
`@thomasfosterau/legation-browser` supplies the service over IndexedDB: one
|
|
559
|
+
`readwrite` transaction on the database and object store the platform
|
|
560
|
+
`KeyValueStore` already uses, so existing databases need no migration. The
|
|
561
|
+
database is opened without a version, so one a later version created still
|
|
562
|
+
opens; the browser store's `KeyValueStore` opens it the same way, in place of
|
|
563
|
+
the platform's, which pins version 1 and fails on a newer database.
|
|
564
|
+
|
|
565
|
+
If another connection upgrades that database, or deletes it, the store's
|
|
566
|
+
connections close, since they would otherwise block the change. From then on
|
|
567
|
+
every call fails with "the IndexedDB database was upgraded by another
|
|
568
|
+
connection; reload to continue", and the store does not reopen itself. A
|
|
569
|
+
database without the `entries` store fails its calls with a message that names
|
|
570
|
+
that store, and a sealed store's message now carries the reason from its cause.
|
|
571
|
+
|
|
572
|
+
- 309f682: Background Sync's flush can now pull between chunks when the journal is larger than half the unpulled bound (see the `@thomasfosterau/legation` changeset). A flush whose pull between chunks fails now rejects its `sync` event with a transport error instead of reporting `Flushed`, so the browser reschedules it and the journal waits for the next flush.
|
|
573
|
+
- 4c59860: Add `observeChanges(subset, options?)`: a public row-level change feed
|
|
574
|
+
(`RowChange`, #115) on `ClientEngine`, `ClientView`, and `Mirror` alike
|
|
575
|
+
(ADR-0021), for keyed-list bindings, side-effect subscriptions, and delta
|
|
576
|
+
pipelines that today have to diff `observe()`'s pages by hand. Batched per
|
|
577
|
+
view-step:
|
|
578
|
+
|
|
579
|
+
- `enter`/`exit` are anchored to the subset's windowed result — a predicate
|
|
580
|
+
or limit-boundary crossing (an insert pushing the last visible row out, a
|
|
581
|
+
delete pulling the next one in) produces the pair a consumer diffing two
|
|
582
|
+
pages would derive; `update` marks a same-membership value change.
|
|
583
|
+
- A `Rebuild` (the optimistic overlay shifted) or a seq-gap collapse (a
|
|
584
|
+
bounded change hub dropped one or more changes) emits a `reset` marker
|
|
585
|
+
followed by fresh `enter`s for the current window, rather than a diff
|
|
586
|
+
against unknown prior state.
|
|
587
|
+
- `{ includeInitial: true }` emits the current window as `enter`s in the
|
|
588
|
+
first batch, with the same subscribe-before-snapshot guard `observe`
|
|
589
|
+
already uses (#84) — a late subscriber sees exactly one batch, no gap or
|
|
590
|
+
duplicate row.
|
|
591
|
+
|
|
592
|
+
`ClientEngine` derives batches from the same incrementally-maintained
|
|
593
|
+
`LiveView` window `observe` already keeps (surfaced, not recomputed);
|
|
594
|
+
`Mirror` — which has no local `LiveView` — diffs the whole-page updates its
|
|
595
|
+
host already broadcasts, so both hosts emit identical batches for identical
|
|
596
|
+
page transitions. `@thomasfosterau/legation-browser`'s `ElectedHost` switches an observer
|
|
597
|
+
onto whichever `ClientView` (mirror or owned engine) is currently active, so
|
|
598
|
+
`observeChanges` works unchanged across a leader-election transition too.
|
|
599
|
+
|
|
600
|
+
A confirmed-row pull and a plain local mutation (#109's delta-maintained
|
|
601
|
+
overlay tracks a mutator's local handler directly and publishes exactly what
|
|
602
|
+
it did) both diff precisely; `Rebuild` is reserved for cases the overlay
|
|
603
|
+
genuinely can't diff — a pull racing a still-pending mutation, an unknown
|
|
604
|
+
mutator, a reset (`Clear`) patch — or a seq-gap collapse.
|
|
605
|
+
|
|
606
|
+
Move events are not yet part of the contract: `LiveView` rows carry a stable
|
|
607
|
+
position (#112) at runtime, but a row repositioned by a neighbor's
|
|
608
|
+
insert/delete without its own value changing isn't distinguished from
|
|
609
|
+
"unchanged" here — a consumer that needs order re-derives it from a paired
|
|
610
|
+
`observe()` or the position of `enter`s within a batch, pending that
|
|
611
|
+
follow-up. See `docs/observing-row-changes.md` for the framework-binding
|
|
612
|
+
guide (a Svelte 5 keyed-list example) and the `runOnChanges`-style
|
|
613
|
+
side-effect pattern.
|
|
614
|
+
|
|
615
|
+
- 62dcb1c: `Hydration.dehydrate` now reads the store as one state. It used to read the
|
|
616
|
+
confirmed rows, the cookie, the journal and the unpulled tail in four separate
|
|
617
|
+
calls, so a pull or a push acknowledgement that landed between them could leave
|
|
618
|
+
a snapshot whose rows lacked an acknowledged mutation's effect, while that
|
|
619
|
+
mutation sat in neither the journal nor the tail. `ClientStore` gains an
|
|
620
|
+
optional `snapshot` member that reads all four together: under the store's lock
|
|
621
|
+
for the memory and key-value stores, and in one read transaction on IndexedDB.
|
|
622
|
+
`dehydrate` uses it when the store offers it and reads member by member
|
|
623
|
+
otherwise, so a third-party store behaves as before.
|
|
624
|
+
|
|
625
|
+
The `AtomicKeyValue` seam on `@thomasfosterau/legation/internal` gains an
|
|
626
|
+
optional `readAll`, which the browser's IndexedDB binding supplies as one
|
|
627
|
+
`readonly` transaction over the entries store. A key-value store with that seam
|
|
628
|
+
decodes its snapshot from that single read.
|
|
629
|
+
|
|
630
|
+
- ae18cb3: Add a one-sentence doc comment to every exported declaration that had none, so editor hovers on the published `.d.ts` say what each export is for. This covers the root `@thomasfosterau/legation` surface (`Backend`, `Server`, `ClientStore`, the `Predicate` and `Aggregate` constructors, …), its `/internal` seams, and the options and config interfaces of the other packages. The `@thomasfosterau/legation` module header now counts seven concepts (Hydration joined ADR-0022's six). Documentation only; no API or behaviour changes.
|
|
631
|
+
- e818dc4: Server-side cache eviction from committed writes (#206). `InvalidationSink` (on `@thomasfosterau/legation/internal`) subscribes a sink to the `InvalidationBus` and passes it the collections and rows each hint batch touched, on a fibre of its own, so an eviction can neither fail nor delay the write. `EdgeInvalidation` in `@thomasfosterau/legation-alchemy-cloudflare` provides the sinks: KV deletes of `{collection}:{id}`, Workers cache purges by tag, and `forward` / `receive` to carry an invalidation from the API Worker to the front-door Worker that holds the cache over a service binding's RPC method. Failures, including a purge that resolves `{ success: false }`, are logged. The `collection:{name}` / `collection:{name}:{id}` tag spelling moves to `HttpCache` (`tagFor`, `fineTagFor`, `evictionTags`) in the core; `HttpCacheInvalidation.defaultTagFor`, `defaultFineTagFor` and `Touched` in `@thomasfosterau/legation-browser` now refer to it, unchanged.
|
|
632
|
+
- d1b01a4: Tested against `effect` 4.0.0 (and the matching `@effect/*` companions); the
|
|
633
|
+
peer floor is unchanged at `effect >=4.0.0-rc.118`.
|
|
634
|
+
|
|
635
|
+
`@thomasfosterau/legation` adapts to 4.0.0's stricter, type-only `Schema.brand`:
|
|
636
|
+
|
|
637
|
+
- `Node.Id` keeps its signature and runtime behaviour; its internal brand
|
|
638
|
+
identifier is cast to satisfy the new concrete-identifier check.
|
|
639
|
+
- `Node.AnyNode`'s `Id` property is now
|
|
640
|
+
`Schema.Codec<string & Brand.Brand<string>, string>` instead of `Id<string>`.
|
|
641
|
+
4.0.0's `brand<String, B>` is type-only and no longer assignable to a wider
|
|
642
|
+
``brand<String, `${string}/id`>``, so no concrete `Node<"todos", …>` was
|
|
643
|
+
assignable to `AnyNode`. Every concrete node is assignable again, and `Id`
|
|
644
|
+
is still a string-encoded branded id schema; only code that reads `.Id` off
|
|
645
|
+
an `AnyNode`-typed value sees the wider brand.
|
|
646
|
+
|
|
647
|
+
Checked the 4.0.0 behavioural changes against the packages and found no impact:
|
|
648
|
+
no use of `partition`/`separate`/`Option.partitionMap`, `Queue.State`,
|
|
649
|
+
`MessageStorage` or `RpcMessage.ExitEncoded`. The RPC client's missed-pong
|
|
650
|
+
handling now fails in-flight WebSocket calls rather than reporting a transient
|
|
651
|
+
error; `RpcTransport.fromOrigin` already treats a failed WebSocket call as a
|
|
652
|
+
`TransportError` and falls back to HTTP. The RPC server's WebSocket route now
|
|
653
|
+
closes with 1000/1001/1011, which nothing in the packages inspects.
|
|
654
|
+
|
|
655
|
+
- a874acb: Bump the pinned Effect version from `4.0.0-beta.75` to `4.0.0-beta.83` across all
|
|
656
|
+
packages (no engine API changes; build, typecheck, and the full test suite pass
|
|
657
|
+
unchanged).
|
|
658
|
+
|
|
659
|
+
The version is now centralised in a root **Bun catalog**
|
|
660
|
+
(`package.json` → `workspaces.catalog`): every package's `devDependencies`
|
|
661
|
+
reference `effect`/`@effect/platform-*`/`@effect/vitest` as `catalog:`, while the
|
|
662
|
+
published `peerDependencies` keep the concrete `4.0.0-beta.83` string. The
|
|
663
|
+
Every example is a workspace member so they share the same `effect` version
|
|
664
|
+
as the engine instead of pinning its own.
|
|
665
|
+
|
|
666
|
+
- c5d3484: README: the `effect` floor (`4.0.0-rc.118`) is now tested on every pull
|
|
667
|
+
request, where CI runs the test suite against it, instead of in a weekly
|
|
668
|
+
canary. The peer range (`effect >=4.0.0-rc.118`) and the tested pin (`4.0.0`)
|
|
669
|
+
are unchanged.
|
|
670
|
+
- e9363c0: Peer `effect ^4.0.0` (was `>=4.0.0-rc.118`), and `@effect/platform-node` /
|
|
671
|
+
`@effect/platform-browser` `^4.0.0` likewise, per the peer-range policy: the
|
|
672
|
+
range is raised only when a dependency requires it. The range form changes
|
|
673
|
+
(the caret no longer admits the 4.0 release candidates or Effect 5); every
|
|
674
|
+
package's tests pass at `effect` 4.0.0.
|
|
675
|
+
|
|
676
|
+
`@thomasfosterau/legation-svelte` peers `effect ^4.0.2`: its dependency
|
|
677
|
+
`@thomasfosterau/effect-svelte` 0.6.0 peers `effect >=4.0.2`.
|
|
678
|
+
|
|
679
|
+
- 604ffd3: The IndexedDB-backed `KeyValueStore` implements `keys`, which `KeyValueStore.make` requires from Effect 4.1. On Effect 4.0 the member is unused.
|
|
680
|
+
- f829146: Rewrite the `@thomasfosterau/legation` and `@thomasfosterau/legation-browser` READMEs against the current API. The stubs are replaced by install, usage and API sections with examples that compile against the published entry points: the root `Node`, `Query`, `Mutation`, `Client` and `Server` surface and the browser host, store, cache and Background Sync modules. The `@thomasfosterau/legation` package description now names the ADR-0022 surface instead of the v1 vocabulary. Documentation only; no API or behaviour changes.
|
|
681
|
+
- 507ccb1: Every tagged error is now a branded `Schema.TaggedError` rather than a
|
|
682
|
+
`Data.TaggedError`: `StoreError`, `TransportError`, `QueryError` and the rest
|
|
683
|
+
of the engine's errors, the JSON:API adapter's `SyncError` / `NetworkError` /
|
|
684
|
+
`ParseError` / `ApiError` / `ConflictError` / `ResourceBridgeError`, and
|
|
685
|
+
`HostNotReady`. Construction, `_tag`, `message` and `Effect.catchTag` are
|
|
686
|
+
unchanged; each class now carries a schema and a nominal brand, so a
|
|
687
|
+
structurally similar object no longer type-checks as one.
|
|
688
|
+
- a01f43b: The public surface is the six concepts (ADR-0022 step 10b). `@thomasfosterau/legation`'s root index shrinks from 43 namespaces to the resource-graph API — `Node`, `Edge`, `Collection`, `Query`, `Aggregate`, `Mutation` (with `Mutator` as a deprecated alias), `Client`, `ClientStore`, `Transport`, `Server`, `Backend`, `MemoryBackend`, `Authorization`, `Predicate`, and the error classes. Everything else — the engine, the mirror seam, plan vocabulary (`Subset`/`Related`), protocol codecs, reactivity plumbing, and server assembly parts — moves to the new `@thomasfosterau/legation/internal` subpath for adapter authors, which the sibling `@thomasfosterau/legation-*` packages now compose from.
|
|
689
|
+
- 97cc5f9: Build with `vp pack` (tsdown) instead of `tsc --build`. Each entry point is now one ESM bundle with one declaration file (the two `@thomasfosterau/legation` entries share a chunk, so a value reached through `.` and `./internal` is still one instance). The `exports` map, entry file names and published dependency and peer ranges are unchanged, and nothing from npm is bundled. The source also moved to the Effect lint presets' idioms (namespace imports from `effect/*` submodules, `effect/Predicate` guards, tag-exact error recovery); behaviour is unchanged.
|
|
690
|
+
- Updated dependencies [2a1f63c]
|
|
691
|
+
- Updated dependencies [ef59fa7]
|
|
692
|
+
- Updated dependencies [56a2e51]
|
|
693
|
+
- Updated dependencies [d09dc65]
|
|
694
|
+
- Updated dependencies [db2366a]
|
|
695
|
+
- Updated dependencies [97db489]
|
|
696
|
+
- Updated dependencies [cc67999]
|
|
697
|
+
- Updated dependencies [a94df19]
|
|
698
|
+
- Updated dependencies [d76b04d]
|
|
699
|
+
- Updated dependencies [6f53fb1]
|
|
700
|
+
- Updated dependencies [d675d2e]
|
|
701
|
+
- Updated dependencies [1743c2b]
|
|
702
|
+
- Updated dependencies [1c56914]
|
|
703
|
+
- Updated dependencies [615e792]
|
|
704
|
+
- Updated dependencies [c8d9afc]
|
|
705
|
+
- Updated dependencies [4709f41]
|
|
706
|
+
- Updated dependencies [f0348b4]
|
|
707
|
+
- Updated dependencies [3e29e1b]
|
|
708
|
+
- Updated dependencies [a053736]
|
|
709
|
+
- Updated dependencies [8a8adfc]
|
|
710
|
+
- Updated dependencies [8995115]
|
|
711
|
+
- Updated dependencies [8995115]
|
|
712
|
+
- Updated dependencies [87fa7a5]
|
|
713
|
+
- Updated dependencies [822001c]
|
|
714
|
+
- Updated dependencies [4c59860]
|
|
715
|
+
- Updated dependencies [961e604]
|
|
716
|
+
- Updated dependencies [5937efe]
|
|
717
|
+
- Updated dependencies [08ac9cd]
|
|
718
|
+
- Updated dependencies [1fc3725]
|
|
719
|
+
- Updated dependencies [8995115]
|
|
720
|
+
- Updated dependencies [8a8adfc]
|
|
721
|
+
- Updated dependencies [025fabb]
|
|
722
|
+
- Updated dependencies [0b8fccb]
|
|
723
|
+
- Updated dependencies [e7369e1]
|
|
724
|
+
- Updated dependencies [8995115]
|
|
725
|
+
- Updated dependencies [c856d66]
|
|
726
|
+
- Updated dependencies [01a8c64]
|
|
727
|
+
- Updated dependencies [3362c37]
|
|
728
|
+
- Updated dependencies [62dcb1c]
|
|
729
|
+
- Updated dependencies [be4be06]
|
|
730
|
+
- Updated dependencies [ae18cb3]
|
|
731
|
+
- Updated dependencies [be0a44d]
|
|
732
|
+
- Updated dependencies [cc2f5b7]
|
|
733
|
+
- Updated dependencies [e818dc4]
|
|
734
|
+
- Updated dependencies [d1b01a4]
|
|
735
|
+
- Updated dependencies [5496ddf]
|
|
736
|
+
- Updated dependencies [19eb4b0]
|
|
737
|
+
- Updated dependencies [a874acb]
|
|
738
|
+
- Updated dependencies [c5d3484]
|
|
739
|
+
- Updated dependencies [38c3147]
|
|
740
|
+
- Updated dependencies [e9363c0]
|
|
741
|
+
- Updated dependencies [fed4634]
|
|
742
|
+
- Updated dependencies [fcc52eb]
|
|
743
|
+
- Updated dependencies [454e75c]
|
|
744
|
+
- Updated dependencies [2003cd6]
|
|
745
|
+
- Updated dependencies [52c614f]
|
|
746
|
+
- Updated dependencies [94d6327]
|
|
747
|
+
- Updated dependencies [91f07db]
|
|
748
|
+
- Updated dependencies [6e9e119]
|
|
749
|
+
- Updated dependencies [6c2c802]
|
|
750
|
+
- Updated dependencies [ebcc6aa]
|
|
751
|
+
- Updated dependencies [a1b95d4]
|
|
752
|
+
- Updated dependencies [c105fbf]
|
|
753
|
+
- Updated dependencies [e1a028a]
|
|
754
|
+
- Updated dependencies [dfb8759]
|
|
755
|
+
- Updated dependencies [9260efb]
|
|
756
|
+
- Updated dependencies [0644af5]
|
|
757
|
+
- Updated dependencies [4b05d55]
|
|
758
|
+
- Updated dependencies
|
|
759
|
+
- Updated dependencies [82911cc]
|
|
760
|
+
- Updated dependencies [9c9f2b0]
|
|
761
|
+
- Updated dependencies [7b21d11]
|
|
762
|
+
- Updated dependencies [6f54de8]
|
|
763
|
+
- Updated dependencies [ab1fb4e]
|
|
764
|
+
- Updated dependencies [792d8f5]
|
|
765
|
+
- Updated dependencies [a3b2ee4]
|
|
766
|
+
- Updated dependencies [eb9555a]
|
|
767
|
+
- Updated dependencies [c0ab703]
|
|
768
|
+
- Updated dependencies [8c65627]
|
|
769
|
+
- Updated dependencies [26ad3f3]
|
|
770
|
+
- Updated dependencies [d7cafd2]
|
|
771
|
+
- Updated dependencies [cb0a33d]
|
|
772
|
+
- Updated dependencies [912d95d]
|
|
773
|
+
- Updated dependencies [7290d2b]
|
|
774
|
+
- Updated dependencies [41c33e3]
|
|
775
|
+
- Updated dependencies [a77261c]
|
|
776
|
+
- Updated dependencies [c47786c]
|
|
777
|
+
- Updated dependencies [501b12c]
|
|
778
|
+
- Updated dependencies [7f1efac]
|
|
779
|
+
- Updated dependencies [a04918b]
|
|
780
|
+
- Updated dependencies [cf0b26c]
|
|
781
|
+
- Updated dependencies [ddb1fda]
|
|
782
|
+
- Updated dependencies [f54f568]
|
|
783
|
+
- Updated dependencies [fc880dc]
|
|
784
|
+
- Updated dependencies [9cbd160]
|
|
785
|
+
- Updated dependencies [0bebedf]
|
|
786
|
+
- Updated dependencies [bc4c599]
|
|
787
|
+
- Updated dependencies [20a6e18]
|
|
788
|
+
- Updated dependencies [b4d2b80]
|
|
789
|
+
- Updated dependencies [2201b4c]
|
|
790
|
+
- Updated dependencies [0cdefe7]
|
|
791
|
+
- Updated dependencies [66f3b5b]
|
|
792
|
+
- Updated dependencies [7e11e61]
|
|
793
|
+
- Updated dependencies [6175e38]
|
|
794
|
+
- Updated dependencies [caf4761]
|
|
795
|
+
- Updated dependencies [821b10d]
|
|
796
|
+
- Updated dependencies [9e74a83]
|
|
797
|
+
- Updated dependencies [49fd066]
|
|
798
|
+
- Updated dependencies [06405ed]
|
|
799
|
+
- Updated dependencies [e7f4871]
|
|
800
|
+
- Updated dependencies [916d91b]
|
|
801
|
+
- Updated dependencies [47f4e15]
|
|
802
|
+
- Updated dependencies [f829146]
|
|
803
|
+
- Updated dependencies [13658a7]
|
|
804
|
+
- Updated dependencies [31cfb2c]
|
|
805
|
+
- Updated dependencies [a1cea1c]
|
|
806
|
+
- Updated dependencies [7e94abc]
|
|
807
|
+
- Updated dependencies [4fbd0b7]
|
|
808
|
+
- Updated dependencies [9d2f21b]
|
|
809
|
+
- Updated dependencies [3016170]
|
|
810
|
+
- Updated dependencies [f4a1a60]
|
|
811
|
+
- Updated dependencies [507ccb1]
|
|
812
|
+
- Updated dependencies [c671ccf]
|
|
813
|
+
- Updated dependencies [16889f0]
|
|
814
|
+
- Updated dependencies [fb2a992]
|
|
815
|
+
- Updated dependencies [22f320d]
|
|
816
|
+
- Updated dependencies [652ae7b]
|
|
817
|
+
- Updated dependencies [83bcf76]
|
|
818
|
+
- Updated dependencies [20e57d3]
|
|
819
|
+
- Updated dependencies [f010749]
|
|
820
|
+
- Updated dependencies [8d02113]
|
|
821
|
+
- Updated dependencies [bf9054d]
|
|
822
|
+
- Updated dependencies [e40c73a]
|
|
823
|
+
- Updated dependencies [ca38874]
|
|
824
|
+
- Updated dependencies [27e1040]
|
|
825
|
+
- Updated dependencies [e68eb3e]
|
|
826
|
+
- Updated dependencies [985c4d5]
|
|
827
|
+
- Updated dependencies [3be5abc]
|
|
828
|
+
- Updated dependencies [97db489]
|
|
829
|
+
- Updated dependencies [4a23603]
|
|
830
|
+
- Updated dependencies [f441941]
|
|
831
|
+
- Updated dependencies [a01f43b]
|
|
832
|
+
- Updated dependencies [9a6426b]
|
|
833
|
+
- Updated dependencies [97cc5f9]
|
|
834
|
+
- Updated dependencies [fc880dc]
|
|
835
|
+
- Updated dependencies [e34bc24]
|
|
836
|
+
- Updated dependencies [0470e1c]
|
|
837
|
+
- Updated dependencies [439f449]
|
|
838
|
+
- Updated dependencies [3a88118]
|
|
839
|
+
- Updated dependencies [1b8b120]
|
|
840
|
+
- Updated dependencies [cf48723]
|
|
841
|
+
- Updated dependencies [b4489c5]
|
|
842
|
+
- @thomasfosterau/legation@0.1.0
|