@tschk/moonshine-vue 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.
@@ -0,0 +1,390 @@
1
+ import { captureWatchErrorHandler, invokeWatch } from "./error-context";
2
+ /** Native composition helpers, not Vue's component or compiler runtime. */
3
+ import {
4
+ batch,
5
+ createComputed,
6
+ createRoot,
7
+ createSignal,
8
+ getOwner,
9
+ onCleanup,
10
+ untrack,
11
+ } from "@tschk/moonshine-dom/reactive";
12
+ import { createMemo } from "@tschk/moonshine";
13
+ import { createMutable } from "@tschk/moonshine-dom/store";
14
+
15
+ const refs = new WeakSet<object>();
16
+ declare const refBrand: unique symbol;
17
+ export class Ref<T> {
18
+ declare readonly [refBrand]: true;
19
+ private readonly read: () => T;
20
+ private readonly write?: (value: T) => void;
21
+ constructor(read: () => T, write?: (value: T) => void) {
22
+ this.read = read;
23
+ this.write = write;
24
+ refs.add(this);
25
+ }
26
+ get value(): T {
27
+ return this.read();
28
+ }
29
+ set value(value: T) {
30
+ if (!this.write) throw new TypeError("Native ref is readonly");
31
+ this.write(value);
32
+ }
33
+ }
34
+ export function isRef(value: unknown): value is Ref<any> {
35
+ return typeof value === "object" && value !== null && refs.has(value);
36
+ }
37
+ function plain(value: unknown): value is object {
38
+ if (value === null || typeof value !== "object") return false;
39
+ const proto = Object.getPrototypeOf(value);
40
+ return Array.isArray(value) || proto === Object.prototype || proto === null;
41
+ }
42
+ const proxies = new WeakMap<object, object>();
43
+ const targets = new WeakMap<object, object>();
44
+ export function isReactive(value: unknown): value is object {
45
+ return value !== null && typeof value === "object" && targets.has(value);
46
+ }
47
+ const arrayIndex = (key: PropertyKey) =>
48
+ typeof key === "string" &&
49
+ /^(0|[1-9]\d*)$/.test(key) &&
50
+ Number(key) < 4294967295;
51
+ export type Unwrap<T> =
52
+ T extends Readonly<Ref<infer V>>
53
+ ? V
54
+ : T extends Function | Date | Map<unknown, unknown> | Set<unknown>
55
+ ? T
56
+ : T extends readonly unknown[]
57
+ ? {
58
+ [K in keyof T]: T[K] extends Readonly<Ref<any>>
59
+ ? T[K]
60
+ : Unwrap<T[K]>;
61
+ }
62
+ : T extends object
63
+ ? { [K in keyof T]: Unwrap<T[K]> }
64
+ : T;
65
+ export function reactive<T extends object>(value: T): Unwrap<T> {
66
+ if (!plain(value))
67
+ throw new TypeError("Native reactive requires a plain object or array");
68
+ const existing = proxies.get(value);
69
+ if (existing) return existing as Unwrap<T>;
70
+ const mutable = createMutable(value, { equals: Object.is });
71
+ const cached = proxies.get(mutable);
72
+ if (cached) return cached as Unwrap<T>;
73
+ const proxy = new Proxy(mutable, {
74
+ get(target, key, receiver) {
75
+ const next = Reflect.get(target, key, receiver);
76
+ if (isRef(next))
77
+ return Array.isArray(target) && arrayIndex(key) ? next : next.value;
78
+ return plain(next) ? reactive(next) : next;
79
+ },
80
+ set(target, key, next) {
81
+ if (typeof next === "object" && next !== null)
82
+ next = targets.get(next) ?? next;
83
+ return batch(() =>
84
+ untrack(() => {
85
+ const previous = Reflect.get(target, key);
86
+ if (
87
+ !(Array.isArray(target) && arrayIndex(key)) &&
88
+ isRef(previous) &&
89
+ !isRef(next)
90
+ )
91
+ previous.value = next;
92
+ else Reflect.set(target, key, next);
93
+ return true;
94
+ }),
95
+ );
96
+ },
97
+ });
98
+ targets.set(proxy, mutable);
99
+ proxies.set(mutable, proxy);
100
+ proxies.set(proxy, proxy);
101
+ return proxy as Unwrap<T>;
102
+ }
103
+ export function ref<T>(value: Ref<T>): Ref<T>;
104
+ export function ref<T>(value: T): Ref<Unwrap<T>>;
105
+ export function ref<T>(value: T | Ref<T>): Ref<any> {
106
+ if (isRef(value)) return value;
107
+ const convert = (next: T): T => (plain(next) ? (reactive(next) as T) : next);
108
+ const [read, write] = createSignal(convert(value as T), {
109
+ equals: Object.is,
110
+ });
111
+ return new Ref(read, (next) => write(() => convert(next)));
112
+ }
113
+ /** Keep external snapshots intact; only replacing .value notifies this ref. */
114
+ export function shallowRef<T extends Readonly<Ref<any>>>(value: T): T;
115
+ export function shallowRef<T>(value: T): Ref<T>;
116
+ export function shallowRef<T = any>(): Ref<T | undefined>;
117
+ export function shallowRef(value?: unknown): Ref<any> {
118
+ if (isRef(value)) return value;
119
+ const [read, write] = createSignal(value, { equals: Object.is });
120
+ return new Ref(read, (next) => write(() => next));
121
+ }
122
+ /** Unwrap native refs without invoking ordinary function values. */
123
+ export function unref<T>(value: T): T extends Readonly<Ref<infer V>> ? V : T;
124
+ export function unref(value: unknown): unknown {
125
+ return isRef(value) ? value.value : value;
126
+ }
127
+ export type ToRef<T> = [T] extends [Readonly<Ref<any>>] ? T : Ref<T>;
128
+ export function toRef<T extends Readonly<Ref<any>>>(value: T): T;
129
+ export function toRef<T>(getter: () => T): Readonly<Ref<T>>;
130
+ export function toRef<T>(value: T): Ref<Unwrap<T>>;
131
+ export function toRef(): Ref<undefined>;
132
+ export function toRef<T extends object, K extends keyof T>(
133
+ object: T,
134
+ key: K,
135
+ ): ToRef<T[K]>;
136
+ export function toRef<T extends object, K extends keyof T>(
137
+ object: T,
138
+ key: K,
139
+ defaultValue: T[K],
140
+ ): ToRef<Exclude<T[K], undefined>>;
141
+ export function toRef(
142
+ source?: unknown,
143
+ key?: PropertyKey,
144
+ defaultValue?: unknown,
145
+ ): Ref<any> {
146
+ if (isRef(source)) return source;
147
+ if (typeof source === "function") return new Ref(() => source());
148
+ if (source !== null && typeof source === "object" && arguments.length > 1) {
149
+ const object = source as Record<PropertyKey, unknown>;
150
+ const current = object[key!];
151
+ if (isRef(current)) return current;
152
+ return new Ref(
153
+ () => {
154
+ const value = object[key!];
155
+ return value === undefined ? defaultValue : value;
156
+ },
157
+ (value) => {
158
+ object[key!] = value;
159
+ },
160
+ );
161
+ }
162
+ return ref(source);
163
+ }
164
+ /** Link the currently enumerable string properties, preserving array holes. */
165
+ export type ToRefs<T = any> = { [K in keyof T]: ToRef<T[K]> };
166
+ export function toRefs<T extends object>(object: T): ToRefs<T> {
167
+ const result =
168
+ // Preserve sparse holes; Array.from would materialize undefined entries.
169
+ // eslint-disable-next-line unicorn/no-new-array
170
+ (Array.isArray(object) ? new Array(object.length) : {}) as ToRefs<T>;
171
+ for (const key in object) {
172
+ Object.defineProperty(result, key, {
173
+ value: toRef(object, key),
174
+ enumerable: true,
175
+ configurable: true,
176
+ writable: true,
177
+ });
178
+ }
179
+ return result;
180
+ }
181
+ export function computed<T>(
182
+ getter: (previous: T | undefined) => T,
183
+ ): Readonly<Ref<T>>;
184
+ export function computed<T>(options: {
185
+ get: (previous: T | undefined) => T;
186
+ set: (value: T) => void;
187
+ }): Ref<T>;
188
+ export function computed<T>(
189
+ input:
190
+ | ((previous: T | undefined) => T)
191
+ | { get: (previous: T | undefined) => T; set: (value: T) => void },
192
+ ): Ref<T> {
193
+ const getter = typeof input === "function" ? input : input.get;
194
+ let previous: T | undefined;
195
+ const read = createMemo(() => {
196
+ const next = getter(previous);
197
+ previous = next;
198
+ return next;
199
+ });
200
+ return new Ref(
201
+ read,
202
+ typeof input === "function"
203
+ ? undefined
204
+ : (value) => batch(() => untrack(() => input.set(value))),
205
+ );
206
+ }
207
+ export type WatchSource<T = unknown> = Readonly<Ref<T>> | (() => T);
208
+ export type WatchOptions = { flush: "sync"; immediate?: boolean };
209
+ export function watch<T>(
210
+ source: WatchSource<T>,
211
+ callback: (
212
+ value: T,
213
+ previous: T | undefined,
214
+ onCleanup: (fn: () => void) => void,
215
+ ) => void,
216
+ options: WatchOptions,
217
+ ): () => void;
218
+ export function watch<T extends readonly unknown[]>(
219
+ source: { [K in keyof T]: WatchSource<T[K]> },
220
+ callback: (
221
+ value: T,
222
+ previous: T | undefined,
223
+ onCleanup: (fn: () => void) => void,
224
+ ) => void,
225
+ options: WatchOptions,
226
+ ): () => void;
227
+ export function watch(
228
+ source: WatchSource<any> | readonly WatchSource<any>[],
229
+ callback: (
230
+ value: any,
231
+ previous: any,
232
+ onCleanup: (fn: () => void) => void,
233
+ ) => void,
234
+ options: WatchOptions,
235
+ ): () => void {
236
+ if (
237
+ !options ||
238
+ options.flush !== "sync" ||
239
+ Object.keys(options).some((key) => key !== "flush" && key !== "immediate")
240
+ )
241
+ throw new TypeError(
242
+ "Native watch supports only flush:'sync' and immediate",
243
+ );
244
+ const sources = Array.isArray(source) ? source : [source];
245
+ if (sources.some((item) => !isRef(item) && typeof item !== "function"))
246
+ throw new TypeError("Native watch requires refs or getters");
247
+ const outer = getOwner();
248
+ if (outer?.disposed)
249
+ throw new Error("Cannot watch from a disposed native owner");
250
+ const reportError = captureWatchErrorHandler();
251
+ let stop!: () => void;
252
+ createRoot((dispose) => {
253
+ stop = dispose;
254
+ let previous: any[] | undefined;
255
+ let reading = false;
256
+ const cleanups: Array<() => void> = [];
257
+ let active = true;
258
+ const clean = () => {
259
+ let failed = false;
260
+ let failure: unknown;
261
+ for (const fn of cleanups.splice(0)) {
262
+ try {
263
+ untrack(() =>
264
+ reportError ? invokeWatch(reportError, "watch cleanup", fn) : fn(),
265
+ );
266
+ } catch (error) {
267
+ if (!failed) failure = error;
268
+ failed = true;
269
+ }
270
+ }
271
+ if (failed) throw failure;
272
+ };
273
+ onCleanup(() => {
274
+ active = false;
275
+ clean();
276
+ });
277
+ createComputed(() => {
278
+ // Getter reporting may write its source; callback feedback remains reentrant.
279
+ if (reportError && reading) return;
280
+ let values: any[];
281
+ reading = true;
282
+ try {
283
+ values = sources.map((item) =>
284
+ isRef(item)
285
+ ? item.value
286
+ : reportError
287
+ ? invokeWatch(reportError, "watch getter", item)
288
+ : item(),
289
+ );
290
+ } finally {
291
+ reading = false;
292
+ }
293
+ if (
294
+ previous &&
295
+ values.every((value, i) => Object.is(value, previous![i]))
296
+ )
297
+ return;
298
+ const old = previous;
299
+ previous = values;
300
+ if (!old && !options.immediate) return;
301
+ untrack(() => {
302
+ clean();
303
+ if (!active) return;
304
+ const invoke = () =>
305
+ callback(
306
+ Array.isArray(source) ? values : values[0],
307
+ Array.isArray(source) ? old : old?.[0],
308
+ (fn) => {
309
+ if (typeof fn !== "function")
310
+ throw new TypeError("Watcher cleanup must be a function");
311
+ if (active) cleanups.push(fn);
312
+ else if (reportError)
313
+ invokeWatch(reportError, "watch cleanup", fn);
314
+ else fn();
315
+ },
316
+ );
317
+ if (reportError) invokeWatch(reportError, "watch callback", invoke);
318
+ else invoke();
319
+ });
320
+ });
321
+ });
322
+ if (outer) onCleanup(stop);
323
+ return stop;
324
+ }
325
+
326
+ /** Synchronously track an owned effect; no component render scheduler is implied. */
327
+ export function watchSyncEffect(
328
+ effect: (onCleanup: (fn: () => void) => void) => void,
329
+ ): () => void {
330
+ if (typeof effect !== "function")
331
+ throw new TypeError("Watcher effect must be a function");
332
+ const outer = getOwner();
333
+ if (outer?.disposed)
334
+ throw new Error("Cannot watch from a disposed native owner");
335
+ const reportError = captureWatchErrorHandler();
336
+ let stop!: () => void;
337
+ createRoot((dispose) => {
338
+ stop = dispose;
339
+ let active = true;
340
+ let running = false;
341
+ const cleanups: Array<() => void> = [];
342
+ const clean = () => {
343
+ let failed = false;
344
+ let failure: unknown;
345
+ for (const fn of cleanups.splice(0)) {
346
+ try {
347
+ untrack(() =>
348
+ reportError ? invokeWatch(reportError, "watch cleanup", fn) : fn(),
349
+ );
350
+ } catch (error) {
351
+ if (!failed) failure = error;
352
+ failed = true;
353
+ }
354
+ }
355
+ if (failed) throw failure;
356
+ };
357
+ onCleanup(() => {
358
+ active = false;
359
+ clean();
360
+ });
361
+ createComputed(() => {
362
+ if (running || !active) return;
363
+ running = true;
364
+ try {
365
+ untrack(clean);
366
+ if (!active) return;
367
+ const invoke = () =>
368
+ effect((fn) => {
369
+ if (typeof fn !== "function")
370
+ throw new TypeError("Watcher cleanup must be a function");
371
+ if (active) cleanups.push(fn);
372
+ });
373
+ if (reportError) invokeWatch(reportError, "watch effect", invoke);
374
+ else invoke();
375
+ } catch (error) {
376
+ if (!reportError)
377
+ try {
378
+ untrack(clean);
379
+ } catch {
380
+ /* Preserve the effect failure. */
381
+ }
382
+ throw error;
383
+ } finally {
384
+ running = false;
385
+ }
386
+ });
387
+ });
388
+ if (outer) onCleanup(stop);
389
+ return stop;
390
+ }