@tschk/moonshine-svelte 0.0.0-stage → 0.4.4

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/src/binding.ts ADDED
@@ -0,0 +1,115 @@
1
+ /** Native binding helpers with Svelte state-proxy identity comparison. */
2
+ import {
3
+ bindSelectValue as nativeBindSelectValue,
4
+ bindValue as nativeBindValue,
5
+ type InputValue,
6
+ createInputGroup as nativeCreateInputGroup,
7
+ } from "@tschk/moonshine-dom/binding";
8
+ import { stateEquals } from "./state";
9
+ import {
10
+ createComponentScope,
11
+ getOwner,
12
+ getRenderScheduler,
13
+ } from "@tschk/moonshine-dom/reactive";
14
+ import {
15
+ bindOwnedReference,
16
+ type ComponentReference,
17
+ type OwnedReference,
18
+ } from "@tschk/moonshine-dom/component-instance";
19
+ import { captureRuneReferenceCleanup } from "./pre";
20
+ export {
21
+ bindChecked,
22
+ bindOpen,
23
+ setOptionValue,
24
+ setInputValue,
25
+ type InputValue,
26
+ } from "@tschk/moonshine-dom/binding";
27
+
28
+ export function bindSelectValue(
29
+ select: HTMLSelectElement,
30
+ read: () => unknown,
31
+ write: (value: unknown) => void,
32
+ ): void {
33
+ nativeBindSelectValue(select, read, write, stateEquals);
34
+ }
35
+
36
+ export function createInputGroup(
37
+ kind: "radio" | "checkbox",
38
+ read: () => unknown,
39
+ write: (value: unknown) => void,
40
+ ): { bind(input: HTMLInputElement): void } {
41
+ return nativeCreateInputGroup(kind, read, write, stateEquals);
42
+ }
43
+
44
+ /** Svelte numeric bindings write null for an empty number/range input. */
45
+ export function bindValue(
46
+ element: HTMLInputElement | HTMLTextAreaElement,
47
+ read: () => InputValue,
48
+ write: (value: string | number | null) => void,
49
+ ): void {
50
+ nativeBindValue(element, read, (value, reset) => {
51
+ write(
52
+ value === undefined
53
+ ? reset && element.defaultValue !== ""
54
+ ? Number(element.defaultValue)
55
+ : null
56
+ : value,
57
+ );
58
+ });
59
+ }
60
+
61
+ /** Compiler-owned element references publish before this element's actions. */
62
+ export function bindThis<
63
+ T extends Element,
64
+ Parts extends readonly unknown[] = [],
65
+ >(element: T, reference: ComponentReference<T, Parts, unknown>): void {
66
+ const owner = getOwner();
67
+ if (!owner || owner.disposed || owner.disposing)
68
+ throw new Error("Element references require a live native owner");
69
+ if (
70
+ !(element instanceof Element) ||
71
+ !reference ||
72
+ typeof reference.get !== "function" ||
73
+ typeof reference.set !== "function"
74
+ )
75
+ throw new TypeError(
76
+ "Element references require an element and getter/setter functions",
77
+ );
78
+ if (reference.cleanup !== undefined && reference.cleanup !== "commit")
79
+ throw new TypeError(
80
+ "Element reference cleanup must be commit when provided",
81
+ );
82
+ if (reference.publication !== undefined && reference.publication !== "post")
83
+ throw new TypeError(
84
+ "Element reference publication must be post when provided",
85
+ );
86
+ const scheduler = getRenderScheduler();
87
+ if (!scheduler)
88
+ throw new Error("Element references require an opt-in render scheduler");
89
+ const enqueueClear = captureRuneReferenceCleanup();
90
+ createComponentScope(() =>
91
+ scheduler.branch(() => {
92
+ const scope = getOwner()!;
93
+ const posts = getRenderScheduler()!;
94
+ let published = false;
95
+ let firstPublication: (() => void) | undefined;
96
+ bindOwnedReference(
97
+ scope,
98
+ element,
99
+ // Tuple callbacks enter the checked internal path protocol here.
100
+ reference as unknown as OwnedReference<T>,
101
+ (publish) => {
102
+ if (published) publish();
103
+ else
104
+ posts.post(
105
+ (firstPublication ??= () => {
106
+ published = true;
107
+ publish();
108
+ }),
109
+ );
110
+ },
111
+ { cleanup: "commit", previousCheck: "after-set", enqueueClear },
112
+ )();
113
+ }),
114
+ );
115
+ }
package/src/class.ts ADDED
@@ -0,0 +1,21 @@
1
+ /** Normalize source class expressions; ordinary native h attributes stay unchanged. */
2
+ export function classValue(value: unknown): string | undefined {
3
+ if (value === null || value === undefined) return undefined;
4
+ if (typeof value !== "object") return `${value}` || undefined;
5
+ const tokens: string[] = [];
6
+ function append(entry: unknown): void {
7
+ if (!entry) return;
8
+ if (typeof entry === "string" || typeof entry === "number") {
9
+ tokens.push(`${entry}`);
10
+ } else if (Array.isArray(entry)) {
11
+ const length = entry.length;
12
+ for (let index = 0; index < length; index++) append(entry[index]);
13
+ } else if (typeof entry === "object") {
14
+ // Enumerable inherited class flags are part of Svelte's class expression API.
15
+ for (const key in entry)
16
+ if ((entry as Record<string, unknown>)[key]) tokens.push(key);
17
+ }
18
+ }
19
+ append(value);
20
+ return tokens.join(" ") || undefined;
21
+ }
@@ -0,0 +1,47 @@
1
+ /** Compiler-owned const evaluation with per-evaluation cleanup and consumer context. */
2
+ import { createDerived } from "./derived";
3
+ import { getRuneConsumer } from "./pre";
4
+ import { isHistoryReading } from "./history";
5
+ import {
6
+ withComponentConsumer,
7
+ withComponentCleanupConsumer,
8
+ } from "@tschk/moonshine-dom/component";
9
+ import {
10
+ createRoot,
11
+ getOwner,
12
+ onCleanup,
13
+ runWithOwner,
14
+ type Owner,
15
+ } from "@tschk/moonshine-dom/reactive";
16
+ export function createConstDerived<T>(fn: () => T): { readonly value: T } {
17
+ const frame = getRuneConsumer();
18
+ const parent = getOwner();
19
+ if (!parent || parent.disposed || !frame || frame.owner.disposed)
20
+ throw Error("Const requires a live consumer");
21
+ let stop: (() => void) | undefined;
22
+ const release = () => {
23
+ const previous = stop;
24
+ stop = undefined;
25
+ previous?.();
26
+ };
27
+ onCleanup(release);
28
+ return createDerived(() => {
29
+ if (isHistoryReading()) return withComponentCleanupConsumer(frame, fn);
30
+ release();
31
+ let evaluation!: Owner;
32
+ createRoot((dispose) => {
33
+ stop = dispose;
34
+ evaluation = getOwner()!;
35
+ });
36
+ try {
37
+ return runWithOwner(evaluation, () => withComponentConsumer(frame, fn));
38
+ } catch (error) {
39
+ try {
40
+ release();
41
+ } catch {
42
+ /* Preserve the computation failure after disposing owned work. */
43
+ }
44
+ throw error;
45
+ }
46
+ });
47
+ }
package/src/derived.ts ADDED
@@ -0,0 +1,276 @@
1
+ /** Lazy native derived values with writable optimistic overrides. */
2
+ import { createMemo, createSignal, untrack } from "@tschk/moonshine";
3
+ import {
4
+ collectDeps,
5
+ subscribeInvalidation,
6
+ trackDependency,
7
+ hasValueDemand,
8
+ type Trackable,
9
+ } from "@tschk/moonshine/tracking";
10
+ import {
11
+ getOwner,
12
+ onCleanup,
13
+ runWithOwner,
14
+ } from "@tschk/moonshine-dom/reactive";
15
+
16
+ import {
17
+ collectHistoryReads,
18
+ hasHistory,
19
+ historicalValue,
20
+ isHistoryReading,
21
+ readHistory,
22
+ recordHistory,
23
+ trackHistoryRead,
24
+ withHistoryCleanup,
25
+ withHistoryMutation,
26
+ type HistoryRead,
27
+ } from "./history";
28
+
29
+ export function createDerived<T>(fn: () => T): {
30
+ get value(): T;
31
+ set value(value: T);
32
+ } {
33
+ const owner = getOwner();
34
+ if (owner?.disposed)
35
+ throw new Error("Cannot create a derived value in a disposed owner");
36
+ const invalidation = createSignal(0);
37
+ const token = {};
38
+ let reads: HistoryRead[] = [];
39
+ let baseValue!: T;
40
+ type Snapshot = { value: T } | undefined;
41
+ function snapshot(): Snapshot {
42
+ return overridden
43
+ ? { value: override }
44
+ : returned
45
+ ? { value: last }
46
+ : undefined;
47
+ }
48
+ let initialized = false,
49
+ overridden = false,
50
+ disposed = false,
51
+ returned = false,
52
+ overrideInvalidated = false;
53
+ let inputRevision = 0;
54
+ let memoDependency: Trackable | undefined;
55
+ let overrideVersions = new Map<Trackable, number>();
56
+ let override!: T, last!: T;
57
+ let subscriptions = new Map<Trackable, () => void>();
58
+ function invalidate(): void {
59
+ if (disposed) return;
60
+ // Invalidating a committed override restores the previous computed inputs.
61
+ // A same-flush assignment already recorded its earlier value, which wins.
62
+ if (overridden) recordHistory(token, "override", undefined, undefined);
63
+ overrideInvalidated = true;
64
+ inputRevision++;
65
+ }
66
+ // Preserve the kernel dependency graph for pull validation and diamond updates.
67
+ // Side observers mark input changes without turning observation into value demand.
68
+ // Only a changed base computation resets an optimistic override.
69
+ const base = createMemo(() => {
70
+ let next!: T,
71
+ failed = false,
72
+ failure: unknown;
73
+ const versions = new Map<Trackable, number>();
74
+ const dependencies = collectDeps(
75
+ () => {
76
+ try {
77
+ const collected = collectHistoryReads(() => runWithOwner(owner, fn));
78
+ next = collected.value;
79
+ reads = collected.reads;
80
+ } catch (error) {
81
+ failed = true;
82
+ failure = error;
83
+ }
84
+ },
85
+ (dependency) => versions.set(dependency, dependency.version),
86
+ true,
87
+ );
88
+ const nextSubscriptions = new Map<Trackable, () => void>();
89
+ try {
90
+ untrack(() => {
91
+ for (const dependency of dependencies)
92
+ nextSubscriptions.set(
93
+ dependency,
94
+ subscriptions.get(dependency) ??
95
+ subscribeInvalidation(dependency, {
96
+ demand: false,
97
+ invalidate,
98
+ notify() {
99
+ if (!disposed) invalidation.set((previous) => previous + 1);
100
+ },
101
+ }),
102
+ );
103
+ for (const [dependency, unsubscribe] of subscriptions)
104
+ if (!nextSubscriptions.has(dependency)) unsubscribe();
105
+ });
106
+ } catch (error) {
107
+ for (const [dependency, unsubscribe] of nextSubscriptions)
108
+ if (!subscriptions.has(dependency)) unsubscribe();
109
+ if (!failed) failure = error;
110
+ failed = true;
111
+ }
112
+ subscriptions = nextSubscriptions;
113
+ if (failed) throw failure;
114
+ if (
115
+ dependencies.some(
116
+ (dependency) => dependency.version !== versions.get(dependency),
117
+ )
118
+ )
119
+ throw new Error("Derived computations must not write their dependencies");
120
+ // Pull validation can run before this dependency's queued notification.
121
+ // Reset here too so optimistic siblings cannot expose a stale override.
122
+ if (initialized) {
123
+ if (hasHistory(reads))
124
+ recordHistory(token, "override", undefined, undefined);
125
+ overridden = false;
126
+ }
127
+ initialized = true;
128
+ baseValue = next;
129
+ return next;
130
+ });
131
+ let baseDependency: Parameters<typeof trackDependency>[0] | undefined;
132
+ const baseView: Parameters<typeof trackDependency>[0] = {
133
+ get version() {
134
+ return inputRevision;
135
+ },
136
+ validate() {},
137
+ subscribe: (
138
+ ...args: Parameters<Parameters<typeof trackDependency>[0]["subscribe"]>
139
+ ) => baseDependency!.subscribe(...args),
140
+ };
141
+ const memo = createMemo(() => {
142
+ invalidation();
143
+ let computed!: T;
144
+ if (overridden && initialized && !overrideInvalidated && baseDependency) {
145
+ computed = override;
146
+ } else if (baseDependency) computed = untrack(base);
147
+ else {
148
+ const dependencies = collectDeps(() => {
149
+ computed = base();
150
+ });
151
+ baseDependency = dependencies[0] as Parameters<typeof trackDependency>[0];
152
+ }
153
+ trackDependency(baseView);
154
+ overrideInvalidated = false;
155
+ last = overridden ? override : computed;
156
+ returned = true;
157
+ return last;
158
+ });
159
+ if (owner)
160
+ onCleanup(() => {
161
+ disposed = true;
162
+ for (const unsubscribe of subscriptions.values()) untrack(unsubscribe);
163
+ subscriptions.clear();
164
+ });
165
+ let frozenFailure = false,
166
+ frozenError: unknown;
167
+ return {
168
+ get value() {
169
+ trackHistoryRead(token, "override");
170
+ for (const read of reads) trackHistoryRead(read.target, read.key);
171
+ if (isHistoryReading())
172
+ return historicalValue(token, () =>
173
+ untrack(() => {
174
+ let recorded = true;
175
+ const previous = readHistory<Snapshot>(token, "override", () => {
176
+ recorded = false;
177
+ return undefined;
178
+ });
179
+ if (previous) return previous.value;
180
+ if (initialized && !hasHistory(reads))
181
+ return !recorded && overridden ? override : baseValue;
182
+ return runWithOwner(owner, () => withHistoryCleanup(fn));
183
+ }),
184
+ );
185
+ if (disposed) {
186
+ if (frozenFailure) throw frozenError;
187
+ if (returned) return last;
188
+ if (overridden) return override;
189
+ // A never-read value may finish once after its owner has stopped, without
190
+ // reconnecting the disposed memo or retaining dependency subscriptions.
191
+ frozenFailure = true;
192
+ frozenError = new Error(
193
+ "Derived value read during its disposed evaluation",
194
+ );
195
+ const versions = new Map<Trackable, number>();
196
+ let next!: T;
197
+ try {
198
+ const dependencies = untrack(() =>
199
+ collectDeps(
200
+ () => {
201
+ next = runWithOwner(owner, fn);
202
+ },
203
+ (dependency) => versions.set(dependency, dependency.version),
204
+ ),
205
+ );
206
+ if (
207
+ dependencies.some(
208
+ (dependency) => dependency.version !== versions.get(dependency),
209
+ )
210
+ )
211
+ throw new Error(
212
+ "Derived computations must not write their dependencies",
213
+ );
214
+ frozenFailure = false;
215
+ last = baseValue = next;
216
+ returned = initialized = true;
217
+ return next;
218
+ } catch (error) {
219
+ frozenFailure = true;
220
+ frozenError = error;
221
+ throw error;
222
+ }
223
+ }
224
+ if (
225
+ overridden &&
226
+ initialized &&
227
+ memoDependency &&
228
+ !hasValueDemand(memoDependency)
229
+ ) {
230
+ let changed = false;
231
+ untrack(() => {
232
+ for (const [dependency, version] of overrideVersions) {
233
+ dependency.validate?.();
234
+ if (dependency.version !== version) changed = true;
235
+ }
236
+ });
237
+ if (changed && !overrideInvalidated) {
238
+ overrideInvalidated = true;
239
+ inputRevision++;
240
+ invalidation.set((previous) => previous + 1);
241
+ }
242
+ }
243
+ if (!memoDependency) {
244
+ let value!: T;
245
+ collectDeps(
246
+ () => {
247
+ value = memo();
248
+ },
249
+ (dependency) => {
250
+ memoDependency = dependency;
251
+ },
252
+ true,
253
+ );
254
+ return value;
255
+ }
256
+ return memo();
257
+ },
258
+ set value(value: T) {
259
+ if (disposed) throw new Error("Cannot override a disposed derived value");
260
+ withHistoryMutation(() => {
261
+ if (!initialized) untrack(base);
262
+ recordHistory(token, "override", snapshot(), { value });
263
+ overrideVersions = new Map(
264
+ [...subscriptions.keys()].map((dependency) => [
265
+ dependency,
266
+ dependency.version,
267
+ ]),
268
+ );
269
+ overrideInvalidated = false;
270
+ override = value;
271
+ overridden = true;
272
+ invalidation.set((previous) => previous + 1);
273
+ });
274
+ },
275
+ };
276
+ }
@@ -0,0 +1,129 @@
1
+ /** Explicit native directive installation; source syntax remains compiler-owned. */
2
+ import {
3
+ createComponentScope,
4
+ templateRetentionKey,
5
+ createRenderEffect,
6
+ getOwner,
7
+ getRenderScheduler,
8
+ onAfterDispose,
9
+ onCleanup,
10
+ runWithOwner,
11
+ untrack,
12
+ } from "@tschk/moonshine-dom/reactive";
13
+ import { isSSRRendering } from "@tschk/moonshine-dom/ssr-owner";
14
+ import { peekComponentContext } from "@tschk/moonshine-dom/component";
15
+ import { mapArray } from "@tschk/moonshine-dom/control";
16
+ import { createRuneEffect } from "./effect";
17
+ import { trackActionState } from "./state";
18
+ import { withHistoryCleanup } from "./history";
19
+
20
+ /** Type-only authored Svelte action contract. */
21
+ export interface ActionReturn<
22
+ Parameter = undefined,
23
+ Attributes extends object = Record<never, never>,
24
+ > {
25
+ update?: (parameter: Parameter) => void;
26
+ destroy?: () => void;
27
+ $$_attributes?: Attributes;
28
+ }
29
+ export interface Action<
30
+ Element = HTMLElement,
31
+ Parameter = undefined,
32
+ Attributes extends object = Record<never, never>,
33
+ > {
34
+ <Node extends Element>(
35
+ ...args: undefined extends Parameter
36
+ ? [node: Node, parameter?: Parameter]
37
+ : [node: Node, parameter: Parameter]
38
+ ): void | ActionReturn<Parameter, Attributes>;
39
+ }
40
+
41
+ export type ActionPayload<T> = { update?(value: T): void; destroy?(): void };
42
+ export type Attachment<E extends Element = Element> = (
43
+ element: E,
44
+ ) => void | (() => void);
45
+ function install(fn: () => void): void {
46
+ if (isSSRRendering() || peekComponentContext()?.mode === "ssr") return;
47
+ const owner = getOwner(),
48
+ scheduler = getRenderScheduler();
49
+ if (!owner || owner.disposed || !scheduler)
50
+ throw Error("Native directives require a live owner and withRuneScheduler");
51
+ createComponentScope(() => scheduler.branch(fn));
52
+ }
53
+ function cleanup(fn: () => void): void {
54
+ const finish = () => withHistoryCleanup(() => untrack(fn));
55
+ const owner = getOwner();
56
+ if (owner?.disposed) finish();
57
+ else if (owner?.context?.has(templateRetentionKey))
58
+ onCleanup(() => runWithOwner(owner, finish));
59
+ else onAfterDispose(finish);
60
+ }
61
+ function different(previous: unknown, next: unknown): boolean {
62
+ return previous !== previous
63
+ ? next === next
64
+ : previous !== next ||
65
+ (previous !== null && typeof previous === "object") ||
66
+ typeof previous === "function";
67
+ }
68
+ /** invoke is a closure: resolve a mutable action reference only when the post factory runs. */
69
+ export function installAction<E extends Element, T>(
70
+ element: E,
71
+ invoke: (element: E, value: T) => ActionPayload<T> | void | false | null,
72
+ read: () => T,
73
+ ): void;
74
+ export function installAction<E extends Element, T>(
75
+ element: E,
76
+ invoke: (element: E) => ActionPayload<T> | void | false | null,
77
+ ): void;
78
+ export function installAction<E extends Element, T>(
79
+ element: E,
80
+ invoke: (element: E, value: T) => ActionPayload<T> | void | false | null,
81
+ read?: () => T,
82
+ ): void {
83
+ install(() =>
84
+ createRuneEffect(() => {
85
+ const payload = untrack(
86
+ () =>
87
+ (read
88
+ ? invoke(element, read())
89
+ : (invoke as (element: E) => ReturnType<typeof invoke>)(element)) ||
90
+ {},
91
+ );
92
+ // Register before updater setup so a failing parameter traversal still releases the action.
93
+ if (payload.destroy) cleanup(() => payload.destroy!());
94
+ if (read && payload.update) {
95
+ let initialized = false;
96
+ let previous: unknown = {};
97
+ createRenderEffect(() => {
98
+ const next = read();
99
+ trackActionState(next);
100
+ if (initialized && different(previous, next)) {
101
+ previous = next;
102
+ payload.update!(next);
103
+ }
104
+ });
105
+ initialized = true;
106
+ }
107
+ }),
108
+ );
109
+ }
110
+ export function installAttachment<E extends Element>(
111
+ element: E,
112
+ read: () => Attachment<E> | false | null | undefined,
113
+ ): void {
114
+ install(() => {
115
+ mapArray(
116
+ () => {
117
+ const fn = read();
118
+ return fn ? [fn] : [];
119
+ },
120
+ (fn) => {
121
+ createRuneEffect(() => {
122
+ const stop = fn(element);
123
+ if (typeof stop === "function") cleanup(stop);
124
+ });
125
+ return undefined;
126
+ },
127
+ );
128
+ });
129
+ }