@thomasfosterau/legation-devtools 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 +319 -0
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -3
- package/svelte/DevtoolsPanel.svelte +169 -0
- package/svelte/index.ts +9 -0
- package/svelte/store.ts +93 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# @thomasfosterau/legation-devtools
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fc880dc: Ship `@thomasfosterau/legation-devtools` (#120): a devtools panel over the introspection
|
|
8
|
+
substrate — journal & mutation timeline, subscriptions & collections (with
|
|
9
|
+
row counts and the pull cookie), a sync activity log, and actions (force
|
|
10
|
+
pull/push, a simulated-offline toggle, drop a subscription, and a destructive
|
|
11
|
+
clear-store behind a confirm).
|
|
12
|
+
|
|
13
|
+
Architecture: a framework-agnostic headless core built entirely over
|
|
14
|
+
already-public substrate (`Introspection`, `ClientEngine.mutationStates`,
|
|
15
|
+
`engineEvents`/`syncFailures`, `ClientStore`), with thin per-framework panels
|
|
16
|
+
— `@thomasfosterau/legation-devtools/react` and `@thomasfosterau/legation-devtools/svelte` — as separate,
|
|
17
|
+
lazily-loadable entry points so an app that never imports them pays nothing
|
|
18
|
+
for the panel in production. This is a functional first cut: plain, legible
|
|
19
|
+
markup; visual polish is deliberately out of scope.
|
|
20
|
+
|
|
21
|
+
Scoped to the owned-engine case (an app that constructs its own `ClientEngine`
|
|
22
|
+
|
|
23
|
+
- `ClientStore` in-process); attaching over the mirror seam needs a
|
|
24
|
+
mirror-protocol introspection extension, tracked separately (#136).
|
|
25
|
+
|
|
26
|
+
Demonstrated in `examples/northwind`.
|
|
27
|
+
|
|
28
|
+
- 5496ddf: Bump the pinned Effect version from `4.0.0-beta.84` to `4.0.0-beta.107` across
|
|
29
|
+
all packages (catalog and exact `peerDependencies`), and move
|
|
30
|
+
`@thomasfosterau/legation-jsonapi` and the commerce example from
|
|
31
|
+
`@thomasfosterau/effect-jsonapi@^0.6.0` to `^0.12.0` (#152).
|
|
32
|
+
|
|
33
|
+
**Why beta.107.** effect-jsonapi 0.12 requires `effect >= 4.0.0-beta.104` and
|
|
34
|
+
develops against `4.0.0-beta.107` exactly, so that is the version its typings
|
|
35
|
+
are known to line up with. The later `4.0.0-rc.*` releases were not attempted
|
|
36
|
+
in this change: the exact-pin policy (ADR-0003) prefers the version the
|
|
37
|
+
adopted library is built against, and every `effect/unstable/*` module this
|
|
38
|
+
repo uses (`httpapi`, `http`, `rpc`, `reactivity`, `persistence`, `sql`)
|
|
39
|
+
compiles and tests clean on beta.107.
|
|
40
|
+
|
|
41
|
+
**Effect API drift absorbed (no behavioural change):**
|
|
42
|
+
|
|
43
|
+
- `Schedule.either` / `Schedule.both` are gone; the default retry policy is now
|
|
44
|
+
`Schedule.max([Schedule.min([exponential, spaced]), recurs(5)])`, which is
|
|
45
|
+
the same policy (`min` recurs while any schedule recurs at the shortest
|
|
46
|
+
delay, `max` recurs while all recur at the longest delay).
|
|
47
|
+
- `Schema.TaggedErrorClass` is now `Schema.TaggedError` (the wire
|
|
48
|
+
`SyncRpcError` is unchanged on the wire).
|
|
49
|
+
- `SchemaRepresentation.fromASTs` is now `SchemaRepresentation.toRepresentations`
|
|
50
|
+
(spec export output is unchanged).
|
|
51
|
+
- `HttpApiGroup.Any` is gone and `HttpApi`'s group parameter is now
|
|
52
|
+
invariant, so `JsonApiSync.layer`, `JsonApiSync.inspect` and
|
|
53
|
+
`Convention.reflectResources` are generic over the concrete `HttpApi`
|
|
54
|
+
instead of accepting a widened alias. The exported `JsonApiSync.JsonApi`
|
|
55
|
+
alias is now `HttpApi.Constraint` (identifier and groups erased); pass your
|
|
56
|
+
concrete api to the functions rather than annotating it with the alias.
|
|
57
|
+
|
|
58
|
+
**effect-jsonapi 0.6 → 0.12 at the ADR-0016 boundary.** `Document`, `Query`,
|
|
59
|
+
`Resource`, `Relationship` and `Endpoint` lost no exports; `Query` gained
|
|
60
|
+
`IncludeOption`/`IncludePathsOf`/`OffsetPageStruct`/`bracketPageKeys` and
|
|
61
|
+
`Resource` gained `IncludeDepth`/`IncludePathsTo`. `schema.ts`, `types.ts` and
|
|
62
|
+
`CollectionLoader.ts` compile unchanged. One typing regression was noticed in
|
|
63
|
+
0.12: endpoints with `:id` params (`Endpoint.get`/`delete`/`update`) declare
|
|
64
|
+
their params as `Schema.Struct<Schema.Struct.Fields & { id }>`, which leaks
|
|
65
|
+
`unknown` into the requirements of the corresponding `HttpApiClient` calls.
|
|
66
|
+
The commerce example narrows this with a small `run` helper in
|
|
67
|
+
`server/client.ts`; nothing in the published packages is affected.
|
|
68
|
+
|
|
69
|
+
The bump is `minor` rather than `patch` because the exact `effect` peer pin
|
|
70
|
+
means every consumer must move `effect` in the same change, and the
|
|
71
|
+
`@thomasfosterau/legation-jsonapi` type signatures above changed shape.
|
|
72
|
+
|
|
73
|
+
- fed4634: Peer the Effect v4 release-candidate line through an open floor rather than
|
|
74
|
+
an exact pin (#222), so an app can install `@thomasfosterau/legation-*` beside
|
|
75
|
+
`@thomasfosterau/effect-auth` and `@thomasfosterau/effect-jsonapi` and resolve
|
|
76
|
+
a single copy of `effect`. The published floor is `>=4.0.0-rc.118` (see
|
|
77
|
+
`effect-rc118-floor.md`), on `effect` and on the `@effect/platform-browser` /
|
|
78
|
+
`@effect/platform-node` peers.
|
|
79
|
+
|
|
80
|
+
**`@thomasfosterau/legation-jsonapi`**: a repeated shorthand filter key
|
|
81
|
+
(`?filter[name]=a&filter[name]=b`) is read as the comma list it spells
|
|
82
|
+
(`filter[name]=a,b`, an `In` predicate) rather than rejected with a 400,
|
|
83
|
+
because `@thomasfosterau/effect-jsonapi` accepts the repeated-key spelling at
|
|
84
|
+
every list-valued position.
|
|
85
|
+
|
|
86
|
+
The packages run in browsers and workers and deliberately declare no `engines`
|
|
87
|
+
(ADR-0003).
|
|
88
|
+
|
|
89
|
+
- fcc52eb: Target Effect `4.0.0-rc.118`: every package peers `effect >=4.0.0-rc.118`
|
|
90
|
+
(#261), and the `@effect/platform-browser` / `@effect/platform-node` peers use
|
|
91
|
+
the same floor. Install `effect` and every `@effect/*` companion at
|
|
92
|
+
`4.0.0-rc.118` or later together.
|
|
93
|
+
|
|
94
|
+
The packages import Effect's modules at their rc.118 paths (`effect/rpc`,
|
|
95
|
+
`effect/http`, `effect/http-api`, …); rc.118 removed the `effect/unstable/*`
|
|
96
|
+
export paths, so the packages don't resolve beside an earlier `effect`.
|
|
97
|
+
|
|
98
|
+
**`@thomasfosterau/legation-jsonapi`** depends on `@thomasfosterau/effect-jsonapi@^0.16.1`,
|
|
99
|
+
the first release that peers `effect >=4.0.0-rc.118`.
|
|
100
|
+
|
|
101
|
+
**`@thomasfosterau/legation-svelte`** depends on `@thomasfosterau/effect-svelte@^0.4.0`, for
|
|
102
|
+
the same reason.
|
|
103
|
+
|
|
104
|
+
- 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.
|
|
105
|
+
- 16889f0: Rename every package from the `@legation/*` scope to
|
|
106
|
+
`@thomasfosterau/legation-*` (#303): `@legation/effect` is now
|
|
107
|
+
`@thomasfosterau/legation`, and likewise for `browser`, `cloudflare`,
|
|
108
|
+
`devtools`, `jsonapi`, `node`, `sql` and `svelte`. Subpath entries keep their
|
|
109
|
+
paths (`@thomasfosterau/legation/internal`,
|
|
110
|
+
`@thomasfosterau/legation-svelte/kit`,
|
|
111
|
+
`@thomasfosterau/legation-devtools/svelte`). Update imports and dependency
|
|
112
|
+
names accordingly; no `@legation/*` name was ever published to npm.
|
|
113
|
+
|
|
114
|
+
`Context.Service` identifiers and `Symbol.for` keys follow the package name
|
|
115
|
+
(`@legation/effect/Client` → `@thomasfosterau/legation/Client`, and
|
|
116
|
+
per-package equivalents). The reserved identity sentinels
|
|
117
|
+
(`@legation/anonymous`, `@legation/unstamped`), the `legation/` client-store
|
|
118
|
+
prefixes, metric and span names are unchanged.
|
|
119
|
+
|
|
120
|
+
### Patch Changes
|
|
121
|
+
|
|
122
|
+
- 7b21d11: The `clearStore` action now clears through the running engine (`ClientEngine.clearRows`). It previously cleared the store directly, so the engine's view kept showing the cleared rows until something else invalidated it.
|
|
123
|
+
- 99edbe2: Correct the package description: the devtools ship one framework panel, the Svelte one on `@thomasfosterau/legation-devtools/svelte`. There is no React panel.
|
|
124
|
+
- 40d28f3: Fix the types of the `@thomasfosterau/legation-devtools/svelte` entry in the published
|
|
125
|
+
package. `svelte/store.ts` and `DevtoolsPanel.svelte` (shipped as source)
|
|
126
|
+
imported their `DevtoolsCore` types from `../src/Devtools.js`, which is not in
|
|
127
|
+
the tarball, so a consumer's type check couldn't resolve them. They now import
|
|
128
|
+
them from `@thomasfosterau/legation-devtools`, the package's own root entry.
|
|
129
|
+
- 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.
|
|
130
|
+
- d1b01a4: Tested against `effect` 4.0.0 (and the matching `@effect/*` companions); the
|
|
131
|
+
peer floor is unchanged at `effect >=4.0.0-rc.118`.
|
|
132
|
+
|
|
133
|
+
`@thomasfosterau/legation` adapts to 4.0.0's stricter, type-only `Schema.brand`:
|
|
134
|
+
|
|
135
|
+
- `Node.Id` keeps its signature and runtime behaviour; its internal brand
|
|
136
|
+
identifier is cast to satisfy the new concrete-identifier check.
|
|
137
|
+
- `Node.AnyNode`'s `Id` property is now
|
|
138
|
+
`Schema.Codec<string & Brand.Brand<string>, string>` instead of `Id<string>`.
|
|
139
|
+
4.0.0's `brand<String, B>` is type-only and no longer assignable to a wider
|
|
140
|
+
``brand<String, `${string}/id`>``, so no concrete `Node<"todos", …>` was
|
|
141
|
+
assignable to `AnyNode`. Every concrete node is assignable again, and `Id`
|
|
142
|
+
is still a string-encoded branded id schema; only code that reads `.Id` off
|
|
143
|
+
an `AnyNode`-typed value sees the wider brand.
|
|
144
|
+
|
|
145
|
+
Checked the 4.0.0 behavioural changes against the packages and found no impact:
|
|
146
|
+
no use of `partition`/`separate`/`Option.partitionMap`, `Queue.State`,
|
|
147
|
+
`MessageStorage` or `RpcMessage.ExitEncoded`. The RPC client's missed-pong
|
|
148
|
+
handling now fails in-flight WebSocket calls rather than reporting a transient
|
|
149
|
+
error; `RpcTransport.fromOrigin` already treats a failed WebSocket call as a
|
|
150
|
+
`TransportError` and falls back to HTTP. The RPC server's WebSocket route now
|
|
151
|
+
closes with 1000/1001/1011, which nothing in the packages inspects.
|
|
152
|
+
|
|
153
|
+
- c5d3484: README: the `effect` floor (`4.0.0-rc.118`) is now tested on every pull
|
|
154
|
+
request, where CI runs the test suite against it, instead of in a weekly
|
|
155
|
+
canary. The peer range (`effect >=4.0.0-rc.118`) and the tested pin (`4.0.0`)
|
|
156
|
+
are unchanged.
|
|
157
|
+
- e9363c0: Peer `effect ^4.0.0` (was `>=4.0.0-rc.118`), and `@effect/platform-node` /
|
|
158
|
+
`@effect/platform-browser` `^4.0.0` likewise, per the peer-range policy: the
|
|
159
|
+
range is raised only when a dependency requires it. The range form changes
|
|
160
|
+
(the caret no longer admits the 4.0 release candidates or Effect 5); every
|
|
161
|
+
package's tests pass at `effect` 4.0.0.
|
|
162
|
+
|
|
163
|
+
`@thomasfosterau/legation-svelte` peers `effect ^4.0.2`: its dependency
|
|
164
|
+
`@thomasfosterau/effect-svelte` 0.6.0 peers `effect >=4.0.2`.
|
|
165
|
+
|
|
166
|
+
- 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.
|
|
167
|
+
- Updated dependencies [2a1f63c]
|
|
168
|
+
- Updated dependencies [ef59fa7]
|
|
169
|
+
- Updated dependencies [56a2e51]
|
|
170
|
+
- Updated dependencies [d09dc65]
|
|
171
|
+
- Updated dependencies [db2366a]
|
|
172
|
+
- Updated dependencies [97db489]
|
|
173
|
+
- Updated dependencies [cc67999]
|
|
174
|
+
- Updated dependencies [a94df19]
|
|
175
|
+
- Updated dependencies [d76b04d]
|
|
176
|
+
- Updated dependencies [6f53fb1]
|
|
177
|
+
- Updated dependencies [d675d2e]
|
|
178
|
+
- Updated dependencies [1743c2b]
|
|
179
|
+
- Updated dependencies [1c56914]
|
|
180
|
+
- Updated dependencies [615e792]
|
|
181
|
+
- Updated dependencies [c8d9afc]
|
|
182
|
+
- Updated dependencies [4709f41]
|
|
183
|
+
- Updated dependencies [f0348b4]
|
|
184
|
+
- Updated dependencies [3e29e1b]
|
|
185
|
+
- Updated dependencies [a053736]
|
|
186
|
+
- Updated dependencies [8a8adfc]
|
|
187
|
+
- Updated dependencies [8995115]
|
|
188
|
+
- Updated dependencies [8995115]
|
|
189
|
+
- Updated dependencies [87fa7a5]
|
|
190
|
+
- Updated dependencies [822001c]
|
|
191
|
+
- Updated dependencies [4c59860]
|
|
192
|
+
- Updated dependencies [961e604]
|
|
193
|
+
- Updated dependencies [5937efe]
|
|
194
|
+
- Updated dependencies [08ac9cd]
|
|
195
|
+
- Updated dependencies [1fc3725]
|
|
196
|
+
- Updated dependencies [8995115]
|
|
197
|
+
- Updated dependencies [8a8adfc]
|
|
198
|
+
- Updated dependencies [025fabb]
|
|
199
|
+
- Updated dependencies [0b8fccb]
|
|
200
|
+
- Updated dependencies [e7369e1]
|
|
201
|
+
- Updated dependencies [8995115]
|
|
202
|
+
- Updated dependencies [c856d66]
|
|
203
|
+
- Updated dependencies [01a8c64]
|
|
204
|
+
- Updated dependencies [3362c37]
|
|
205
|
+
- Updated dependencies [62dcb1c]
|
|
206
|
+
- Updated dependencies [be4be06]
|
|
207
|
+
- Updated dependencies [ae18cb3]
|
|
208
|
+
- Updated dependencies [be0a44d]
|
|
209
|
+
- Updated dependencies [cc2f5b7]
|
|
210
|
+
- Updated dependencies [e818dc4]
|
|
211
|
+
- Updated dependencies [d1b01a4]
|
|
212
|
+
- Updated dependencies [5496ddf]
|
|
213
|
+
- Updated dependencies [19eb4b0]
|
|
214
|
+
- Updated dependencies [a874acb]
|
|
215
|
+
- Updated dependencies [c5d3484]
|
|
216
|
+
- Updated dependencies [38c3147]
|
|
217
|
+
- Updated dependencies [e9363c0]
|
|
218
|
+
- Updated dependencies [fed4634]
|
|
219
|
+
- Updated dependencies [fcc52eb]
|
|
220
|
+
- Updated dependencies [454e75c]
|
|
221
|
+
- Updated dependencies [2003cd6]
|
|
222
|
+
- Updated dependencies [52c614f]
|
|
223
|
+
- Updated dependencies [94d6327]
|
|
224
|
+
- Updated dependencies [91f07db]
|
|
225
|
+
- Updated dependencies [6e9e119]
|
|
226
|
+
- Updated dependencies [6c2c802]
|
|
227
|
+
- Updated dependencies [ebcc6aa]
|
|
228
|
+
- Updated dependencies [a1b95d4]
|
|
229
|
+
- Updated dependencies [c105fbf]
|
|
230
|
+
- Updated dependencies [e1a028a]
|
|
231
|
+
- Updated dependencies [dfb8759]
|
|
232
|
+
- Updated dependencies [9260efb]
|
|
233
|
+
- Updated dependencies [0644af5]
|
|
234
|
+
- Updated dependencies [4b05d55]
|
|
235
|
+
- Updated dependencies
|
|
236
|
+
- Updated dependencies [82911cc]
|
|
237
|
+
- Updated dependencies [9c9f2b0]
|
|
238
|
+
- Updated dependencies [7b21d11]
|
|
239
|
+
- Updated dependencies [6f54de8]
|
|
240
|
+
- Updated dependencies [ab1fb4e]
|
|
241
|
+
- Updated dependencies [792d8f5]
|
|
242
|
+
- Updated dependencies [a3b2ee4]
|
|
243
|
+
- Updated dependencies [eb9555a]
|
|
244
|
+
- Updated dependencies [c0ab703]
|
|
245
|
+
- Updated dependencies [8c65627]
|
|
246
|
+
- Updated dependencies [26ad3f3]
|
|
247
|
+
- Updated dependencies [d7cafd2]
|
|
248
|
+
- Updated dependencies [cb0a33d]
|
|
249
|
+
- Updated dependencies [912d95d]
|
|
250
|
+
- Updated dependencies [7290d2b]
|
|
251
|
+
- Updated dependencies [41c33e3]
|
|
252
|
+
- Updated dependencies [a77261c]
|
|
253
|
+
- Updated dependencies [c47786c]
|
|
254
|
+
- Updated dependencies [501b12c]
|
|
255
|
+
- Updated dependencies [7f1efac]
|
|
256
|
+
- Updated dependencies [a04918b]
|
|
257
|
+
- Updated dependencies [cf0b26c]
|
|
258
|
+
- Updated dependencies [ddb1fda]
|
|
259
|
+
- Updated dependencies [f54f568]
|
|
260
|
+
- Updated dependencies [fc880dc]
|
|
261
|
+
- Updated dependencies [9cbd160]
|
|
262
|
+
- Updated dependencies [0bebedf]
|
|
263
|
+
- Updated dependencies [bc4c599]
|
|
264
|
+
- Updated dependencies [20a6e18]
|
|
265
|
+
- Updated dependencies [b4d2b80]
|
|
266
|
+
- Updated dependencies [2201b4c]
|
|
267
|
+
- Updated dependencies [0cdefe7]
|
|
268
|
+
- Updated dependencies [66f3b5b]
|
|
269
|
+
- Updated dependencies [7e11e61]
|
|
270
|
+
- Updated dependencies [6175e38]
|
|
271
|
+
- Updated dependencies [caf4761]
|
|
272
|
+
- Updated dependencies [821b10d]
|
|
273
|
+
- Updated dependencies [9e74a83]
|
|
274
|
+
- Updated dependencies [49fd066]
|
|
275
|
+
- Updated dependencies [06405ed]
|
|
276
|
+
- Updated dependencies [e7f4871]
|
|
277
|
+
- Updated dependencies [916d91b]
|
|
278
|
+
- Updated dependencies [47f4e15]
|
|
279
|
+
- Updated dependencies [f829146]
|
|
280
|
+
- Updated dependencies [13658a7]
|
|
281
|
+
- Updated dependencies [31cfb2c]
|
|
282
|
+
- Updated dependencies [a1cea1c]
|
|
283
|
+
- Updated dependencies [7e94abc]
|
|
284
|
+
- Updated dependencies [4fbd0b7]
|
|
285
|
+
- Updated dependencies [9d2f21b]
|
|
286
|
+
- Updated dependencies [3016170]
|
|
287
|
+
- Updated dependencies [f4a1a60]
|
|
288
|
+
- Updated dependencies [507ccb1]
|
|
289
|
+
- Updated dependencies [c671ccf]
|
|
290
|
+
- Updated dependencies [16889f0]
|
|
291
|
+
- Updated dependencies [fb2a992]
|
|
292
|
+
- Updated dependencies [22f320d]
|
|
293
|
+
- Updated dependencies [652ae7b]
|
|
294
|
+
- Updated dependencies [83bcf76]
|
|
295
|
+
- Updated dependencies [20e57d3]
|
|
296
|
+
- Updated dependencies [f010749]
|
|
297
|
+
- Updated dependencies [8d02113]
|
|
298
|
+
- Updated dependencies [bf9054d]
|
|
299
|
+
- Updated dependencies [e40c73a]
|
|
300
|
+
- Updated dependencies [ca38874]
|
|
301
|
+
- Updated dependencies [27e1040]
|
|
302
|
+
- Updated dependencies [e68eb3e]
|
|
303
|
+
- Updated dependencies [985c4d5]
|
|
304
|
+
- Updated dependencies [3be5abc]
|
|
305
|
+
- Updated dependencies [97db489]
|
|
306
|
+
- Updated dependencies [4a23603]
|
|
307
|
+
- Updated dependencies [f441941]
|
|
308
|
+
- Updated dependencies [a01f43b]
|
|
309
|
+
- Updated dependencies [9a6426b]
|
|
310
|
+
- Updated dependencies [97cc5f9]
|
|
311
|
+
- Updated dependencies [fc880dc]
|
|
312
|
+
- Updated dependencies [e34bc24]
|
|
313
|
+
- Updated dependencies [0470e1c]
|
|
314
|
+
- Updated dependencies [439f449]
|
|
315
|
+
- Updated dependencies [3a88118]
|
|
316
|
+
- Updated dependencies [1b8b120]
|
|
317
|
+
- Updated dependencies [cf48723]
|
|
318
|
+
- Updated dependencies [b4489c5]
|
|
319
|
+
- @thomasfosterau/legation@0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Foster
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @thomasfosterau/legation-devtools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A devtools panel over the introspection substrate (#120): the journal &
|
|
4
|
+
mutation timeline, subscriptions & collections, sync activity log, and
|
|
5
|
+
actions (force pull/push, a simulated-offline toggle, drop a subscription,
|
|
6
|
+
and a destructive clear-store behind a confirm).
|
|
7
|
+
|
|
8
|
+
Architecture: a framework-agnostic headless core (`@thomasfosterau/legation-devtools`) that
|
|
9
|
+
owns all data acquisition — built entirely over already-public substrate
|
|
10
|
+
(`@thomasfosterau/legation/internal`'s `Introspection`, `ClientEngine.mutationStates`,
|
|
11
|
+
`ClientEngine.engineEvents`/`syncFailures`, `ClientStore`) — plus thin,
|
|
12
|
+
idiomatic panels per framework — today `@thomasfosterau/legation-devtools/svelte`. A panel
|
|
13
|
+
just runs the core's Effects/Streams through its own framework's reactivity;
|
|
14
|
+
it has no data-acquisition path of its own.
|
|
15
|
+
|
|
16
|
+
This is a **functional first cut**: plain, legible, correct. Visual polish is
|
|
17
|
+
out of scope — a human design pass comes later.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @thomasfosterau/legation-devtools @thomasfosterau/legation effect
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Add `svelte` (an optional peer, `>=5.40`) for the `@thomasfosterau/legation-devtools/svelte` panel.
|
|
26
|
+
|
|
27
|
+
## Effect version
|
|
28
|
+
|
|
29
|
+
Peers `effect ^4.0.0`. Tested against `4.1.2` (the version CI pins) and `4.0.0` (the floor; CI runs the tests against it on every PR); install one copy of `effect` in your app (ADR-0003).
|
|
30
|
+
|
|
31
|
+
## Scope
|
|
32
|
+
|
|
33
|
+
The owned-engine case only: an app that constructs its own `ClientEngine` +
|
|
34
|
+
`ClientStore` in-process (see `examples/northwind`).
|
|
35
|
+
Attaching over the mirror seam (a service-worker-hosted engine) needs a
|
|
36
|
+
mirror-protocol introspection extension that doesn't exist yet — tracked as a
|
|
37
|
+
follow-up substrate issue (#136).
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import * as Devtools from "@thomasfosterau/legation-devtools";
|
|
43
|
+
|
|
44
|
+
// `engine`/`store` are the same values you provided to `ClientEngine.make`.
|
|
45
|
+
const devtools = Devtools.make(engine, store, {
|
|
46
|
+
// Optional: wires the "simulated offline" action. Build `offline` with
|
|
47
|
+
// `Transport.simulateOffline` and wrap your real transport with it.
|
|
48
|
+
offline: { setOffline, isOffline },
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Svelte:
|
|
53
|
+
|
|
54
|
+
```svelte
|
|
55
|
+
<script lang="ts">
|
|
56
|
+
import { DevtoolsPanel } from "@thomasfosterau/legation-devtools/svelte";
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<DevtoolsPanel core={devtools} />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The `/svelte` entry point is a separate export so an app that never imports
|
|
63
|
+
it pays nothing for the panel in its production bundle. The headless core is
|
|
64
|
+
framework-agnostic — a binding for another framework is a component over the
|
|
65
|
+
same `DevtoolsCore`.
|
|
66
|
+
|
|
67
|
+
Note: `@thomasfosterau/legation-devtools/svelte` ships raw `.svelte`/`.ts` source (not a
|
|
68
|
+
pre-compiled bundle) — any Svelte 5 app already has a bundler that compiles
|
|
69
|
+
`.svelte` files, so this composes directly with your app's own build.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { ClientEngine, MutationState, Subset } from "@thomasfosterau/legation/internal";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import { ClientStore, StoreError } from "@thomasfosterau/legation";
|
|
5
|
+
/** One collection's name plus its confirmed row count, for the subscriptions
|
|
6
|
+
* & collections panel's "rows in window" summary. */
|
|
7
|
+
export interface CollectionSummary {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly rowCount: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The optional simulated-offline control (built by `Transport.simulateOffline`
|
|
13
|
+
* and wired into the app's transport at boot) — the devtools actions panel's
|
|
14
|
+
* offline toggle. Omitted when the app never wired a toggleable transport, in
|
|
15
|
+
* which case the panel simply doesn't render that action.
|
|
16
|
+
*/
|
|
17
|
+
export interface OfflineControl {
|
|
18
|
+
readonly setOffline: (offline: boolean) => Effect.Effect<void>;
|
|
19
|
+
readonly isOffline: Effect.Effect<boolean>;
|
|
20
|
+
}
|
|
21
|
+
/** Options for {@link make}. */
|
|
22
|
+
export interface DevtoolsOptions {
|
|
23
|
+
readonly offline?: OfflineControl;
|
|
24
|
+
}
|
|
25
|
+
/** The devtools headless core: one instance per engine, shared by every panel mounted over it. */
|
|
26
|
+
export interface DevtoolsCore {
|
|
27
|
+
/** The keyed, reactive mutation-state map (#116): re-emits on every fold. */
|
|
28
|
+
readonly mutationStates: Stream.Stream<MutationState.MutationStateMap>;
|
|
29
|
+
/** Active subsets: collection, predicate, order, and window (limit/cursor). */
|
|
30
|
+
readonly subscriptions: Effect.Effect<ReadonlyArray<Subset.Subset>>;
|
|
31
|
+
/** The current pull cookie, or `undefined` before the first pull. */
|
|
32
|
+
readonly cookie: Effect.Effect<string | undefined, StoreError>;
|
|
33
|
+
/** Every collection currently holding confirmed rows, with its row count. */
|
|
34
|
+
readonly collections: Effect.Effect<ReadonlyArray<CollectionSummary>, StoreError>;
|
|
35
|
+
/** The unified sync-lifecycle event stream (push/pull/poke/mutation/rebuild). */
|
|
36
|
+
readonly events: Stream.Stream<ClientEngine.EngineEvent>;
|
|
37
|
+
/** Terminal sync failures (retry exhausted) — the "last error" surface. */
|
|
38
|
+
readonly syncFailures: Stream.Stream<ClientEngine.SyncFailure>;
|
|
39
|
+
/** Push the journal now. */
|
|
40
|
+
readonly forcePush: ClientEngine.ClientEngine["push"];
|
|
41
|
+
/** Pull a patch now and rebase. */
|
|
42
|
+
readonly forcePull: ClientEngine.ClientEngine["pull"];
|
|
43
|
+
/** Drop an active subscription. */
|
|
44
|
+
readonly dropSubscription: (subset: Subset.Subset) => Effect.Effect<void>;
|
|
45
|
+
/** Destructive: clear every confirmed row from the store. Gate behind a confirm in the UI. */
|
|
46
|
+
readonly clearStore: Effect.Effect<void, StoreError>;
|
|
47
|
+
/** The simulated-offline toggle, if the app wired one — see {@link OfflineControl}. */
|
|
48
|
+
readonly offline?: OfflineControl;
|
|
49
|
+
}
|
|
50
|
+
/** Build the devtools core over a running engine + its store. */
|
|
51
|
+
export declare const make: (engine: ClientEngine.ClientEngine, store: ClientStore.ClientStore, options?: DevtoolsOptions) => DevtoolsCore;
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/Devtools.ts"],"mappings":";;;;;;iBAuCiB;WACN;WACA;;;;;;;;iBASM;WACN,aAAa,qBAAqB,OAAO;WACzC,WAAW,OAAO;;;iBAIZ;WACN,UAAU;;;iBAIJ;;WAGN,gBAAgB,OAAO,OAAO,cAAc;;WAI5C,eAAe,OAAO,OAAO,cAAc,OAAO;;WAElD,QAAQ,OAAO,2BAA2B;;WAE1C,aAAa,OAAO,OAAO,cAAc,oBAAoB;;WAI7D,QAAQ,OAAO,OAAO,aAAa;;WAEnC,cAAc,OAAO,OAAO,aAAa;;WAIzC,WAAW,aAAa;;WAExB,WAAW,aAAa;;WAExB,mBAAmB,QAAQ,OAAO,WAAW,OAAO;;WAEpD,YAAY,OAAO,aAAa;;WAEhC,UAAU;;;qBAIR,OACX,QAAQ,aAAa,cACrB,OAAO,YAAY,aACnB,UAAU,oBACT"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { Introspection } from "@thomasfosterau/legation/internal";
|
|
3
|
+
// src/Devtools.ts
|
|
4
|
+
/**
|
|
5
|
+
* Devtools
|
|
6
|
+
*
|
|
7
|
+
* The headless core over the introspection substrate (#120): all data
|
|
8
|
+
* acquisition and every action live here as plain `Effect`/`Stream` values —
|
|
9
|
+
* the same surfaces `@thomasfosterau/legation-svelte` already exposes bindings over — so a
|
|
10
|
+
* framework panel (`@thomasfosterau/legation-devtools/svelte`) is a thin idiomatic UI over
|
|
11
|
+
* one shared, tested read model, never its own data-acquisition path.
|
|
12
|
+
*
|
|
13
|
+
* Fed exclusively by already-public substrate (the `@thomasfosterau/legation/internal`
|
|
14
|
+
* entrypoint, the same one `@thomasfosterau/legation-svelte` itself depends on):
|
|
15
|
+
* `Introspection` (subscriptions, cookie, collections),
|
|
16
|
+
* `ClientEngine.mutationStates` (#116, the journal/mutation timeline —
|
|
17
|
+
* including the `"dropped"` phase paced compaction settles a compacted entry
|
|
18
|
+
* to, so a devtools timeline never shows a phantom in-flight mutation),
|
|
19
|
+
* `ClientEngine.engineEvents` (the sync activity log), and
|
|
20
|
+
* `ClientEngine.syncFailures`. Nothing here reaches into engine-private
|
|
21
|
+
* state; a capability this core would need that isn't already exposed there
|
|
22
|
+
* is substrate work, not a private hook.
|
|
23
|
+
*
|
|
24
|
+
* Scope: the owned-engine case only — an app that constructed its own
|
|
25
|
+
* `ClientEngine` + `ClientStore` in this process. Attaching over the mirror
|
|
26
|
+
* seam (a service-worker-hosted engine, ADR-0010/0021) needs a
|
|
27
|
+
* mirror-protocol introspection extension that does not exist yet; that is
|
|
28
|
+
* substrate work for a follow-up issue, not something this package works
|
|
29
|
+
* around with a private hook of its own.
|
|
30
|
+
*/
|
|
31
|
+
/** Build the devtools core over a running engine + its store. */
|
|
32
|
+
const make = (engine, store, options) => {
|
|
33
|
+
const introspection = Introspection.make(engine, store);
|
|
34
|
+
const collections = introspection.collections.pipe(Effect.flatMap((names) => Effect.forEach(names, (name) => store.listRows(name).pipe(Effect.map((rows) => ({
|
|
35
|
+
name,
|
|
36
|
+
rowCount: rows.length
|
|
37
|
+
}))))));
|
|
38
|
+
return {
|
|
39
|
+
mutationStates: engine.mutationStates,
|
|
40
|
+
subscriptions: introspection.subscriptions,
|
|
41
|
+
cookie: introspection.cookie,
|
|
42
|
+
collections,
|
|
43
|
+
events: introspection.events,
|
|
44
|
+
syncFailures: engine.syncFailures,
|
|
45
|
+
forcePush: engine.push,
|
|
46
|
+
forcePull: engine.pull,
|
|
47
|
+
dropSubscription: engine.unsubscribe,
|
|
48
|
+
clearStore: engine.clearRows,
|
|
49
|
+
...options?.offline === void 0 ? {} : { offline: options.offline }
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export { make };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/Devtools.ts"],"sourcesContent":["/**\n * Devtools\n *\n * The headless core over the introspection substrate (#120): all data\n * acquisition and every action live here as plain `Effect`/`Stream` values —\n * the same surfaces `@thomasfosterau/legation-svelte` already exposes bindings over — so a\n * framework panel (`@thomasfosterau/legation-devtools/svelte`) is a thin idiomatic UI over\n * one shared, tested read model, never its own data-acquisition path.\n *\n * Fed exclusively by already-public substrate (the `@thomasfosterau/legation/internal`\n * entrypoint, the same one `@thomasfosterau/legation-svelte` itself depends on):\n * `Introspection` (subscriptions, cookie, collections),\n * `ClientEngine.mutationStates` (#116, the journal/mutation timeline —\n * including the `\"dropped\"` phase paced compaction settles a compacted entry\n * to, so a devtools timeline never shows a phantom in-flight mutation),\n * `ClientEngine.engineEvents` (the sync activity log), and\n * `ClientEngine.syncFailures`. Nothing here reaches into engine-private\n * state; a capability this core would need that isn't already exposed there\n * is substrate work, not a private hook.\n *\n * Scope: the owned-engine case only — an app that constructed its own\n * `ClientEngine` + `ClientStore` in this process. Attaching over the mirror\n * seam (a service-worker-hosted engine, ADR-0010/0021) needs a\n * mirror-protocol introspection extension that does not exist yet; that is\n * substrate work for a follow-up issue, not something this package works\n * around with a private hook of its own.\n */\nimport * as Effect from \"effect/Effect\";\nimport * as Stream from \"effect/Stream\";\nimport type { ClientStore, StoreError } from \"@thomasfosterau/legation\";\nimport {\n Introspection,\n type ClientEngine,\n type MutationState,\n type Subset,\n} from \"@thomasfosterau/legation/internal\";\n\n/** One collection's name plus its confirmed row count, for the subscriptions\n * & collections panel's \"rows in window\" summary. */\nexport interface CollectionSummary {\n readonly name: string;\n readonly rowCount: number;\n}\n\n/**\n * The optional simulated-offline control (built by `Transport.simulateOffline`\n * and wired into the app's transport at boot) — the devtools actions panel's\n * offline toggle. Omitted when the app never wired a toggleable transport, in\n * which case the panel simply doesn't render that action.\n */\nexport interface OfflineControl {\n readonly setOffline: (offline: boolean) => Effect.Effect<void>;\n readonly isOffline: Effect.Effect<boolean>;\n}\n\n/** Options for {@link make}. */\nexport interface DevtoolsOptions {\n readonly offline?: OfflineControl;\n}\n\n/** The devtools headless core: one instance per engine, shared by every panel mounted over it. */\nexport interface DevtoolsCore {\n // ─── Journal & mutation timeline (panel #1) ──────────────────────────────\n /** The keyed, reactive mutation-state map (#116): re-emits on every fold. */\n readonly mutationStates: Stream.Stream<MutationState.MutationStateMap>;\n\n // ─── Subscriptions & collections (panel #2) ──────────────────────────────\n /** Active subsets: collection, predicate, order, and window (limit/cursor). */\n readonly subscriptions: Effect.Effect<ReadonlyArray<Subset.Subset>>;\n /** The current pull cookie, or `undefined` before the first pull. */\n readonly cookie: Effect.Effect<string | undefined, StoreError>;\n /** Every collection currently holding confirmed rows, with its row count. */\n readonly collections: Effect.Effect<ReadonlyArray<CollectionSummary>, StoreError>;\n\n // ─── Sync activity (panel #3) ────────────────────────────────────────────\n /** The unified sync-lifecycle event stream (push/pull/poke/mutation/rebuild). */\n readonly events: Stream.Stream<ClientEngine.EngineEvent>;\n /** Terminal sync failures (retry exhausted) — the \"last error\" surface. */\n readonly syncFailures: Stream.Stream<ClientEngine.SyncFailure>;\n\n // ─── Actions (panel #4) ──────────────────────────────────────────────────\n /** Push the journal now. */\n readonly forcePush: ClientEngine.ClientEngine[\"push\"];\n /** Pull a patch now and rebase. */\n readonly forcePull: ClientEngine.ClientEngine[\"pull\"];\n /** Drop an active subscription. */\n readonly dropSubscription: (subset: Subset.Subset) => Effect.Effect<void>;\n /** Destructive: clear every confirmed row from the store. Gate behind a confirm in the UI. */\n readonly clearStore: Effect.Effect<void, StoreError>;\n /** The simulated-offline toggle, if the app wired one — see {@link OfflineControl}. */\n readonly offline?: OfflineControl;\n}\n\n/** Build the devtools core over a running engine + its store. */\nexport const make = (\n engine: ClientEngine.ClientEngine,\n store: ClientStore.ClientStore,\n options?: DevtoolsOptions\n): DevtoolsCore => {\n const introspection = Introspection.make(engine, store);\n\n const collections: Effect.Effect<\n ReadonlyArray<CollectionSummary>,\n StoreError\n > = introspection.collections.pipe(\n Effect.flatMap((names) =>\n Effect.forEach(names, (name) =>\n store\n .listRows(name)\n .pipe(Effect.map((rows): CollectionSummary => ({ name, rowCount: rows.length })))\n )\n )\n );\n\n return {\n mutationStates: engine.mutationStates,\n subscriptions: introspection.subscriptions,\n cookie: introspection.cookie,\n collections,\n events: introspection.events,\n syncFailures: engine.syncFailures,\n forcePush: engine.push,\n forcePull: engine.pull,\n dropSubscription: engine.unsubscribe,\n clearStore: engine.clearRows,\n ...(options?.offline === undefined ? {} : { offline: options.offline }),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FA,MAAa,QACX,QACA,OACA,YACiB;CACjB,MAAM,gBAAgB,cAAc,KAAK,QAAQ,KAAK;CAEtD,MAAM,cAGF,cAAc,YAAY,KAC5B,OAAO,SAAS,UACd,OAAO,QAAQ,QAAQ,SACrB,MACG,SAAS,IAAI,CAAC,CACd,KAAK,OAAO,KAAK,UAA6B;EAAE;EAAM,UAAU,KAAK;CAAO,EAAE,CAAC,CACpF,CACF,CACF;CAEA,OAAO;EACL,gBAAgB,OAAO;EACvB,eAAe,cAAc;EAC7B,QAAQ,cAAc;EACtB;EACA,QAAQ,cAAc;EACtB,cAAc,OAAO;EACrB,WAAW,OAAO;EAClB,WAAW,OAAO;EAClB,kBAAkB,OAAO;EACzB,YAAY,OAAO;EACnB,GAAI,SAAS,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,QAAQ,QAAQ;CACvE;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thomasfosterau/legation-devtools",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A devtools headless core over the introspection substrate (#120): journal/mutation timeline, subscriptions & collections, sync activity, and actions — with a Svelte panel on /svelte",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"devtools",
|
|
7
|
+
"effect",
|
|
8
|
+
"introspection",
|
|
9
|
+
"react",
|
|
10
|
+
"svelte",
|
|
11
|
+
"sync-engine",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/thomasfosterau/legation/tree/trunk/packages/devtools#readme",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/thomasfosterau/legation/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Thomas Foster",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/thomasfosterau/legation.git",
|
|
23
|
+
"directory": "packages/devtools"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"svelte",
|
|
28
|
+
"!svelte/tsconfig.json",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"sideEffects": false,
|
|
35
|
+
"main": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./svelte": {
|
|
43
|
+
"svelte": "./svelte/index.ts",
|
|
44
|
+
"types": "./svelte/index.ts",
|
|
45
|
+
"default": "./svelte/index.ts"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@thomasfosterau/legation": "^0.1.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@effect/vitest": "4.1.2",
|
|
57
|
+
"effect": "4.1.2",
|
|
58
|
+
"svelte": "^5.57.3",
|
|
59
|
+
"vitest": "5.0.5"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"effect": "^4.0.0",
|
|
63
|
+
"svelte": ">=5.40"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"svelte": {
|
|
67
|
+
"optional": true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
6
70
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @thomasfosterau/legation-devtools/svelte — DevtoolsPanel
|
|
4
|
+
*
|
|
5
|
+
* A thin Svelte panel over the headless `DevtoolsCore` (#120): every
|
|
6
|
+
* section below just reads a store built by `store.ts` (which in turn
|
|
7
|
+
* just runs the core's Effects/Streams) — no data acquisition of its own.
|
|
8
|
+
* Visual polish is out of scope for this first cut: plain, legible markup.
|
|
9
|
+
*/
|
|
10
|
+
import { untrack } from "svelte";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Fiber from "effect/Fiber";
|
|
13
|
+
import * as HashMap from "effect/HashMap";
|
|
14
|
+
import * as Result from "effect/Result";
|
|
15
|
+
import type { DevtoolsCore } from "@thomasfosterau/legation-devtools";
|
|
16
|
+
import * as Stores from "./store.js";
|
|
17
|
+
|
|
18
|
+
const props: { core: DevtoolsCore } = $props();
|
|
19
|
+
// `core` is a stable singleton built once by the app's `createWorld`, the
|
|
20
|
+
// same as `App.svelte`'s own `world` prop; read it untracked so the
|
|
21
|
+
// one-time store wiring below isn't flagged as reading reactive state.
|
|
22
|
+
const core = untrack(() => props.core);
|
|
23
|
+
|
|
24
|
+
const { mutationStates, subscriptions, cookie, collections, events, syncFailures } =
|
|
25
|
+
Stores.makeStores(core);
|
|
26
|
+
|
|
27
|
+
const journalEntries = $derived(
|
|
28
|
+
[...HashMap.values($mutationStates)].toSorted((a, b) => b.mutationId - a.mutationId)
|
|
29
|
+
);
|
|
30
|
+
const pendingCount = $derived(journalEntries.filter((e) => e.phase === "pending").length);
|
|
31
|
+
|
|
32
|
+
// `JSON.stringify` throws on a cycle or a BigInt; the mutator name alone stands in then.
|
|
33
|
+
const summarise = (mutator: string, args: unknown): string =>
|
|
34
|
+
Result.getOrElse(
|
|
35
|
+
Result.try(() => `${mutator} ${JSON.stringify(args)}`),
|
|
36
|
+
() => mutator
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
let offline = $state<boolean | undefined>(undefined);
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (core.offline === undefined) return;
|
|
43
|
+
const fiber = Effect.runFork(
|
|
44
|
+
Effect.map(core.offline.isOffline, (value) => {
|
|
45
|
+
offline = value;
|
|
46
|
+
})
|
|
47
|
+
);
|
|
48
|
+
return () => {
|
|
49
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const toggleOffline = (): void => {
|
|
54
|
+
if (core.offline === undefined) return;
|
|
55
|
+
const next = !(offline ?? false);
|
|
56
|
+
void Effect.runPromise(core.offline.setOffline(next)).then(() => {
|
|
57
|
+
offline = next;
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const forcePull = (): void => void Effect.runPromise(core.forcePull);
|
|
62
|
+
const forcePush = (): void => void Effect.runPromise(core.forcePush);
|
|
63
|
+
const dropSubscription = (subset: (typeof $subscriptions)[number]): void =>
|
|
64
|
+
void Effect.runPromise(core.dropSubscription(subset));
|
|
65
|
+
const clearStore = (): void => {
|
|
66
|
+
if (!confirm("Clear every confirmed row from the store? This cannot be undone.")) return;
|
|
67
|
+
void Effect.runPromise(core.clearStore);
|
|
68
|
+
};
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div>
|
|
72
|
+
<h2>Devtools</h2>
|
|
73
|
+
|
|
74
|
+
<section>
|
|
75
|
+
<h3>Journal & mutations</h3>
|
|
76
|
+
<p>{pendingCount} pending, {journalEntries.length} total (newest first)</p>
|
|
77
|
+
{#if journalEntries.length === 0}
|
|
78
|
+
<p>Journal empty.</p>
|
|
79
|
+
{:else}
|
|
80
|
+
<ul>
|
|
81
|
+
{#each journalEntries as entry (entry.mutationId)}
|
|
82
|
+
<li>
|
|
83
|
+
<strong>{entry.phase}</strong> #{entry.mutationId}
|
|
84
|
+
{summarise(entry.mutator, entry.args)}
|
|
85
|
+
{#if entry.phase === "rejected" && entry.reason !== undefined}
|
|
86
|
+
— {entry.reason}
|
|
87
|
+
{/if}
|
|
88
|
+
</li>
|
|
89
|
+
{/each}
|
|
90
|
+
</ul>
|
|
91
|
+
{/if}
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<section>
|
|
95
|
+
<h3>Subscriptions & collections</h3>
|
|
96
|
+
<p>Cookie: {$cookie ?? "(none — no pull yet)"}</p>
|
|
97
|
+
<h4>Active subscriptions</h4>
|
|
98
|
+
{#if $subscriptions.length === 0}
|
|
99
|
+
<p>None.</p>
|
|
100
|
+
{:else}
|
|
101
|
+
<ul>
|
|
102
|
+
{#each $subscriptions as s, i (i)}
|
|
103
|
+
<li>
|
|
104
|
+
{s.collection} — where {JSON.stringify(s.where)}
|
|
105
|
+
{#if s.orderBy.length > 0}, order {JSON.stringify(s.orderBy)}{/if}
|
|
106
|
+
{#if s.limit !== undefined}, limit {s.limit}{/if}
|
|
107
|
+
</li>
|
|
108
|
+
{/each}
|
|
109
|
+
</ul>
|
|
110
|
+
{/if}
|
|
111
|
+
<h4>Collections</h4>
|
|
112
|
+
{#if $collections.length === 0}
|
|
113
|
+
<p>None synced yet.</p>
|
|
114
|
+
{:else}
|
|
115
|
+
<ul>
|
|
116
|
+
{#each $collections as c (c.name)}
|
|
117
|
+
<li>{c.name}: {c.rowCount} row{c.rowCount === 1 ? "" : "s"}</li>
|
|
118
|
+
{/each}
|
|
119
|
+
</ul>
|
|
120
|
+
{/if}
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<section>
|
|
124
|
+
<h3>Sync activity</h3>
|
|
125
|
+
{#if $syncFailures.length === 0}
|
|
126
|
+
<p>No sync failures.</p>
|
|
127
|
+
{:else}
|
|
128
|
+
<p>
|
|
129
|
+
Last failure: {$syncFailures[$syncFailures.length - 1].operation} —
|
|
130
|
+
{String($syncFailures[$syncFailures.length - 1].error)}
|
|
131
|
+
</p>
|
|
132
|
+
{/if}
|
|
133
|
+
{#if $events.length === 0}
|
|
134
|
+
<p>No activity yet.</p>
|
|
135
|
+
{:else}
|
|
136
|
+
<ul>
|
|
137
|
+
{#each $events.toReversed() as event, i (i)}
|
|
138
|
+
<li>{JSON.stringify(event)}</li>
|
|
139
|
+
{/each}
|
|
140
|
+
</ul>
|
|
141
|
+
{/if}
|
|
142
|
+
</section>
|
|
143
|
+
|
|
144
|
+
<section>
|
|
145
|
+
<h3>Actions</h3>
|
|
146
|
+
<button type="button" onclick={forcePull}>Force pull</button>
|
|
147
|
+
<button type="button" onclick={forcePush}>Force push</button>
|
|
148
|
+
{#if core.offline !== undefined}
|
|
149
|
+
<button type="button" onclick={toggleOffline}>
|
|
150
|
+
{offline === true ? "Go online" : "Simulate offline"}
|
|
151
|
+
</button>
|
|
152
|
+
{/if}
|
|
153
|
+
<h4>Drop a subscription</h4>
|
|
154
|
+
{#if $subscriptions.length === 0}
|
|
155
|
+
<p>No active subscriptions.</p>
|
|
156
|
+
{:else}
|
|
157
|
+
<ul>
|
|
158
|
+
{#each $subscriptions as s, i (i)}
|
|
159
|
+
<li>
|
|
160
|
+
{s.collection}
|
|
161
|
+
<button type="button" onclick={() => dropSubscription(s)}>drop</button>
|
|
162
|
+
</li>
|
|
163
|
+
{/each}
|
|
164
|
+
</ul>
|
|
165
|
+
{/if}
|
|
166
|
+
<h4>Destructive</h4>
|
|
167
|
+
<button type="button" onclick={clearStore}>Clear store…</button>
|
|
168
|
+
</section>
|
|
169
|
+
</div>
|
package/svelte/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @thomasfosterau/legation-devtools/svelte
|
|
3
|
+
*
|
|
4
|
+
* The Svelte panel entry point (#120): `DevtoolsPanel.svelte` is the thin
|
|
5
|
+
* UI, `store.ts` bridges the headless core's Effects/Streams to Svelte
|
|
6
|
+
* `Readable`s. Shipped as raw source — see `store.ts`'s module doc.
|
|
7
|
+
*/
|
|
8
|
+
export { default as DevtoolsPanel } from "./DevtoolsPanel.svelte";
|
|
9
|
+
export * from "./store.js";
|
package/svelte/store.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @thomasfosterau/legation-devtools/svelte — store adapters
|
|
3
|
+
*
|
|
4
|
+
* Bridges the headless {@link Devtools.DevtoolsCore} (#120) to Svelte
|
|
5
|
+
* `Readable` stores, the same shape `@thomasfosterau/legation-svelte`'s own `Query.ts`
|
|
6
|
+
* bridges live queries with: a store's `start` callback forks the source in
|
|
7
|
+
* a fiber and pushes each value through `set`; `stop` interrupts that fiber
|
|
8
|
+
* when the last subscriber unsubscribes. No data acquisition of its own —
|
|
9
|
+
* every store here just runs one of the core's Effects/Streams.
|
|
10
|
+
*
|
|
11
|
+
* Shipped as raw source (not pre-compiled): this subpath is consumed
|
|
12
|
+
* directly by a Svelte-aware bundler (any Svelte 5 app already has one), the
|
|
13
|
+
* same as the `.svelte` panel component alongside it.
|
|
14
|
+
*/
|
|
15
|
+
import { readable, type Readable } from "svelte/store";
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import * as Fiber from "effect/Fiber";
|
|
18
|
+
import * as HashMap from "effect/HashMap";
|
|
19
|
+
import * as Stream from "effect/Stream";
|
|
20
|
+
import type { ClientEngine, MutationState, Subset } from "@thomasfosterau/legation/internal";
|
|
21
|
+
import type * as Devtools from "@thomasfosterau/legation-devtools";
|
|
22
|
+
|
|
23
|
+
/** Subscribe to a `Stream` as a store; `initial` until the first emission. */
|
|
24
|
+
export const toStreamStore = <A>(stream: Stream.Stream<A>, initial: A): Readable<A> =>
|
|
25
|
+
readable<A>(initial, (set) => {
|
|
26
|
+
const fiber = Effect.runFork(Stream.runForEach(stream, (a) => Effect.sync(() => set(a))));
|
|
27
|
+
return () => {
|
|
28
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Re-run a one-shot Effect snapshot whenever `signal` (a live Stream) emits,
|
|
34
|
+
* plus once on subscribe — see `react.tsx`'s `useSnapshot` for the same
|
|
35
|
+
* pattern and its rationale (the Introspection substrate's plain-Effect
|
|
36
|
+
* reads aren't Streams; `events` is the "something changed" trigger).
|
|
37
|
+
*/
|
|
38
|
+
export const toSnapshotStore = <A, E, S>(
|
|
39
|
+
effect: Effect.Effect<A, E>,
|
|
40
|
+
signal: Stream.Stream<S>,
|
|
41
|
+
initial: A
|
|
42
|
+
): Readable<A> =>
|
|
43
|
+
readable<A>(initial, (set) => {
|
|
44
|
+
const safe = Effect.catch(effect, () => Effect.succeed(initial));
|
|
45
|
+
const refresh = Effect.runFork(Effect.map(safe, (a) => set(a)));
|
|
46
|
+
const fiber = Effect.runFork(Stream.runForEach(signal, () => Effect.map(safe, (a) => set(a))));
|
|
47
|
+
return () => {
|
|
48
|
+
Effect.runFork(Fiber.interrupt(refresh));
|
|
49
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** A capped rolling log of a Stream's emissions, newest last. */
|
|
54
|
+
export const toLogStore = <A>(
|
|
55
|
+
stream: Stream.Stream<A>,
|
|
56
|
+
limit: number
|
|
57
|
+
): Readable<ReadonlyArray<A>> =>
|
|
58
|
+
readable<ReadonlyArray<A>>([], (set) => {
|
|
59
|
+
let log: ReadonlyArray<A> = [];
|
|
60
|
+
const fiber = Effect.runFork(
|
|
61
|
+
Stream.runForEach(stream, (a) =>
|
|
62
|
+
Effect.sync(() => {
|
|
63
|
+
log = [...log, a].slice(-limit);
|
|
64
|
+
set(log);
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
return () => {
|
|
69
|
+
Effect.runFork(Fiber.interrupt(fiber));
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const EMPTY_STATES: MutationState.MutationStateMap = HashMap.empty();
|
|
74
|
+
|
|
75
|
+
/** Devtools stores for one core instance — the panel's whole data surface. */
|
|
76
|
+
export interface DevtoolsStores {
|
|
77
|
+
readonly mutationStates: Readable<MutationState.MutationStateMap>;
|
|
78
|
+
readonly subscriptions: Readable<ReadonlyArray<Subset.Subset>>;
|
|
79
|
+
readonly cookie: Readable<string | undefined>;
|
|
80
|
+
readonly collections: Readable<ReadonlyArray<Devtools.CollectionSummary>>;
|
|
81
|
+
readonly events: Readable<ReadonlyArray<ClientEngine.EngineEvent>>;
|
|
82
|
+
readonly syncFailures: Readable<ReadonlyArray<ClientEngine.SyncFailure>>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Build every store the panel needs from one {@link Devtools.DevtoolsCore}. */
|
|
86
|
+
export const makeStores = (core: Devtools.DevtoolsCore): DevtoolsStores => ({
|
|
87
|
+
mutationStates: toStreamStore(core.mutationStates, EMPTY_STATES),
|
|
88
|
+
subscriptions: toSnapshotStore(core.subscriptions, core.events, []),
|
|
89
|
+
cookie: toSnapshotStore(core.cookie, core.events, undefined as string | undefined),
|
|
90
|
+
collections: toSnapshotStore(core.collections, core.events, []),
|
|
91
|
+
events: toLogStore(core.events, 50),
|
|
92
|
+
syncFailures: toLogStore(core.syncFailures, 10),
|
|
93
|
+
});
|