diff --git a/.changeset/quiet-pans-allow.md b/.changeset/quiet-pans-allow.md new file mode 100644 index 00000000..55183e75 --- /dev/null +++ b/.changeset/quiet-pans-allow.md @@ -0,0 +1,5 @@ +--- +"runed": patch +--- + +box.readonly perfomance adjustment diff --git a/packages/runed/src/lib/functions/box/box.svelte.ts b/packages/runed/src/lib/functions/box/box.svelte.ts index 8ff08e2a..51e87388 100644 --- a/packages/runed/src/lib/functions/box/box.svelte.ts +++ b/packages/runed/src/lib/functions/box/box.svelte.ts @@ -100,12 +100,12 @@ function boxWith(getter: () => T, setter?: (v: T) => void) { export type BoxFrom = T extends WritableBox - ? WritableBox - : T extends ReadableBox - ? ReadableBox - : T extends Getter - ? ReadableBox - : WritableBox; + ? WritableBox + : T extends ReadableBox + ? ReadableBox + : T extends Getter + ? ReadableBox + : WritableBox; /** * Creates a box from either a static value, a box, or a getter function. @@ -131,16 +131,16 @@ type BoxFlatten> = Expand< }, never > & - RemoveValues< - { - readonly [K in keyof R]: R[K] extends WritableBox - ? never - : R[K] extends ReadableBox - ? T - : never; - }, - never - > + RemoveValues< + { + readonly [K in keyof R]: R[K] extends WritableBox + ? never + : R[K] extends ReadableBox + ? T + : never; + }, + never + > > & RemoveValues< { @@ -192,11 +192,13 @@ function boxFlatten>(boxes: R): BoxFlatten * const count = box(0) // WritableBox * const countReadonly = box.readonly(count) // ReadableBox */ -function toReadonlyBox(box: ReadableBox): ReadableBox { +function toReadonlyBox(b: ReadableBox): ReadableBox { + if (!box.isWritableBox(b)) return b + return { [BoxSymbol]: true, get value() { - return box.value; + return b.value; }, }; } diff --git a/packages/runed/src/lib/functions/box/box.test.svelte.ts b/packages/runed/src/lib/functions/box/box.test.svelte.ts index b86f9517..0b967870 100644 --- a/packages/runed/src/lib/functions/box/box.test.svelte.ts +++ b/packages/runed/src/lib/functions/box/box.test.svelte.ts @@ -115,6 +115,33 @@ describe("box.flatten", () => { }); }); +describe("box.readonly", () => { + test("box.readonly returns a non-settable box", () => { + const count = box(0); + const readonlyCount = box.readonly(count); + + function setReadOnlyCount() { + // eslint-disable-next-line ts/no-explicit-any + (readonlyCount as any).value = 1; + } + + expect(setReadOnlyCount).toThrow(); + }); + + test("box.readonly returned box should update with original box", () => { + const count = box(0); + const readonlyCount = box.readonly(count); + + expect(readonlyCount.value).toBe(0); + count.value = 1; + expect(readonlyCount.value).toBe(1); + + count.value = 2; + expect(readonlyCount.value).toBe(2); + }); +}); + + describe("box types", () => { test("box without initial value", () => { const count = box(); @@ -163,28 +190,3 @@ describe("box types", () => { }); }); -describe("box.readonly", () => { - test("box.readonly returns a non-settable box", () => { - const count = box(0); - const readonlyCount = box.readonly(count); - - function setReadOnlyCount() { - // eslint-disable-next-line ts/no-explicit-any - (readonlyCount as any).value = 1; - } - - expect(setReadOnlyCount).toThrow(); - }); - - test("box.readonly returned box should update with original box", () => { - const count = box(0); - const readonlyCount = box.readonly(count); - - expect(readonlyCount.value).toBe(0); - count.value = 1; - expect(readonlyCount.value).toBe(1); - - count.value = 2; - expect(readonlyCount.value).toBe(2); - }); -}); diff --git a/packages/runed/src/lib/functions/box/new-box.svelte.ts b/packages/runed/src/lib/functions/box/new-box.svelte.ts new file mode 100644 index 00000000..f988b324 --- /dev/null +++ b/packages/runed/src/lib/functions/box/new-box.svelte.ts @@ -0,0 +1,114 @@ +/* eslint-disable ts/consistent-type-definitions */ + +import type { Getter, Setter } from "$lib/internal/types.js"; + +const BoxSymbol = Symbol('box'); + +interface Box { + [BoxSymbol]: true + value: T, +} + +interface ReadonlyBox extends Box { + readonly value: T, +} + +export function box(): Box +export function box(initial: T): Box +export function box(initial?: T) { + let value = $state(initial) + + return { + get value() { + return value + }, + set value(v) { + value = v; + }, + [BoxSymbol]: true + } +} + +box.isBox = function isBox(value: unknown): value is Box { + return typeof value === 'object' && value !== null && (BoxSymbol in value); +} + +function isWritable(obj: T, key: keyof T) { + const desc = Object.getOwnPropertyDescriptor(obj, key) || {} + return Boolean(desc.writable) +} + +box.isReadonly = function boxIsReadonly(value: unknown): value is ReadonlyBox { + return box.isBox(value) && !isWritable(value, 'value') +} + +box.isWritable = function boxIsWritable(value: unknown): value is Box { + return box.isBox(value) && isWritable(value, 'value') +} + +function boxFrom(value: T): T extends ReadonlyBox ? T : Box { + // eslint-disable-next-line ts/no-explicit-any -- I'm fucking something up here + if (box.isBox(value)) return value as any; + // eslint-disable-next-line ts/no-explicit-any + return box(value) as any; +} +box.from = boxFrom + +function boxWith(get: Getter): ReadonlyBox +function boxWith(get: Getter, set: Setter): Box +function boxWith(get: Getter, set?: Setter) { + const value = $derived.by(get) + + if (set) { + return { + get value() { + return value; + }, + set value(v) { + set(v) + }, + [BoxSymbol]: true + } + } else { + return { + get value() { + return value; + }, + [BoxSymbol]: true + } + } +} +box.with = boxWith + +// Usage examples +function acceptsReadonly(disabled: boolean | ReadonlyBox) { + const disabledBox = box.from(disabled); + // @ts-expect-error -- testing + disabledBox.value = false + if (box.isWritable(disabledBox)) { + disabledBox.value = true + } +} + +function acceptsMutable(disabled: boolean | Box) { + const disabledBox = box.from(disabled); + disabledBox.value = false +} + +let disabled = $state(false); + +acceptsReadonly( + box.with(() => disabled) +); + +acceptsReadonly( + box(false) +); + +acceptsMutable( + box.with(() => disabled, (v) => disabled = v) +) + +acceptsMutable( + box(false) +) \ No newline at end of file