From 09c6d5a3f790f13239f33600f1d1c182e7506a97 Mon Sep 17 00:00:00 2001 From: abdel-17 Date: Wed, 15 Jan 2025 16:19:14 +0200 Subject: [PATCH 1/3] feat: add box --- .../src/lib/utilities/box/box.svelte.test.ts | 115 ++++++++++++++++++ .../runed/src/lib/utilities/box/box.svelte.ts | 89 ++++++++++++++ packages/runed/src/lib/utilities/box/index.ts | 1 + 3 files changed, 205 insertions(+) create mode 100644 packages/runed/src/lib/utilities/box/box.svelte.test.ts create mode 100644 packages/runed/src/lib/utilities/box/box.svelte.ts create mode 100644 packages/runed/src/lib/utilities/box/index.ts diff --git a/packages/runed/src/lib/utilities/box/box.svelte.test.ts b/packages/runed/src/lib/utilities/box/box.svelte.test.ts new file mode 100644 index 00000000..fc0181a9 --- /dev/null +++ b/packages/runed/src/lib/utilities/box/box.svelte.test.ts @@ -0,0 +1,115 @@ +import { Derived, Ref } from "./box.svelte.js"; + +describe("box", () => { + it("updates when the referenced value updates", () => { + let count = 0; + const box = Ref.to(() => count); + expect(box.current).toBe(0); + + count = 1; + expect(box.current).toBe(1); + }); + + it("creates a readonly box when only a getter is passed", () => { + const box = Ref.to(() => 0); + expect(box.current).toBe(0); + + function set() { + // @ts-expect-error + box.current = 1; + } + + expect(set).toThrowError(); + }); + + it("creates a writable box when a getter and setter are passed", () => { + let count = 0; + const box = Ref.to( + () => count, + (value) => { + count = value; + } + ); + expect(box.current).toBe(0); + + box.current = 1; + expect(box.current).toBe(1); + expect(count).toBe(1); + + count = 2; + expect(box.current).toBe(2); + }); + + it("does not memoize the getter", () => { + const fn = vi.fn(() => 0); + const box = Ref.to(fn); + expect(fn).not.toHaveBeenCalled(); + + box.current; + expect(fn).toHaveBeenCalledTimes(1); + + box.current; + expect(fn).toHaveBeenCalledTimes(2); + }); +}); + +describe("Derived", () => { + it("updates when the referenced value updates", () => { + let count = $state(0); + const box = Derived.by(() => count); + expect(box.current).toBe(0); + + count = 1; + expect(box.current).toBe(1); + }); + + it("creates a readonly box when only a getter is passed", () => { + const box = Derived.by(() => 0); + expect(box.current).toBe(0); + + function set() { + // @ts-expect-error + box.current = 1; + } + + expect(set).toThrowError(); + }); + + it("creates a writable box when a getter and setter are passed", () => { + let count = $state(0); + const box = Derived.by( + () => count, + (value) => { + count = value; + } + ); + expect(box.current).toBe(0); + + box.current = 1; + expect(box.current).toBe(1); + expect(count).toBe(1); + + count = 2; + expect(box.current).toBe(2); + }); + + it("memoizes the getter", () => { + let count = $state(0); + const fn = vi.fn(() => count); + const box = Derived.by(fn); + expect(fn).not.toHaveBeenCalled(); + + box.current; + expect(fn).toHaveBeenCalledTimes(1); + + box.current; + expect(fn).toHaveBeenCalledTimes(1); + + count = 1; + box.current; + expect(fn).toHaveBeenCalledTimes(2); + + box.current; + expect(fn).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/runed/src/lib/utilities/box/box.svelte.ts b/packages/runed/src/lib/utilities/box/box.svelte.ts new file mode 100644 index 00000000..e8d7a5ab --- /dev/null +++ b/packages/runed/src/lib/utilities/box/box.svelte.ts @@ -0,0 +1,89 @@ +import type { Getter, Setter } from "$lib/internal/types.js"; + +export type Box = { + readonly current: T; + readonly get: Getter; +}; + +export type WritableBox = { + current: T; + readonly get: Getter; + readonly set: Setter; +}; + +export class Ref implements Box { + readonly get: Getter; + + constructor(get: Getter) { + this.get = get; + } + + static to(get: Getter): Box; + static to(get: Getter, set: Setter): WritableBox; + static to(get: Getter, set?: Setter): Box { + if (set !== undefined) { + return new WritableRef(get, set); + } + return new Ref(get); + } + + get current(): T { + return this.get(); + } +} + +class WritableRef extends Ref implements WritableBox { + readonly set: Setter; + + constructor(get: Getter, set: Setter) { + super(get); + this.set = set; + } + + get current(): T { + return this.get(); + } + + set current(value: T) { + this.set(value); + } +} + +export class Derived implements Box { + readonly get: Getter; + + constructor(get: Getter) { + const current = $derived.by(get); + this.get = () => current; + } + + static by(get: Getter): Derived; + static by(get: Getter, set: Setter): WritableDerived; + static by(get: Getter, set?: Setter): Derived { + if (set !== undefined) { + return new WritableDerived(get, set); + } + return new Derived(get); + } + + get current(): T { + return this.get(); + } +} + +class WritableDerived extends Derived implements WritableBox { + readonly set: Setter; + + constructor(get: Getter, set: Setter) { + super(get); + this.set = set; + } + + get current(): T { + return this.get(); + } + + set current(value: T) { + this.set(value); + } +} diff --git a/packages/runed/src/lib/utilities/box/index.ts b/packages/runed/src/lib/utilities/box/index.ts new file mode 100644 index 00000000..35ec4e2c --- /dev/null +++ b/packages/runed/src/lib/utilities/box/index.ts @@ -0,0 +1 @@ +export * from "./box.svelte.js"; From 82a1c745fb68e03006a54e7fc1fce4978c1e4be5 Mon Sep 17 00:00:00 2001 From: abdel-17 Date: Wed, 15 Jan 2025 17:10:10 +0200 Subject: [PATCH 2/3] chore: shut up eslint --- packages/runed/src/lib/utilities/box/box.svelte.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runed/src/lib/utilities/box/box.svelte.test.ts b/packages/runed/src/lib/utilities/box/box.svelte.test.ts index fc0181a9..cf0b270e 100644 --- a/packages/runed/src/lib/utilities/box/box.svelte.test.ts +++ b/packages/runed/src/lib/utilities/box/box.svelte.test.ts @@ -15,7 +15,7 @@ describe("box", () => { expect(box.current).toBe(0); function set() { - // @ts-expect-error + // @ts-expect-error It's readonly box.current = 1; } @@ -68,7 +68,7 @@ describe("Derived", () => { expect(box.current).toBe(0); function set() { - // @ts-expect-error + // @ts-expect-error It's readonly box.current = 1; } From f87a5efe75d9b3a513acecdd96795b5ddb089902 Mon Sep 17 00:00:00 2001 From: abdel-17 Date: Thu, 16 Jan 2025 00:29:51 +0200 Subject: [PATCH 3/3] update box --- .../src/lib/utilities/box/box.svelte.test.ts | 90 +++++++++---------- .../runed/src/lib/utilities/box/box.svelte.ts | 66 ++++---------- 2 files changed, 63 insertions(+), 93 deletions(-) diff --git a/packages/runed/src/lib/utilities/box/box.svelte.test.ts b/packages/runed/src/lib/utilities/box/box.svelte.test.ts index cf0b270e..82d04c16 100644 --- a/packages/runed/src/lib/utilities/box/box.svelte.test.ts +++ b/packages/runed/src/lib/utilities/box/box.svelte.test.ts @@ -1,115 +1,113 @@ -import { Derived, Ref } from "./box.svelte.js"; +import { box, ref } from "./box.svelte.js"; describe("box", () => { it("updates when the referenced value updates", () => { - let count = 0; - const box = Ref.to(() => count); - expect(box.current).toBe(0); + let count = $state(0); + const boxed = box(() => count); + expect(boxed.current).toBe(0); count = 1; - expect(box.current).toBe(1); + expect(boxed.current).toBe(1); }); it("creates a readonly box when only a getter is passed", () => { - const box = Ref.to(() => 0); - expect(box.current).toBe(0); + const boxed = box(() => 0); function set() { // @ts-expect-error It's readonly - box.current = 1; + boxed.current = 1; } expect(set).toThrowError(); }); it("creates a writable box when a getter and setter are passed", () => { - let count = 0; - const box = Ref.to( + let count = $state(0); + const boxed = box( () => count, (value) => { count = value; } ); - expect(box.current).toBe(0); + expect(boxed.current).toBe(0); - box.current = 1; - expect(box.current).toBe(1); + boxed.current = 1; + expect(boxed.current).toBe(1); expect(count).toBe(1); count = 2; - expect(box.current).toBe(2); + expect(boxed.current).toBe(2); }); - it("does not memoize the getter", () => { - const fn = vi.fn(() => 0); - const box = Ref.to(fn); + it("memoizes the getter", () => { + let count = $state(0); + const fn = vi.fn(() => count); + const boxed = box(fn); expect(fn).not.toHaveBeenCalled(); - box.current; + boxed.current; + expect(fn).toHaveBeenCalledTimes(1); + + boxed.current; expect(fn).toHaveBeenCalledTimes(1); - box.current; + count = 1; + boxed.current; + expect(fn).toHaveBeenCalledTimes(2); + + boxed.current; expect(fn).toHaveBeenCalledTimes(2); }); }); -describe("Derived", () => { +describe("ref", () => { it("updates when the referenced value updates", () => { - let count = $state(0); - const box = Derived.by(() => count); - expect(box.current).toBe(0); + let count = 0; + const boxed = ref(() => count); + expect(boxed.current).toBe(0); count = 1; - expect(box.current).toBe(1); + expect(boxed.current).toBe(1); }); it("creates a readonly box when only a getter is passed", () => { - const box = Derived.by(() => 0); - expect(box.current).toBe(0); + const boxed = ref(() => 0); function set() { // @ts-expect-error It's readonly - box.current = 1; + boxed.current = 1; } expect(set).toThrowError(); }); it("creates a writable box when a getter and setter are passed", () => { - let count = $state(0); - const box = Derived.by( + let count = 0; + const boxed = ref( () => count, (value) => { count = value; } ); - expect(box.current).toBe(0); + expect(boxed.current).toBe(0); - box.current = 1; - expect(box.current).toBe(1); + boxed.current = 1; + expect(boxed.current).toBe(1); expect(count).toBe(1); count = 2; - expect(box.current).toBe(2); + expect(boxed.current).toBe(2); }); - it("memoizes the getter", () => { - let count = $state(0); - const fn = vi.fn(() => count); - const box = Derived.by(fn); + it("does not memoize the getter", () => { + const fn = vi.fn(() => 0); + const boxed = ref(fn); expect(fn).not.toHaveBeenCalled(); - box.current; - expect(fn).toHaveBeenCalledTimes(1); - - box.current; + boxed.current; expect(fn).toHaveBeenCalledTimes(1); - count = 1; - box.current; - expect(fn).toHaveBeenCalledTimes(2); - - box.current; + boxed.current; expect(fn).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/runed/src/lib/utilities/box/box.svelte.ts b/packages/runed/src/lib/utilities/box/box.svelte.ts index e8d7a5ab..6c565685 100644 --- a/packages/runed/src/lib/utilities/box/box.svelte.ts +++ b/packages/runed/src/lib/utilities/box/box.svelte.ts @@ -11,59 +11,30 @@ export type WritableBox = { readonly set: Setter; }; -export class Ref implements Box { - readonly get: Getter; - - constructor(get: Getter) { - this.get = get; - } - - static to(get: Getter): Box; - static to(get: Getter, set: Setter): WritableBox; - static to(get: Getter, set?: Setter): Box { - if (set !== undefined) { - return new WritableRef(get, set); - } - return new Ref(get); - } - - get current(): T { - return this.get(); - } +export function box(get: Getter): Box; +export function box(get: Getter, set: Setter): WritableBox; +export function box(get: Getter, set?: Setter): Box { + const current = $derived.by(get); + if (set !== undefined) { + return new WritableRef(() => current, set); + } + return new Ref(() => current); } -class WritableRef extends Ref implements WritableBox { - readonly set: Setter; - - constructor(get: Getter, set: Setter) { - super(get); - this.set = set; - } - - get current(): T { - return this.get(); - } - - set current(value: T) { - this.set(value); +export function ref(get: Getter): Box; +export function ref(get: Getter, set: Setter): WritableBox; +export function ref(get: Getter, set?: Setter): Box { + if (set !== undefined) { + return new WritableRef(get, set); } + return new Ref(get); } -export class Derived implements Box { +class Ref { readonly get: Getter; constructor(get: Getter) { - const current = $derived.by(get); - this.get = () => current; - } - - static by(get: Getter): Derived; - static by(get: Getter, set: Setter): WritableDerived; - static by(get: Getter, set?: Setter): Derived { - if (set !== undefined) { - return new WritableDerived(get, set); - } - return new Derived(get); + this.get = get; } get current(): T { @@ -71,11 +42,12 @@ export class Derived implements Box { } } -class WritableDerived extends Derived implements WritableBox { +class WritableRef { + readonly get: Getter; readonly set: Setter; constructor(get: Getter, set: Setter) { - super(get); + this.get = get; this.set = set; }