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.
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * The module does not import SvelteKit: the hooks take the fields of SvelteKit's input they read
6
6
  * and return the fields they set, so they type-check against `HandleClientError` and
7
- * `HandleServerError` once `App.Error` declares `tag?: string`.
7
+ * `HandleServerError` once `App.Error` declares `tag?: string`. In SvelteKit 2, whose `App.Error`
8
+ * requires a message, they are typed to return one when there is no `kind`, as they then always
9
+ * set one.
8
10
  *
9
11
  * @since 0.1.0
10
12
  */
@@ -38,6 +40,27 @@ export interface EffectErrorBody {
38
40
  readonly message?: string;
39
41
  readonly tag?: string;
40
42
  }
43
+ /**
44
+ * SvelteKit 2's `handleError` input: no `kind`, and always a message, which the hooks fall back on.
45
+ *
46
+ * @stability unstable
47
+ * @since 0.2.1
48
+ * @category models
49
+ */
50
+ export interface CaughtErrorWithoutKind extends CaughtError {
51
+ readonly kind?: undefined;
52
+ readonly message: string;
53
+ }
54
+ /**
55
+ * What the hooks return for SvelteKit 2's input: always a message, as its `App.Error` requires one.
56
+ *
57
+ * @stability unstable
58
+ * @since 0.2.1
59
+ * @category models
60
+ */
61
+ export interface EffectErrorBodyWithMessage extends EffectErrorBody {
62
+ readonly message: string;
63
+ }
41
64
  /**
42
65
  * A client `handleError` hook (`src/hooks.client.ts`) that keeps the message and `_tag` of errors
43
66
  * thrown by your code, so a boundary's `failed` snippet shows them instead of
@@ -56,7 +79,8 @@ export interface EffectErrorBody {
56
79
  * @since 0.1.0
57
80
  * @category hooks
58
81
  */
59
- export declare const handleClientError: (input: CaughtError) => EffectErrorBody | undefined;
82
+ export declare function handleClientError(input: CaughtErrorWithoutKind): EffectErrorBodyWithMessage;
83
+ export declare function handleClientError(input: CaughtError): EffectErrorBody | undefined;
60
84
  /**
61
85
  * A server `handleError` hook (`src/hooks.server.ts`) that keeps the `_tag` of errors thrown by
62
86
  * your code but not their message, which could expose details of the server to users: the
@@ -75,4 +99,5 @@ export declare const handleClientError: (input: CaughtError) => EffectErrorBody
75
99
  * @since 0.1.0
76
100
  * @category hooks
77
101
  */
78
- export declare const handleServerError: (input: CaughtError) => EffectErrorBody | undefined;
102
+ export declare function handleServerError(input: CaughtErrorWithoutKind): EffectErrorBodyWithMessage;
103
+ export declare function handleServerError(input: CaughtError): EffectErrorBody | undefined;
package/dist/SvelteKit.js CHANGED
@@ -4,7 +4,9 @@
4
4
  *
5
5
  * The module does not import SvelteKit: the hooks take the fields of SvelteKit's input they read
6
6
  * and return the fields they set, so they type-check against `HandleClientError` and
7
- * `HandleServerError` once `App.Error` declares `tag?: string`.
7
+ * `HandleServerError` once `App.Error` declares `tag?: string`. In SvelteKit 2, whose `App.Error`
8
+ * requires a message, they are typed to return one when there is no `kind`, as they then always
9
+ * set one.
8
10
  *
9
11
  * @since 0.1.0
10
12
  */
@@ -31,52 +33,17 @@ const body = (message, tag) => {
31
33
  }
32
34
  return { ...(message ? { message } : {}), ...(tag ? { tag } : {}) };
33
35
  };
34
- /**
35
- * A client `handleError` hook (`src/hooks.client.ts`) that keeps the message and `_tag` of errors
36
- * thrown by your code, so a boundary's `failed` snippet shows them instead of
37
- * `{ status: 500, message: "Internal Error" }`. It logs those errors like SvelteKit's default
38
- * hook, and leaves `error(...)` and SvelteKit's own errors as they are. Made for SvelteKit 3:
39
- * SvelteKit 2 doesn't say which errors are its own, so there every error is kept and logged.
40
- *
41
- * **Example** (Using it as the client hook)
42
- *
43
- * ```ts
44
- * // src/hooks.client.ts
45
- * export { handleClientError as handleError } from "effect-atom-svelte/sveltekit";
46
- * ```
47
- *
48
- * @stability unstable
49
- * @since 0.1.0
50
- * @category hooks
51
- */
52
- export const handleClientError = (input) => {
36
+ export function handleClientError(input) {
53
37
  // Destructured here, not in the parameter list, so the API reference shows a named parameter.
54
38
  const { error, kind = "unknown" } = input;
55
39
  if (kind !== "unknown") {
56
40
  return undefined;
57
41
  }
58
42
  console.error(error);
59
- return body(messageOf(error) ?? fallbackMessage(input), tagOf(error));
60
- };
61
- /**
62
- * A server `handleError` hook (`src/hooks.server.ts`) that keeps the `_tag` of errors thrown by
63
- * your code but not their message, which could expose details of the server to users: the
64
- * message stays SvelteKit's `"Internal Error"`. It logs errors with `console.error`.
65
- * Server errors reach a boundary's `failed` snippet when it renders on the server. Made for
66
- * SvelteKit 3: SvelteKit 2 doesn't say which errors are its own, so there every error is logged.
67
- *
68
- * **Example** (Using it as the server hook)
69
- *
70
- * ```ts
71
- * // src/hooks.server.ts
72
- * export { handleServerError as handleError } from "effect-atom-svelte/sveltekit";
73
- * ```
74
- *
75
- * @stability unstable
76
- * @since 0.1.0
77
- * @category hooks
78
- */
79
- export const handleServerError = (input) => {
43
+ // `||`, not `??`: an error with an empty message gets SvelteKit 2's instead.
44
+ return body(messageOf(error) || fallbackMessage(input), tagOf(error));
45
+ }
46
+ export function handleServerError(input) {
80
47
  const { error, issues, kind = "unknown" } = input;
81
48
  if (kind === "validation") {
82
49
  console.error("Remote function schema validation failed:", issues);
@@ -87,4 +54,4 @@ export const handleServerError = (input) => {
87
54
  }
88
55
  console.error(error);
89
56
  return body(fallbackMessage(input), tagOf(error));
90
- };
57
+ }
package/dist/index.d.ts CHANGED
@@ -47,4 +47,4 @@ export * as ScopedAtom from "./ScopedAtom.ts";
47
47
  * @since 0.1.0
48
48
  * @category re-exports
49
49
  */
50
- export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, } from "effect/reactivity";
50
+ export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, Reactivity, } from "effect/reactivity";
package/dist/index.js CHANGED
@@ -47,4 +47,4 @@ export * as ScopedAtom from "./ScopedAtom.js";
47
47
  * @since 0.1.0
48
48
  * @category re-exports
49
49
  */
50
- export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, } from "effect/reactivity";
50
+ export { AsyncResult, Atom, AtomHttpApi, AtomRef, AtomRegistry, AtomRpc, Hydration, Reactivity, } from "effect/reactivity";
@@ -0,0 +1,9 @@
1
+ import type { AtomRegistry } from "effect/reactivity";
2
+ /**
3
+ * Per registry, key and queued value, how many live boundaries queued it. Boundaries given the
4
+ * same state, as a layout and its page might be, queue the same values, and one destroyed first
5
+ * must leave them for the other's children.
6
+ */
7
+ export declare const queuedBy: WeakMap<AtomRegistry.AtomRegistry, Map<string, Map<unknown, number>>>;
8
+ /** Whether a live boundary brought a value for the key: it sends that value to the browser itself. */
9
+ export declare const queuedByBoundary: (registry: AtomRegistry.AtomRegistry, key: string) => boolean;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Per registry, key and queued value, how many live boundaries queued it. Boundaries given the
3
+ * same state, as a layout and its page might be, queue the same values, and one destroyed first
4
+ * must leave them for the other's children.
5
+ */
6
+ export const queuedBy = new WeakMap();
7
+ /** Whether a live boundary brought a value for the key: it sends that value to the browser itself. */
8
+ export const queuedByBoundary = (registry, key) => queuedBy.get(registry)?.has(key) === true;
@@ -0,0 +1,13 @@
1
+ import type { AtomRegistry } from "effect/reactivity";
2
+ /** Calls `f` once if `registry` is disposed of through `disposeRegistry`; returns the canceler. */
3
+ export declare const onDispose: (registry: AtomRegistry.AtomRegistry, f: () => void) => (() => void);
4
+ /**
5
+ * Disposes of `registry`, then runs what waited on it, even when a finalizer the disposal runs
6
+ * throws.
7
+ *
8
+ * In the browser that error is rethrown on a microtask, so it is still reported as uncaught but
9
+ * stays out of Svelte's teardown: thrown from the `onDestroy` of a component being destroyed, it
10
+ * escapes Svelte's flush and leaves every `$derived` on the page running again on every read. On
11
+ * the server it is thrown here.
12
+ */
13
+ export declare const disposeRegistry: (registry: AtomRegistry.AtomRegistry) => void;
@@ -0,0 +1,43 @@
1
+ import { BROWSER } from "esm-env";
2
+ const callbacks = new WeakMap();
3
+ /** Calls `f` once if `registry` is disposed of through `disposeRegistry`; returns the canceler. */
4
+ export const onDispose = (registry, f) => {
5
+ let set = callbacks.get(registry);
6
+ if (set === undefined) {
7
+ set = new Set();
8
+ callbacks.set(registry, set);
9
+ }
10
+ set.add(f);
11
+ return () => {
12
+ set.delete(f);
13
+ };
14
+ };
15
+ /**
16
+ * Disposes of `registry`, then runs what waited on it, even when a finalizer the disposal runs
17
+ * throws.
18
+ *
19
+ * In the browser that error is rethrown on a microtask, so it is still reported as uncaught but
20
+ * stays out of Svelte's teardown: thrown from the `onDestroy` of a component being destroyed, it
21
+ * escapes Svelte's flush and leaves every `$derived` on the page running again on every read. On
22
+ * the server it is thrown here.
23
+ */
24
+ export const disposeRegistry = (registry) => {
25
+ try {
26
+ registry.dispose();
27
+ }
28
+ catch (error) {
29
+ if (!BROWSER) {
30
+ throw error;
31
+ }
32
+ queueMicrotask(() => {
33
+ throw error;
34
+ });
35
+ }
36
+ finally {
37
+ const set = callbacks.get(registry);
38
+ callbacks.delete(registry);
39
+ for (const f of set ?? []) {
40
+ f();
41
+ }
42
+ }
43
+ };
@@ -6,6 +6,11 @@ export declare const setRevalidateOnHydrate: (value: boolean) => void;
6
6
  * provider's, else no (JND-19). Call during component init.
7
7
  */
8
8
  export declare const revalidatesOnHydrate: (option: boolean | undefined) => boolean;
9
+ /**
10
+ * What the server sends for a result it doesn't pass on, so the browser computes the atom itself.
11
+ * An encoded `AsyncResult` is an object, never `null`.
12
+ */
13
+ export declare const noSeed: null;
9
14
  /**
10
15
  * Encodes the server's result for the page, or returns the no-seed marker for a result that must
11
16
  * not or cannot travel:
@@ -17,8 +22,12 @@ export declare const revalidatesOnHydrate: (option: boolean | undefined) => bool
17
22
  * failing the whole render. Development builds warn.
18
23
  */
19
24
  export declare const encodeSeed: (key: string, encode: (value: AsyncResult.AsyncResult<unknown, unknown>) => unknown, result: AsyncResult.AsyncResult<unknown, unknown>) => unknown;
20
- /** The server's result, decoded, or undefined when the server sent none. */
21
- export declare const decodeSeed: (value: unknown, decode: (value: unknown) => unknown) => unknown;
25
+ /**
26
+ * The server's result, decoded, or undefined when the server sent none or it doesn't decode with
27
+ * the browser's schema: the browser then computes the atom itself, as for a result the server
28
+ * couldn't encode. Development builds warn.
29
+ */
30
+ export declare const decodeSeed: (key: string, value: unknown, decode: (value: unknown) => unknown) => unknown;
22
31
  /**
23
32
  * Whether a seed is still waiting, as a stream's result is between its values. The browser runs
24
33
  * the atom again from it, as the server's run ended with the render.
@@ -19,7 +19,7 @@ export const revalidatesOnHydrate = (option) => option ?? (hasDefault() ? getDef
19
19
  * What the server sends for a result it doesn't pass on, so the browser computes the atom itself.
20
20
  * An encoded `AsyncResult` is an object, never `null`.
21
21
  */
22
- const noSeed = null;
22
+ export const noSeed = null;
23
23
  /**
24
24
  * Encodes the server's result for the page, or returns the no-seed marker for a result that must
25
25
  * not or cannot travel:
@@ -48,8 +48,28 @@ export const encodeSeed = (key, encode, result) => {
48
48
  return noSeed;
49
49
  }
50
50
  };
51
- /** The server's result, decoded, or undefined when the server sent none. */
52
- export const decodeSeed = (value, decode) => (value === noSeed ? undefined : decode(value));
51
+ /**
52
+ * The server's result, decoded, or undefined when the server sent none or it doesn't decode with
53
+ * the browser's schema: the browser then computes the atom itself, as for a result the server
54
+ * couldn't encode. Development builds warn.
55
+ */
56
+ export const decodeSeed = (key, value, decode) => {
57
+ if (value === noSeed) {
58
+ return undefined;
59
+ }
60
+ try {
61
+ return decode(value);
62
+ }
63
+ catch (error) {
64
+ if (!Schema.isSchemaError(error)) {
65
+ throw error;
66
+ }
67
+ if (DEV) {
68
+ console.warn(`effect-atom-svelte: the server's result for the serializable atom "${key}" doesn't decode with its schema, so the browser computes the atom again.`, error);
69
+ }
70
+ return undefined;
71
+ }
72
+ };
53
73
  /**
54
74
  * Whether a seed is still waiting, as a stream's result is between its values. The browser runs
55
75
  * the atom again from it, as the server's run ended with the render.
@@ -0,0 +1,30 @@
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
+ /** The node's remove() has returned. */
18
+ readonly removed: () => void;
19
+ }
20
+ /** Reports what happens to a node, or returns false if it isn't the implementation this expects. */
21
+ export declare const instrumentNode: (node: AtomRegistry.Node<unknown>, hooks: NodeHooks) => boolean;
22
+ /**
23
+ * The idle TTL a registry gives atoms without their own (`AtomRegistry.make`'s `defaultIdleTTL`),
24
+ * which the implementation keeps but the interface doesn't show.
25
+ */
26
+ export declare const defaultIdleTTL: (registry: AtomRegistry.AtomRegistry) => number | undefined;
27
+ /** Whether the node holds an initial value it hasn't computed (setInitialValue on a new node). */
28
+ export declare const holdsInitialValue: (node: AtomRegistry.Node<unknown>) => boolean;
29
+ /** Whether the node is building and its running build has not read `parent` yet. */
30
+ export declare const buildWillRead: (node: AtomRegistry.Node<unknown>, parent: AtomRegistry.Node<unknown>) => boolean;
@@ -0,0 +1,107 @@
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
+ clear() {
24
+ const had = this.size > 0;
25
+ super.clear();
26
+ if (had) {
27
+ this.#changed();
28
+ }
29
+ }
30
+ delete(listener) {
31
+ const deleted = super.delete(listener);
32
+ if (deleted) {
33
+ this.#changed();
34
+ }
35
+ return deleted;
36
+ }
37
+ }
38
+ /** Reports what happens to a node, or returns false if it isn't the implementation this expects. */
39
+ export const instrumentNode = (node, hooks) => {
40
+ if (!isNodeImpl(node)) {
41
+ return false;
42
+ }
43
+ const { build: compute, remove: removeNode } = node;
44
+ node.remove = function remove() {
45
+ try {
46
+ removeNode.call(this);
47
+ }
48
+ finally {
49
+ hooks.removed();
50
+ }
51
+ };
52
+ let value = node._value;
53
+ let { lifetime } = node;
54
+ node.build = function build() {
55
+ hooks.build();
56
+ try {
57
+ compute.call(this);
58
+ }
59
+ finally {
60
+ hooks.built();
61
+ }
62
+ };
63
+ Object.defineProperties(node, {
64
+ _value: {
65
+ configurable: true,
66
+ enumerable: true,
67
+ get: () => value,
68
+ set: (next) => {
69
+ const previous = value;
70
+ value = next;
71
+ hooks.value(previous, next);
72
+ },
73
+ },
74
+ lifetime: {
75
+ configurable: true,
76
+ enumerable: true,
77
+ get: () => lifetime,
78
+ set: (next) => {
79
+ const previous = lifetime;
80
+ lifetime = next;
81
+ if (next === undefined && previous !== undefined) {
82
+ hooks.dispose(previous.finalizers?.length ?? 0, value);
83
+ }
84
+ },
85
+ },
86
+ });
87
+ node.listeners = new Listeners(node.listeners, hooks.readers);
88
+ return true;
89
+ };
90
+ /**
91
+ * The idle TTL a registry gives atoms without their own (`AtomRegistry.make`'s `defaultIdleTTL`),
92
+ * which the implementation keeps but the interface doesn't show.
93
+ */
94
+ export const defaultIdleTTL = (registry) => {
95
+ const ttl = registry
96
+ .defaultIdleTTL;
97
+ return typeof ttl === "number" ? ttl : undefined;
98
+ };
99
+ /** Whether the node holds an initial value it hasn't computed (setInitialValue on a new node). */
100
+ export const holdsInitialValue = (node) => node
101
+ .preserveInitialValueOnBuild === true;
102
+ /** Whether the node is building and its running build has not read `parent` yet. */
103
+ export const buildWillRead = (node, parent) => {
104
+ const reads = node.lifetime
105
+ ?.reads;
106
+ return reads !== undefined && !reads.has(parent);
107
+ };
@@ -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,13 @@
1
+ /**
2
+ * Runs `f` once when the server render ends: in `onDestroy`, and failing that when the render's
3
+ * abort signal fires. A `<svelte:boundary>` with a `failed` snippet discards the content of children
4
+ * that throw while setting up, and their `onDestroy` callbacks with it, so a component set up before
5
+ * the throw would otherwise keep its mounts and holds on a caller-owned registry, and the next
6
+ * request would render this one's initial values (JND-17). On the server `getAbortSignal` returns the
7
+ * render's signal, which Svelte aborts once every `onDestroy` has run. A dropped component whose
8
+ * script awaits before calling this resumes after the render has ended and the signal has aborted,
9
+ * where a listener would never fire, so `f` then runs in a microtask: callers take what `f` releases
10
+ * synchronously after calling this. Call it during component init. Svelte bug workaround, to be
11
+ * removed with the fix (https://github.com/jarrednorrisdev/effect-atom-svelte/issues/35).
12
+ */
13
+ export declare const onRenderEnd: (f: () => void) => void;
@@ -0,0 +1,34 @@
1
+ // When a server render ends, for the hooks that release what they hold in a registry the caller may
2
+ // keep across requests. Internal: not exported from the package.
3
+ import { getAbortSignal, onDestroy } from "svelte";
4
+ /**
5
+ * Runs `f` once when the server render ends: in `onDestroy`, and failing that when the render's
6
+ * abort signal fires. A `<svelte:boundary>` with a `failed` snippet discards the content of children
7
+ * that throw while setting up, and their `onDestroy` callbacks with it, so a component set up before
8
+ * the throw would otherwise keep its mounts and holds on a caller-owned registry, and the next
9
+ * request would render this one's initial values (JND-17). On the server `getAbortSignal` returns the
10
+ * render's signal, which Svelte aborts once every `onDestroy` has run. A dropped component whose
11
+ * script awaits before calling this resumes after the render has ended and the signal has aborted,
12
+ * where a listener would never fire, so `f` then runs in a microtask: callers take what `f` releases
13
+ * synchronously after calling this. Call it during component init. Svelte bug workaround, to be
14
+ * removed with the fix (https://github.com/jarrednorrisdev/effect-atom-svelte/issues/35).
15
+ */
16
+ export const onRenderEnd = (f) => {
17
+ let done = false;
18
+ const once = () => {
19
+ if (!done) {
20
+ done = true;
21
+ f();
22
+ }
23
+ };
24
+ onDestroy(once);
25
+ const signal = getAbortSignal();
26
+ if (signal.aborted) {
27
+ // The render already ended, as for a component a failed boundary dropped whose script resumed
28
+ // after an await. Deferred, as callers register before taking what `f` releases.
29
+ queueMicrotask(once);
30
+ }
31
+ else {
32
+ signal.addEventListener("abort", once, { once: true });
33
+ }
34
+ };
@@ -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
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Runs `f` when the component is destroyed, even while its script is still awaiting. In the browser
3
+ * `onDestroy` only takes effect once the component mounts, so a component removed while pending
4
+ * would never call it. A pre effect runs during init and is torn down with the component (JND-16).
5
+ * On the server, `f` runs when the render ends, even for a component a failed boundary dropped.
6
+ */
7
+ export declare const onTeardown: (f: () => void) => void;
8
+ /**
9
+ * As `onTeardown`, but for a mounted component `f` runs after its children are destroyed, as with
10
+ * `onDestroy`: a pre effect is torn down before the children, whose own teardown may still use what
11
+ * `f` ends, such as a provider's registry. The pre effect only covers a component destroyed before
12
+ * it mounted. On the server `f` runs through `onRenderEnd`, so `provideRegistry` disposes its
13
+ * registry even when a failed boundary dropped the provider. A successful render keeps the
14
+ * children-first order through `onDestroy`; when the render ends without it, `f` may run before
15
+ * the children's releases, which are then no-ops on the disposed registry.
16
+ */
17
+ export declare const onTeardownAfterChildren: (f: () => void) => void;
@@ -0,0 +1,44 @@
1
+ // When a component is destroyed, for the hooks and providers. Internal: not exported from the
2
+ // package.
3
+ import { BROWSER } from "esm-env";
4
+ import { onDestroy, onMount } from "svelte";
5
+ import { onRenderEnd } from "./renderEnd.js";
6
+ /**
7
+ * Runs `f` when the component is destroyed, even while its script is still awaiting. In the browser
8
+ * `onDestroy` only takes effect once the component mounts, so a component removed while pending
9
+ * would never call it. A pre effect runs during init and is torn down with the component (JND-16).
10
+ * On the server, `f` runs when the render ends, even for a component a failed boundary dropped.
11
+ */
12
+ export const onTeardown = (f) => {
13
+ if (BROWSER) {
14
+ $effect.pre(() => f);
15
+ }
16
+ else {
17
+ onRenderEnd(f);
18
+ }
19
+ };
20
+ /**
21
+ * As `onTeardown`, but for a mounted component `f` runs after its children are destroyed, as with
22
+ * `onDestroy`: a pre effect is torn down before the children, whose own teardown may still use what
23
+ * `f` ends, such as a provider's registry. The pre effect only covers a component destroyed before
24
+ * it mounted. On the server `f` runs through `onRenderEnd`, so `provideRegistry` disposes its
25
+ * registry even when a failed boundary dropped the provider. A successful render keeps the
26
+ * children-first order through `onDestroy`; when the render ends without it, `f` may run before
27
+ * the children's releases, which are then no-ops on the disposed registry.
28
+ */
29
+ export const onTeardownAfterChildren = (f) => {
30
+ if (!BROWSER) {
31
+ onRenderEnd(f);
32
+ return;
33
+ }
34
+ let mounted = false;
35
+ $effect.pre(() => () => {
36
+ if (!mounted) {
37
+ f();
38
+ }
39
+ });
40
+ onMount(() => {
41
+ mounted = true;
42
+ });
43
+ onDestroy(f);
44
+ };
@@ -0,0 +1,2 @@
1
+ /** The sentence that ends an error message: a link to its entry on the troubleshooting page. */
2
+ export declare const troubleshooting: (entry: string) => string;
@@ -0,0 +1,3 @@
1
+ // Where error messages point for more. Internal: not exported from the package.
2
+ /** The sentence that ends an error message: a link to its entry on the troubleshooting page. */
3
+ export const troubleshooting = (entry) => `See https://atom.jarrednorris.dev/troubleshooting#${entry}`;