@thomasfosterau/legation-jsonapi 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 +973 -0
- package/LICENSE +21 -0
- package/README.md +755 -2
- package/dist/index.d.ts +1708 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4304 -0
- package/dist/index.js.map +1 -0
- package/dist/rolldown-runtime-D8jQpDG7.js +12 -0
- package/package.json +53 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
# @thomasfosterau/legation-jsonapi
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1fc3725: Add relationship declarations to collections and a protocol-agnostic `Related`
|
|
8
|
+
read over the mirror. A collection can now declare `one`/`many` relationships by
|
|
9
|
+
field mapping (`localField`/`foreignField`); `Collection.registry` rejects a
|
|
10
|
+
duplicate relationship name within a collection and a relationship pointing at an
|
|
11
|
+
unknown collection at build time. The new core `Related` module resolves and
|
|
12
|
+
incrementally maintains those joins over any `ClientEngine` mirror —
|
|
13
|
+
`query`/`observe` (a write to a related row re-hydrates without re-resolving the
|
|
14
|
+
base) and `related` (auto-subscribes the narrowed `foreignField IN (...)` foreign
|
|
15
|
+
subsets and tears them down with the scope). The incremental-join plumbing is
|
|
16
|
+
exposed as a strategy seam (`queryWith`/`observeWith`/`relatedWith` plus
|
|
17
|
+
`fieldMappingStrategy`), and the JSON:API `Relationships` module now delegates to
|
|
18
|
+
it with a linkage-based strategy — no behaviour change for existing users.
|
|
19
|
+
`Related.resolveExistsLocally` renders `Predicate.exists` on the client by
|
|
20
|
+
rewriting each node to an `In` over the synced foreign collection.
|
|
21
|
+
- c856d66: A covered read is judged and answered from one snapshot of its root rows, so a
|
|
22
|
+
pull between the coverage judgment and the answer can no longer serve a root row
|
|
23
|
+
whose edge windows are not pulled yet (#426). `Client.query` and the façade read
|
|
24
|
+
the root once. `Client.coverageOf` takes an optional `{ snapshot }` (the root rows
|
|
25
|
+
already read, with their subset) that the verdict is judged over for that subset;
|
|
26
|
+
`Query.runRows` answers a read from rows already read. A mirror that predates
|
|
27
|
+
`PulledRegistrations` still judges coverage from its registrations alone, so the
|
|
28
|
+
#422 race persists for it until it is upgraded.
|
|
29
|
+
- 5496ddf: Bump the pinned Effect version from `4.0.0-beta.84` to `4.0.0-beta.107` across
|
|
30
|
+
all packages (catalog and exact `peerDependencies`), and move
|
|
31
|
+
`@thomasfosterau/legation-jsonapi` and the commerce example from
|
|
32
|
+
`@thomasfosterau/effect-jsonapi@^0.6.0` to `^0.12.0` (#152).
|
|
33
|
+
|
|
34
|
+
**Why beta.107.** effect-jsonapi 0.12 requires `effect >= 4.0.0-beta.104` and
|
|
35
|
+
develops against `4.0.0-beta.107` exactly, so that is the version its typings
|
|
36
|
+
are known to line up with. The later `4.0.0-rc.*` releases were not attempted
|
|
37
|
+
in this change: the exact-pin policy (ADR-0003) prefers the version the
|
|
38
|
+
adopted library is built against, and every `effect/unstable/*` module this
|
|
39
|
+
repo uses (`httpapi`, `http`, `rpc`, `reactivity`, `persistence`, `sql`)
|
|
40
|
+
compiles and tests clean on beta.107.
|
|
41
|
+
|
|
42
|
+
**Effect API drift absorbed (no behavioural change):**
|
|
43
|
+
|
|
44
|
+
- `Schedule.either` / `Schedule.both` are gone; the default retry policy is now
|
|
45
|
+
`Schedule.max([Schedule.min([exponential, spaced]), recurs(5)])`, which is
|
|
46
|
+
the same policy (`min` recurs while any schedule recurs at the shortest
|
|
47
|
+
delay, `max` recurs while all recur at the longest delay).
|
|
48
|
+
- `Schema.TaggedErrorClass` is now `Schema.TaggedError` (the wire
|
|
49
|
+
`SyncRpcError` is unchanged on the wire).
|
|
50
|
+
- `SchemaRepresentation.fromASTs` is now `SchemaRepresentation.toRepresentations`
|
|
51
|
+
(spec export output is unchanged).
|
|
52
|
+
- `HttpApiGroup.Any` is gone and `HttpApi`'s group parameter is now
|
|
53
|
+
invariant, so `JsonApiSync.layer`, `JsonApiSync.inspect` and
|
|
54
|
+
`Convention.reflectResources` are generic over the concrete `HttpApi`
|
|
55
|
+
instead of accepting a widened alias. The exported `JsonApiSync.JsonApi`
|
|
56
|
+
alias is now `HttpApi.Constraint` (identifier and groups erased); pass your
|
|
57
|
+
concrete api to the functions rather than annotating it with the alias.
|
|
58
|
+
|
|
59
|
+
**effect-jsonapi 0.6 → 0.12 at the ADR-0016 boundary.** `Document`, `Query`,
|
|
60
|
+
`Resource`, `Relationship` and `Endpoint` lost no exports; `Query` gained
|
|
61
|
+
`IncludeOption`/`IncludePathsOf`/`OffsetPageStruct`/`bracketPageKeys` and
|
|
62
|
+
`Resource` gained `IncludeDepth`/`IncludePathsTo`. `schema.ts`, `types.ts` and
|
|
63
|
+
`CollectionLoader.ts` compile unchanged. One typing regression was noticed in
|
|
64
|
+
0.12: endpoints with `:id` params (`Endpoint.get`/`delete`/`update`) declare
|
|
65
|
+
their params as `Schema.Struct<Schema.Struct.Fields & { id }>`, which leaks
|
|
66
|
+
`unknown` into the requirements of the corresponding `HttpApiClient` calls.
|
|
67
|
+
The commerce example narrows this with a small `run` helper in
|
|
68
|
+
`server/client.ts`; nothing in the published packages is affected.
|
|
69
|
+
|
|
70
|
+
The bump is `minor` rather than `patch` because the exact `effect` peer pin
|
|
71
|
+
means every consumer must move `effect` in the same change, and the
|
|
72
|
+
`@thomasfosterau/legation-jsonapi` type signatures above changed shape.
|
|
73
|
+
|
|
74
|
+
- 19eb4b0: Bump the pinned Effect version from `4.0.0-beta.65` to `4.0.0-beta.75` across all packages.
|
|
75
|
+
|
|
76
|
+
`@thomasfosterau/legation-browser` now builds its IndexedDB cache store on
|
|
77
|
+
`@effect/platform-browser` (`BrowserKeyValueStore.layerIndexedDb` + core's
|
|
78
|
+
`KeyValueStoreAdapter`) instead of a hand-rolled IndexedDB wrapper.
|
|
79
|
+
`@effect/platform-browser` is a new peer dependency of the browser package, and
|
|
80
|
+
`IndexedDbCacheStore.make` is now a scoped resource (requires `Scope`); the
|
|
81
|
+
`storeName` option was removed (the underlying object store is managed by
|
|
82
|
+
platform-browser).
|
|
83
|
+
|
|
84
|
+
- fed4634: Peer the Effect v4 release-candidate line through an open floor rather than
|
|
85
|
+
an exact pin (#222), so an app can install `@thomasfosterau/legation-*` beside
|
|
86
|
+
`@thomasfosterau/effect-auth` and `@thomasfosterau/effect-jsonapi` and resolve
|
|
87
|
+
a single copy of `effect`. The published floor is `>=4.0.0-rc.118` (see
|
|
88
|
+
`effect-rc118-floor.md`), on `effect` and on the `@effect/platform-browser` /
|
|
89
|
+
`@effect/platform-node` peers.
|
|
90
|
+
|
|
91
|
+
**`@thomasfosterau/legation-jsonapi`**: a repeated shorthand filter key
|
|
92
|
+
(`?filter[name]=a&filter[name]=b`) is read as the comma list it spells
|
|
93
|
+
(`filter[name]=a,b`, an `In` predicate) rather than rejected with a 400,
|
|
94
|
+
because `@thomasfosterau/effect-jsonapi` accepts the repeated-key spelling at
|
|
95
|
+
every list-valued position.
|
|
96
|
+
|
|
97
|
+
The packages run in browsers and workers and deliberately declare no `engines`
|
|
98
|
+
(ADR-0003).
|
|
99
|
+
|
|
100
|
+
- fcc52eb: Target Effect `4.0.0-rc.118`: every package peers `effect >=4.0.0-rc.118`
|
|
101
|
+
(#261), and the `@effect/platform-browser` / `@effect/platform-node` peers use
|
|
102
|
+
the same floor. Install `effect` and every `@effect/*` companion at
|
|
103
|
+
`4.0.0-rc.118` or later together.
|
|
104
|
+
|
|
105
|
+
The packages import Effect's modules at their rc.118 paths (`effect/rpc`,
|
|
106
|
+
`effect/http`, `effect/http-api`, …); rc.118 removed the `effect/unstable/*`
|
|
107
|
+
export paths, so the packages don't resolve beside an earlier `effect`.
|
|
108
|
+
|
|
109
|
+
**`@thomasfosterau/legation-jsonapi`** depends on `@thomasfosterau/effect-jsonapi@^0.16.1`,
|
|
110
|
+
the first release that peers `effect >=4.0.0-rc.118`.
|
|
111
|
+
|
|
112
|
+
**`@thomasfosterau/legation-svelte`** depends on `@thomasfosterau/effect-svelte@^0.4.0`, for
|
|
113
|
+
the same reason.
|
|
114
|
+
|
|
115
|
+
- 19cc12d: The JSON:API façade answers the mutation status URL (ADR-0030 §2, #511):
|
|
116
|
+
`GET {prefix}/_legation/mutations/{mutationId}`, the `Location` of every write
|
|
117
|
+
receipt, served from the engine's mutation-state map (new option
|
|
118
|
+
`mutationStates`) and never forwarded. `200` with one `legation-mutations`
|
|
119
|
+
resource (`phase`, `mutator`, `journaledAt`, `settledAt` once settled,
|
|
120
|
+
`reason` only when rejected, `links.self` / `links.resource`); `404`
|
|
121
|
+
`code: "unknown_mutation"` for an id the map does not hold. `Prefer: wait=N`
|
|
122
|
+
holds a pending answer until the mutation settles or `N` seconds pass, capped
|
|
123
|
+
by the new `maxWait` option (default 30 s), with `Preference-Applied: wait=N`.
|
|
124
|
+
New module `MutationStatus` builds the documents.
|
|
125
|
+
- aba1401: The JSON:API façade answers a write it cannot journal offline with ADR-0030
|
|
126
|
+
§3's error documents (#512): `400 invalid_document`, `409 type_mismatch` /
|
|
127
|
+
`id_mismatch` and `422 unsupported_relationship` for a malformed or mismatched
|
|
128
|
+
body, each with `source.pointer`; the `503` `offline` document with a second
|
|
129
|
+
error `client_id_required` (`source.pointer: "/data/id"`) for a create without
|
|
130
|
+
a client id, or `not_journalable` (the `MutatorError`'s message) for a
|
|
131
|
+
mutation the engine refuses; the plain `503` `offline` document for
|
|
132
|
+
`Prefer: handling=strict`. Online, the same requests are forwarded untouched.
|
|
133
|
+
Attribute values are never validated by the façade.
|
|
134
|
+
- 3362c37: The JSON:API façade intercepts writes (ADR-0030 §1, §3, §5, #510). Given the
|
|
135
|
+
engine's journal seam (`mutate`, the worker engine's `mutateWithAck`), a
|
|
136
|
+
`POST` with a client id, a `PATCH` or a `DELETE` on a reflected write route is
|
|
137
|
+
journaled as the graph-derived CRUD mutation and answered `202 Accepted` with a
|
|
138
|
+
write receipt — the resource as journaled, `meta.legation.status: "pending"`
|
|
139
|
+
and a `Location` naming the mutation status URL — when offline, when the
|
|
140
|
+
forward rejected at the transport, or on `Prefer: respond-async`
|
|
141
|
+
(`Preference-Applied: respond-async`). Online and unasked, writes are
|
|
142
|
+
forwarded unchanged and journal nothing; `Prefer: handling=strict` never
|
|
143
|
+
journals. New options `mutate`, `intentOf` (default
|
|
144
|
+
`FacadeWrites.graphIntentOf(nodes)`), `writes` and `pull`; new
|
|
145
|
+
`FetchHandler.forwarded(request, response)`, which signals `pull` after a
|
|
146
|
+
`2xx` forwarded write.
|
|
147
|
+
|
|
148
|
+
**Breaking:** `FetchHandler.offline` is now a function of the request,
|
|
149
|
+
`offline(request)`: a write candidate is journaled and answered with its
|
|
150
|
+
receipt (pass the clone taken before forwarding), anything else gets the 503
|
|
151
|
+
`offline` document as before. Replace `facade.offline` with
|
|
152
|
+
`facade.offline(request)`.
|
|
153
|
+
|
|
154
|
+
- 2dbbce7: `FacadeWrites.writesOf(nodes, options?)` is the inverse of `graphIntentOf` (ADR-0030 §5, #529): for every graph-derived CRUD mutation (`CrudMutation`, filtered by `writes` as the registry is) it says how the args replay as one JSON:API write, with each `one` edge's `via` key moved out of the attributes into `relationships[edge].data` (`null` linkage when the key is `null`). Pass it as a `JsonApiBackend`'s `writes`, so a node-graph app's journaled `{type}/create`, `{type}/update` and `{type}/delete` reach the server as the `POST`, `PATCH` and `DELETE` the app would have sent.
|
|
155
|
+
|
|
156
|
+
The write's `type` is always the mutation's own: args naming another type are refused (`FacadeWrites.InvalidWriteArgs`), and `JsonApiBackend` now reports any `writes` function that throws as invalid args, a `BackendError`, rather than a defect.
|
|
157
|
+
|
|
158
|
+
- d12e078: Issue #218: the filter grammar (effect-jsonapi's query-parameter format) and
|
|
159
|
+
`Predicate` (legation's own subset language) are two different things, not
|
|
160
|
+
one language with an internal engine form — `CONTEXT.md` and ADR-0016 are
|
|
161
|
+
corrected to say so; ADR-0028, `Predicate`, every interpreter and the
|
|
162
|
+
generated corpus are unchanged.
|
|
163
|
+
|
|
164
|
+
`JsonApiQuery.fromFilterAst(ast)` no longer claims to be total and no longer
|
|
165
|
+
throws for a literal outside `string | number | boolean | Date`: it now
|
|
166
|
+
returns `Result.Result<Predicate.Predicate, QueryError>`, so a caller
|
|
167
|
+
bridging a filter AST built over a resource with a richer decoded attribute
|
|
168
|
+
type gets a typed failure instead of an uncaught exception. Its doc comment
|
|
169
|
+
now states the field-namespace precondition (field names are copied
|
|
170
|
+
verbatim, correct only because a caller pre-synthesises a resource whose
|
|
171
|
+
fields are already the engine's) and names the `Date → toISOString()` step
|
|
172
|
+
as a load-bearing encode (ADR-0028 rule e), and states plainly that
|
|
173
|
+
`toFilterAst` is not its inverse. `JsonApiQuery.parseParams` /
|
|
174
|
+
`JsonApiQuery.canonical` are unaffected in observable behaviour: both call
|
|
175
|
+
sites' literals are always wire scalars by construction (decoded through
|
|
176
|
+
the node-derived synthetic resource, or copied from a `Predicate.Literal`),
|
|
177
|
+
so the typed failure is unreachable there — the change makes that
|
|
178
|
+
unreachability an enforced fact instead of an assumption, and gives the
|
|
179
|
+
exported function's own general contract a typed failure instead of a
|
|
180
|
+
thrown defect.
|
|
181
|
+
|
|
182
|
+
A new property test exercises the Date encode step directly (the existing
|
|
183
|
+
`FilterCorpus`-generated corpus can never produce a `Date`, since
|
|
184
|
+
`Predicate.Literal` is a closed wire-scalar union, so it could not catch a
|
|
185
|
+
regression there) and checks it against the wire text a resource that
|
|
186
|
+
actually declares a `Date`-decoded attribute would send — the façade/REST
|
|
187
|
+
equivalence obligation of ADR-0029 §6.
|
|
188
|
+
|
|
189
|
+
- 1f4a9ce: Push the journal as one `atomic:operations` request when the server advertises
|
|
190
|
+
the JSON:API Atomic Operations extension (#160). `JsonApiBackendConfig.atomic`
|
|
191
|
+
(`{ path }`) switches `applyMutations` from one `POST`/`PATCH`/`DELETE` per
|
|
192
|
+
mutation to a single all-or-nothing request per batch — `create` → `add`,
|
|
193
|
+
`update` → `update`, `delete` → `remove`, sent with the extension's media type
|
|
194
|
+
— and `JsonApiSync.layer` fills it in automatically when the reflected api has
|
|
195
|
+
an endpoint taking an `atomic:operations` document (effect-jsonapi's
|
|
196
|
+
`Endpoint.operations`); `JsonApiSyncOptions.atomic: false` opts out, `{ path }`
|
|
197
|
+
names the endpoint explicitly.
|
|
198
|
+
|
|
199
|
+
Delivery over the atomic path is exactly-once per batch rather than
|
|
200
|
+
at-least-once: the in-session ledger advances only once the server has applied
|
|
201
|
+
every operation. A transient failure (transport, 5xx, or a 4xx naming no
|
|
202
|
+
operation) moves nothing and the client retries the batch; a 4xx error document
|
|
203
|
+
whose `source.pointer` names an operation is a permanent rejection of that
|
|
204
|
+
mutation — it is reported in `rejected`, the batch is resent without it, and
|
|
205
|
+
every mutation is then acknowledged in order (the poison-mutation semantics of
|
|
206
|
+
ADR-0008). Change hints come from the result document's resources. Creates
|
|
207
|
+
keep sending the client-generated `id`; `lid` is supported by the wire format
|
|
208
|
+
but unused until a mutator needs a server-assigned id. The per-request path is
|
|
209
|
+
unchanged and still documented as at-least-once.
|
|
210
|
+
|
|
211
|
+
Route reflection now recognises an atomic operations endpoint structurally (a
|
|
212
|
+
request payload with a top-level `atomic:operations` member) and skips it:
|
|
213
|
+
previously `POST /operations` classified as `create` on a phantom `operations`
|
|
214
|
+
resource, which derived a phantom collection and mutator. `Convention` gains
|
|
215
|
+
`reflectAtomic` and `isAtomicEndpoint`; `JsonApiSync.inspect` reports the
|
|
216
|
+
endpoint as `atomic`. Reflected endpoint refs now carry the endpoint's
|
|
217
|
+
`identifier` as their `name` (Effect 4 renamed the field).
|
|
218
|
+
|
|
219
|
+
Also: `JsonApiBackendConfig.writes` expresses mutators beyond the derived CRUD
|
|
220
|
+
ones as JSON:API writes (a `Write` is one create/update/delete of one
|
|
221
|
+
resource), which is what lets `BackendContract.verifyBackendContract` run over
|
|
222
|
+
the backend in atomic mode against an effect-jsonapi `Endpoint.operations`
|
|
223
|
+
server; the atomic path rejects mutation-id gaps as the contract requires; and
|
|
224
|
+
`getRows` now omits a row the server answers `404` for instead of failing.
|
|
225
|
+
|
|
226
|
+
- 6f62539: Auto-subscribe and `include` for relationship resolution.
|
|
227
|
+
|
|
228
|
+
- `Relationships.related(engine, subset, names)` is an auto-subscribing
|
|
229
|
+
relational read: it subscribes the base subset and, from its linkage, the
|
|
230
|
+
narrowed `id IN (...)` subsets of each referenced collection, tracking the
|
|
231
|
+
live linkage and tearing the subscriptions down with the scope. Callers no
|
|
232
|
+
longer wire up the related collections by hand. Composes with the query
|
|
233
|
+
encoder so the related subsets narrow server-side via `filter[id]`.
|
|
234
|
+
- `JsonApiSync`/`JsonApiBackend` gain an `includes` option (collection →
|
|
235
|
+
relationship names). Listings then carry `?include=...`, and the compound
|
|
236
|
+
document's related resources are harvested so an auto-subscribed
|
|
237
|
+
`id IN (...)` subset is served from that response — the related rows arrive
|
|
238
|
+
with their parent, no extra round trip.
|
|
239
|
+
|
|
240
|
+
- 454e75c: Derived REST `list` / `get` handlers over the engine's read path (#163): a
|
|
241
|
+
JSON:API `GET` is ADR-0025's `read` operation with a document body, so the
|
|
242
|
+
REST tier and the sync tier now share one grammar, one evaluator and one auth
|
|
243
|
+
path — the premise of ADR-0029's "same URL, same result".
|
|
244
|
+
|
|
245
|
+
- `RestHandlers.make({ nodes, backend, policy, context, middleware?, prefix? })`
|
|
246
|
+
binds handler factories for effect-jsonapi's `Endpoint.list` / `Endpoint.get`:
|
|
247
|
+
`rest.list(resource)` and `rest.get(resource, { notFound })`. Each parses the
|
|
248
|
+
request URL into a `Query.Read` with `JsonApiQuery.parseParams`, resolves the
|
|
249
|
+
request's identity through the `context` seam (HTTP request → `SyncContext`,
|
|
250
|
+
the HTTP analogue of `IdentityResolver`), runs the read through the
|
|
251
|
+
filterable check, the deny-by-default auth filters and `Backend.evaluate` +
|
|
252
|
+
`getRows` with the two-phase include resolution, and serialises the rows.
|
|
253
|
+
Scoping is the policy's alone: an anonymous request to a private collection is
|
|
254
|
+
an empty collection for `list` and a 404 for `get`, as the sync tier's empty
|
|
255
|
+
allow-set behaves. A URL the grammar rejects is the codec's 400 error
|
|
256
|
+
document with `source.parameter`, before any backend call; a collection the
|
|
257
|
+
policy does not declare is a 403; a missing row fails with the endpoint's own
|
|
258
|
+
404 class. Linkage follows the resource's declaration: every
|
|
259
|
+
`Relationship.many` the endpoint's resource declares is resolved — on the
|
|
260
|
+
primary resource and on each included resource's own declaration — because
|
|
261
|
+
effect-jsonapi resource schemas require every declared relationship, and
|
|
262
|
+
nothing the resource does not declare is touched; `include` decides
|
|
263
|
+
`included`. A paginated node edge lists and includes under a
|
|
264
|
+
`Relationship.many` declaration (links plus its rows' linkage in the edge's
|
|
265
|
+
order) and carries links only under `Relationship.paginated`. Derivation is
|
|
266
|
+
opt-in per endpoint, beside hand-written handlers.
|
|
267
|
+
- `DocumentWriter.collection(input)` / `DocumentWriter.data(input)` is the one
|
|
268
|
+
rows-to-document function both the REST handlers and the service-worker façade
|
|
269
|
+
(#164) serialise through: a pure function of the node graph, the read, the
|
|
270
|
+
resolved rows and the `fields[]` projection. Attributes are the node's, `one`
|
|
271
|
+
edges link through their key fields, collection edges through the resolved
|
|
272
|
+
edge rows, paginated edges carry the spec's `links` (and their rows' linkage
|
|
273
|
+
when included); `included` is deduplicated
|
|
274
|
+
with full linkage checked; `fields[type]` projects attributes after linkage;
|
|
275
|
+
`links.self` is the read's canonical string and `links.next` the same query
|
|
276
|
+
with `page[after]` at the last row when the window is full; `linkage` carries
|
|
277
|
+
the edge rows resolved for included resources. Node rows only — `Derive.ts`'s
|
|
278
|
+
`JsonApiRow` has no node graph to plan against.
|
|
279
|
+
- `RestHandlers.queryErrorDocument(error)` renders a `QueryError` as the 400
|
|
280
|
+
document (the codec's own when the rejection was a decoding failure).
|
|
281
|
+
|
|
282
|
+
- a3b2ee4: The JSON:API façade (ADR-0029, issue #166): a fetch handler that answers a
|
|
283
|
+
covered JSON:API `GET` from the replica and stays out of the way otherwise.
|
|
284
|
+
|
|
285
|
+
- `JsonApiFacade.fetchHandler({ nodes, api, view, coverageOf, cookie?, resources?, baseUrl?, prefix?, online? })`
|
|
286
|
+
in `@thomasfosterau/legation-jsonapi` builds the façade from the app's `HttpApi`
|
|
287
|
+
(`Convention.reflectResources` gives the `list` / `getOne` routes it
|
|
288
|
+
recognises; those whose type is no node are left to the server), the
|
|
289
|
+
node graph, the replica's view and a coverage source. It presumes the
|
|
290
|
+
api's read endpoints are declared with the graph's codec
|
|
291
|
+
(`JsonApiQuery.querySchema` / `singleRowSchema`) and served by the
|
|
292
|
+
derived handlers, which is what lets a parse failure be answered locally.
|
|
293
|
+
`matches(request)` says whether a request is its business — a `GET` whose
|
|
294
|
+
`Accept` names `application/vnd.api+json` on a reflected route under
|
|
295
|
+
`baseUrl` — and `handle(request)` answers: a covered request gets `200`
|
|
296
|
+
with the document the one writer produces (byte-equal in content to the
|
|
297
|
+
derived REST handler's for the same declarations, the optimistic overlay
|
|
298
|
+
visible as `Client.query` sees it), `Cache-Control: no-store` and
|
|
299
|
+
`x-legation-served-at` set to the replica's CVR cookie (the store's,
|
|
300
|
+
empty until the first pull); a URL the grammar rejects gets the codec's
|
|
301
|
+
`400` document (`RestHandlers.queryErrorDocument`), except an `include`
|
|
302
|
+
the graph does not declare, which the server rules on; an uncovered
|
|
303
|
+
request while offline gets the `503` `offline` error document of
|
|
304
|
+
ADR-0029 §4 (`handler.offline`, with the cookie, also for a forward that
|
|
305
|
+
rejected at the transport). Everything else is `undefined` for the caller
|
|
306
|
+
to forward unchanged: a non-candidate, an uncovered request while online,
|
|
307
|
+
a collection no bound resource declares, and a single-resource request
|
|
308
|
+
whose row the replica does not hold (the 404 is the endpoint's own).
|
|
309
|
+
Coverage is every read `ReplicaDocuments.toCover` names, each judged by
|
|
310
|
+
`coverageOf` — `client.coverageOf`, or the host's registrations below.
|
|
311
|
+
The module lives in `@thomasfosterau/legation-jsonapi` by ADR-0006: it needs this
|
|
312
|
+
package's codec, writer and route reflection plus the standard
|
|
313
|
+
`Request` / `Response` globals, and `navigator.onLine` is an option with a
|
|
314
|
+
guarded default, so `@thomasfosterau/legation-browser` stays free of effect-jsonapi.
|
|
315
|
+
- `EngineHost.run` returns a `Host` whose `registrations` lists what the
|
|
316
|
+
host holds on its mirrors' behalf — root windows, relational registrations
|
|
317
|
+
with their edge selects, both halves of a `SubscribeUrl` with its canonical
|
|
318
|
+
URL while a party holds it by URL — as `Coverage.Subscription`s: the
|
|
319
|
+
ledger a façade's coverage check scans in a worker, where no `Client`'s
|
|
320
|
+
own ledger sees the tabs' pins. A registration is listed once its engine
|
|
321
|
+
registration exists (a relational one once its reconcile loop has
|
|
322
|
+
started; one naming an edge the registry does not declare registers
|
|
323
|
+
nothing) and dropped when the last mirror releases it or is swept.
|
|
324
|
+
`Coverage.verdictOf` and
|
|
325
|
+
`Coverage.coverageOf(nodes, subscriptions)` give the same `Verdict` as
|
|
326
|
+
`Client.coverageOf` over any such ledger; `Client` uses the former.
|
|
327
|
+
- `ServiceWorkerHost.start` serves exactly as `run` does and returns the
|
|
328
|
+
`Host` — the engine and its registrations — for the worker to build the
|
|
329
|
+
façade from; `run` is `start` then serve forever.
|
|
330
|
+
- The commerce worker composes façade, then its unchanged HTTP cache, then
|
|
331
|
+
the network (`examples/commerce/src/service-worker.ts`,
|
|
332
|
+
`src/lib/facade.ts`, `src/lib/host.ts`); the api's shape moves to
|
|
333
|
+
`server/api.ts` so the worker reflects the routes the server serves; and
|
|
334
|
+
the replica test runs the façade over the same seed: every URL the
|
|
335
|
+
shell's pins cover is answered with the served document's bytes, and the
|
|
336
|
+
rest go to the network. `Convention.segmentsOf` and `Convention.isParam`
|
|
337
|
+
are exported.
|
|
338
|
+
|
|
339
|
+
- eb9555a: The filter-grammar bridge (ADR-0028 §5, #161): `@thomasfosterau/legation-jsonapi` now
|
|
340
|
+
translates both ways between effect-jsonapi's filter AST and the engine's
|
|
341
|
+
`Predicate`, parses a JSON:API URL into a `Query.Read`, and encodes the whole
|
|
342
|
+
predicate exactly. Bumps `@thomasfosterau/effect-jsonapi` to ^0.13.0, which
|
|
343
|
+
carries the grammar (`Filter`, `Sort`, `Query.Filter`, `Query.canonical`,
|
|
344
|
+
`Resource.filterable` / `Resource.sortable`).
|
|
345
|
+
|
|
346
|
+
- `JsonApiQuery.fromFilterAst(ast)` is total — every grammar node has an
|
|
347
|
+
engine node — and `toFilterAst(predicate)` is partial: `Exists`, an empty
|
|
348
|
+
`In` / `NotIn` list and a `null` literal have no URL form and return `None`,
|
|
349
|
+
so callers fail closed. `foldConstants` folds the two definite cases away
|
|
350
|
+
exactly (an empty `In` is false, an empty `NotIn` true under ADR-0028).
|
|
351
|
+
- `JsonApiQuery.parseRead(nodes, url)` / `parseParams(nodes, collection,
|
|
352
|
+
params)` decode `include`, `fields`, `sort`, `page` and `filter` with
|
|
353
|
+
effect-jsonapi's `Query.schema` derived from the **node graph** (each node's
|
|
354
|
+
filterable set — operators and literal kinds — and edges become the codec's
|
|
355
|
+
declaration; `resourcesOf(nodes)` exposes the synthesised resources) and map
|
|
356
|
+
them onto the read: `filter` → `where`, `sort` → `order`, `page[limit]` /
|
|
357
|
+
`page[after]` → `first` / `after`, `include` → edge selects. Include paths
|
|
358
|
+
are single level (ADR-0028 D1). `fields[]` is carried beside the read as a
|
|
359
|
+
projection, never into the plan. Every rejection is a `QueryError` whose new
|
|
360
|
+
`parameter` field names the offending query key (the 400's
|
|
361
|
+
`source.parameter`). A `/{type}/{id}` URL reads as `filter[id]`, first 1.
|
|
362
|
+
- `JsonApiQuery.canonical(nodes, read)` compiles the read and encodes its plan
|
|
363
|
+
through effect-jsonapi's canonical encoder — one string per plan, the
|
|
364
|
+
subscription identity of ADR-0029 — and `parseRead` accepts it back.
|
|
365
|
+
- The encoder is exact: `defaultEncoder` pushes the whole predicate as the
|
|
366
|
+
grammar's canonical `filter[...]` form (operators, `or`, `not`, `isnull`,
|
|
367
|
+
`nin` included — nothing is dropped any more); `safeFilters` is gone. A
|
|
368
|
+
subset whose predicate folds to false is answered without a request, and one
|
|
369
|
+
with no URL form fails closed. `QueryEncoder.encode` now returns
|
|
370
|
+
`Effect<Option<string>, QueryError>`; `JsonApiRequestOptions.filter` is a
|
|
371
|
+
filter AST (`Filter.eq("status", "open")`) instead of a `Record<string,
|
|
372
|
+
string>`; query strings are serialised through `Query.serialise` (readable
|
|
373
|
+
`[`/`]` in keys). `buildQueryString` lives in `JsonApiQuery` and stays
|
|
374
|
+
re-exported from `CollectionLoader`.
|
|
375
|
+
- `NodeBridge.fromResource` / `registry` read the filterable set off the
|
|
376
|
+
resource's own declaration (`Resource.filterable` / `Resource.sortable`): an
|
|
377
|
+
attribute keeps its name and derives its kind from its schema; a to-one
|
|
378
|
+
relationship's declaration lands on the edge's key field (`author` →
|
|
379
|
+
`authorId`, kind `text`). The `filterable` / `sortable` options remain as
|
|
380
|
+
per-field overrides (the way to give an `instant` its kind).
|
|
381
|
+
|
|
382
|
+
- a01f775: Make relationship live queries incremental. `Relationships.observe` (and the
|
|
383
|
+
auto-subscribing `related`) now maintain the join instead of re-resolving it on
|
|
384
|
+
every change: the base page is kept current by `engine.observe(subset)` and each
|
|
385
|
+
referenced collection by its own `engine.observe`, so a write to a related row
|
|
386
|
+
re-hydrates the join without re-querying the base, and a base change that shifts
|
|
387
|
+
_which_ collections are referenced re-establishes the referenced observers (via
|
|
388
|
+
`switchMap`). Per-collection lookup indexes are cached and only the changed
|
|
389
|
+
slice is rebuilt; changes that don't move the resolved result are deduped away.
|
|
390
|
+
|
|
391
|
+
This also fixes a correctness gap exposed by the now-deduped
|
|
392
|
+
`ClientEngine.observe`: a change to _only_ a referenced collection (e.g. editing
|
|
393
|
+
a comment without touching its issue) no longer fails to update the join.
|
|
394
|
+
|
|
395
|
+
- f24f45b: `JsonApiSync.layer` keeps the client store and the ledger durable together: a new `keyValueStore` option (the durable `KeyValueStore` the client store is built over) makes the client store default to `ClientStore.layerKeyValue()` over it, and the ledger default to `Ledger.keyValue` over it, so a reload keeps the journal, the acknowledged mutation ids and the remembered rejections (#468). An explicit `store` or `ledger` still overrides its default, and without `keyValueStore` both stay in memory as before. The README and the backend's delivery notes now say that a failed ledger write after a successful server write fails the push: mutations acknowledged earlier in that push stay acknowledged, and the resend re-applies the rest, the same window as a crash.
|
|
396
|
+
- 8a6fc54: `JsonApiBackend` takes an optional `ledger` store, so its ledger (the acknowledged mutation ids and the remembered rejections of each client group) survives a restart (#468). `Ledger.keyValue` keeps it in an Effect `KeyValueStore`, and `JsonApiSync.layer` accepts the same option. Without one, the ledger stays in memory as before, and a restart of the backend forgets it: a resent mutation that was already applied looks new, and a permanent rejection can no longer be reported to a replayed copy or a pull. The README documents the restart caveat.
|
|
397
|
+
- 74fe923: Extend the JSON:API sync derivation with server-side query narrowing and
|
|
398
|
+
relationship resolution.
|
|
399
|
+
|
|
400
|
+
- `JsonApiQuery`: a pluggable `QueryEncoder` seam that translates a `Subset`
|
|
401
|
+
into a JSON:API query string, with `JsonApiSync` accepting a `queryEncoder`
|
|
402
|
+
override. The shipped default soundly encodes the `eq`/`in` conjuncts and
|
|
403
|
+
`sort` it can express (`filter[field]=…`, `sort=field,-field`) and drops the
|
|
404
|
+
rest; since `Subset.apply` re-runs the full predicate locally, the server
|
|
405
|
+
filter only needs to return a superset. A dedicated JSON:API package can
|
|
406
|
+
supply a richer encoder (operator filters, cursor pages).
|
|
407
|
+
- `Relationships`: resolve JSON:API relationship linkage against the local
|
|
408
|
+
mirror — `resolve`/`hydrate` (pure) plus `query`/`observe` over a
|
|
409
|
+
`ClientEngine`, giving a reactive `related()` (to-one and to-many) that
|
|
410
|
+
re-resolves when a referenced row changes.
|
|
411
|
+
- Engine rows for JSON:API resources are now **flattened** (attributes at the
|
|
412
|
+
top level, `id` and `relationships` reserved) so predicates and `orderBy`
|
|
413
|
+
address attribute fields directly — matching how JSON:API `filter[field]`
|
|
414
|
+
works. Mutator arguments remain structured (`{ id, attributes, … }`).
|
|
415
|
+
|
|
416
|
+
- fc467ea: Compound documents from the replica (#164): the worker-side caller of the one
|
|
417
|
+
serialiser, so the same URL yields the same document content from the
|
|
418
|
+
replica as from the derived REST handler (ADR-0029 §6).
|
|
419
|
+
|
|
420
|
+
- `ReplicaDocuments.make({ nodes, resources?, prefix? })` binds the
|
|
421
|
+
replica-side writer. `fromUrl(view, url)` parses a URL through
|
|
422
|
+
`JsonApiQuery.parseRead` (the read and its `fields[]` projection together),
|
|
423
|
+
runs it over a local view — the replica's rows, optimistic overlay
|
|
424
|
+
included, through the two-phase include with the plan's edge windows — and
|
|
425
|
+
writes a single-resource document for `/{type}/{id}` (`data: null` when
|
|
426
|
+
the replica holds no row) or a collection document otherwise, through
|
|
427
|
+
`DocumentWriter`; `collection(view, parsed)` and `data(view, parsed)` take
|
|
428
|
+
a parsed read. Linkage follows the resource declarations it is given —
|
|
429
|
+
the resources the REST endpoints are declared with, so both sides emit the
|
|
430
|
+
same relationships by construction — or, without any, the node graph's
|
|
431
|
+
synthetic resources (`JsonApiQuery.resourcesOf(nodes)`). Parity holds when
|
|
432
|
+
the endpoint is declared with the node-derived codec
|
|
433
|
+
(`JsonApiQuery.querySchema`) and collection edges are `many`; an `include`
|
|
434
|
+
edge or a `fields[type]` type the bound resource does not declare is
|
|
435
|
+
refused with the codec's `QueryError` shape before any query runs.
|
|
436
|
+
`toCover(read)` names the reads a request needs covered for its document
|
|
437
|
+
to be faithful: the read itself, plus one whole-collection read per
|
|
438
|
+
collection the declared linkage resolves from beyond the read's includes;
|
|
439
|
+
the caller (the façade) serves a URL only when every one is covered
|
|
440
|
+
(`Client.coverageOf`). A collection no bound resource declares is a
|
|
441
|
+
`QueryError` from every entry point, never a defect. The module depends on
|
|
442
|
+
nothing but `effect` and the engine, so it runs in a service worker.
|
|
443
|
+
- `RestHandlers` derives its linkage through the same package-internal
|
|
444
|
+
derivation the replica-side writer uses (what a document's relationships
|
|
445
|
+
are resolved from beside the URL's includes, over any row source); its
|
|
446
|
+
behaviour is unchanged.
|
|
447
|
+
- `JsonApiQuery.parseRead` reports the row id a `/{type}/{id}` path
|
|
448
|
+
addressed (`ParsedRead.id`), so a caller knows to write a single-resource
|
|
449
|
+
document. `JsonApiQuery.invalidParameter` builds the `QueryError` shape
|
|
450
|
+
the codec's checks raise.
|
|
451
|
+
- The commerce example checks every read `src/lib/domain.ts` declares
|
|
452
|
+
against the same seed on both sides, byte for byte
|
|
453
|
+
(`examples/commerce/tests/replica-documents.test.ts`).
|
|
454
|
+
|
|
455
|
+
- 0cddcc4: `JsonApiSync.layer` accepts a `store` option to back the engine with a
|
|
456
|
+
durable `ClientStore` (IndexedDB, localStorage) instead of the in-memory
|
|
457
|
+
default, so the journal and confirmed rows survive reloads — the happy path
|
|
458
|
+
becomes offline-first with one option.
|
|
459
|
+
- b44e9ae: Add convention-first sync derivation from an app's JSON:API `HttpApi`
|
|
460
|
+
(`JsonApiSync`). Point it at an Effect `HttpApi` and it reflects the endpoints,
|
|
461
|
+
classifies them by method + path into JSON:API CRUD roles (`Convention`),
|
|
462
|
+
derives one collection and the create/update/delete mutators per resource type
|
|
463
|
+
(`Derive`), and backs the BYO `Backend` contract with HTTP calls
|
|
464
|
+
(`JsonApiBackend`) — content-hash row versions stand in for the version token a
|
|
465
|
+
plain REST API doesn't expose. `JsonApiSync.layer` composes the whole
|
|
466
|
+
client-direct stack (in-process sync room + client engine) so an existing REST
|
|
467
|
+
API gains optimistic writes, a durable journal, rebase, and live queries with
|
|
468
|
+
no bespoke backend; `JsonApiSync.start` runs the sync loop and polls for remote
|
|
469
|
+
changes.
|
|
470
|
+
|
|
471
|
+
Delivery is at-least-once (the in-session ledger has no cooperating server), so
|
|
472
|
+
keep writes idempotent: client-generated ids on create, naturally-idempotent
|
|
473
|
+
PATCH/DELETE.
|
|
474
|
+
|
|
475
|
+
- 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.
|
|
476
|
+
- ddb1fda: Restructure the repository into a monorepo of sync-engine packages, with package boundaries drawn by dependency footprint:
|
|
477
|
+
|
|
478
|
+
- `@thomasfosterau/legation` — the Effect-native sync engine. Everything that needs only a dependency on `effect`: sync keys, cache stores, the write-through cache engine (read-through, dedupe, stale-while-revalidate, invalidation, events), queries, pagination, mutations with optimistic updates, a durable offline outbox, the client/server sync protocol, transports, conflict resolution, the TanStack-Query-style client (queries/mutations/subscriptions/offline sync), the service-worker/edge HTTP cache engine with KeyValueStore persistence, the server-side sync hub with fan-out, and promise facades for non-Effect consumers.
|
|
479
|
+
- `@thomasfosterau/legation-jsonapi` — the previous `@thomasfosterau/effect-chancery` JSON:API helpers, migrated into the monorepo as a protocol adapter with Effect tagged errors and query/mutation descriptor builders that plug into the core engine.
|
|
480
|
+
|
|
481
|
+
- e1b8333: `NodeBridge` reads more sync metadata off the resource itself (#220). A `paginated` relationship's edge order comes from its own declaration, `Relationship.paginated(ref, { order })`, so `options.order` is no longer required. Two `@legation/` resource annotations, built with `NodeBridge.syncAnnotations({ inverses, kinds })`, name an inverse edge (`@legation/inverses`) and give a filterable field its literal kind (`@legation/kinds`, for an `instant` stored as text). They survive `Resource.annotate` and `Resource.extend`. `FromResourceOptions` stay as explicit overrides and win over the resource's declaration. A malformed annotation, or a kind for a field the resource does not declare filterable, throws `ResourceBridgeError` when the graph is built.
|
|
482
|
+
- f4f0688: The effect-jsonapi Node bridge (ADR-0022 program step 9): `NodeBridge.fromResource` / `NodeBridge.registry` derive the sync engine's node graph from `@thomasfosterau/effect-jsonapi` resource definitions — one/optional relationships become one-edges with derived key fields, many/paginated relationships resolve their inverses across bridged one-edges, and the `edges` option promotes a foreign-key attribute to an edge so the library's acyclic resource graphs bridge without hand-naming inverses. Bumps the effect-jsonapi dependency to ^0.6.0.
|
|
483
|
+
- 7e11e61: Permanent mutation rejection semantics (#35). A mutator handler can now fail
|
|
484
|
+
with the new `MutationRejected` error to mark a failure permanent (failed
|
|
485
|
+
validation, revoked permission, violated business rule). The backend records
|
|
486
|
+
the mutation as processed — bumps `lastMutationId` atomically **without**
|
|
487
|
+
applying effects — so a poison mutation is acknowledged and skipped rather than
|
|
488
|
+
wedging the queue: mutations behind it still apply, and the client drops the
|
|
489
|
+
optimistic effect on the next rebase with no app code. A `BackendError` keeps
|
|
490
|
+
today's transient behaviour (abort the batch, client retries).
|
|
491
|
+
|
|
492
|
+
- `Backend.ApplyResult` gains an optional `rejected` list; `PushResponse`
|
|
493
|
+
gains an additive optional `rejected` field (older clients ignore it, and the
|
|
494
|
+
per-mutation outcome surface #36 reads it).
|
|
495
|
+
- `MemoryBackend` rolls a rejected mutation's staged writes back (snapshot),
|
|
496
|
+
`SqlBackend` via a per-mutation `SAVEPOINT`/`ROLLBACK TO SAVEPOINT`, and
|
|
497
|
+
`JsonApiBackend` classifies HTTP 4xx (except 408/429) as permanent rejections.
|
|
498
|
+
- `BackendContract.verifyBackendContract` gains a rule covering both failure
|
|
499
|
+
classes.
|
|
500
|
+
- `CONTEXT.md` documents the "transient failure" vs "permanent rejection"
|
|
501
|
+
glossary entries.
|
|
502
|
+
|
|
503
|
+
- 6175e38: Predicate-aware fan-out matching. A committed write now pokes a client group
|
|
504
|
+
only when some hinted change plausibly affects one of its subscriptions,
|
|
505
|
+
rather than every group whose subscriptions touch the hinted collection.
|
|
506
|
+
|
|
507
|
+
Change hints gain an optional, wire-compatible `values` field carrying the
|
|
508
|
+
current row values of the PUTs in a change (`MemoryBackend`, `SqlBackend` and
|
|
509
|
+
`JsonApiBackend` populate it best-effort; a backend that can't cheaply produce
|
|
510
|
+
a value omits it). An old, value-less hint decodes and behaves exactly as
|
|
511
|
+
before. The `SyncServer` fan-out loop matches a value-carrying PUT against the
|
|
512
|
+
subscription's predicate, a delete against the group's last-synced CVR, and
|
|
513
|
+
falls back to the collection-level match for value-less hints and `Exists`
|
|
514
|
+
predicates (soundness first — over-matching is safe, under-matching would
|
|
515
|
+
delay convergence). A new `legation_pokes_suppressed_total` metric counts the
|
|
516
|
+
pokes predicate matching avoids.
|
|
517
|
+
|
|
518
|
+
- 49fd066: A pull now carries the permanent rejections its watermark covers, so a client whose push response was lost settles the rejected mutation as rejected, not applied (#365). Before, the pull's watermark acknowledged the rejected id as applied, and its effect was dropped with no way to learn the outcome.
|
|
519
|
+
|
|
520
|
+
`PullResponse` gains an optional `rejected` list (`{ clientId, mutationId, reason }`), as `PushResponse` already has (#355). The client settles those entries before the watermark acknowledges them. The field is additive: an older client ignores it, and a pull from an older server omits it, so no protocol version changes.
|
|
521
|
+
|
|
522
|
+
`Backend` gains a required `rejections(context)` method, which lists the group's remembered rejections with their reasons (Backend guarantee 4). A custom backend must implement it before it upgrades. `MemoryBackend`, `SqlBackend` and `JsonApiBackend` already remember their rejections and implement it. `BackendContract` rule 13 checks that the list agrees with the reported outcome.
|
|
523
|
+
|
|
524
|
+
- 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.
|
|
525
|
+
|
|
526
|
+
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.
|
|
527
|
+
|
|
528
|
+
- 9d2f21b: Rename the project to **Legation**. Every package moves to the `@legation`
|
|
529
|
+
scope: the engine is now `@legation/effect` (formerly
|
|
530
|
+
`@thomasfosterau/effect-sync-core`), and the adapters/bindings are
|
|
531
|
+
`@legation/browser`, `@legation/node`, `@legation/jsonapi`, `@legation/sql`,
|
|
532
|
+
and `@legation/svelte` (formerly `@thomasfosterau/effect-sync-*`).
|
|
533
|
+
|
|
534
|
+
Runtime identifiers follow the brand, which is **breaking for persisted local
|
|
535
|
+
state and dashboards**:
|
|
536
|
+
|
|
537
|
+
- `Context.Service` identifiers: `@thomasfosterau/effect-sync-core/*` →
|
|
538
|
+
`@legation/effect/*` (and per-package equivalents)
|
|
539
|
+
- Client-store key prefix: `effect-sync/` → `legation/`; CVR store prefix:
|
|
540
|
+
`effect-sync/cvr/` → `legation/cvr/`. Existing IndexedDB/KV state under the
|
|
541
|
+
old prefix is orphaned — clients rehydrate via a full pull; export any
|
|
542
|
+
pending journal before upgrading offline clients.
|
|
543
|
+
- Metrics: `effect_sync_*` → `legation_*`; spans: `effect-sync.*` →
|
|
544
|
+
`legation.*`
|
|
545
|
+
- Default BroadcastChannel name: `effect-sync` → `legation`; Redis hint
|
|
546
|
+
stream: `effect-sync:hints` → `legation:hints`; cache metadata header:
|
|
547
|
+
`x-effect-sync-cache-meta` → `x-legation-cache-meta`
|
|
548
|
+
|
|
549
|
+
- f4a1a60: Schema constants are named after the domain type they decode, with a
|
|
550
|
+
same-named type beside each: `Protocol.MutationSchema` is now
|
|
551
|
+
`Protocol.Mutation` (value and type), and likewise `PushRequest`,
|
|
552
|
+
`PushResponse`, `RejectedMutation`, `PatchOp`, `NamedSubscription`,
|
|
553
|
+
`PullRequest`, `PullResponse`, `ReadRequest`, `ReadResponse` and `Poke`;
|
|
554
|
+
`Subset.Subset` and `Subset.OrderBy`; `ClientStore.StoredRow`;
|
|
555
|
+
`Hydration.Snapshot` and `Hydration.SnapshotRow`; `Cvr.WireCvr`;
|
|
556
|
+
`MirrorProtocol.EdgeSelect` and `MirrorProtocol.MutationStateEntry` (was
|
|
557
|
+
`MutationStateSchema`); the JSON:API `JsonApiSchema.Links`, `LinkObject`,
|
|
558
|
+
`ResourceIdentifier`, `Relationship`, `ResourceObject`, `JsonApiError` and
|
|
559
|
+
`JsonApiDocument`; and the Cloudflare `RoomProtocol.ShardPoke`, `Register` and
|
|
560
|
+
`Publish`. `Derive.CreateArgs` / `UpdateArgs` / `DeleteArgs`, the room
|
|
561
|
+
protocol's bodies and `Hydration.SnapshotFromJson` gain their matching types.
|
|
562
|
+
- 16889f0: Rename every package from the `@legation/*` scope to
|
|
563
|
+
`@thomasfosterau/legation-*` (#303): `@legation/effect` is now
|
|
564
|
+
`@thomasfosterau/legation`, and likewise for `browser`, `cloudflare`,
|
|
565
|
+
`devtools`, `jsonapi`, `node`, `sql` and `svelte`. Subpath entries keep their
|
|
566
|
+
paths (`@thomasfosterau/legation/internal`,
|
|
567
|
+
`@thomasfosterau/legation-svelte/kit`,
|
|
568
|
+
`@thomasfosterau/legation-devtools/svelte`). Update imports and dependency
|
|
569
|
+
names accordingly; no `@legation/*` name was ever published to npm.
|
|
570
|
+
|
|
571
|
+
`Context.Service` identifiers and `Symbol.for` keys follow the package name
|
|
572
|
+
(`@legation/effect/Client` → `@thomasfosterau/legation/Client`, and
|
|
573
|
+
per-package equivalents). The reserved identity sentinels
|
|
574
|
+
(`@legation/anonymous`, `@legation/unstamped`), the `legation/` client-store
|
|
575
|
+
prefixes, metric and span names are unchanged.
|
|
576
|
+
|
|
577
|
+
- 9a6426b: Sync-loop scheduling (#33). `ClientEngine` grows a configurable scheduler on
|
|
578
|
+
`runSync`, driven by `Effect.Schedule` and the existing `statusChanges` seam:
|
|
579
|
+
|
|
580
|
+
- `syncLoop.pushDebounce` auto-pushes after `mutate`/`mutateEncoded`, debounced
|
|
581
|
+
so a burst of mutations coalesces into a single push.
|
|
582
|
+
- `syncLoop.pullInterval` pulls periodically as the correctness backstop, so a
|
|
583
|
+
client converges over pull alone even without pokes.
|
|
584
|
+
- The baseline triggers (pull on poke, push+pull on reconnect) are retained,
|
|
585
|
+
and every scheduling fibre is scoped — interrupted when the engine scope
|
|
586
|
+
closes.
|
|
587
|
+
|
|
588
|
+
`@thomasfosterau/legation-browser` adds `BrowserConnectivity.withBrowserConnectivity`, which
|
|
589
|
+
layers `navigator.onLine` + `online`/`offline` + `visibilitychange` onto any
|
|
590
|
+
`ClientTransport` so `statusChanges` becomes truthful for carriers (like the RPC
|
|
591
|
+
transport) that otherwise report themselves permanently connected, and a
|
|
592
|
+
return-to-visible surfaces a poke.
|
|
593
|
+
|
|
594
|
+
`@thomasfosterau/legation-jsonapi` `JsonApiSync` now configures the core loop (`pullInterval`,
|
|
595
|
+
`pushDebounce` options, 5-second default interval) instead of running its own
|
|
596
|
+
`Effect.forever` poll; `JsonApiSync.start` just runs `runSync`.
|
|
597
|
+
|
|
598
|
+
- 1b8b120: URL-keyed subscriptions over the mirror channel (ADR-0029 section 2, issue #159).
|
|
599
|
+
|
|
600
|
+
- `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)`.
|
|
601
|
+
- 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.
|
|
602
|
+
- 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).
|
|
603
|
+
- 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.
|
|
604
|
+
- 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.
|
|
605
|
+
- `Query.tryPlan(nodes, read)` is `Query.plan` with its `QueryError` as a typed failure.
|
|
606
|
+
|
|
607
|
+
### Patch Changes
|
|
608
|
+
|
|
609
|
+
- 1ea4723: Add `JsonApiQuery.querySchema(nodes, collection)` and
|
|
610
|
+
`JsonApiQuery.singleRowSchema(nodes, collection)`: the query codecs of a
|
|
611
|
+
collection's `list` and `get` endpoints, as the node graph declares them —
|
|
612
|
+
the codecs `parseParams` and `singleRowParams` decode with — for
|
|
613
|
+
`Endpoint.list(R, { query })` / `Endpoint.get(R, { query })`, so an
|
|
614
|
+
effect-jsonapi endpoint served by `RestHandlers` declares the grammar the
|
|
615
|
+
handler parses (the key-field spelling of a relationship filter,
|
|
616
|
+
`filter[categoryId]`, and `sort=id`).
|
|
617
|
+
- a053736: `BackendContract.verifyBackendContract` probes write-then-`BackendError` (#343).
|
|
618
|
+
A new contract mutator, `BackendContract.writeThenFail` (`contract/write-then-fail`),
|
|
619
|
+
writes a row and then fails transiently. The new rule 10 checks that the row keeps
|
|
620
|
+
its value and version, no new row appears, and `lastMutationId` is not bumped, so
|
|
621
|
+
the client's retry applies the mutation again. Guarantee 1 (`Backend.ts`) requires
|
|
622
|
+
this, and `failAlways` does not probe it because it writes nothing first.
|
|
623
|
+
|
|
624
|
+
Visibility before the bump is documented in `BackendContract` as a per-backend
|
|
625
|
+
obligation, not a generic rule: the harness cannot observe a backend mid-mutation.
|
|
626
|
+
|
|
627
|
+
The unknown-mutator `BackendError` that `MemoryBackend`, `SqlBackend` and
|
|
628
|
+
`JsonApiBackend` raise now carries a typed cause, built by `UnknownMutator.make`
|
|
629
|
+
(`@thomasfosterau/legation/internal`). Rule 10 and rule 4 (`contract/fail`, #382)
|
|
630
|
+
recognise an unregistered mutator by that cause, so a backend that words the message
|
|
631
|
+
differently is still caught. The exact message `Unknown mutator: <name>` stays as a
|
|
632
|
+
fallback for a third-party backend that does not use the helper yet. Rule 4 now also
|
|
633
|
+
needs the probe to fail with a `BackendError`: a `failAlways` that dies with a defect
|
|
634
|
+
fails the rule instead of passing it.
|
|
635
|
+
|
|
636
|
+
**Action required:** backend authors who run `verifyBackendContract` (imported from
|
|
637
|
+
`@thomasfosterau/legation/internal`) must register `BackendContract.failAlways`
|
|
638
|
+
and `BackendContract.writeThenFail`. The second is a handler that writes a row and then
|
|
639
|
+
fails with `BackendError`, called with args `{ collection, id }`. An unregistered mutator
|
|
640
|
+
fails rule 4 or rule 10. `BackendContract.memoryHandlers` already includes both. A
|
|
641
|
+
non-transactional executor (D1-style) fails rule 10 by design, since each statement
|
|
642
|
+
commits on its own (#342), so it is not run through this contract.
|
|
643
|
+
|
|
644
|
+
- 5937efe: Add the relational `Predicate.exists` node: a correlated relationship test
|
|
645
|
+
(this row matches if a row exists in another collection whose `where` holds and
|
|
646
|
+
whose `foreignField` equals this row's `localField`). Backends resolve it to an
|
|
647
|
+
allow-set with `Predicate.resolveExists` before evaluation — MemoryBackend over
|
|
648
|
+
its rows, the SQL backend via a `SELECT DISTINCT` subquery (the `Sql` translator
|
|
649
|
+
also compiles `Exists` to a correlated `IN (SELECT …)`). This closes the deeper
|
|
650
|
+
parts of the relational-query and relationship-scoped-auth gaps: server-side
|
|
651
|
+
joins and "visible if you're a member of the project" auth **without
|
|
652
|
+
denormalizing** the join key onto the row. The JSON:API backend can't resolve
|
|
653
|
+
correlated joins over REST and fails closed on `Exists`.
|
|
654
|
+
- 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.
|
|
655
|
+
- d1b01a4: Tested against `effect` 4.0.0 (and the matching `@effect/*` companions); the
|
|
656
|
+
peer floor is unchanged at `effect >=4.0.0-rc.118`.
|
|
657
|
+
|
|
658
|
+
`@thomasfosterau/legation` adapts to 4.0.0's stricter, type-only `Schema.brand`:
|
|
659
|
+
|
|
660
|
+
- `Node.Id` keeps its signature and runtime behaviour; its internal brand
|
|
661
|
+
identifier is cast to satisfy the new concrete-identifier check.
|
|
662
|
+
- `Node.AnyNode`'s `Id` property is now
|
|
663
|
+
`Schema.Codec<string & Brand.Brand<string>, string>` instead of `Id<string>`.
|
|
664
|
+
4.0.0's `brand<String, B>` is type-only and no longer assignable to a wider
|
|
665
|
+
``brand<String, `${string}/id`>``, so no concrete `Node<"todos", …>` was
|
|
666
|
+
assignable to `AnyNode`. Every concrete node is assignable again, and `Id`
|
|
667
|
+
is still a string-encoded branded id schema; only code that reads `.Id` off
|
|
668
|
+
an `AnyNode`-typed value sees the wider brand.
|
|
669
|
+
|
|
670
|
+
Checked the 4.0.0 behavioural changes against the packages and found no impact:
|
|
671
|
+
no use of `partition`/`separate`/`Option.partitionMap`, `Queue.State`,
|
|
672
|
+
`MessageStorage` or `RpcMessage.ExitEncoded`. The RPC client's missed-pong
|
|
673
|
+
handling now fails in-flight WebSocket calls rather than reporting a transient
|
|
674
|
+
error; `RpcTransport.fromOrigin` already treats a failed WebSocket call as a
|
|
675
|
+
`TransportError` and falls back to HTTP. The RPC server's WebSocket route now
|
|
676
|
+
closes with 1000/1001/1011, which nothing in the packages inspects.
|
|
677
|
+
|
|
678
|
+
- a874acb: Bump the pinned Effect version from `4.0.0-beta.75` to `4.0.0-beta.83` across all
|
|
679
|
+
packages (no engine API changes; build, typecheck, and the full test suite pass
|
|
680
|
+
unchanged).
|
|
681
|
+
|
|
682
|
+
The version is now centralised in a root **Bun catalog**
|
|
683
|
+
(`package.json` → `workspaces.catalog`): every package's `devDependencies`
|
|
684
|
+
reference `effect`/`@effect/platform-*`/`@effect/vitest` as `catalog:`, while the
|
|
685
|
+
published `peerDependencies` keep the concrete `4.0.0-beta.83` string. The
|
|
686
|
+
Every example is a workspace member so they share the same `effect` version
|
|
687
|
+
as the engine instead of pinning its own.
|
|
688
|
+
|
|
689
|
+
- c5d3484: README: the `effect` floor (`4.0.0-rc.118`) is now tested on every pull
|
|
690
|
+
request, where CI runs the test suite against it, instead of in a weekly
|
|
691
|
+
canary. The peer range (`effect >=4.0.0-rc.118`) and the tested pin (`4.0.0`)
|
|
692
|
+
are unchanged.
|
|
693
|
+
- ea200ab: Depend on `@thomasfosterau/effect-jsonapi` `^0.17.0`. A document whose read carries `include` now has an `included` member even when nothing resolved (`included: []`), as JSON:API 1.1 requires of a compound document; a read without `include` still omits it. An effect-jsonapi 0.17 endpoint answers 400 to a `filter[…]` its query schema doesn't declare, so an endpoint served by `RestHandlers` must declare the handler's grammar with `Endpoint.list(R, { query: JsonApiQuery.querySchema(nodes, collection) })`, as documented.
|
|
694
|
+
- e9363c0: Peer `effect ^4.0.0` (was `>=4.0.0-rc.118`), and `@effect/platform-node` /
|
|
695
|
+
`@effect/platform-browser` `^4.0.0` likewise, per the peer-range policy: the
|
|
696
|
+
range is raised only when a dependency requires it. The range form changes
|
|
697
|
+
(the caret no longer admits the 4.0 release candidates or Effect 5); every
|
|
698
|
+
package's tests pass at `effect` 4.0.0.
|
|
699
|
+
|
|
700
|
+
`@thomasfosterau/legation-svelte` peers `effect ^4.0.2`: its dependency
|
|
701
|
+
`@thomasfosterau/effect-svelte` 0.6.0 peers `effect >=4.0.2`.
|
|
702
|
+
|
|
703
|
+
- 6c2c802: Close the sync tier's filter surface with a per-node **filterable set** (ADR-0028
|
|
704
|
+
rule d, #154).
|
|
705
|
+
|
|
706
|
+
- `Node.make` gains `filterable` (per attribute: the operators a client may use
|
|
707
|
+
— `eq ne lt lte gt gte in nin isnull` — optionally with an explicit literal
|
|
708
|
+
kind) and `sortable`. The resolved declaration is on `Node.filterable` /
|
|
709
|
+
`Node.sortable` and travels onto the derived `Collection` like `indexes`, so
|
|
710
|
+
the server enforces it without Node definitions.
|
|
711
|
+
- Defaults: `id` and every `one`-edge `via` field are filterable by `eq` / `in`
|
|
712
|
+
(kind `text`; a declaration naming a key field widens it, never replaces
|
|
713
|
+
it); `sortable` always includes `id`, and `Node.registry` adds every
|
|
714
|
+
paginated edge's `order` fields to its target's `sortable`, since the
|
|
715
|
+
per-parent edge windows sort by them. The literal kind of a declared
|
|
716
|
+
attribute is derived from its schema's encoded form (`String` → `text`,
|
|
717
|
+
`Number` → `double`, `Schema.Int` → `integer`, `Boolean` → `boolean`); an
|
|
718
|
+
`instant` must be declared explicitly. A declaration naming a field the row
|
|
719
|
+
does not carry, or an attribute whose kind cannot be derived, throws a
|
|
720
|
+
`FilterDeclarationError` at definition time.
|
|
721
|
+
- `Authorization.policy(filters, { collections })` carries the declarations;
|
|
722
|
+
`authorize` checks every client-authored `where` / `orderBy` against them
|
|
723
|
+
**before** the auth filter is AND-ed in and before `Backend.evaluate` runs:
|
|
724
|
+
an undeclared field, a declared field with an undeclared operator, a literal
|
|
725
|
+
of the wrong kind, a client-supplied `Exists`, or an unsortable `orderBy`
|
|
726
|
+
fails the pull or read with an `AuthorizationError` naming the collection,
|
|
727
|
+
field and operator. Today's wire forms `Not(In)` and a null-literal
|
|
728
|
+
`Compare` are checked as `nin` and `isnull`. The check runs on the request
|
|
729
|
+
as the client sent it: the auth filter's own predicate, what a
|
|
730
|
+
`SyncMiddleware` rewrites in, and named-query resolver output are
|
|
731
|
+
server-authored and exempt. A policy built without `collections` keeps the
|
|
732
|
+
previous open behaviour.
|
|
733
|
+
- `NodeBridge.fromResource` / `registry` accept `filterable` / `sortable` per
|
|
734
|
+
resource until effect-jsonapi carries the per-attribute annotation.
|
|
735
|
+
|
|
736
|
+
- 74d8699: `JsonApiBackend` no longer emits a stale value in a push's change hint for a row that a later mutation of the same push deleted. A create or update in an earlier mutation left its value in the batch's hint values after a later mutation deleted the row, on both the per-resource and the atomic-operations paths, so the hint carried a row that no longer exists. The delete now removes the row from the batch's values, as a delete within one push already does in the other backends.
|
|
737
|
+
- 96702f4: `JsonApiBackend` change hints now carry `oldValues`: each row an update or
|
|
738
|
+
delete touches is hinted with its value before the push, as `MemoryBackend`
|
|
739
|
+
does, so topic-derived fan-out (ADR-0023) pokes the windows a row moved away
|
|
740
|
+
from. A REST API keeps no change token, so the old value is read: each update
|
|
741
|
+
or delete makes one `GET /{type}/{id}` before its write, once per row per push,
|
|
742
|
+
on both the per-resource and the atomic path. That is one extra request per
|
|
743
|
+
update and delete. A row the push creates has no old value. A read that fails
|
|
744
|
+
fails the push, transiently or refused: a 404 or other 4xx included, so no update
|
|
745
|
+
or delete is sent without its old value. On the atomic path nothing has been
|
|
746
|
+
written by then, so the retry reads again. On the per-resource path the writes
|
|
747
|
+
before the failed read have already applied, so their hints are lost, as with any
|
|
748
|
+
transient write failure there.
|
|
749
|
+
|
|
750
|
+
A create or update's hinted value is now the server's returned document whenever
|
|
751
|
+
it sends one (the per-resource path used to ignore it), else the row read before
|
|
752
|
+
the push merged with the write, so a partial PATCH hints the full row.
|
|
753
|
+
|
|
754
|
+
Fix: a hinted create or update's relationships were the request's raw JSON:API
|
|
755
|
+
`{ data }` form. They now use the linkage shape `toRow` gives a server document
|
|
756
|
+
(`{ type, id }`, an array, or `null`), the shape listings and stored rows use, so
|
|
757
|
+
a hint matches the row it describes.
|
|
758
|
+
|
|
759
|
+
- 4230ed5: `JsonApiBackend` keeps its row cache current with its own writes, so `getRows` no longer serves a row a push has updated or deleted at its pre-push value. After an applied create, update or delete, the cache holds the row's value after the write (the server's returned document when it sends one), or no row after a delete. Before this, a row a listing had cached was served from the cache until the next listing.
|
|
760
|
+
- f1d0232: A per-resource push (no `atomic` endpoint) now refuses a mutation-id gap with a `BackendError` before it sends any write, as the atomic path already did. Before, a push could acknowledge a later mutation while an earlier one was never applied.
|
|
761
|
+
- 159997a: `JsonApiBackend` now fails the push when the pre-write read of an update or
|
|
762
|
+
delete does not return a row. A 4xx, a 404 included, fails the push the same way
|
|
763
|
+
a 5xx, timeout or transport error does: a `BackendError`, with nothing written on
|
|
764
|
+
the atomic path. A 2xx whose document names no resource fails it too. Before, a
|
|
765
|
+
refused read went ahead with no `oldValues` for that row. Every update or delete
|
|
766
|
+
now carries its old value or is not sent. A row the push creates is not read
|
|
767
|
+
unless the server refuses that create, so a 404 on it cannot fail an applied
|
|
768
|
+
create.
|
|
769
|
+
|
|
770
|
+
The cost is accepted: a row the server refuses to read stalls that client's
|
|
771
|
+
queue until the read can succeed or the client is reset. Deleting a row that is
|
|
772
|
+
already gone therefore fails with a 404 on every retry, where it used to be
|
|
773
|
+
acknowledged as a no-op. The failure reaches the client as a `TransportError`,
|
|
774
|
+
is retried by the client's retry policy, then surfaces as a `SyncFailure` with
|
|
775
|
+
`operation: "push"`. The in-session ledger belongs to one backend instance, so
|
|
776
|
+
after a restart an already-applied delete is sent again and stalls the same way.
|
|
777
|
+
A create the server refuses for a reason other than a conflict (a 422, say)
|
|
778
|
+
stalls the queue the same way when a later update or delete in the same push
|
|
779
|
+
touches that row, since the row is absent and its read finds a 404.
|
|
780
|
+
|
|
781
|
+
On the atomic path, a create the server refuses, followed by an update or delete
|
|
782
|
+
of the same row in the same batch, now reads that row once the refusal arrives,
|
|
783
|
+
before the batch is resent, so the later write carries the row's pre-push value.
|
|
784
|
+
The read follows the same rule as any other: a failed read fails the push with
|
|
785
|
+
nothing written.
|
|
786
|
+
|
|
787
|
+
A resource that receives updates or deletes but has no `getOne` route now fails
|
|
788
|
+
any push containing one of those writes (one whose args decode) before any write
|
|
789
|
+
in that push is sent, instead of logging a warning and proceeding without
|
|
790
|
+
`oldValues`. A mutation whose args do not decode is still refused at its turn, as
|
|
791
|
+
before: on the per-resource path the writes before it are delivered.
|
|
792
|
+
|
|
793
|
+
- 3be5abc: `JsonApiBackend` remembers the permanent rejections its in-session ledger has made, so a replayed copy of a rejected mutation lists it in `rejected` with the same reason, as Backend guarantee 1 requires (#366). Before, the copy was skipped with no outcome, so a client whose push response was lost settled the rejected mutation as applied. A push forgets the rejections below its lowest id per client, as `MemoryBackend` does. The ledger is in-session, so the memory lasts as long as the backend instance.
|
|
794
|
+
- b4a9656: Correct the account of what effect-jsonapi 0.15 changed in URL parsing (the
|
|
795
|
+
`effect-rc-floor` changeset mentioned only `filter`). Every comma-list query
|
|
796
|
+
parameter the façade, `JsonApiQuery.parseRead` and the REST handlers decode
|
|
797
|
+
now accepts the repeated-key spelling instead of answering 400: the shorthand
|
|
798
|
+
`filter[f]`, `sort`, and `fields[TYPE]`. Each repeated
|
|
799
|
+
form, and any mix of repeated and comma forms, decodes to the same read as
|
|
800
|
+
the single comma list (`?sort=-name&sort=id` ≡ `?sort=-name,id`,
|
|
801
|
+
`?fields[products]=name&fields[products]=description` ≡
|
|
802
|
+
`?fields[products]=name,description`). Positions that take exactly one value still reject a repeated key with a 400:
|
|
803
|
+
`page[...]`, a scalar operator such as `filter[f][gt]`, and the group form's
|
|
804
|
+
`[conjunction]` / `[path]` / `[operator]`. Encoding is unchanged: always the canonical comma
|
|
805
|
+
form.
|
|
806
|
+
- a1cea1c: Share the rejection-memory forgetting bound. `floorsOf` (a push's lowest mutation
|
|
807
|
+
id per client) and `forgetBelow` (drop a client's remembered rejections below its
|
|
808
|
+
floor) now live in a new `RejectionMemory` module, exported on
|
|
809
|
+
`@thomasfosterau/legation/internal` for adapter authors. `MemoryBackend` and
|
|
810
|
+
`JsonApiBackend` bound their rejection memory with `forgetBelow`; `SqlBackend` shares
|
|
811
|
+
only `floorsOf` and keeps its own forgetting loop. No behaviour changes.
|
|
812
|
+
- 507ccb1: Every tagged error is now a branded `Schema.TaggedError` rather than a
|
|
813
|
+
`Data.TaggedError`: `StoreError`, `TransportError`, `QueryError` and the rest
|
|
814
|
+
of the engine's errors, the JSON:API adapter's `SyncError` / `NetworkError` /
|
|
815
|
+
`ParseError` / `ApiError` / `ConflictError` / `ResourceBridgeError`, and
|
|
816
|
+
`HostNotReady`. Construction, `_tag`, `message` and `Effect.catchTag` are
|
|
817
|
+
unchanged; each class now carries a schema and a nominal brand, so a
|
|
818
|
+
structurally similar object no longer type-checks as one.
|
|
819
|
+
- 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.
|
|
820
|
+
- 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.
|
|
821
|
+
- Updated dependencies [2a1f63c]
|
|
822
|
+
- Updated dependencies [ef59fa7]
|
|
823
|
+
- Updated dependencies [56a2e51]
|
|
824
|
+
- Updated dependencies [d09dc65]
|
|
825
|
+
- Updated dependencies [db2366a]
|
|
826
|
+
- Updated dependencies [97db489]
|
|
827
|
+
- Updated dependencies [cc67999]
|
|
828
|
+
- Updated dependencies [a94df19]
|
|
829
|
+
- Updated dependencies [d76b04d]
|
|
830
|
+
- Updated dependencies [6f53fb1]
|
|
831
|
+
- Updated dependencies [d675d2e]
|
|
832
|
+
- Updated dependencies [1743c2b]
|
|
833
|
+
- Updated dependencies [1c56914]
|
|
834
|
+
- Updated dependencies [615e792]
|
|
835
|
+
- Updated dependencies [c8d9afc]
|
|
836
|
+
- Updated dependencies [4709f41]
|
|
837
|
+
- Updated dependencies [f0348b4]
|
|
838
|
+
- Updated dependencies [3e29e1b]
|
|
839
|
+
- Updated dependencies [a053736]
|
|
840
|
+
- Updated dependencies [8a8adfc]
|
|
841
|
+
- Updated dependencies [8995115]
|
|
842
|
+
- Updated dependencies [8995115]
|
|
843
|
+
- Updated dependencies [87fa7a5]
|
|
844
|
+
- Updated dependencies [822001c]
|
|
845
|
+
- Updated dependencies [4c59860]
|
|
846
|
+
- Updated dependencies [961e604]
|
|
847
|
+
- Updated dependencies [5937efe]
|
|
848
|
+
- Updated dependencies [08ac9cd]
|
|
849
|
+
- Updated dependencies [1fc3725]
|
|
850
|
+
- Updated dependencies [8995115]
|
|
851
|
+
- Updated dependencies [8a8adfc]
|
|
852
|
+
- Updated dependencies [025fabb]
|
|
853
|
+
- Updated dependencies [0b8fccb]
|
|
854
|
+
- Updated dependencies [e7369e1]
|
|
855
|
+
- Updated dependencies [8995115]
|
|
856
|
+
- Updated dependencies [c856d66]
|
|
857
|
+
- Updated dependencies [01a8c64]
|
|
858
|
+
- Updated dependencies [3362c37]
|
|
859
|
+
- Updated dependencies [62dcb1c]
|
|
860
|
+
- Updated dependencies [be4be06]
|
|
861
|
+
- Updated dependencies [ae18cb3]
|
|
862
|
+
- Updated dependencies [be0a44d]
|
|
863
|
+
- Updated dependencies [cc2f5b7]
|
|
864
|
+
- Updated dependencies [e818dc4]
|
|
865
|
+
- Updated dependencies [d1b01a4]
|
|
866
|
+
- Updated dependencies [5496ddf]
|
|
867
|
+
- Updated dependencies [19eb4b0]
|
|
868
|
+
- Updated dependencies [a874acb]
|
|
869
|
+
- Updated dependencies [c5d3484]
|
|
870
|
+
- Updated dependencies [38c3147]
|
|
871
|
+
- Updated dependencies [e9363c0]
|
|
872
|
+
- Updated dependencies [fed4634]
|
|
873
|
+
- Updated dependencies [fcc52eb]
|
|
874
|
+
- Updated dependencies [454e75c]
|
|
875
|
+
- Updated dependencies [2003cd6]
|
|
876
|
+
- Updated dependencies [52c614f]
|
|
877
|
+
- Updated dependencies [94d6327]
|
|
878
|
+
- Updated dependencies [91f07db]
|
|
879
|
+
- Updated dependencies [6e9e119]
|
|
880
|
+
- Updated dependencies [6c2c802]
|
|
881
|
+
- Updated dependencies [ebcc6aa]
|
|
882
|
+
- Updated dependencies [a1b95d4]
|
|
883
|
+
- Updated dependencies [c105fbf]
|
|
884
|
+
- Updated dependencies [e1a028a]
|
|
885
|
+
- Updated dependencies [dfb8759]
|
|
886
|
+
- Updated dependencies [9260efb]
|
|
887
|
+
- Updated dependencies [0644af5]
|
|
888
|
+
- Updated dependencies [4b05d55]
|
|
889
|
+
- Updated dependencies
|
|
890
|
+
- Updated dependencies [82911cc]
|
|
891
|
+
- Updated dependencies [9c9f2b0]
|
|
892
|
+
- Updated dependencies [7b21d11]
|
|
893
|
+
- Updated dependencies [6f54de8]
|
|
894
|
+
- Updated dependencies [ab1fb4e]
|
|
895
|
+
- Updated dependencies [792d8f5]
|
|
896
|
+
- Updated dependencies [a3b2ee4]
|
|
897
|
+
- Updated dependencies [eb9555a]
|
|
898
|
+
- Updated dependencies [c0ab703]
|
|
899
|
+
- Updated dependencies [8c65627]
|
|
900
|
+
- Updated dependencies [26ad3f3]
|
|
901
|
+
- Updated dependencies [d7cafd2]
|
|
902
|
+
- Updated dependencies [cb0a33d]
|
|
903
|
+
- Updated dependencies [912d95d]
|
|
904
|
+
- Updated dependencies [7290d2b]
|
|
905
|
+
- Updated dependencies [41c33e3]
|
|
906
|
+
- Updated dependencies [a77261c]
|
|
907
|
+
- Updated dependencies [c47786c]
|
|
908
|
+
- Updated dependencies [501b12c]
|
|
909
|
+
- Updated dependencies [7f1efac]
|
|
910
|
+
- Updated dependencies [a04918b]
|
|
911
|
+
- Updated dependencies [cf0b26c]
|
|
912
|
+
- Updated dependencies [ddb1fda]
|
|
913
|
+
- Updated dependencies [f54f568]
|
|
914
|
+
- Updated dependencies [fc880dc]
|
|
915
|
+
- Updated dependencies [9cbd160]
|
|
916
|
+
- Updated dependencies [0bebedf]
|
|
917
|
+
- Updated dependencies [bc4c599]
|
|
918
|
+
- Updated dependencies [20a6e18]
|
|
919
|
+
- Updated dependencies [b4d2b80]
|
|
920
|
+
- Updated dependencies [2201b4c]
|
|
921
|
+
- Updated dependencies [0cdefe7]
|
|
922
|
+
- Updated dependencies [66f3b5b]
|
|
923
|
+
- Updated dependencies [7e11e61]
|
|
924
|
+
- Updated dependencies [6175e38]
|
|
925
|
+
- Updated dependencies [caf4761]
|
|
926
|
+
- Updated dependencies [821b10d]
|
|
927
|
+
- Updated dependencies [9e74a83]
|
|
928
|
+
- Updated dependencies [49fd066]
|
|
929
|
+
- Updated dependencies [06405ed]
|
|
930
|
+
- Updated dependencies [e7f4871]
|
|
931
|
+
- Updated dependencies [916d91b]
|
|
932
|
+
- Updated dependencies [47f4e15]
|
|
933
|
+
- Updated dependencies [f829146]
|
|
934
|
+
- Updated dependencies [13658a7]
|
|
935
|
+
- Updated dependencies [31cfb2c]
|
|
936
|
+
- Updated dependencies [a1cea1c]
|
|
937
|
+
- Updated dependencies [7e94abc]
|
|
938
|
+
- Updated dependencies [4fbd0b7]
|
|
939
|
+
- Updated dependencies [9d2f21b]
|
|
940
|
+
- Updated dependencies [3016170]
|
|
941
|
+
- Updated dependencies [f4a1a60]
|
|
942
|
+
- Updated dependencies [507ccb1]
|
|
943
|
+
- Updated dependencies [c671ccf]
|
|
944
|
+
- Updated dependencies [16889f0]
|
|
945
|
+
- Updated dependencies [fb2a992]
|
|
946
|
+
- Updated dependencies [22f320d]
|
|
947
|
+
- Updated dependencies [652ae7b]
|
|
948
|
+
- Updated dependencies [83bcf76]
|
|
949
|
+
- Updated dependencies [20e57d3]
|
|
950
|
+
- Updated dependencies [f010749]
|
|
951
|
+
- Updated dependencies [8d02113]
|
|
952
|
+
- Updated dependencies [bf9054d]
|
|
953
|
+
- Updated dependencies [e40c73a]
|
|
954
|
+
- Updated dependencies [ca38874]
|
|
955
|
+
- Updated dependencies [27e1040]
|
|
956
|
+
- Updated dependencies [e68eb3e]
|
|
957
|
+
- Updated dependencies [985c4d5]
|
|
958
|
+
- Updated dependencies [3be5abc]
|
|
959
|
+
- Updated dependencies [97db489]
|
|
960
|
+
- Updated dependencies [4a23603]
|
|
961
|
+
- Updated dependencies [f441941]
|
|
962
|
+
- Updated dependencies [a01f43b]
|
|
963
|
+
- Updated dependencies [9a6426b]
|
|
964
|
+
- Updated dependencies [97cc5f9]
|
|
965
|
+
- Updated dependencies [fc880dc]
|
|
966
|
+
- Updated dependencies [e34bc24]
|
|
967
|
+
- Updated dependencies [0470e1c]
|
|
968
|
+
- Updated dependencies [439f449]
|
|
969
|
+
- Updated dependencies [3a88118]
|
|
970
|
+
- Updated dependencies [1b8b120]
|
|
971
|
+
- Updated dependencies [cf48723]
|
|
972
|
+
- Updated dependencies [b4489c5]
|
|
973
|
+
- @thomasfosterau/legation@0.1.0
|