@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 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
- # Temporary Holding Version
1
+ # @thomasfosterau/legation-devtools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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 &amp; 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 &amp; 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>
@@ -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";
@@ -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
+ });