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.
@@ -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
  *
@@ -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, onDestroy } from "svelte";
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("No AtomRegistry in context. Wrap the app in <RegistryProvider> so each server request gets its own registry.");
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("provideRegistry takes an existing registry or options for a new one, not both. Apply initialValues to the existing registry yourself.");
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
- onDestroy(() => registry.dispose());
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
  };
@@ -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 adds `Atom.serializable` with a fixed key
53
- * gives every copy the same key: once two providers are on a page, reading them with
54
- * `useAtomResult` or `useAtomSuspense` throws, as two different atoms share the serialization
55
- * key. Put the input in the key, or leave scoped atoms unserialized.
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
  *
@@ -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 adds `Atom.serializable` with a fixed key
17
- * gives every copy the same key: once two providers are on a page, reading them with
18
- * `useAtomResult` or `useAtomSuspense` throws, as two different atoms share the serialization
19
- * key. Put the input in the key, or leave scoped atoms unserialized.
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
  *