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
|
@@ -0,0 +1,558 @@
|
|
|
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 { buildWillRead, defaultIdleTTL, holdsInitialValue, 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
|
+
/**
|
|
18
|
+
* An `Atom.runtime` atom, by the `layer` and `factory` it carries. It builds its layer with
|
|
19
|
+
* `uninterruptible: true`, so a build torn down midway keeps running.
|
|
20
|
+
*/
|
|
21
|
+
const isRuntime = (atom) => "layer" in atom &&
|
|
22
|
+
Atom.isAtom(atom.layer) &&
|
|
23
|
+
"factory" in atom &&
|
|
24
|
+
typeof atom.factory === "function";
|
|
25
|
+
const install = (registry) => {
|
|
26
|
+
const listeners = new Set();
|
|
27
|
+
const records = new WeakMap();
|
|
28
|
+
// The node that held each result first: the one whose computation or effect made it.
|
|
29
|
+
const origins = new WeakMap();
|
|
30
|
+
let writing = 0;
|
|
31
|
+
let complete = true;
|
|
32
|
+
const emit = (event) => {
|
|
33
|
+
for (const listener of listeners) {
|
|
34
|
+
try {
|
|
35
|
+
listener(event);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// A tool's mistake mustn't break the app's registry.
|
|
39
|
+
console.error("effect-atom-svelte: an inspector listener threw", error);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const record = (node) => {
|
|
44
|
+
let found = records.get(node);
|
|
45
|
+
if (found === undefined) {
|
|
46
|
+
found = {
|
|
47
|
+
building: 0,
|
|
48
|
+
builds: 0,
|
|
49
|
+
changed: new Set(),
|
|
50
|
+
refreshed: false,
|
|
51
|
+
removing: false,
|
|
52
|
+
values: 0,
|
|
53
|
+
};
|
|
54
|
+
records.set(node, found);
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
};
|
|
58
|
+
const instrument = (node) => {
|
|
59
|
+
const instrumented = instrumentNode(node, {
|
|
60
|
+
build: () => {
|
|
61
|
+
const self = record(node);
|
|
62
|
+
let cause = { _tag: "Invalidated" };
|
|
63
|
+
if (self.builds === 0) {
|
|
64
|
+
cause = { _tag: "FirstRead" };
|
|
65
|
+
}
|
|
66
|
+
else if (self.refreshed) {
|
|
67
|
+
cause = { _tag: "Refreshed" };
|
|
68
|
+
}
|
|
69
|
+
else if (self.changed.size > 0) {
|
|
70
|
+
cause = { _tag: "ParentChanged", parents: [...self.changed] };
|
|
71
|
+
}
|
|
72
|
+
self.builds += 1;
|
|
73
|
+
self.refreshed = false;
|
|
74
|
+
self.changed = new Set();
|
|
75
|
+
self.building += 1;
|
|
76
|
+
emit({ _tag: "Built", cause, node, time: performance.now() });
|
|
77
|
+
},
|
|
78
|
+
built: () => {
|
|
79
|
+
record(node).building -= 1;
|
|
80
|
+
},
|
|
81
|
+
dispose: (finalizers, value) => {
|
|
82
|
+
if (finalizers === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// An effect atom's computation leaves a waiting result until its fiber is done, and a
|
|
86
|
+
// finalizer that interrupts the fiber.
|
|
87
|
+
// A wrapper that passes on another atom's result (debounce, makeRefreshOnSignal) holds a
|
|
88
|
+
// value that atom made, and has no fiber of its own. A runtime's fiber isn't interrupted.
|
|
89
|
+
if (AsyncResult.isAsyncResult(value) &&
|
|
90
|
+
value.waiting &&
|
|
91
|
+
(origins.get(value) ?? node) === node &&
|
|
92
|
+
!isRuntime(node.atom)) {
|
|
93
|
+
emit({ _tag: "Interrupted", node, time: performance.now() });
|
|
94
|
+
}
|
|
95
|
+
emit({ _tag: "Finalized", finalizers, node, time: performance.now() });
|
|
96
|
+
},
|
|
97
|
+
readers: () => {
|
|
98
|
+
emit({
|
|
99
|
+
_tag: "ReadersChanged",
|
|
100
|
+
node,
|
|
101
|
+
readers: node.listeners.size,
|
|
102
|
+
time: performance.now(),
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
removed: () => {
|
|
106
|
+
const self = record(node);
|
|
107
|
+
if (self.removing) {
|
|
108
|
+
self.removing = false;
|
|
109
|
+
emit({ _tag: "NodeRemoved", node, time: performance.now() });
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
value: (previous, value) => {
|
|
113
|
+
const self = record(node);
|
|
114
|
+
if (AsyncResult.isAsyncResult(value) && !origins.has(value)) {
|
|
115
|
+
origins.set(value, node);
|
|
116
|
+
}
|
|
117
|
+
const first = self.values === 0;
|
|
118
|
+
self.values += 1;
|
|
119
|
+
if (!first) {
|
|
120
|
+
for (const child of node.children) {
|
|
121
|
+
// A running build that hasn't read this node yet reads the new value: not a cause of
|
|
122
|
+
// its next computation.
|
|
123
|
+
if (!buildWillRead(child, node)) {
|
|
124
|
+
record(child).changed.add(node);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
let source = "async";
|
|
129
|
+
if (self.building > 0) {
|
|
130
|
+
source = "build";
|
|
131
|
+
}
|
|
132
|
+
else if (writing > 0) {
|
|
133
|
+
source = "write";
|
|
134
|
+
}
|
|
135
|
+
emit({
|
|
136
|
+
_tag: "Updated",
|
|
137
|
+
first,
|
|
138
|
+
node,
|
|
139
|
+
previous,
|
|
140
|
+
source,
|
|
141
|
+
time: performance.now(),
|
|
142
|
+
value,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
complete &&= instrumented;
|
|
147
|
+
};
|
|
148
|
+
const patchable = registry;
|
|
149
|
+
const { modify, onNodeAdded, onNodeRemoved, refresh, set, update } = patchable;
|
|
150
|
+
const writes = (f) => {
|
|
151
|
+
writing += 1;
|
|
152
|
+
try {
|
|
153
|
+
return f();
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
writing -= 1;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
patchable.set = (atom, value) => writes(() => set.call(registry, atom, value));
|
|
160
|
+
patchable.update = (atom, f) => writes(() => update.call(registry, atom, f));
|
|
161
|
+
patchable.modify = (atom, f) => writes(() => modify.call(registry, atom, f));
|
|
162
|
+
// An atom with its own refresh (a wrapper such as withRefresh) calls the registry's refresh for
|
|
163
|
+
// the atoms it wraps, through this replacement, so each of those is marked too.
|
|
164
|
+
patchable.refresh = (atom) => {
|
|
165
|
+
const node = registry.getNodes().get(nodeKey(atom));
|
|
166
|
+
if (node !== undefined) {
|
|
167
|
+
record(node).refreshed = true;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
refresh.call(registry, atom);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
// Still valid: the refresh didn't reach it (what it wraps computed an equal value), or it
|
|
174
|
+
// computed already. Either way the mark isn't the cause of a later computation.
|
|
175
|
+
if (node?.currentState() === "valid") {
|
|
176
|
+
record(node).refreshed = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
patchable.onNodeAdded = (node) => {
|
|
181
|
+
onNodeAdded?.(node);
|
|
182
|
+
instrument(node);
|
|
183
|
+
emit({ _tag: "NodeAdded", node, time: performance.now() });
|
|
184
|
+
};
|
|
185
|
+
patchable.onNodeRemoved = (node) => {
|
|
186
|
+
onNodeRemoved?.(node);
|
|
187
|
+
// An idle node is announced before it is removed; its teardown is reported first.
|
|
188
|
+
if (node.currentState() !== "removed" && records.has(node)) {
|
|
189
|
+
record(node).removing = true;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
emit({ _tag: "NodeRemoved", node, time: performance.now() });
|
|
193
|
+
};
|
|
194
|
+
for (const node of registry.getNodes().values()) {
|
|
195
|
+
instrument(node);
|
|
196
|
+
// A node that has computed before isn't on its first read or value any more.
|
|
197
|
+
if (node.currentState() !== "uninitialized") {
|
|
198
|
+
const self = record(node);
|
|
199
|
+
self.builds = holdsInitialValue(node) ? 0 : 1;
|
|
200
|
+
self.values = 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
get complete() {
|
|
205
|
+
return complete;
|
|
206
|
+
},
|
|
207
|
+
idleTTL: (atom) => {
|
|
208
|
+
// An idle TTL of 0 is none: the registry removes the atom as soon as nothing reads it.
|
|
209
|
+
if (atom.keepAlive || atom.idleTTL === 0) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
return atom.idleTTL ?? defaultIdleTTL(registry);
|
|
213
|
+
},
|
|
214
|
+
registry,
|
|
215
|
+
subscribe: (listener) => {
|
|
216
|
+
listeners.add(listener);
|
|
217
|
+
return () => {
|
|
218
|
+
listeners.delete(listener);
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
};
|
|
223
|
+
const inspectors = new WeakMap();
|
|
224
|
+
/**
|
|
225
|
+
* Starts watching a registry, and returns its inspector: the same one each time it is called for
|
|
226
|
+
* the same registry.
|
|
227
|
+
*
|
|
228
|
+
* Until then a registry runs as it always does. From then on, in a registry made by Effect 4.0,
|
|
229
|
+
* each node reports what happens to it, which costs a little on every read and write, so call it
|
|
230
|
+
* from developer tools rather than from the app.
|
|
231
|
+
*
|
|
232
|
+
* Nodes the registry already holds are watched from now on; what happened to them before isn't
|
|
233
|
+
* known. Read them with `registry.getNodes()`.
|
|
234
|
+
*
|
|
235
|
+
* **Example** (Logging every computation)
|
|
236
|
+
*
|
|
237
|
+
* ```ts
|
|
238
|
+
* import { getRegistry } from "effect-atom-svelte";
|
|
239
|
+
* import { inspect } from "effect-atom-svelte/inspector";
|
|
240
|
+
*
|
|
241
|
+
* const events = [];
|
|
242
|
+
* inspect(getRegistry()).subscribe((event) => events.push(event));
|
|
243
|
+
* setInterval(() => {
|
|
244
|
+
* for (const event of events.splice(0)) {
|
|
245
|
+
* if (event._tag === "Built") {
|
|
246
|
+
* console.log(event.node.atom.label?.[0], event.cause._tag);
|
|
247
|
+
* }
|
|
248
|
+
* }
|
|
249
|
+
* }, 1000);
|
|
250
|
+
* ```
|
|
251
|
+
*
|
|
252
|
+
* @stability unstable
|
|
253
|
+
* @since 0.2.0
|
|
254
|
+
* @category inspecting
|
|
255
|
+
*/
|
|
256
|
+
export const inspect = (registry) => {
|
|
257
|
+
let inspector = inspectors.get(registry);
|
|
258
|
+
if (inspector === undefined) {
|
|
259
|
+
inspector = install(registry);
|
|
260
|
+
inspectors.set(registry, inspector);
|
|
261
|
+
}
|
|
262
|
+
return inspector;
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* The registries the app has in the browser during development: each one a `RegistryProvider` or
|
|
266
|
+
* `provideRegistry` gives its children, while it is mounted, and the default browser registry once
|
|
267
|
+
* something has used it. Empty on the server and in production builds.
|
|
268
|
+
*
|
|
269
|
+
* @stability unstable
|
|
270
|
+
* @since 0.2.0
|
|
271
|
+
* @category inspecting
|
|
272
|
+
*/
|
|
273
|
+
export const registries = () => list();
|
|
274
|
+
/**
|
|
275
|
+
* Calls `f` with `registries()` now and whenever it changes, as providers mount and unmount;
|
|
276
|
+
* returns the function that stops it.
|
|
277
|
+
*
|
|
278
|
+
* @stability unstable
|
|
279
|
+
* @since 0.2.0
|
|
280
|
+
* @category inspecting
|
|
281
|
+
*/
|
|
282
|
+
export const watchRegistries = (f) => watch(f);
|
|
283
|
+
const emptySnapshot = { edges: [], nodes: [], readers: [] };
|
|
284
|
+
/** The scope on the server: always empty. */
|
|
285
|
+
const inertScope = {
|
|
286
|
+
snapshot: () => emptySnapshot,
|
|
287
|
+
subscribe: () => () => undefined,
|
|
288
|
+
};
|
|
289
|
+
const stateOf = (node) => {
|
|
290
|
+
// A valid node's value is read without computing anything.
|
|
291
|
+
const value = node.currentState() === "valid" ? node.value() : undefined;
|
|
292
|
+
return AsyncResult.isAsyncResult(value)
|
|
293
|
+
? { state: value._tag, waiting: value.waiting }
|
|
294
|
+
: { state: "Value", waiting: false };
|
|
295
|
+
};
|
|
296
|
+
class Scope {
|
|
297
|
+
#reads = new Map();
|
|
298
|
+
#ids = new WeakMap();
|
|
299
|
+
#keyIds = new Map();
|
|
300
|
+
#listeners = new Set();
|
|
301
|
+
// The inspectors the scope follows while it has listeners, by registry.
|
|
302
|
+
#following = new Map();
|
|
303
|
+
#nextId = 0;
|
|
304
|
+
#nextRead = 0;
|
|
305
|
+
// The nodes in the scope, worked out again only after something that can change them.
|
|
306
|
+
#members;
|
|
307
|
+
// The last nodes worked out, so a node removed from the scope still has its removal passed on.
|
|
308
|
+
#previous = new Set();
|
|
309
|
+
#signature = "";
|
|
310
|
+
#checking = false;
|
|
311
|
+
reporter = {
|
|
312
|
+
report: ({ atom, component, kind, registry }) => {
|
|
313
|
+
this.#nextRead += 1;
|
|
314
|
+
const id = this.#nextRead;
|
|
315
|
+
this.#reads.set(id, { atom, component, id, kind, registry });
|
|
316
|
+
this.#follow(registry);
|
|
317
|
+
this.#changed();
|
|
318
|
+
return () => {
|
|
319
|
+
this.#reads.delete(id);
|
|
320
|
+
this.#changed();
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
#id(atom) {
|
|
325
|
+
const key = nodeKey(atom);
|
|
326
|
+
let id = typeof key === "string" ? this.#keyIds.get(key) : this.#ids.get(atom);
|
|
327
|
+
if (id === undefined) {
|
|
328
|
+
this.#nextId += 1;
|
|
329
|
+
id = this.#nextId;
|
|
330
|
+
if (typeof key === "string") {
|
|
331
|
+
this.#keyIds.set(key, id);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
this.#ids.set(atom, id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return id;
|
|
338
|
+
}
|
|
339
|
+
/** Every node the scope's hooks use, and every node upstream of them. */
|
|
340
|
+
#nodes() {
|
|
341
|
+
// Without a listener the scope follows no inspector, so nothing would say the graph changed.
|
|
342
|
+
if (this.#listeners.size === 0) {
|
|
343
|
+
return this.#walk();
|
|
344
|
+
}
|
|
345
|
+
if (this.#members === undefined) {
|
|
346
|
+
this.#members = this.#walk();
|
|
347
|
+
}
|
|
348
|
+
return this.#members;
|
|
349
|
+
}
|
|
350
|
+
/** Works out the scope's nodes from the graph as it is now, without keeping them. */
|
|
351
|
+
#walk() {
|
|
352
|
+
const members = new Set();
|
|
353
|
+
const add = (node) => {
|
|
354
|
+
if (!members.has(node)) {
|
|
355
|
+
members.add(node);
|
|
356
|
+
for (const parent of node.parents) {
|
|
357
|
+
add(parent);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
for (const read of this.#reads.values()) {
|
|
362
|
+
const node = read.registry.getNodes().get(nodeKey(read.atom));
|
|
363
|
+
if (node !== undefined) {
|
|
364
|
+
add(node);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return members;
|
|
368
|
+
}
|
|
369
|
+
/** The nearest shown atoms upstream of `node`, looking through those not shown. */
|
|
370
|
+
#shownParents(node, shown) {
|
|
371
|
+
const found = new Set();
|
|
372
|
+
const seen = new Set();
|
|
373
|
+
const walk = (parents) => {
|
|
374
|
+
for (const parent of parents) {
|
|
375
|
+
const id = this.#id(parent.atom);
|
|
376
|
+
if (shown.has(id)) {
|
|
377
|
+
found.add(id);
|
|
378
|
+
}
|
|
379
|
+
else if (!seen.has(parent)) {
|
|
380
|
+
seen.add(parent);
|
|
381
|
+
walk(parent.parents);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
walk(node.parents);
|
|
386
|
+
found.delete(this.#id(node.atom));
|
|
387
|
+
return found;
|
|
388
|
+
}
|
|
389
|
+
snapshot(options) {
|
|
390
|
+
const members = [...this.#nodes()];
|
|
391
|
+
const read = new Set([...this.#reads.values()].map((entry) => this.#id(entry.atom)));
|
|
392
|
+
const labelled = members.some((node) => node.atom.label !== undefined);
|
|
393
|
+
const nodes = members.map((node) => {
|
|
394
|
+
const id = this.#id(node.atom);
|
|
395
|
+
return {
|
|
396
|
+
atom: node.atom,
|
|
397
|
+
id,
|
|
398
|
+
label: node.atom.label?.[0],
|
|
399
|
+
listeners: node.listeners.size,
|
|
400
|
+
node,
|
|
401
|
+
plumbing: labelled && node.atom.label === undefined,
|
|
402
|
+
read: read.has(id),
|
|
403
|
+
...stateOf(node),
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
// An atom a hook uses is shown even without a label: it is what the component uses.
|
|
407
|
+
const shown = nodes.filter((node) => options?.plumbing === true || !node.plumbing || node.read);
|
|
408
|
+
const shownIds = new Set(shown.map((node) => node.id));
|
|
409
|
+
const edges = shown.flatMap((node) => [...this.#shownParents(node.node, shownIds)].map((from) => ({
|
|
410
|
+
from,
|
|
411
|
+
to: node.id,
|
|
412
|
+
})));
|
|
413
|
+
const readers = [...this.#reads.values()].map((entry) => ({
|
|
414
|
+
atom: this.#id(entry.atom),
|
|
415
|
+
component: entry.component?.name,
|
|
416
|
+
file: entry.component?.file,
|
|
417
|
+
id: entry.id,
|
|
418
|
+
instance: entry.component?.instance,
|
|
419
|
+
kind: entry.kind,
|
|
420
|
+
}));
|
|
421
|
+
return { edges, nodes: shown, readers };
|
|
422
|
+
}
|
|
423
|
+
subscribe(listener) {
|
|
424
|
+
if (this.#listeners.size === 0) {
|
|
425
|
+
// Kept from before the scope last stopped following, and the graph may have changed since.
|
|
426
|
+
this.#members = undefined;
|
|
427
|
+
this.#previous = new Set();
|
|
428
|
+
}
|
|
429
|
+
this.#listeners.add(listener);
|
|
430
|
+
for (const read of this.#reads.values()) {
|
|
431
|
+
this.#follow(read.registry);
|
|
432
|
+
}
|
|
433
|
+
this.#signature = this.#sign();
|
|
434
|
+
return () => {
|
|
435
|
+
this.#listeners.delete(listener);
|
|
436
|
+
if (this.#listeners.size === 0) {
|
|
437
|
+
for (const stop of this.#following.values()) {
|
|
438
|
+
stop();
|
|
439
|
+
}
|
|
440
|
+
this.#following.clear();
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
#follow(registry) {
|
|
445
|
+
if (this.#listeners.size === 0 || this.#following.has(registry)) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
this.#following.set(registry, inspect(registry).subscribe((event) => this.#receive(event)));
|
|
449
|
+
}
|
|
450
|
+
#receive(event) {
|
|
451
|
+
// Events arrive in the middle of the registry's work, while a computation is still changing
|
|
452
|
+
// the edges it reads through: what is worked out now isn't kept for the snapshot.
|
|
453
|
+
const inScope = this.#previous.has(event.node) ||
|
|
454
|
+
(this.#members ?? this.#walk()).has(event.node);
|
|
455
|
+
if (event._tag === "NodeAdded" ||
|
|
456
|
+
event._tag === "NodeRemoved" ||
|
|
457
|
+
event._tag === "Built") {
|
|
458
|
+
this.#changed();
|
|
459
|
+
}
|
|
460
|
+
if (inScope) {
|
|
461
|
+
this.#emit({ ...event, id: this.#id(event.node.atom) });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
#emit(event) {
|
|
465
|
+
for (const listener of this.#listeners) {
|
|
466
|
+
try {
|
|
467
|
+
listener(event);
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
console.error("effect-atom-svelte: an inspector scope listener threw", error);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/** What the snapshot shows, in short, to tell whether it changed. */
|
|
475
|
+
#sign() {
|
|
476
|
+
const { edges, nodes, readers } = this.snapshot();
|
|
477
|
+
return [
|
|
478
|
+
nodes.map((node) => node.id).join(","),
|
|
479
|
+
edges.map((edge) => `${edge.from}>${edge.to}`).join(","),
|
|
480
|
+
readers.map((reader) => `${reader.id}:${reader.atom}`).join(","),
|
|
481
|
+
].join("|");
|
|
482
|
+
}
|
|
483
|
+
/** The scope may have changed: work it out again once the registry's work is done. */
|
|
484
|
+
#changed() {
|
|
485
|
+
if (this.#members !== undefined) {
|
|
486
|
+
this.#previous = this.#members;
|
|
487
|
+
}
|
|
488
|
+
this.#members = undefined;
|
|
489
|
+
if (this.#checking || this.#listeners.size === 0) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
this.#checking = true;
|
|
493
|
+
queueMicrotask(() => {
|
|
494
|
+
this.#checking = false;
|
|
495
|
+
const signature = this.#sign();
|
|
496
|
+
this.#previous = this.#nodes();
|
|
497
|
+
if (signature !== this.#signature) {
|
|
498
|
+
this.#signature = signature;
|
|
499
|
+
this.#emit({ _tag: "ScopeChanged" });
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Makes the component being set up, and everything below it, an inspector scope, and returns it.
|
|
506
|
+
* The library's hooks below report the atoms they use to the nearest scope, which shows those
|
|
507
|
+
* atoms, everything upstream of them, and the hooks that use them. Call it while the component
|
|
508
|
+
* sets up, as context requires.
|
|
509
|
+
*
|
|
510
|
+
* Only in the browser, in development and production alike (the effect-atom-svelte docs draw each
|
|
511
|
+
* example's atoms from one); on the server it returns a scope that is always empty. Until the
|
|
512
|
+
* scope has a listener it only keeps a list of its hooks: the registry's inspector starts with the
|
|
513
|
+
* first listener.
|
|
514
|
+
*
|
|
515
|
+
* **Example** (A graph of what an example reads)
|
|
516
|
+
*
|
|
517
|
+
* ```ts
|
|
518
|
+
* import { provideInspectorScope } from "effect-atom-svelte/inspector";
|
|
519
|
+
*
|
|
520
|
+
* // In the component that frames the example, around its content
|
|
521
|
+
* const scope = provideInspectorScope();
|
|
522
|
+
* $effect(() =>
|
|
523
|
+
* scope.subscribe((event) => {
|
|
524
|
+
* if (event._tag === "ScopeChanged") {
|
|
525
|
+
* draw(scope.snapshot());
|
|
526
|
+
* } else if (event._tag === "Updated") {
|
|
527
|
+
* pulse(event.id);
|
|
528
|
+
* }
|
|
529
|
+
* })
|
|
530
|
+
* );
|
|
531
|
+
* ```
|
|
532
|
+
*
|
|
533
|
+
* @stability unstable
|
|
534
|
+
* @since 0.2.0
|
|
535
|
+
* @category inspecting
|
|
536
|
+
*/
|
|
537
|
+
export const provideInspectorScope = () => {
|
|
538
|
+
if (!BROWSER) {
|
|
539
|
+
return inertScope;
|
|
540
|
+
}
|
|
541
|
+
const scope = new Scope();
|
|
542
|
+
setReporter(scope.reporter);
|
|
543
|
+
return scope;
|
|
544
|
+
};
|
|
545
|
+
/**
|
|
546
|
+
* Names the component being set up, for the scopes its hooks report to. The atomLabels plugin of
|
|
547
|
+
* effect-atom-svelte-devtools calls it at the top of each component's script; without the plugin,
|
|
548
|
+
* call it yourself. Only in the browser.
|
|
549
|
+
*
|
|
550
|
+
* @stability unstable
|
|
551
|
+
* @since 0.2.0
|
|
552
|
+
* @category inspecting
|
|
553
|
+
*/
|
|
554
|
+
export const nameComponent = (name, file) => {
|
|
555
|
+
if (BROWSER) {
|
|
556
|
+
setComponentName(name, file);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
@@ -99,6 +99,8 @@ export type ProvideRegistryOptions = ProvideExistingRegistry | ProvideNewRegistr
|
|
|
99
99
|
*
|
|
100
100
|
* A registry created here is owned by the component and disposed of with it, which on the server
|
|
101
101
|
* means at the end of the request. A registry passed in is left for the caller to dispose of.
|
|
102
|
+
* In the browser, a finalizer that throws while the provider disposes of its registry is reported
|
|
103
|
+
* as an uncaught error after teardown, rather than thrown into Svelte's teardown.
|
|
102
104
|
*
|
|
103
105
|
* **Example** (Giving the app a registry from the root layout)
|
|
104
106
|
*
|
package/dist/RegistryContext.js
CHANGED
|
@@ -4,9 +4,13 @@
|
|
|
4
4
|
* @since 0.1.0
|
|
5
5
|
*/
|
|
6
6
|
import { AtomRegistry } from "effect/reactivity";
|
|
7
|
-
import { BROWSER } from "esm-env";
|
|
8
|
-
import { createContext
|
|
7
|
+
import { BROWSER, DEV } from "esm-env";
|
|
8
|
+
import { createContext } from "svelte";
|
|
9
|
+
import { disposeRegistry } from "./internal/disposal.js";
|
|
9
10
|
import { setRevalidateOnHydrate } from "./internal/hydration.js";
|
|
11
|
+
import { track } from "./internal/registries.js";
|
|
12
|
+
import { onTeardownAfterChildren } from "./internal/teardown.svelte.js";
|
|
13
|
+
import { troubleshooting } from "./internal/troubleshooting.js";
|
|
10
14
|
const [getContextRegistry, setContextRegistry, hasContextRegistry] = createContext();
|
|
11
15
|
let browserRegistry;
|
|
12
16
|
/**
|
|
@@ -34,9 +38,14 @@ export const getRegistry = () => {
|
|
|
34
38
|
return getContextRegistry();
|
|
35
39
|
}
|
|
36
40
|
if (!BROWSER) {
|
|
37
|
-
throw new Error(
|
|
41
|
+
throw new Error(`No AtomRegistry in context. Wrap the app in <RegistryProvider> so each server request gets its own registry. ${troubleshooting("no-atomregistry-in-context")}`);
|
|
42
|
+
}
|
|
43
|
+
if (browserRegistry === undefined) {
|
|
44
|
+
browserRegistry = AtomRegistry.make();
|
|
45
|
+
if (DEV) {
|
|
46
|
+
track(browserRegistry);
|
|
47
|
+
}
|
|
38
48
|
}
|
|
39
|
-
browserRegistry ??= AtomRegistry.make();
|
|
40
49
|
return browserRegistry;
|
|
41
50
|
};
|
|
42
51
|
/**
|
|
@@ -44,6 +53,8 @@ export const getRegistry = () => {
|
|
|
44
53
|
*
|
|
45
54
|
* A registry created here is owned by the component and disposed of with it, which on the server
|
|
46
55
|
* means at the end of the request. A registry passed in is left for the caller to dispose of.
|
|
56
|
+
* In the browser, a finalizer that throws while the provider disposes of its registry is reported
|
|
57
|
+
* as an uncaught error after teardown, rather than thrown into Svelte's teardown.
|
|
47
58
|
*
|
|
48
59
|
* **Example** (Giving the app a registry from the root layout)
|
|
49
60
|
*
|
|
@@ -71,15 +82,20 @@ export const provideRegistry = (options = {}) => {
|
|
|
71
82
|
if (provided &&
|
|
72
83
|
Object.values(registryOptions).some((value) => value !== undefined)) {
|
|
73
84
|
// The types rule this out; a caller without them would otherwise lose the options silently.
|
|
74
|
-
throw new Error(
|
|
85
|
+
throw new Error(`provideRegistry takes an existing registry or options for a new one, not both. Apply initialValues to the existing registry yourself. ${troubleshooting("provideregistry-takes-an-existing-registry-or-options-for-a-new-one-not-both")}`);
|
|
75
86
|
}
|
|
76
87
|
const registry = provided ?? AtomRegistry.make(registryOptions);
|
|
77
88
|
setContextRegistry(registry);
|
|
78
89
|
if (revalidateOnHydrate !== undefined) {
|
|
79
90
|
setRevalidateOnHydrate(revalidateOnHydrate);
|
|
80
91
|
}
|
|
92
|
+
// Not onDestroy: in the browser it never runs for a component destroyed while its script awaits.
|
|
81
93
|
if (!provided) {
|
|
82
|
-
|
|
94
|
+
onTeardownAfterChildren(() => disposeRegistry(registry));
|
|
95
|
+
}
|
|
96
|
+
if (DEV && BROWSER) {
|
|
97
|
+
// For developer tools to find (effect-atom-svelte/inspector).
|
|
98
|
+
onTeardownAfterChildren(track(registry));
|
|
83
99
|
}
|
|
84
100
|
return registry;
|
|
85
101
|
};
|
package/dist/ScopedAtom.d.ts
CHANGED
|
@@ -49,10 +49,11 @@ export interface MakeOptions {
|
|
|
49
49
|
/**
|
|
50
50
|
* Creates a scoped atom from a factory, which runs once per providing component.
|
|
51
51
|
*
|
|
52
|
-
* Each provider gets its own atom, so a factory that
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
52
|
+
* Each provider gets its own atom, so a factory that makes a serializable atom gives every copy of
|
|
53
|
+
* one input the same key, and two such providers on a server-rendered page make the render throw:
|
|
54
|
+
* two different atoms share the serialization key. For a value from the server, return a family's
|
|
55
|
+
* atom instead, `ScopedAtom.make((id: string) => userAtom(id))`, so every provider of one input
|
|
56
|
+
* holds the same atom.
|
|
56
57
|
*
|
|
57
58
|
* **Example** (A counter per subtree, started from an input)
|
|
58
59
|
*
|
package/dist/ScopedAtom.js
CHANGED
|
@@ -13,10 +13,11 @@ export const TypeId = "~effect-atom-svelte/ScopedAtom";
|
|
|
13
13
|
/**
|
|
14
14
|
* Creates a scoped atom from a factory, which runs once per providing component.
|
|
15
15
|
*
|
|
16
|
-
* Each provider gets its own atom, so a factory that
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Each provider gets its own atom, so a factory that makes a serializable atom gives every copy of
|
|
17
|
+
* one input the same key, and two such providers on a server-rendered page make the render throw:
|
|
18
|
+
* two different atoms share the serialization key. For a value from the server, return a family's
|
|
19
|
+
* atom instead, `ScopedAtom.make((id: string) => userAtom(id))`, so every provider of one input
|
|
20
|
+
* holds the same atom.
|
|
20
21
|
*
|
|
21
22
|
* **Example** (A counter per subtree, started from an input)
|
|
22
23
|
*
|