effect-atom-svelte 0.1.2 → 0.2.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/dist/Hooks.svelte.js +12 -4
- package/dist/Inspector.d.ts +329 -0
- package/dist/Inspector.js +491 -0
- package/dist/RegistryContext.js +12 -2
- package/dist/internal/nodeInternals.d.ts +24 -0
- package/dist/internal/nodeInternals.js +83 -0
- package/dist/internal/registries.d.ts +6 -0
- package/dist/internal/registries.js +40 -0
- package/dist/internal/scope.svelte.d.ts +29 -0
- package/dist/internal/scope.svelte.js +23 -0
- package/package.json +5 -1
package/dist/Hooks.svelte.js
CHANGED
|
@@ -9,6 +9,7 @@ import { BROWSER, DEV } from "esm-env";
|
|
|
9
9
|
import { getAbortSignal, hydratable, onDestroy, untrack } from "svelte";
|
|
10
10
|
import { createSubscriber } from "svelte/reactivity";
|
|
11
11
|
import { decodeSeed, encodeSeed, isWaiting, revalidatesOnHydrate, } from "./internal/hydration.js";
|
|
12
|
+
import { reportReads } from "./internal/scope.svelte.js";
|
|
12
13
|
import { getRegistry } from "./RegistryContext.js";
|
|
13
14
|
const toGetter = (input) => typeof input === "function" && !Atom.isAtom(input)
|
|
14
15
|
? input
|
|
@@ -151,6 +152,7 @@ const subscribedReader = (registry, getAtom) => {
|
|
|
151
152
|
return Atom.getServerValue(atom, registry);
|
|
152
153
|
};
|
|
153
154
|
}
|
|
155
|
+
reportReads(registry, getAtom, "read");
|
|
154
156
|
// The atom is picked on every read, not held in a $derived. In async mode Svelte renders a batch
|
|
155
157
|
// with other pending batches' changes rolled back, deriveds included, but registry reads always
|
|
156
158
|
// see the latest state; a derived atom could then pair an old atom with new state, and that
|
|
@@ -290,6 +292,13 @@ export const useAtom = (input) => {
|
|
|
290
292
|
const getAtom = toGetter(input);
|
|
291
293
|
return new AtomCell(subscribedReader(registry, getAtom), (value) => registry.set(getAtom(), value));
|
|
292
294
|
};
|
|
295
|
+
/** useAtomMount, telling an inspector scope why the atom is held: `useAtomSet` holds it to write. */
|
|
296
|
+
const mountWhileAlive = (input, kind) => {
|
|
297
|
+
const registry = getRegistry();
|
|
298
|
+
const getAtom = toGetter(input);
|
|
299
|
+
reportReads(registry, getAtom, kind);
|
|
300
|
+
$effect(() => registry.mount(getAtom()));
|
|
301
|
+
};
|
|
293
302
|
/**
|
|
294
303
|
* Keeps an atom alive for as long as this component is mounted, even when no component reads
|
|
295
304
|
* it. Use it in a component that outlives the readers, such as a layout, so the atom keeps its
|
|
@@ -310,14 +319,12 @@ export const useAtom = (input) => {
|
|
|
310
319
|
* @category hooks
|
|
311
320
|
*/
|
|
312
321
|
export const useAtomMount = (input) => {
|
|
313
|
-
|
|
314
|
-
const getAtom = toGetter(input);
|
|
315
|
-
$effect(() => registry.mount(getAtom()));
|
|
322
|
+
mountWhileAlive(input, "mount");
|
|
316
323
|
};
|
|
317
324
|
export function useAtomSet(input, options) {
|
|
318
325
|
const registry = getRegistry();
|
|
319
326
|
const getAtom = toGetter(input);
|
|
320
|
-
|
|
327
|
+
mountWhileAlive(getAtom, "write");
|
|
321
328
|
const mode = options?.mode ?? "value";
|
|
322
329
|
if (mode === "value") {
|
|
323
330
|
return (value) => {
|
|
@@ -442,6 +449,7 @@ const orderedDelivery = (f) => {
|
|
|
442
449
|
export const useAtomSubscribe = (input, f, options) => {
|
|
443
450
|
const registry = getRegistry();
|
|
444
451
|
const getAtom = toGetter(input);
|
|
452
|
+
reportReads(registry, getAtom, "subscribe");
|
|
445
453
|
$effect(() => {
|
|
446
454
|
const atom = getAtom();
|
|
447
455
|
// `immediate` calls `f` now, inside this effect; what `f` reads must not re-run it.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watches what an atom registry does, for developer tools: every node added and removed, every
|
|
3
|
+
* computation and why it ran, every value, reader, interruption and finalizer.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is watched until `inspect` is called. In development, the registries an app provides
|
|
6
|
+
* (and the default browser registry) are listed by `registries`, so a tool can find them without
|
|
7
|
+
* being handed one.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.2.0
|
|
10
|
+
*/
|
|
11
|
+
import { Atom } from "effect/reactivity";
|
|
12
|
+
import type { AtomRegistry } from "effect/reactivity";
|
|
13
|
+
import type { ReadKind } from "./internal/scope.svelte.ts";
|
|
14
|
+
/**
|
|
15
|
+
* Why a node computed its value.
|
|
16
|
+
*
|
|
17
|
+
* - `FirstRead`: its first computation.
|
|
18
|
+
* - `ParentChanged`: atoms it read changed since its last computation; `parents` are those nodes.
|
|
19
|
+
* - `Refreshed`: `registry.refresh` (or `useAtomRefresh`) asked for it, or for an atom it wraps.
|
|
20
|
+
* - `Invalidated`: something else made it stale, such as the atom refreshing itself (`refreshSelf`,
|
|
21
|
+
* a reactivity key, a window-focus signal).
|
|
22
|
+
*
|
|
23
|
+
* @stability unstable
|
|
24
|
+
* @since 0.2.0
|
|
25
|
+
* @category models
|
|
26
|
+
*/
|
|
27
|
+
export type BuildCause = {
|
|
28
|
+
readonly _tag: "FirstRead";
|
|
29
|
+
} | {
|
|
30
|
+
readonly _tag: "ParentChanged";
|
|
31
|
+
readonly parents: readonly AtomRegistry.Node<unknown>[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly _tag: "Refreshed";
|
|
34
|
+
} | {
|
|
35
|
+
readonly _tag: "Invalidated";
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Where a node's new value came from.
|
|
39
|
+
*
|
|
40
|
+
* - `build`: its computation returned it.
|
|
41
|
+
* - `write`: `registry.set`, `update` or `modify` (which the write hooks use), on it or on a
|
|
42
|
+
* writable atom that wrote it, and another atom's `get.set`, which goes through `registry.set`.
|
|
43
|
+
* - `async`: anything else, such as an effect settling or a stream emitting after the computation
|
|
44
|
+
* returned.
|
|
45
|
+
*
|
|
46
|
+
* @stability unstable
|
|
47
|
+
* @since 0.2.0
|
|
48
|
+
* @category models
|
|
49
|
+
*/
|
|
50
|
+
export type UpdateSource = "build" | "write" | "async";
|
|
51
|
+
/**
|
|
52
|
+
* Something a registry did to one of its nodes. `time` is from `performance.now()`.
|
|
53
|
+
*
|
|
54
|
+
* - `NodeAdded`: an atom was read, mounted or written for the first time, or again after it was
|
|
55
|
+
* removed, and the registry made a node for it.
|
|
56
|
+
* - `Built`: the node is about to compute its value, for the `cause` given.
|
|
57
|
+
* - `Updated`: its value changed. `first` marks its first value, which has no `previous`.
|
|
58
|
+
* - `ReadersChanged`: a listener was added or removed; `readers` is how many it has now. Hooks,
|
|
59
|
+
* `registry.subscribe` and `mount`, and atoms that subscribe to others all count.
|
|
60
|
+
* - `Interrupted`: its effect hadn't finished when what its computation started was torn down, so
|
|
61
|
+
* the fiber was interrupted. Followed by `Finalized`.
|
|
62
|
+
* - `Finalized`: what its last computation started was torn down, before it computes again or as
|
|
63
|
+
* it is removed; `finalizers` ran.
|
|
64
|
+
* - `NodeRemoved`: the registry disposed of the node, as nothing read it any more.
|
|
65
|
+
*
|
|
66
|
+
* @stability unstable
|
|
67
|
+
* @since 0.2.0
|
|
68
|
+
* @category models
|
|
69
|
+
*/
|
|
70
|
+
export type Event = {
|
|
71
|
+
readonly node: AtomRegistry.Node<unknown>;
|
|
72
|
+
readonly time: number;
|
|
73
|
+
} & ({
|
|
74
|
+
readonly _tag: "NodeAdded";
|
|
75
|
+
} | {
|
|
76
|
+
readonly _tag: "Built";
|
|
77
|
+
readonly cause: BuildCause;
|
|
78
|
+
} | {
|
|
79
|
+
readonly _tag: "Updated";
|
|
80
|
+
readonly first: boolean;
|
|
81
|
+
readonly previous: unknown;
|
|
82
|
+
readonly value: unknown;
|
|
83
|
+
readonly source: UpdateSource;
|
|
84
|
+
} | {
|
|
85
|
+
readonly _tag: "ReadersChanged";
|
|
86
|
+
readonly readers: number;
|
|
87
|
+
} | {
|
|
88
|
+
readonly _tag: "Interrupted";
|
|
89
|
+
} | {
|
|
90
|
+
readonly _tag: "Finalized";
|
|
91
|
+
readonly finalizers: number;
|
|
92
|
+
} | {
|
|
93
|
+
readonly _tag: "NodeRemoved";
|
|
94
|
+
});
|
|
95
|
+
/**
|
|
96
|
+
* What `inspect` returns for a registry.
|
|
97
|
+
*
|
|
98
|
+
* @stability unstable
|
|
99
|
+
* @since 0.2.0
|
|
100
|
+
* @category models
|
|
101
|
+
*/
|
|
102
|
+
export interface Inspector {
|
|
103
|
+
readonly registry: AtomRegistry.AtomRegistry;
|
|
104
|
+
/**
|
|
105
|
+
* Calls `listener` with each event as it happens; returns the function that stops it. Events
|
|
106
|
+
* arrive in the middle of the registry's work, so a listener must not read or write atoms, nor
|
|
107
|
+
* write Svelte state: queue the event and handle it later, for example on the next animation
|
|
108
|
+
* frame.
|
|
109
|
+
*/
|
|
110
|
+
readonly subscribe: (listener: (event: Event) => void) => () => void;
|
|
111
|
+
/**
|
|
112
|
+
* How long an atom nobody reads is kept before it is removed, in milliseconds: its own idle TTL,
|
|
113
|
+
* or the registry's default. `undefined` for an atom kept alive, and for one removed as soon as
|
|
114
|
+
* its last reader goes.
|
|
115
|
+
*/
|
|
116
|
+
readonly idleTTL: (atom: Atom.Atom<unknown>) => number | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* `false` when the registry's nodes aren't the implementation the inspector was written against
|
|
119
|
+
* (a later Effect, say). Then only `NodeAdded` and `NodeRemoved` are reported.
|
|
120
|
+
*/
|
|
121
|
+
readonly complete: boolean;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Starts watching a registry, and returns its inspector: the same one each time it is called for
|
|
125
|
+
* the same registry.
|
|
126
|
+
*
|
|
127
|
+
* Until then a registry runs as it always does. From then on, in a registry made by Effect 4.0,
|
|
128
|
+
* each node reports what happens to it, which costs a little on every read and write, so call it
|
|
129
|
+
* from developer tools rather than from the app.
|
|
130
|
+
*
|
|
131
|
+
* Nodes the registry already holds are watched from now on; what happened to them before isn't
|
|
132
|
+
* known. Read them with `registry.getNodes()`.
|
|
133
|
+
*
|
|
134
|
+
* **Example** (Logging every computation)
|
|
135
|
+
*
|
|
136
|
+
* ```ts
|
|
137
|
+
* import { getRegistry } from "effect-atom-svelte";
|
|
138
|
+
* import { inspect } from "effect-atom-svelte/inspector";
|
|
139
|
+
*
|
|
140
|
+
* const events = [];
|
|
141
|
+
* inspect(getRegistry()).subscribe((event) => events.push(event));
|
|
142
|
+
* setInterval(() => {
|
|
143
|
+
* for (const event of events.splice(0)) {
|
|
144
|
+
* if (event._tag === "Built") {
|
|
145
|
+
* console.log(event.node.atom.label?.[0], event.cause._tag);
|
|
146
|
+
* }
|
|
147
|
+
* }
|
|
148
|
+
* }, 1000);
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* @stability unstable
|
|
152
|
+
* @since 0.2.0
|
|
153
|
+
* @category inspecting
|
|
154
|
+
*/
|
|
155
|
+
export declare const inspect: (registry: AtomRegistry.AtomRegistry) => Inspector;
|
|
156
|
+
/**
|
|
157
|
+
* The registries the app has in the browser during development: each one a `RegistryProvider` or
|
|
158
|
+
* `provideRegistry` gives its children, while it is mounted, and the default browser registry once
|
|
159
|
+
* something has used it. Empty on the server and in production builds.
|
|
160
|
+
*
|
|
161
|
+
* @stability unstable
|
|
162
|
+
* @since 0.2.0
|
|
163
|
+
* @category inspecting
|
|
164
|
+
*/
|
|
165
|
+
export declare const registries: () => readonly AtomRegistry.AtomRegistry[];
|
|
166
|
+
/**
|
|
167
|
+
* Calls `f` with `registries()` now and whenever it changes, as providers mount and unmount;
|
|
168
|
+
* returns the function that stops it.
|
|
169
|
+
*
|
|
170
|
+
* @stability unstable
|
|
171
|
+
* @since 0.2.0
|
|
172
|
+
* @category inspecting
|
|
173
|
+
*/
|
|
174
|
+
export declare const watchRegistries: (f: (registries: readonly AtomRegistry.AtomRegistry[]) => void) => (() => void);
|
|
175
|
+
/**
|
|
176
|
+
* How a component uses an atom it reports to a scope: `read` (`useAtomValue`, `useAtom`,
|
|
177
|
+
* `useAtomResult`, `useAtomSuspense`), `mount` (`useAtomMount`), `subscribe`
|
|
178
|
+
* (`useAtomSubscribe`) or `write` (`useAtomSet`, which holds the atom mounted to write it).
|
|
179
|
+
*
|
|
180
|
+
* @stability unstable
|
|
181
|
+
* @since 0.2.0
|
|
182
|
+
* @category models
|
|
183
|
+
*/
|
|
184
|
+
export type ReaderKind = ReadKind;
|
|
185
|
+
/**
|
|
186
|
+
* A hook in the scope's part of the component tree that uses an atom. `component` and `file` name
|
|
187
|
+
* the component it is in, when the atomLabels plugin of effect-atom-svelte-devtools (or
|
|
188
|
+
* `nameComponent`) names components; `instance` tells two instances of one component apart.
|
|
189
|
+
*
|
|
190
|
+
* @stability unstable
|
|
191
|
+
* @since 0.2.0
|
|
192
|
+
* @category models
|
|
193
|
+
*/
|
|
194
|
+
export interface ScopeReader {
|
|
195
|
+
readonly id: number;
|
|
196
|
+
readonly kind: ReaderKind;
|
|
197
|
+
/** The `id` of the atom's node in the snapshot. */
|
|
198
|
+
readonly atom: number;
|
|
199
|
+
readonly component: string | undefined;
|
|
200
|
+
readonly file: string | undefined;
|
|
201
|
+
readonly instance: number | undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* An atom in a scope's snapshot.
|
|
205
|
+
*
|
|
206
|
+
* @stability unstable
|
|
207
|
+
* @since 0.2.0
|
|
208
|
+
* @category models
|
|
209
|
+
*/
|
|
210
|
+
export interface ScopeNode {
|
|
211
|
+
/** Stable for the atom while the scope lives. */
|
|
212
|
+
readonly id: number;
|
|
213
|
+
readonly atom: Atom.Atom<unknown>;
|
|
214
|
+
readonly node: AtomRegistry.Node<unknown>;
|
|
215
|
+
/** The atom's label (its variable's name, with the atomLabels plugin), if it has one. */
|
|
216
|
+
readonly label: string | undefined;
|
|
217
|
+
/** Used by a hook in the scope, rather than only upstream of one. */
|
|
218
|
+
readonly read: boolean;
|
|
219
|
+
/** Has no label while other atoms in the scope do: made by a runtime, mutation or store. */
|
|
220
|
+
readonly plumbing: boolean;
|
|
221
|
+
/** Every listener the node has: the scope's hooks, and anything else that subscribes. */
|
|
222
|
+
readonly listeners: number;
|
|
223
|
+
/** The tag of its value when that is an `AsyncResult`, otherwise `Value`. */
|
|
224
|
+
readonly state: "Value" | "Initial" | "Success" | "Failure";
|
|
225
|
+
readonly waiting: boolean;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* An edge from an atom to one that reads it. With plumbing left out, an atom that reads plumbing is
|
|
229
|
+
* linked to the nearest atoms upstream of it that are shown.
|
|
230
|
+
*
|
|
231
|
+
* @stability unstable
|
|
232
|
+
* @since 0.2.0
|
|
233
|
+
* @category models
|
|
234
|
+
*/
|
|
235
|
+
export interface ScopeEdge {
|
|
236
|
+
readonly from: number;
|
|
237
|
+
readonly to: number;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* What a scope's part of the component tree reads, at one moment: the atoms its hooks use, the
|
|
241
|
+
* atoms upstream of them, the edges between, and the hooks.
|
|
242
|
+
*
|
|
243
|
+
* @stability unstable
|
|
244
|
+
* @since 0.2.0
|
|
245
|
+
* @category models
|
|
246
|
+
*/
|
|
247
|
+
export interface ScopeSnapshot {
|
|
248
|
+
readonly nodes: readonly ScopeNode[];
|
|
249
|
+
readonly edges: readonly ScopeEdge[];
|
|
250
|
+
readonly readers: readonly ScopeReader[];
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* An event a scope passes on: the inspector's `Event` for a node in the scope, with the node's
|
|
254
|
+
* `id`, or `ScopeChanged` once its atoms, edges or hooks have changed, after the registry's work,
|
|
255
|
+
* to take a new `snapshot()`. A node's `NodeAdded` arrives before the node is linked into the
|
|
256
|
+
* scope, so it isn't passed on; the `ScopeChanged` that follows is.
|
|
257
|
+
*
|
|
258
|
+
* @stability unstable
|
|
259
|
+
* @since 0.2.0
|
|
260
|
+
* @category models
|
|
261
|
+
*/
|
|
262
|
+
export type ScopeEvent = (Event & {
|
|
263
|
+
readonly id: number;
|
|
264
|
+
}) | {
|
|
265
|
+
readonly _tag: "ScopeChanged";
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* A part of the component tree, as `provideInspectorScope` returns it.
|
|
269
|
+
*
|
|
270
|
+
* @stability unstable
|
|
271
|
+
* @since 0.2.0
|
|
272
|
+
* @category models
|
|
273
|
+
*/
|
|
274
|
+
export interface InspectorScope {
|
|
275
|
+
/** The atoms the scope reads now. `plumbing: true` includes those otherwise left out. */
|
|
276
|
+
readonly snapshot: (options?: {
|
|
277
|
+
readonly plumbing?: boolean;
|
|
278
|
+
}) => ScopeSnapshot;
|
|
279
|
+
/**
|
|
280
|
+
* Calls `listener` with each event for the scope's atoms; returns the function that stops it. The
|
|
281
|
+
* first listener starts the inspector on the registries the scope reads from. As with
|
|
282
|
+
* `Inspector.subscribe`, a listener must not read or write atoms or write Svelte state.
|
|
283
|
+
*/
|
|
284
|
+
readonly subscribe: (listener: (event: ScopeEvent) => void) => () => void;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Makes the component being set up, and everything below it, an inspector scope, and returns it.
|
|
288
|
+
* The library's hooks below report the atoms they use to the nearest scope, which shows those
|
|
289
|
+
* atoms, everything upstream of them, and the hooks that use them. Call it while the component
|
|
290
|
+
* sets up, as context requires.
|
|
291
|
+
*
|
|
292
|
+
* Only in the browser, in development and production alike (the effect-atom-svelte docs draw each
|
|
293
|
+
* example's atoms from one); on the server it returns a scope that is always empty. Until the
|
|
294
|
+
* scope has a listener it only keeps a list of its hooks: the registry's inspector starts with the
|
|
295
|
+
* first listener.
|
|
296
|
+
*
|
|
297
|
+
* **Example** (A graph of what an example reads)
|
|
298
|
+
*
|
|
299
|
+
* ```ts
|
|
300
|
+
* import { provideInspectorScope } from "effect-atom-svelte/inspector";
|
|
301
|
+
*
|
|
302
|
+
* // In the component that frames the example, around its content
|
|
303
|
+
* const scope = provideInspectorScope();
|
|
304
|
+
* $effect(() =>
|
|
305
|
+
* scope.subscribe((event) => {
|
|
306
|
+
* if (event._tag === "ScopeChanged") {
|
|
307
|
+
* draw(scope.snapshot());
|
|
308
|
+
* } else if (event._tag === "Updated") {
|
|
309
|
+
* pulse(event.id);
|
|
310
|
+
* }
|
|
311
|
+
* })
|
|
312
|
+
* );
|
|
313
|
+
* ```
|
|
314
|
+
*
|
|
315
|
+
* @stability unstable
|
|
316
|
+
* @since 0.2.0
|
|
317
|
+
* @category inspecting
|
|
318
|
+
*/
|
|
319
|
+
export declare const provideInspectorScope: () => InspectorScope;
|
|
320
|
+
/**
|
|
321
|
+
* Names the component being set up, for the scopes its hooks report to. The atomLabels plugin of
|
|
322
|
+
* effect-atom-svelte-devtools calls it at the top of each component's script; without the plugin,
|
|
323
|
+
* call it yourself. Only in the browser.
|
|
324
|
+
*
|
|
325
|
+
* @stability unstable
|
|
326
|
+
* @since 0.2.0
|
|
327
|
+
* @category inspecting
|
|
328
|
+
*/
|
|
329
|
+
export declare const nameComponent: (name: string, file?: string) => void;
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watches what an atom registry does, for developer tools: every node added and removed, every
|
|
3
|
+
* computation and why it ran, every value, reader, interruption and finalizer.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is watched until `inspect` is called. In development, the registries an app provides
|
|
6
|
+
* (and the default browser registry) are listed by `registries`, so a tool can find them without
|
|
7
|
+
* being handed one.
|
|
8
|
+
*
|
|
9
|
+
* @since 0.2.0
|
|
10
|
+
*/
|
|
11
|
+
import { AsyncResult, Atom } from "effect/reactivity";
|
|
12
|
+
import { BROWSER } from "esm-env";
|
|
13
|
+
import { defaultIdleTTL, instrumentNode } from "./internal/nodeInternals.js";
|
|
14
|
+
import { list, watch } from "./internal/registries.js";
|
|
15
|
+
import { nameComponent as setComponentName, setReporter, } from "./internal/scope.svelte.js";
|
|
16
|
+
const nodeKey = (atom) => Atom.isSerializable(atom) ? atom[Atom.SerializableTypeId].key : atom;
|
|
17
|
+
const install = (registry) => {
|
|
18
|
+
const listeners = new Set();
|
|
19
|
+
const records = new WeakMap();
|
|
20
|
+
let writing = 0;
|
|
21
|
+
let complete = true;
|
|
22
|
+
const emit = (event) => {
|
|
23
|
+
for (const listener of listeners) {
|
|
24
|
+
try {
|
|
25
|
+
listener(event);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
// A tool's mistake mustn't break the app's registry.
|
|
29
|
+
console.error("effect-atom-svelte: an inspector listener threw", error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const record = (node) => {
|
|
34
|
+
let found = records.get(node);
|
|
35
|
+
if (found === undefined) {
|
|
36
|
+
found = {
|
|
37
|
+
building: 0,
|
|
38
|
+
builds: 0,
|
|
39
|
+
changed: new Set(),
|
|
40
|
+
refreshed: false,
|
|
41
|
+
values: 0,
|
|
42
|
+
};
|
|
43
|
+
records.set(node, found);
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
};
|
|
47
|
+
const instrument = (node) => {
|
|
48
|
+
const instrumented = instrumentNode(node, {
|
|
49
|
+
build: () => {
|
|
50
|
+
const self = record(node);
|
|
51
|
+
let cause = { _tag: "Invalidated" };
|
|
52
|
+
if (self.builds === 0) {
|
|
53
|
+
cause = { _tag: "FirstRead" };
|
|
54
|
+
}
|
|
55
|
+
else if (self.refreshed) {
|
|
56
|
+
cause = { _tag: "Refreshed" };
|
|
57
|
+
}
|
|
58
|
+
else if (self.changed.size > 0) {
|
|
59
|
+
cause = { _tag: "ParentChanged", parents: [...self.changed] };
|
|
60
|
+
}
|
|
61
|
+
self.builds += 1;
|
|
62
|
+
self.refreshed = false;
|
|
63
|
+
self.changed = new Set();
|
|
64
|
+
self.building += 1;
|
|
65
|
+
emit({ _tag: "Built", cause, node, time: performance.now() });
|
|
66
|
+
},
|
|
67
|
+
built: () => {
|
|
68
|
+
record(node).building -= 1;
|
|
69
|
+
},
|
|
70
|
+
dispose: (finalizers, value) => {
|
|
71
|
+
if (finalizers === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// An effect atom's computation leaves a waiting result until its fiber is done, and a
|
|
75
|
+
// finalizer that interrupts the fiber.
|
|
76
|
+
if (AsyncResult.isAsyncResult(value) && value.waiting) {
|
|
77
|
+
emit({ _tag: "Interrupted", node, time: performance.now() });
|
|
78
|
+
}
|
|
79
|
+
emit({ _tag: "Finalized", finalizers, node, time: performance.now() });
|
|
80
|
+
},
|
|
81
|
+
readers: () => {
|
|
82
|
+
emit({
|
|
83
|
+
_tag: "ReadersChanged",
|
|
84
|
+
node,
|
|
85
|
+
readers: node.listeners.size,
|
|
86
|
+
time: performance.now(),
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
value: (previous, value) => {
|
|
90
|
+
const self = record(node);
|
|
91
|
+
const first = self.values === 0;
|
|
92
|
+
self.values += 1;
|
|
93
|
+
if (!first) {
|
|
94
|
+
for (const child of node.children) {
|
|
95
|
+
record(child).changed.add(node);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let source = "async";
|
|
99
|
+
if (self.building > 0) {
|
|
100
|
+
source = "build";
|
|
101
|
+
}
|
|
102
|
+
else if (writing > 0) {
|
|
103
|
+
source = "write";
|
|
104
|
+
}
|
|
105
|
+
emit({
|
|
106
|
+
_tag: "Updated",
|
|
107
|
+
first,
|
|
108
|
+
node,
|
|
109
|
+
previous,
|
|
110
|
+
source,
|
|
111
|
+
time: performance.now(),
|
|
112
|
+
value,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
complete &&= instrumented;
|
|
117
|
+
};
|
|
118
|
+
const patchable = registry;
|
|
119
|
+
const { modify, onNodeAdded, onNodeRemoved, refresh, set, update } = patchable;
|
|
120
|
+
const writes = (f) => {
|
|
121
|
+
writing += 1;
|
|
122
|
+
try {
|
|
123
|
+
return f();
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
writing -= 1;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
patchable.set = (atom, value) => writes(() => set.call(registry, atom, value));
|
|
130
|
+
patchable.update = (atom, f) => writes(() => update.call(registry, atom, f));
|
|
131
|
+
patchable.modify = (atom, f) => writes(() => modify.call(registry, atom, f));
|
|
132
|
+
// An atom with its own refresh (a wrapper such as withRefresh) calls the registry's refresh for
|
|
133
|
+
// the atoms it wraps, through this replacement, so each of those is marked too.
|
|
134
|
+
patchable.refresh = (atom) => {
|
|
135
|
+
const node = registry.getNodes().get(nodeKey(atom));
|
|
136
|
+
if (node !== undefined) {
|
|
137
|
+
record(node).refreshed = true;
|
|
138
|
+
}
|
|
139
|
+
refresh.call(registry, atom);
|
|
140
|
+
};
|
|
141
|
+
patchable.onNodeAdded = (node) => {
|
|
142
|
+
onNodeAdded?.(node);
|
|
143
|
+
instrument(node);
|
|
144
|
+
emit({ _tag: "NodeAdded", node, time: performance.now() });
|
|
145
|
+
};
|
|
146
|
+
patchable.onNodeRemoved = (node) => {
|
|
147
|
+
onNodeRemoved?.(node);
|
|
148
|
+
emit({ _tag: "NodeRemoved", node, time: performance.now() });
|
|
149
|
+
};
|
|
150
|
+
for (const node of registry.getNodes().values()) {
|
|
151
|
+
instrument(node);
|
|
152
|
+
// A node that has computed before isn't on its first read or value any more.
|
|
153
|
+
if (node.currentState() !== "uninitialized") {
|
|
154
|
+
const self = record(node);
|
|
155
|
+
self.builds = 1;
|
|
156
|
+
self.values = 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
get complete() {
|
|
161
|
+
return complete;
|
|
162
|
+
},
|
|
163
|
+
idleTTL: (atom) => {
|
|
164
|
+
if (atom.keepAlive) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return atom.idleTTL ?? defaultIdleTTL(registry);
|
|
168
|
+
},
|
|
169
|
+
registry,
|
|
170
|
+
subscribe: (listener) => {
|
|
171
|
+
listeners.add(listener);
|
|
172
|
+
return () => {
|
|
173
|
+
listeners.delete(listener);
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
const inspectors = new WeakMap();
|
|
179
|
+
/**
|
|
180
|
+
* Starts watching a registry, and returns its inspector: the same one each time it is called for
|
|
181
|
+
* the same registry.
|
|
182
|
+
*
|
|
183
|
+
* Until then a registry runs as it always does. From then on, in a registry made by Effect 4.0,
|
|
184
|
+
* each node reports what happens to it, which costs a little on every read and write, so call it
|
|
185
|
+
* from developer tools rather than from the app.
|
|
186
|
+
*
|
|
187
|
+
* Nodes the registry already holds are watched from now on; what happened to them before isn't
|
|
188
|
+
* known. Read them with `registry.getNodes()`.
|
|
189
|
+
*
|
|
190
|
+
* **Example** (Logging every computation)
|
|
191
|
+
*
|
|
192
|
+
* ```ts
|
|
193
|
+
* import { getRegistry } from "effect-atom-svelte";
|
|
194
|
+
* import { inspect } from "effect-atom-svelte/inspector";
|
|
195
|
+
*
|
|
196
|
+
* const events = [];
|
|
197
|
+
* inspect(getRegistry()).subscribe((event) => events.push(event));
|
|
198
|
+
* setInterval(() => {
|
|
199
|
+
* for (const event of events.splice(0)) {
|
|
200
|
+
* if (event._tag === "Built") {
|
|
201
|
+
* console.log(event.node.atom.label?.[0], event.cause._tag);
|
|
202
|
+
* }
|
|
203
|
+
* }
|
|
204
|
+
* }, 1000);
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* @stability unstable
|
|
208
|
+
* @since 0.2.0
|
|
209
|
+
* @category inspecting
|
|
210
|
+
*/
|
|
211
|
+
export const inspect = (registry) => {
|
|
212
|
+
let inspector = inspectors.get(registry);
|
|
213
|
+
if (inspector === undefined) {
|
|
214
|
+
inspector = install(registry);
|
|
215
|
+
inspectors.set(registry, inspector);
|
|
216
|
+
}
|
|
217
|
+
return inspector;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* The registries the app has in the browser during development: each one a `RegistryProvider` or
|
|
221
|
+
* `provideRegistry` gives its children, while it is mounted, and the default browser registry once
|
|
222
|
+
* something has used it. Empty on the server and in production builds.
|
|
223
|
+
*
|
|
224
|
+
* @stability unstable
|
|
225
|
+
* @since 0.2.0
|
|
226
|
+
* @category inspecting
|
|
227
|
+
*/
|
|
228
|
+
export const registries = () => list();
|
|
229
|
+
/**
|
|
230
|
+
* Calls `f` with `registries()` now and whenever it changes, as providers mount and unmount;
|
|
231
|
+
* returns the function that stops it.
|
|
232
|
+
*
|
|
233
|
+
* @stability unstable
|
|
234
|
+
* @since 0.2.0
|
|
235
|
+
* @category inspecting
|
|
236
|
+
*/
|
|
237
|
+
export const watchRegistries = (f) => watch(f);
|
|
238
|
+
const emptySnapshot = { edges: [], nodes: [], readers: [] };
|
|
239
|
+
/** The scope on the server: always empty. */
|
|
240
|
+
const inertScope = {
|
|
241
|
+
snapshot: () => emptySnapshot,
|
|
242
|
+
subscribe: () => () => undefined,
|
|
243
|
+
};
|
|
244
|
+
const stateOf = (node) => {
|
|
245
|
+
// A valid node's value is read without computing anything.
|
|
246
|
+
const value = node.currentState() === "valid" ? node.value() : undefined;
|
|
247
|
+
return AsyncResult.isAsyncResult(value)
|
|
248
|
+
? { state: value._tag, waiting: value.waiting }
|
|
249
|
+
: { state: "Value", waiting: false };
|
|
250
|
+
};
|
|
251
|
+
class Scope {
|
|
252
|
+
#reads = new Map();
|
|
253
|
+
#ids = new WeakMap();
|
|
254
|
+
#listeners = new Set();
|
|
255
|
+
// The inspectors the scope follows while it has listeners, by registry.
|
|
256
|
+
#following = new Map();
|
|
257
|
+
#nextId = 0;
|
|
258
|
+
#nextRead = 0;
|
|
259
|
+
// The nodes in the scope, worked out again only after something that can change them.
|
|
260
|
+
#members;
|
|
261
|
+
// The last nodes worked out, so a node removed from the scope still has its removal passed on.
|
|
262
|
+
#previous = new Set();
|
|
263
|
+
#signature = "";
|
|
264
|
+
#checking = false;
|
|
265
|
+
reporter = {
|
|
266
|
+
report: ({ atom, component, kind, registry }) => {
|
|
267
|
+
this.#nextRead += 1;
|
|
268
|
+
const id = this.#nextRead;
|
|
269
|
+
this.#reads.set(id, { atom, component, id, kind, registry });
|
|
270
|
+
this.#follow(registry);
|
|
271
|
+
this.#changed();
|
|
272
|
+
return () => {
|
|
273
|
+
this.#reads.delete(id);
|
|
274
|
+
this.#changed();
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
#id(atom) {
|
|
279
|
+
let id = this.#ids.get(atom);
|
|
280
|
+
if (id === undefined) {
|
|
281
|
+
this.#nextId += 1;
|
|
282
|
+
id = this.#nextId;
|
|
283
|
+
this.#ids.set(atom, id);
|
|
284
|
+
}
|
|
285
|
+
return id;
|
|
286
|
+
}
|
|
287
|
+
/** Every node the scope's hooks use, and every node upstream of them. */
|
|
288
|
+
#nodes() {
|
|
289
|
+
if (this.#members !== undefined) {
|
|
290
|
+
return this.#members;
|
|
291
|
+
}
|
|
292
|
+
const members = new Set();
|
|
293
|
+
const add = (node) => {
|
|
294
|
+
if (!members.has(node)) {
|
|
295
|
+
members.add(node);
|
|
296
|
+
for (const parent of node.parents) {
|
|
297
|
+
add(parent);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
for (const read of this.#reads.values()) {
|
|
302
|
+
const node = read.registry.getNodes().get(nodeKey(read.atom));
|
|
303
|
+
if (node !== undefined) {
|
|
304
|
+
add(node);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this.#members = members;
|
|
308
|
+
return members;
|
|
309
|
+
}
|
|
310
|
+
/** The nearest shown atoms upstream of `node`, looking through those not shown. */
|
|
311
|
+
#shownParents(node, shown) {
|
|
312
|
+
const found = new Set();
|
|
313
|
+
const seen = new Set();
|
|
314
|
+
const walk = (parents) => {
|
|
315
|
+
for (const parent of parents) {
|
|
316
|
+
const id = this.#id(parent.atom);
|
|
317
|
+
if (shown.has(id)) {
|
|
318
|
+
found.add(id);
|
|
319
|
+
}
|
|
320
|
+
else if (!seen.has(parent)) {
|
|
321
|
+
seen.add(parent);
|
|
322
|
+
walk(parent.parents);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
walk(node.parents);
|
|
327
|
+
found.delete(this.#id(node.atom));
|
|
328
|
+
return found;
|
|
329
|
+
}
|
|
330
|
+
snapshot(options) {
|
|
331
|
+
const members = [...this.#nodes()];
|
|
332
|
+
const read = new Set([...this.#reads.values()].map((entry) => this.#id(entry.atom)));
|
|
333
|
+
const labelled = members.some((node) => node.atom.label !== undefined);
|
|
334
|
+
const nodes = members.map((node) => {
|
|
335
|
+
const id = this.#id(node.atom);
|
|
336
|
+
return {
|
|
337
|
+
atom: node.atom,
|
|
338
|
+
id,
|
|
339
|
+
label: node.atom.label?.[0],
|
|
340
|
+
listeners: node.listeners.size,
|
|
341
|
+
node,
|
|
342
|
+
plumbing: labelled && node.atom.label === undefined,
|
|
343
|
+
read: read.has(id),
|
|
344
|
+
...stateOf(node),
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
// An atom a hook uses is shown even without a label: it is what the component uses.
|
|
348
|
+
const shown = nodes.filter((node) => options?.plumbing === true || !node.plumbing || node.read);
|
|
349
|
+
const shownIds = new Set(shown.map((node) => node.id));
|
|
350
|
+
const edges = shown.flatMap((node) => [...this.#shownParents(node.node, shownIds)].map((from) => ({
|
|
351
|
+
from,
|
|
352
|
+
to: node.id,
|
|
353
|
+
})));
|
|
354
|
+
const readers = [...this.#reads.values()].map((entry) => ({
|
|
355
|
+
atom: this.#id(entry.atom),
|
|
356
|
+
component: entry.component?.name,
|
|
357
|
+
file: entry.component?.file,
|
|
358
|
+
id: entry.id,
|
|
359
|
+
instance: entry.component?.instance,
|
|
360
|
+
kind: entry.kind,
|
|
361
|
+
}));
|
|
362
|
+
return { edges, nodes: shown, readers };
|
|
363
|
+
}
|
|
364
|
+
subscribe(listener) {
|
|
365
|
+
this.#listeners.add(listener);
|
|
366
|
+
for (const read of this.#reads.values()) {
|
|
367
|
+
this.#follow(read.registry);
|
|
368
|
+
}
|
|
369
|
+
this.#signature = this.#sign();
|
|
370
|
+
return () => {
|
|
371
|
+
this.#listeners.delete(listener);
|
|
372
|
+
if (this.#listeners.size === 0) {
|
|
373
|
+
for (const stop of this.#following.values()) {
|
|
374
|
+
stop();
|
|
375
|
+
}
|
|
376
|
+
this.#following.clear();
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
#follow(registry) {
|
|
381
|
+
if (this.#listeners.size === 0 || this.#following.has(registry)) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
this.#following.set(registry, inspect(registry).subscribe((event) => this.#receive(event)));
|
|
385
|
+
}
|
|
386
|
+
#receive(event) {
|
|
387
|
+
const inScope = this.#previous.has(event.node) || this.#nodes().has(event.node);
|
|
388
|
+
if (event._tag === "NodeAdded" ||
|
|
389
|
+
event._tag === "NodeRemoved" ||
|
|
390
|
+
event._tag === "Built") {
|
|
391
|
+
this.#changed();
|
|
392
|
+
}
|
|
393
|
+
if (inScope) {
|
|
394
|
+
this.#emit({ ...event, id: this.#id(event.node.atom) });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
#emit(event) {
|
|
398
|
+
for (const listener of this.#listeners) {
|
|
399
|
+
try {
|
|
400
|
+
listener(event);
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
console.error("effect-atom-svelte: an inspector scope listener threw", error);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** What the snapshot shows, in short, to tell whether it changed. */
|
|
408
|
+
#sign() {
|
|
409
|
+
const { edges, nodes, readers } = this.snapshot();
|
|
410
|
+
return [
|
|
411
|
+
nodes.map((node) => node.id).join(","),
|
|
412
|
+
edges.map((edge) => `${edge.from}>${edge.to}`).join(","),
|
|
413
|
+
readers.map((reader) => `${reader.id}:${reader.atom}`).join(","),
|
|
414
|
+
].join("|");
|
|
415
|
+
}
|
|
416
|
+
/** The scope may have changed: work it out again once the registry's work is done. */
|
|
417
|
+
#changed() {
|
|
418
|
+
if (this.#members !== undefined) {
|
|
419
|
+
this.#previous = this.#members;
|
|
420
|
+
}
|
|
421
|
+
this.#members = undefined;
|
|
422
|
+
if (this.#checking || this.#listeners.size === 0) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
this.#checking = true;
|
|
426
|
+
queueMicrotask(() => {
|
|
427
|
+
this.#checking = false;
|
|
428
|
+
const signature = this.#sign();
|
|
429
|
+
this.#previous = this.#nodes();
|
|
430
|
+
if (signature !== this.#signature) {
|
|
431
|
+
this.#signature = signature;
|
|
432
|
+
this.#emit({ _tag: "ScopeChanged" });
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Makes the component being set up, and everything below it, an inspector scope, and returns it.
|
|
439
|
+
* The library's hooks below report the atoms they use to the nearest scope, which shows those
|
|
440
|
+
* atoms, everything upstream of them, and the hooks that use them. Call it while the component
|
|
441
|
+
* sets up, as context requires.
|
|
442
|
+
*
|
|
443
|
+
* Only in the browser, in development and production alike (the effect-atom-svelte docs draw each
|
|
444
|
+
* example's atoms from one); on the server it returns a scope that is always empty. Until the
|
|
445
|
+
* scope has a listener it only keeps a list of its hooks: the registry's inspector starts with the
|
|
446
|
+
* first listener.
|
|
447
|
+
*
|
|
448
|
+
* **Example** (A graph of what an example reads)
|
|
449
|
+
*
|
|
450
|
+
* ```ts
|
|
451
|
+
* import { provideInspectorScope } from "effect-atom-svelte/inspector";
|
|
452
|
+
*
|
|
453
|
+
* // In the component that frames the example, around its content
|
|
454
|
+
* const scope = provideInspectorScope();
|
|
455
|
+
* $effect(() =>
|
|
456
|
+
* scope.subscribe((event) => {
|
|
457
|
+
* if (event._tag === "ScopeChanged") {
|
|
458
|
+
* draw(scope.snapshot());
|
|
459
|
+
* } else if (event._tag === "Updated") {
|
|
460
|
+
* pulse(event.id);
|
|
461
|
+
* }
|
|
462
|
+
* })
|
|
463
|
+
* );
|
|
464
|
+
* ```
|
|
465
|
+
*
|
|
466
|
+
* @stability unstable
|
|
467
|
+
* @since 0.2.0
|
|
468
|
+
* @category inspecting
|
|
469
|
+
*/
|
|
470
|
+
export const provideInspectorScope = () => {
|
|
471
|
+
if (!BROWSER) {
|
|
472
|
+
return inertScope;
|
|
473
|
+
}
|
|
474
|
+
const scope = new Scope();
|
|
475
|
+
setReporter(scope.reporter);
|
|
476
|
+
return scope;
|
|
477
|
+
};
|
|
478
|
+
/**
|
|
479
|
+
* Names the component being set up, for the scopes its hooks report to. The atomLabels plugin of
|
|
480
|
+
* effect-atom-svelte-devtools calls it at the top of each component's script; without the plugin,
|
|
481
|
+
* call it yourself. Only in the browser.
|
|
482
|
+
*
|
|
483
|
+
* @stability unstable
|
|
484
|
+
* @since 0.2.0
|
|
485
|
+
* @category inspecting
|
|
486
|
+
*/
|
|
487
|
+
export const nameComponent = (name, file) => {
|
|
488
|
+
if (BROWSER) {
|
|
489
|
+
setComponentName(name, file);
|
|
490
|
+
}
|
|
491
|
+
};
|
package/dist/RegistryContext.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* @since 0.1.0
|
|
5
5
|
*/
|
|
6
6
|
import { AtomRegistry } from "effect/reactivity";
|
|
7
|
-
import { BROWSER } from "esm-env";
|
|
7
|
+
import { BROWSER, DEV } from "esm-env";
|
|
8
8
|
import { createContext, onDestroy } from "svelte";
|
|
9
9
|
import { setRevalidateOnHydrate } from "./internal/hydration.js";
|
|
10
|
+
import { track } from "./internal/registries.js";
|
|
10
11
|
const [getContextRegistry, setContextRegistry, hasContextRegistry] = createContext();
|
|
11
12
|
let browserRegistry;
|
|
12
13
|
/**
|
|
@@ -36,7 +37,12 @@ export const getRegistry = () => {
|
|
|
36
37
|
if (!BROWSER) {
|
|
37
38
|
throw new Error("No AtomRegistry in context. Wrap the app in <RegistryProvider> so each server request gets its own registry.");
|
|
38
39
|
}
|
|
39
|
-
browserRegistry
|
|
40
|
+
if (browserRegistry === undefined) {
|
|
41
|
+
browserRegistry = AtomRegistry.make();
|
|
42
|
+
if (DEV) {
|
|
43
|
+
track(browserRegistry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
40
46
|
return browserRegistry;
|
|
41
47
|
};
|
|
42
48
|
/**
|
|
@@ -81,5 +87,9 @@ export const provideRegistry = (options = {}) => {
|
|
|
81
87
|
if (!provided) {
|
|
82
88
|
onDestroy(() => registry.dispose());
|
|
83
89
|
}
|
|
90
|
+
if (DEV && BROWSER) {
|
|
91
|
+
// For developer tools to find (effect-atom-svelte/inspector).
|
|
92
|
+
onDestroy(track(registry));
|
|
93
|
+
}
|
|
84
94
|
return registry;
|
|
85
95
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AtomRegistry } from "effect/reactivity";
|
|
2
|
+
/** What `instrumentNode` reports. */
|
|
3
|
+
export interface NodeHooks {
|
|
4
|
+
/** The node is about to compute its value. */
|
|
5
|
+
readonly build: () => void;
|
|
6
|
+
/** The computation `build` announced has returned or thrown. */
|
|
7
|
+
readonly built: () => void;
|
|
8
|
+
/** The node's value changed from `previous` to `next`. */
|
|
9
|
+
readonly value: (previous: unknown, next: unknown) => void;
|
|
10
|
+
/**
|
|
11
|
+
* What the last computation started is about to be torn down: `finalizers` is how many will run,
|
|
12
|
+
* and `value` is the node's value, still the one that computation produced.
|
|
13
|
+
*/
|
|
14
|
+
readonly dispose: (finalizers: number, value: unknown) => void;
|
|
15
|
+
/** A listener was added or removed; the node's `listeners` hold the new set. */
|
|
16
|
+
readonly readers: () => void;
|
|
17
|
+
}
|
|
18
|
+
/** Reports what happens to a node, or returns false if it isn't the implementation this expects. */
|
|
19
|
+
export declare const instrumentNode: (node: AtomRegistry.Node<unknown>, hooks: NodeHooks) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The idle TTL a registry gives atoms without their own (`AtomRegistry.make`'s `defaultIdleTTL`),
|
|
22
|
+
* which the implementation keeps but the interface doesn't show.
|
|
23
|
+
*/
|
|
24
|
+
export declare const defaultIdleTTL: (registry: AtomRegistry.AtomRegistry) => number | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const isNodeImpl = (node) => Object.hasOwn(node, "_value") &&
|
|
2
|
+
Object.hasOwn(node, "lifetime") &&
|
|
3
|
+
typeof node.build === "function" &&
|
|
4
|
+
node.listeners instanceof Set;
|
|
5
|
+
/** A node's listeners that report each one added or removed. */
|
|
6
|
+
class Listeners extends Set {
|
|
7
|
+
#changed;
|
|
8
|
+
constructor(listeners, changed) {
|
|
9
|
+
super();
|
|
10
|
+
for (const listener of listeners) {
|
|
11
|
+
super.add(listener);
|
|
12
|
+
}
|
|
13
|
+
this.#changed = changed;
|
|
14
|
+
}
|
|
15
|
+
add(listener) {
|
|
16
|
+
const added = !this.has(listener);
|
|
17
|
+
super.add(listener);
|
|
18
|
+
if (added) {
|
|
19
|
+
this.#changed();
|
|
20
|
+
}
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
delete(listener) {
|
|
24
|
+
const deleted = super.delete(listener);
|
|
25
|
+
if (deleted) {
|
|
26
|
+
this.#changed();
|
|
27
|
+
}
|
|
28
|
+
return deleted;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Reports what happens to a node, or returns false if it isn't the implementation this expects. */
|
|
32
|
+
export const instrumentNode = (node, hooks) => {
|
|
33
|
+
if (!isNodeImpl(node)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const { build: compute } = node;
|
|
37
|
+
let value = node._value;
|
|
38
|
+
let { lifetime } = node;
|
|
39
|
+
node.build = function build() {
|
|
40
|
+
hooks.build();
|
|
41
|
+
try {
|
|
42
|
+
compute.call(this);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
hooks.built();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
Object.defineProperties(node, {
|
|
49
|
+
_value: {
|
|
50
|
+
configurable: true,
|
|
51
|
+
enumerable: true,
|
|
52
|
+
get: () => value,
|
|
53
|
+
set: (next) => {
|
|
54
|
+
const previous = value;
|
|
55
|
+
value = next;
|
|
56
|
+
hooks.value(previous, next);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
lifetime: {
|
|
60
|
+
configurable: true,
|
|
61
|
+
enumerable: true,
|
|
62
|
+
get: () => lifetime,
|
|
63
|
+
set: (next) => {
|
|
64
|
+
const previous = lifetime;
|
|
65
|
+
lifetime = next;
|
|
66
|
+
if (next === undefined && previous !== undefined) {
|
|
67
|
+
hooks.dispose(previous.finalizers?.length ?? 0, value);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
node.listeners = new Listeners(node.listeners, hooks.readers);
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* The idle TTL a registry gives atoms without their own (`AtomRegistry.make`'s `defaultIdleTTL`),
|
|
77
|
+
* which the implementation keeps but the interface doesn't show.
|
|
78
|
+
*/
|
|
79
|
+
export const defaultIdleTTL = (registry) => {
|
|
80
|
+
const ttl = registry
|
|
81
|
+
.defaultIdleTTL;
|
|
82
|
+
return typeof ttl === "number" ? ttl : undefined;
|
|
83
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AtomRegistry } from "effect/reactivity";
|
|
2
|
+
export declare const list: () => readonly AtomRegistry.AtomRegistry[];
|
|
3
|
+
/** Adds a registry to the list; returns the function that takes it off again. */
|
|
4
|
+
export declare const track: (registry: AtomRegistry.AtomRegistry) => (() => void);
|
|
5
|
+
/** Calls `f` with the list now and whenever it changes; returns the function that stops it. */
|
|
6
|
+
export declare const watch: (f: (registries: readonly AtomRegistry.AtomRegistry[]) => void) => (() => void);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// How many providers hold each registry: one passed to several providers stays until all unmount.
|
|
2
|
+
const holders = new Map();
|
|
3
|
+
const watchers = new Set();
|
|
4
|
+
export const list = () => [
|
|
5
|
+
...holders.keys(),
|
|
6
|
+
];
|
|
7
|
+
const announce = () => {
|
|
8
|
+
const registries = list();
|
|
9
|
+
for (const watcher of watchers) {
|
|
10
|
+
watcher(registries);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** Adds a registry to the list; returns the function that takes it off again. */
|
|
14
|
+
export const track = (registry) => {
|
|
15
|
+
holders.set(registry, (holders.get(registry) ?? 0) + 1);
|
|
16
|
+
announce();
|
|
17
|
+
let released = false;
|
|
18
|
+
return () => {
|
|
19
|
+
if (released) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
released = true;
|
|
23
|
+
const count = (holders.get(registry) ?? 1) - 1;
|
|
24
|
+
if (count === 0) {
|
|
25
|
+
holders.delete(registry);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
holders.set(registry, count);
|
|
29
|
+
}
|
|
30
|
+
announce();
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
/** Calls `f` with the list now and whenever it changes; returns the function that stops it. */
|
|
34
|
+
export const watch = (f) => {
|
|
35
|
+
watchers.add(f);
|
|
36
|
+
f(list());
|
|
37
|
+
return () => {
|
|
38
|
+
watchers.delete(f);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Atom, AtomRegistry } from "effect/reactivity";
|
|
2
|
+
/** How a hook uses its atom: reads it, holds it mounted, subscribes to it, or writes it. */
|
|
3
|
+
export type ReadKind = "read" | "mount" | "subscribe" | "write";
|
|
4
|
+
/** A component, as the atomLabels plugin names it: `Counter`, from `/src/routes/counter.svelte`. */
|
|
5
|
+
export interface ComponentName {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly file: string | undefined;
|
|
8
|
+
/** One per instance of the component, so two `<Counter>`s are told apart. */
|
|
9
|
+
readonly instance: number;
|
|
10
|
+
}
|
|
11
|
+
/** What a scope takes from the hooks. */
|
|
12
|
+
export interface Reporter {
|
|
13
|
+
/** A hook started using `atom`; returns the function to call when it stops. */
|
|
14
|
+
readonly report: (read: {
|
|
15
|
+
readonly registry: AtomRegistry.AtomRegistry;
|
|
16
|
+
readonly atom: Atom.Atom<unknown>;
|
|
17
|
+
readonly kind: ReadKind;
|
|
18
|
+
readonly component: ComponentName | undefined;
|
|
19
|
+
}) => () => void;
|
|
20
|
+
}
|
|
21
|
+
declare const setReporter: (context: Reporter) => Reporter;
|
|
22
|
+
export { setReporter };
|
|
23
|
+
/** Names the component being set up, for the scopes its hooks report to. */
|
|
24
|
+
export declare const nameComponent: (name: string, file?: string) => void;
|
|
25
|
+
/**
|
|
26
|
+
* Reports the atom a hook uses, following its getter, while the component lives. Called while the
|
|
27
|
+
* hook sets up; adds an effect only in the browser, and only under a scope.
|
|
28
|
+
*/
|
|
29
|
+
export declare const reportReads: (registry: AtomRegistry.AtomRegistry, getAtom: () => Atom.Atom<unknown>, kind: ReadKind) => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BROWSER } from "esm-env";
|
|
2
|
+
import { createContext } from "svelte";
|
|
3
|
+
const [getReporter, setReporter, hasReporter] = createContext();
|
|
4
|
+
const [getComponent, setComponent, hasComponent] = createContext();
|
|
5
|
+
export { setReporter };
|
|
6
|
+
let instances = 0;
|
|
7
|
+
/** Names the component being set up, for the scopes its hooks report to. */
|
|
8
|
+
export const nameComponent = (name, file) => {
|
|
9
|
+
instances += 1;
|
|
10
|
+
setComponent({ file, instance: instances, name });
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Reports the atom a hook uses, following its getter, while the component lives. Called while the
|
|
14
|
+
* hook sets up; adds an effect only in the browser, and only under a scope.
|
|
15
|
+
*/
|
|
16
|
+
export const reportReads = (registry, getAtom, kind) => {
|
|
17
|
+
if (!BROWSER || !hasReporter()) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const reporter = getReporter();
|
|
21
|
+
const component = hasComponent() ? getComponent() : undefined;
|
|
22
|
+
$effect(() => reporter.report({ atom: getAtom(), component, kind, registry }));
|
|
23
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-atom-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Community-built Svelte 5 bindings for Effect Atom (not affiliated with the Effect team)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"effect",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"svelte": "./dist/index.js",
|
|
34
34
|
"default": "./dist/index.js"
|
|
35
35
|
},
|
|
36
|
+
"./inspector": {
|
|
37
|
+
"types": "./dist/Inspector.d.ts",
|
|
38
|
+
"default": "./dist/Inspector.js"
|
|
39
|
+
},
|
|
36
40
|
"./sveltekit": {
|
|
37
41
|
"types": "./dist/SvelteKit.d.ts",
|
|
38
42
|
"default": "./dist/SvelteKit.js"
|