effect-atom-svelte 0.1.2 → 0.3.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/README.md +1 -1
- package/dist/Hooks.svelte.d.ts +16 -10
- package/dist/Hooks.svelte.js +443 -143
- package/dist/HydrationBoundary.svelte +130 -12
- package/dist/HydrationBoundary.svelte.d.ts +13 -4
- package/dist/Inspector.d.ts +333 -0
- package/dist/Inspector.js +558 -0
- package/dist/RegistryContext.d.ts +2 -0
- package/dist/RegistryContext.js +22 -6
- package/dist/ScopedAtom.d.ts +5 -4
- package/dist/ScopedAtom.js +5 -4
- package/dist/SvelteKit.d.ts +28 -3
- package/dist/SvelteKit.js +9 -42
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/boundaries.d.ts +9 -0
- package/dist/internal/boundaries.js +8 -0
- package/dist/internal/disposal.d.ts +13 -0
- package/dist/internal/disposal.js +43 -0
- package/dist/internal/hydration.d.ts +11 -2
- package/dist/internal/hydration.js +23 -3
- package/dist/internal/nodeInternals.d.ts +30 -0
- package/dist/internal/nodeInternals.js +107 -0
- package/dist/internal/registries.d.ts +6 -0
- package/dist/internal/registries.js +40 -0
- package/dist/internal/renderEnd.d.ts +13 -0
- package/dist/internal/renderEnd.js +34 -0
- package/dist/internal/scope.svelte.d.ts +29 -0
- package/dist/internal/scope.svelte.js +23 -0
- package/dist/internal/teardown.svelte.d.ts +17 -0
- package/dist/internal/teardown.svelte.js +44 -0
- package/dist/internal/troubleshooting.d.ts +2 -0
- package/dist/internal/troubleshooting.js +3 -0
- package/package.json +5 -1
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
@component
|
|
3
3
|
Hydrates dehydrated atom state, for example from `Hydration.dehydrate` returned by a remote
|
|
4
|
-
function. Atoms new to the registry
|
|
5
|
-
|
|
6
|
-
mid-render. On the server, where nothing
|
|
4
|
+
function. Atoms new to the registry, or holding only an initial value nobody has read yet, are
|
|
5
|
+
hydrated before children render; in the browser, atoms that already exist are updated after
|
|
6
|
+
render, so current UI does not jump to the incoming data mid-render. On the server, where nothing
|
|
7
|
+
renders again, and in the browser while it hydrates the server's markup, they are updated at once.
|
|
8
|
+
That needs Svelte's `experimental.async`: without it, the browser can't tell that it is hydrating,
|
|
9
|
+
and atoms that already exist, as one read above the boundary, are updated after the first render.
|
|
10
|
+
|
|
11
|
+
With `experimental.async` on, custom server rendering must `await render(...)`, as SvelteKit does,
|
|
12
|
+
so the boundary's `hydratable` entry is written into the page. Otherwise a production build of
|
|
13
|
+
Svelte logs `hydratable_missing_but_expected` once per boundary as the page hydrates.
|
|
7
14
|
|
|
8
15
|
A value for an atom nobody reads waits in the registry until something does. When the boundary
|
|
9
16
|
is destroyed, as at the end of a server render, the values it brought that are still unread are
|
|
10
|
-
dropped,
|
|
17
|
+
dropped, as is a promise-encoded value that lands afterwards, so a registry that outlives the
|
|
18
|
+
boundary doesn't hand them to a later reader. In the browser, a reader that already held the atom
|
|
19
|
+
when the boundary ended and still holds it, as a layout's reader above it, gets the late value.
|
|
11
20
|
|
|
12
21
|
**Example** (Hydrating the state a load function dehydrated)
|
|
13
22
|
|
|
@@ -28,10 +37,15 @@
|
|
|
28
37
|
-->
|
|
29
38
|
<script lang="ts">
|
|
30
39
|
import { Hydration } from "effect/reactivity";
|
|
40
|
+
import type { AtomRegistry } from "effect/reactivity";
|
|
31
41
|
import { BROWSER } from "esm-env";
|
|
32
|
-
import {
|
|
42
|
+
import { hydratable } from "svelte";
|
|
33
43
|
import type { Snippet } from "svelte";
|
|
34
44
|
|
|
45
|
+
import { queuedBy } from "./internal/boundaries.js";
|
|
46
|
+
import { holdsInitialValue } from "./internal/nodeInternals.js";
|
|
47
|
+
import { onRenderEnd } from "./internal/renderEnd.js";
|
|
48
|
+
import { onTeardownAfterChildren } from "./internal/teardown.svelte.js";
|
|
35
49
|
import { getRegistry } from "./RegistryContext.js";
|
|
36
50
|
|
|
37
51
|
interface Props {
|
|
@@ -49,14 +63,93 @@
|
|
|
49
63
|
};
|
|
50
64
|
// The values this boundary queued, by key, so it can drop the ones nobody took.
|
|
51
65
|
const queued = new Map<string, unknown>();
|
|
66
|
+
// Whether the boundary has ended, as at the end of the server render or once the browser destroys
|
|
67
|
+
// it, after which a late promise-encoded value is dropped.
|
|
68
|
+
let ended = false;
|
|
69
|
+
|
|
70
|
+
const counts =
|
|
71
|
+
queuedBy.get(registry) ?? new Map<string, Map<unknown, number>>();
|
|
72
|
+
queuedBy.set(registry, counts);
|
|
73
|
+
/** Adds one to or takes one from the boundaries queueing a value, and returns the new count. */
|
|
74
|
+
const count = (key: string, value: unknown, delta: 1 | -1): number => {
|
|
75
|
+
const byValue = counts.get(key) ?? new Map<unknown, number>();
|
|
76
|
+
counts.set(key, byValue);
|
|
77
|
+
const next = (byValue.get(value) ?? 0) + delta;
|
|
78
|
+
if (next > 0) {
|
|
79
|
+
byValue.set(value, next);
|
|
80
|
+
} else {
|
|
81
|
+
byValue.delete(value);
|
|
82
|
+
if (byValue.size === 0) {
|
|
83
|
+
counts.delete(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return next;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// SAFETY: Hydration.hydrate ignores a promise-encoded value that resolves to this marker, which
|
|
90
|
+
// Hydration.ts registers with Symbol.for (Effect 4.0.1) but doesn't export.
|
|
91
|
+
const skipped = Symbol.for("effect/reactivity/Hydration/Skipped");
|
|
92
|
+
// The keys of the promise-encoded values the boundary is waiting for.
|
|
93
|
+
const waiting = new Set<string>();
|
|
94
|
+
// In the browser, the node that held each of those keys when the boundary ended. That node, as a
|
|
95
|
+
// layout's reader above the boundary that took its waiting value, still needs the result, or it
|
|
96
|
+
// waits forever. A node a later reader creates doesn't get it, nor does one that replaced a node
|
|
97
|
+
// swept since, as a reader inside the boundary's is just after it ends.
|
|
98
|
+
const nodesAtEnd = new Map<string, AtomRegistry.Node<unknown>>();
|
|
99
|
+
/** A promise-encoded value as the boundary waits for it; one landing after the boundary ends is skipped. */
|
|
100
|
+
const lateDropped = async (
|
|
101
|
+
key: string,
|
|
102
|
+
late: Promise<unknown>
|
|
103
|
+
): Promise<unknown> => {
|
|
104
|
+
waiting.add(key);
|
|
105
|
+
const value = await late;
|
|
106
|
+
if (!ended) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
const held = BROWSER ? nodesAtEnd.get(key) : undefined;
|
|
110
|
+
return held !== undefined && registry.getNodes().get(key) === held
|
|
111
|
+
? value
|
|
112
|
+
: skipped;
|
|
113
|
+
};
|
|
52
114
|
|
|
53
115
|
const queue = (atoms: readonly Hydration.DehydratedAtomValue[]): void => {
|
|
54
|
-
|
|
116
|
+
// A promise-encoded value landing after the boundary has ended, after the server render or once
|
|
117
|
+
// the browser has navigated away, would stay queued in a registry that outlives it for a later
|
|
118
|
+
// reader, however late: it is ignored once the boundary ends, unless, in the browser, the node
|
|
119
|
+
// that held its key then still does.
|
|
120
|
+
Hydration.hydrate(
|
|
121
|
+
registry,
|
|
122
|
+
atoms.map((atom) =>
|
|
123
|
+
atom.resultPromise === undefined
|
|
124
|
+
? atom
|
|
125
|
+
: {
|
|
126
|
+
...atom,
|
|
127
|
+
resultPromise: lateDropped(atom.key, atom.resultPromise),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
);
|
|
55
131
|
for (const { key, value } of atoms) {
|
|
132
|
+
if (queued.has(key)) {
|
|
133
|
+
if (queued.get(key) === value) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
count(key, queued.get(key), -1);
|
|
137
|
+
}
|
|
56
138
|
queued.set(key, value);
|
|
139
|
+
count(key, value, 1);
|
|
57
140
|
}
|
|
58
141
|
};
|
|
59
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Whether a node's value may be on the page already. A node holding only an initial value nobody
|
|
145
|
+
* has read, as RegistryProvider's initialValues leave it, shows nowhere yet: it takes the incoming
|
|
146
|
+
* value before children render, as on the server, so the browser's first render matches the
|
|
147
|
+
* server's markup.
|
|
148
|
+
*/
|
|
149
|
+
const shown = (node: AtomRegistry.Node<unknown> | undefined): boolean =>
|
|
150
|
+
node !== undefined &&
|
|
151
|
+
!(holdsInitialValue(node) && node.currentState() === "stale");
|
|
152
|
+
|
|
60
153
|
const hydrateNew = (
|
|
61
154
|
incoming: Iterable<Hydration.DehydratedAtom> | undefined
|
|
62
155
|
): Hydration.DehydratedAtomValue[] => {
|
|
@@ -64,7 +157,7 @@
|
|
|
64
157
|
const fresh: Hydration.DehydratedAtomValue[] = [];
|
|
65
158
|
const existing: Hydration.DehydratedAtomValue[] = [];
|
|
66
159
|
for (const atom of Hydration.toValues([...(incoming ?? [])])) {
|
|
67
|
-
(nodes.
|
|
160
|
+
(shown(nodes.get(atom.key)) ? existing : fresh).push(atom);
|
|
68
161
|
}
|
|
69
162
|
queue(fresh);
|
|
70
163
|
return existing;
|
|
@@ -91,8 +184,18 @@
|
|
|
91
184
|
// svelte-ignore state_referenced_locally
|
|
92
185
|
let deferred = hydrateNew(state);
|
|
93
186
|
// Effects don't run on the server, and nothing there renders again, so the atoms that already
|
|
94
|
-
// exist are updated now, before the children read them.
|
|
95
|
-
|
|
187
|
+
// exist are updated now, before the children read them. The browser does the same while it
|
|
188
|
+
// hydrates the server's markup, so its first render matches the server's. The server writes true;
|
|
189
|
+
// the browser reads it only while hydrating, and false otherwise. hydratable throws in development
|
|
190
|
+
// for a key the server didn't write, as for a boundary the server didn't render: that is false too.
|
|
191
|
+
const hydratingServerMarkup = (() => {
|
|
192
|
+
try {
|
|
193
|
+
return hydratable("effect-atom-svelte/HydrationBoundary", () => !BROWSER);
|
|
194
|
+
} catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
if (!BROWSER || hydratingServerMarkup) {
|
|
96
199
|
hydrateExisting(deferred);
|
|
97
200
|
deferred = [];
|
|
98
201
|
}
|
|
@@ -111,13 +214,28 @@
|
|
|
111
214
|
|
|
112
215
|
// A queued value stays in the registry until its key is looked up. With a registry that outlives
|
|
113
216
|
// the boundary, such as one the caller passes to the server's provider, it would reach a later
|
|
114
|
-
// request. Drop the ones still waiting, unless something queued another value since
|
|
115
|
-
|
|
217
|
+
// request. Drop the ones still waiting, unless something queued another value since or another
|
|
218
|
+
// live boundary queued the same one. On the server they're dropped when the render ends, even if
|
|
219
|
+
// a failed boundary discarded this one; in the browser, also when it is destroyed while pending.
|
|
220
|
+
(BROWSER ? onTeardownAfterChildren : onRenderEnd)(() => {
|
|
221
|
+
if (ended) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
ended = true;
|
|
225
|
+
if (BROWSER) {
|
|
226
|
+
const nodes = registry.getNodes();
|
|
227
|
+
for (const key of waiting) {
|
|
228
|
+
const node = nodes.get(key);
|
|
229
|
+
if (node) {
|
|
230
|
+
nodesAtEnd.set(key, node);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
116
234
|
if (!preloaded) {
|
|
117
235
|
return;
|
|
118
236
|
}
|
|
119
237
|
for (const [key, value] of queued) {
|
|
120
|
-
if (preloaded.get(key) === value) {
|
|
238
|
+
if (count(key, value, -1) <= 0 && preloaded.get(key) === value) {
|
|
121
239
|
preloaded.delete(key);
|
|
122
240
|
}
|
|
123
241
|
}
|
|
@@ -6,13 +6,22 @@ interface Props {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Hydrates dehydrated atom state, for example from `Hydration.dehydrate` returned by a remote
|
|
9
|
-
* function. Atoms new to the registry
|
|
10
|
-
*
|
|
11
|
-
* mid-render. On the server, where nothing
|
|
9
|
+
* function. Atoms new to the registry, or holding only an initial value nobody has read yet, are
|
|
10
|
+
* hydrated before children render; in the browser, atoms that already exist are updated after
|
|
11
|
+
* render, so current UI does not jump to the incoming data mid-render. On the server, where nothing
|
|
12
|
+
* renders again, and in the browser while it hydrates the server's markup, they are updated at once.
|
|
13
|
+
* That needs Svelte's `experimental.async`: without it, the browser can't tell that it is hydrating,
|
|
14
|
+
* and atoms that already exist, as one read above the boundary, are updated after the first render.
|
|
15
|
+
*
|
|
16
|
+
* With `experimental.async` on, custom server rendering must `await render(...)`, as SvelteKit does,
|
|
17
|
+
* so the boundary's `hydratable` entry is written into the page. Otherwise a production build of
|
|
18
|
+
* Svelte logs `hydratable_missing_but_expected` once per boundary as the page hydrates.
|
|
12
19
|
*
|
|
13
20
|
* A value for an atom nobody reads waits in the registry until something does. When the boundary
|
|
14
21
|
* is destroyed, as at the end of a server render, the values it brought that are still unread are
|
|
15
|
-
* dropped,
|
|
22
|
+
* dropped, as is a promise-encoded value that lands afterwards, so a registry that outlives the
|
|
23
|
+
* boundary doesn't hand them to a later reader. In the browser, a reader that already held the atom
|
|
24
|
+
* when the boundary ended and still holds it, as a layout's reader above it, gets the late value.
|
|
16
25
|
*
|
|
17
26
|
* **Example** (Hydrating the state a load function dehydrated)
|
|
18
27
|
*
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
* A signal that refreshes the atom (`refreshOnWindowFocus`, `makeRefreshOnSignal`, `swr`) calls
|
|
21
|
+
* `registry.refresh` too, so it is reported the same way.
|
|
22
|
+
* - `Invalidated`: something else made it stale, such as the atom refreshing itself (`refreshSelf`)
|
|
23
|
+
* or a reactivity key.
|
|
24
|
+
*
|
|
25
|
+
* @stability unstable
|
|
26
|
+
* @since 0.2.0
|
|
27
|
+
* @category models
|
|
28
|
+
*/
|
|
29
|
+
export type BuildCause = {
|
|
30
|
+
readonly _tag: "FirstRead";
|
|
31
|
+
} | {
|
|
32
|
+
readonly _tag: "ParentChanged";
|
|
33
|
+
readonly parents: readonly AtomRegistry.Node<unknown>[];
|
|
34
|
+
} | {
|
|
35
|
+
readonly _tag: "Refreshed";
|
|
36
|
+
} | {
|
|
37
|
+
readonly _tag: "Invalidated";
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Where a node's new value came from.
|
|
41
|
+
*
|
|
42
|
+
* - `build`: its computation returned it.
|
|
43
|
+
* - `write`: `registry.set`, `update` or `modify` (which the write hooks use), on it or on a
|
|
44
|
+
* writable atom that wrote it, and another atom's `get.set`, which goes through `registry.set`.
|
|
45
|
+
* - `async`: anything else, such as an effect settling or a stream emitting after the computation
|
|
46
|
+
* returned.
|
|
47
|
+
*
|
|
48
|
+
* @stability unstable
|
|
49
|
+
* @since 0.2.0
|
|
50
|
+
* @category models
|
|
51
|
+
*/
|
|
52
|
+
export type UpdateSource = "build" | "write" | "async";
|
|
53
|
+
/**
|
|
54
|
+
* Something a registry did to one of its nodes. `time` is from `performance.now()`.
|
|
55
|
+
*
|
|
56
|
+
* - `NodeAdded`: an atom was read, mounted or written for the first time, or again after it was
|
|
57
|
+
* removed, and the registry made a node for it.
|
|
58
|
+
* - `Built`: the node is about to compute its value, for the `cause` given.
|
|
59
|
+
* - `Updated`: its value changed. `first` marks its first value, which has no `previous`.
|
|
60
|
+
* - `ReadersChanged`: a listener was added or removed; `readers` is how many it has now. Hooks,
|
|
61
|
+
* `registry.subscribe` and `mount`, and atoms that subscribe to others all count.
|
|
62
|
+
* - `Interrupted`: its effect hadn't finished when what its computation started was torn down, so
|
|
63
|
+
* the fiber was interrupted. Followed by `Finalized`. Not reported for an `Atom.runtime` atom,
|
|
64
|
+
* whose layer build isn't interrupted. An effect atom made with `{ uninterruptible: true }` is
|
|
65
|
+
* reported all the same, though its fiber keeps running: the inspector can't tell it apart.
|
|
66
|
+
* - `Finalized`: what its last computation started was torn down, before it computes again or as
|
|
67
|
+
* it is removed; `finalizers` ran.
|
|
68
|
+
* - `NodeRemoved`: the registry disposed of the node, as nothing read it any more.
|
|
69
|
+
*
|
|
70
|
+
* @stability unstable
|
|
71
|
+
* @since 0.2.0
|
|
72
|
+
* @category models
|
|
73
|
+
*/
|
|
74
|
+
export type Event = {
|
|
75
|
+
readonly node: AtomRegistry.Node<unknown>;
|
|
76
|
+
readonly time: number;
|
|
77
|
+
} & ({
|
|
78
|
+
readonly _tag: "NodeAdded";
|
|
79
|
+
} | {
|
|
80
|
+
readonly _tag: "Built";
|
|
81
|
+
readonly cause: BuildCause;
|
|
82
|
+
} | {
|
|
83
|
+
readonly _tag: "Updated";
|
|
84
|
+
readonly first: boolean;
|
|
85
|
+
readonly previous: unknown;
|
|
86
|
+
readonly value: unknown;
|
|
87
|
+
readonly source: UpdateSource;
|
|
88
|
+
} | {
|
|
89
|
+
readonly _tag: "ReadersChanged";
|
|
90
|
+
readonly readers: number;
|
|
91
|
+
} | {
|
|
92
|
+
readonly _tag: "Interrupted";
|
|
93
|
+
} | {
|
|
94
|
+
readonly _tag: "Finalized";
|
|
95
|
+
readonly finalizers: number;
|
|
96
|
+
} | {
|
|
97
|
+
readonly _tag: "NodeRemoved";
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* What `inspect` returns for a registry.
|
|
101
|
+
*
|
|
102
|
+
* @stability unstable
|
|
103
|
+
* @since 0.2.0
|
|
104
|
+
* @category models
|
|
105
|
+
*/
|
|
106
|
+
export interface Inspector {
|
|
107
|
+
readonly registry: AtomRegistry.AtomRegistry;
|
|
108
|
+
/**
|
|
109
|
+
* Calls `listener` with each event as it happens; returns the function that stops it. Events
|
|
110
|
+
* arrive in the middle of the registry's work, so a listener must not read or write atoms, nor
|
|
111
|
+
* write Svelte state: queue the event and handle it later, for example on the next animation
|
|
112
|
+
* frame.
|
|
113
|
+
*/
|
|
114
|
+
readonly subscribe: (listener: (event: Event) => void) => () => void;
|
|
115
|
+
/**
|
|
116
|
+
* How long an atom nobody reads is kept before it is removed, in milliseconds: its own idle TTL,
|
|
117
|
+
* or the registry's default. `undefined` for an atom kept alive, and for one removed as soon as
|
|
118
|
+
* its last reader goes.
|
|
119
|
+
*/
|
|
120
|
+
readonly idleTTL: (atom: Atom.Atom<unknown>) => number | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* `false` when the registry's nodes aren't the implementation the inspector was written against
|
|
123
|
+
* (a later Effect, say). Then only `NodeAdded` and `NodeRemoved` are reported.
|
|
124
|
+
*/
|
|
125
|
+
readonly complete: boolean;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Starts watching a registry, and returns its inspector: the same one each time it is called for
|
|
129
|
+
* the same registry.
|
|
130
|
+
*
|
|
131
|
+
* Until then a registry runs as it always does. From then on, in a registry made by Effect 4.0,
|
|
132
|
+
* each node reports what happens to it, which costs a little on every read and write, so call it
|
|
133
|
+
* from developer tools rather than from the app.
|
|
134
|
+
*
|
|
135
|
+
* Nodes the registry already holds are watched from now on; what happened to them before isn't
|
|
136
|
+
* known. Read them with `registry.getNodes()`.
|
|
137
|
+
*
|
|
138
|
+
* **Example** (Logging every computation)
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { getRegistry } from "effect-atom-svelte";
|
|
142
|
+
* import { inspect } from "effect-atom-svelte/inspector";
|
|
143
|
+
*
|
|
144
|
+
* const events = [];
|
|
145
|
+
* inspect(getRegistry()).subscribe((event) => events.push(event));
|
|
146
|
+
* setInterval(() => {
|
|
147
|
+
* for (const event of events.splice(0)) {
|
|
148
|
+
* if (event._tag === "Built") {
|
|
149
|
+
* console.log(event.node.atom.label?.[0], event.cause._tag);
|
|
150
|
+
* }
|
|
151
|
+
* }
|
|
152
|
+
* }, 1000);
|
|
153
|
+
* ```
|
|
154
|
+
*
|
|
155
|
+
* @stability unstable
|
|
156
|
+
* @since 0.2.0
|
|
157
|
+
* @category inspecting
|
|
158
|
+
*/
|
|
159
|
+
export declare const inspect: (registry: AtomRegistry.AtomRegistry) => Inspector;
|
|
160
|
+
/**
|
|
161
|
+
* The registries the app has in the browser during development: each one a `RegistryProvider` or
|
|
162
|
+
* `provideRegistry` gives its children, while it is mounted, and the default browser registry once
|
|
163
|
+
* something has used it. Empty on the server and in production builds.
|
|
164
|
+
*
|
|
165
|
+
* @stability unstable
|
|
166
|
+
* @since 0.2.0
|
|
167
|
+
* @category inspecting
|
|
168
|
+
*/
|
|
169
|
+
export declare const registries: () => readonly AtomRegistry.AtomRegistry[];
|
|
170
|
+
/**
|
|
171
|
+
* Calls `f` with `registries()` now and whenever it changes, as providers mount and unmount;
|
|
172
|
+
* returns the function that stops it.
|
|
173
|
+
*
|
|
174
|
+
* @stability unstable
|
|
175
|
+
* @since 0.2.0
|
|
176
|
+
* @category inspecting
|
|
177
|
+
*/
|
|
178
|
+
export declare const watchRegistries: (f: (registries: readonly AtomRegistry.AtomRegistry[]) => void) => (() => void);
|
|
179
|
+
/**
|
|
180
|
+
* How a component uses an atom it reports to a scope: `read` (`useAtomValue`, `useAtom`,
|
|
181
|
+
* `useAtomResult`, `useAtomSuspense`), `mount` (`useAtomMount`), `subscribe`
|
|
182
|
+
* (`useAtomSubscribe`) or `write` (`useAtomSet`, which holds the atom mounted to write it).
|
|
183
|
+
*
|
|
184
|
+
* @stability unstable
|
|
185
|
+
* @since 0.2.0
|
|
186
|
+
* @category models
|
|
187
|
+
*/
|
|
188
|
+
export type ReaderKind = ReadKind;
|
|
189
|
+
/**
|
|
190
|
+
* A hook in the scope's part of the component tree that uses an atom. `component` and `file` name
|
|
191
|
+
* the component it is in, when the atomLabels plugin of effect-atom-svelte-devtools (or
|
|
192
|
+
* `nameComponent`) names components; `instance` tells two instances of one component apart.
|
|
193
|
+
*
|
|
194
|
+
* @stability unstable
|
|
195
|
+
* @since 0.2.0
|
|
196
|
+
* @category models
|
|
197
|
+
*/
|
|
198
|
+
export interface ScopeReader {
|
|
199
|
+
readonly id: number;
|
|
200
|
+
readonly kind: ReaderKind;
|
|
201
|
+
/** The `id` of the atom's node in the snapshot. */
|
|
202
|
+
readonly atom: number;
|
|
203
|
+
readonly component: string | undefined;
|
|
204
|
+
readonly file: string | undefined;
|
|
205
|
+
readonly instance: number | undefined;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* An atom in a scope's snapshot.
|
|
209
|
+
*
|
|
210
|
+
* @stability unstable
|
|
211
|
+
* @since 0.2.0
|
|
212
|
+
* @category models
|
|
213
|
+
*/
|
|
214
|
+
export interface ScopeNode {
|
|
215
|
+
/** Stable for the atom while the scope lives. */
|
|
216
|
+
readonly id: number;
|
|
217
|
+
readonly atom: Atom.Atom<unknown>;
|
|
218
|
+
readonly node: AtomRegistry.Node<unknown>;
|
|
219
|
+
/** The atom's label (its variable's name, with the atomLabels plugin), if it has one. */
|
|
220
|
+
readonly label: string | undefined;
|
|
221
|
+
/** Used by a hook in the scope, rather than only upstream of one. */
|
|
222
|
+
readonly read: boolean;
|
|
223
|
+
/** Has no label while other atoms in the scope do: made by a runtime, mutation or store. */
|
|
224
|
+
readonly plumbing: boolean;
|
|
225
|
+
/** Every listener the node has: the scope's hooks, and anything else that subscribes. */
|
|
226
|
+
readonly listeners: number;
|
|
227
|
+
/** The tag of its value when that is an `AsyncResult`, otherwise `Value`. */
|
|
228
|
+
readonly state: "Value" | "Initial" | "Success" | "Failure";
|
|
229
|
+
readonly waiting: boolean;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* An edge from an atom to one that reads it. With plumbing left out, an atom that reads plumbing is
|
|
233
|
+
* linked to the nearest atoms upstream of it that are shown.
|
|
234
|
+
*
|
|
235
|
+
* @stability unstable
|
|
236
|
+
* @since 0.2.0
|
|
237
|
+
* @category models
|
|
238
|
+
*/
|
|
239
|
+
export interface ScopeEdge {
|
|
240
|
+
readonly from: number;
|
|
241
|
+
readonly to: number;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* What a scope's part of the component tree reads, at one moment: the atoms its hooks use, the
|
|
245
|
+
* atoms upstream of them, the edges between, and the hooks.
|
|
246
|
+
*
|
|
247
|
+
* @stability unstable
|
|
248
|
+
* @since 0.2.0
|
|
249
|
+
* @category models
|
|
250
|
+
*/
|
|
251
|
+
export interface ScopeSnapshot {
|
|
252
|
+
readonly nodes: readonly ScopeNode[];
|
|
253
|
+
readonly edges: readonly ScopeEdge[];
|
|
254
|
+
readonly readers: readonly ScopeReader[];
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* An event a scope passes on: the inspector's `Event` for a node in the scope, with the node's
|
|
258
|
+
* `id`, or `ScopeChanged` once its atoms, edges or hooks have changed, after the registry's work,
|
|
259
|
+
* to take a new `snapshot()`. A node's `NodeAdded` arrives before the node is linked into the
|
|
260
|
+
* scope, so it isn't passed on; the `ScopeChanged` that follows is.
|
|
261
|
+
*
|
|
262
|
+
* @stability unstable
|
|
263
|
+
* @since 0.2.0
|
|
264
|
+
* @category models
|
|
265
|
+
*/
|
|
266
|
+
export type ScopeEvent = (Event & {
|
|
267
|
+
readonly id: number;
|
|
268
|
+
}) | {
|
|
269
|
+
readonly _tag: "ScopeChanged";
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* A part of the component tree, as `provideInspectorScope` returns it.
|
|
273
|
+
*
|
|
274
|
+
* @stability unstable
|
|
275
|
+
* @since 0.2.0
|
|
276
|
+
* @category models
|
|
277
|
+
*/
|
|
278
|
+
export interface InspectorScope {
|
|
279
|
+
/** The atoms the scope reads now. `plumbing: true` includes those otherwise left out. */
|
|
280
|
+
readonly snapshot: (options?: {
|
|
281
|
+
readonly plumbing?: boolean;
|
|
282
|
+
}) => ScopeSnapshot;
|
|
283
|
+
/**
|
|
284
|
+
* Calls `listener` with each event for the scope's atoms; returns the function that stops it. The
|
|
285
|
+
* first listener starts the inspector on the registries the scope reads from. As with
|
|
286
|
+
* `Inspector.subscribe`, a listener must not read or write atoms or write Svelte state.
|
|
287
|
+
*/
|
|
288
|
+
readonly subscribe: (listener: (event: ScopeEvent) => void) => () => void;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Makes the component being set up, and everything below it, an inspector scope, and returns it.
|
|
292
|
+
* The library's hooks below report the atoms they use to the nearest scope, which shows those
|
|
293
|
+
* atoms, everything upstream of them, and the hooks that use them. Call it while the component
|
|
294
|
+
* sets up, as context requires.
|
|
295
|
+
*
|
|
296
|
+
* Only in the browser, in development and production alike (the effect-atom-svelte docs draw each
|
|
297
|
+
* example's atoms from one); on the server it returns a scope that is always empty. Until the
|
|
298
|
+
* scope has a listener it only keeps a list of its hooks: the registry's inspector starts with the
|
|
299
|
+
* first listener.
|
|
300
|
+
*
|
|
301
|
+
* **Example** (A graph of what an example reads)
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* import { provideInspectorScope } from "effect-atom-svelte/inspector";
|
|
305
|
+
*
|
|
306
|
+
* // In the component that frames the example, around its content
|
|
307
|
+
* const scope = provideInspectorScope();
|
|
308
|
+
* $effect(() =>
|
|
309
|
+
* scope.subscribe((event) => {
|
|
310
|
+
* if (event._tag === "ScopeChanged") {
|
|
311
|
+
* draw(scope.snapshot());
|
|
312
|
+
* } else if (event._tag === "Updated") {
|
|
313
|
+
* pulse(event.id);
|
|
314
|
+
* }
|
|
315
|
+
* })
|
|
316
|
+
* );
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* @stability unstable
|
|
320
|
+
* @since 0.2.0
|
|
321
|
+
* @category inspecting
|
|
322
|
+
*/
|
|
323
|
+
export declare const provideInspectorScope: () => InspectorScope;
|
|
324
|
+
/**
|
|
325
|
+
* Names the component being set up, for the scopes its hooks report to. The atomLabels plugin of
|
|
326
|
+
* effect-atom-svelte-devtools calls it at the top of each component's script; without the plugin,
|
|
327
|
+
* call it yourself. Only in the browser.
|
|
328
|
+
*
|
|
329
|
+
* @stability unstable
|
|
330
|
+
* @since 0.2.0
|
|
331
|
+
* @category inspecting
|
|
332
|
+
*/
|
|
333
|
+
export declare const nameComponent: (name: string, file?: string) => void;
|