From 34fe9a887e70625de5f7bcc8e905c3af1a784b21 Mon Sep 17 00:00:00 2001 From: Thomas Stokes Date: Fri, 1 Aug 2025 21:30:52 +0800 Subject: [PATCH 1/2] replace key weakmap with Keyed type because the WeakMap.prototype.set inside keyed was showing up high on traces. also moves keyed from client.ts to index.ts because its inert now. --- examples/uibench/main.js | 4 ++-- src/client.ts | 2 +- src/client/controller.ts | 15 +-------------- src/client/parts.ts | 6 ++++-- src/client/root.ts | 5 +++-- src/client/tests/hydration.test.ts | 4 ++-- src/client/tests/lists.test.ts | 4 ++-- src/index.ts | 10 +++++++++- src/shared.ts | 13 ++++++++++++- 9 files changed, 36 insertions(+), 27 deletions(-) diff --git a/examples/uibench/main.js b/examples/uibench/main.js index 1db47190..94b7d507 100644 --- a/examples/uibench/main.js +++ b/examples/uibench/main.js @@ -1,8 +1,8 @@ // @ts-check /// -import { html } from 'dhtml' -import { createRoot, keyed } from 'dhtml/client' +import { html, keyed } from 'dhtml' +import { createRoot } from 'dhtml/client' /** @param {string} text */ function tableCell(text) { diff --git a/src/client.ts b/src/client.ts index dd0b38c7..5a1f999b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,3 +1,3 @@ -export { invalidate, keyed, onMount, onUnmount } from './client/controller.ts' +export { invalidate, onMount, onUnmount } from './client/controller.ts' export { attr_directive as attr, on_directive as on, type Directive } from './client/parts.ts' export { createRoot, hydrate, type Root } from './client/root.ts' diff --git a/src/client/controller.ts b/src/client/controller.ts index 5a38a112..e1645402 100644 --- a/src/client/controller.ts +++ b/src/client/controller.ts @@ -1,9 +1,7 @@ import type { Displayable, Renderable } from '../index.ts' -import { assert, is_renderable } from '../shared.ts' +import { assert, is_renderable, type Key } from '../shared.ts' import { type Cleanup } from './util.ts' -export type Key = string | number | bigint | boolean | symbol | object | null - export interface Controller { _mount_callbacks: (() => Cleanup)[] _unmount_callbacks: Cleanup[] @@ -47,14 +45,3 @@ export function onMount(renderable: Renderable, callback: () => Cleanup): void { export function onUnmount(renderable: Renderable, callback: () => void): void { onMount(renderable, () => callback) } - -export function keyed(displayable: T, key: Key): T { - assert(!keys.has(displayable), 'renderable already has a key') - keys.set(displayable, key) - return displayable -} - -export function get_key(displayable: unknown): unknown { - // the cast is fine because getting any non-object will return null - return keys.get(displayable as object) ?? displayable -} diff --git a/src/client/parts.ts b/src/client/parts.ts index bf511794..a7e0d356 100644 --- a/src/client/parts.ts +++ b/src/client/parts.ts @@ -2,9 +2,11 @@ import { assert, is_html, is_iterable, + is_keyed, is_renderable, single_part_template, type Displayable, + type Key, type Renderable, } from '../shared.ts' import { @@ -15,7 +17,7 @@ import { PART_PROPERTY, type CompiledTemplate, } from './compiler.ts' -import { controllers, get_controller, get_key, type Key } from './controller.ts' +import { controllers, get_controller } from './controller.ts' import { create_span_after, delete_contents, extract_contents, insert_node, type Span } from './span.ts' import type { Cleanup } from './util.ts' @@ -117,7 +119,7 @@ export function create_child_part( let i = 0 let end = span._start for (const item of value) { - const key = get_key(item) as Key + const key = is_keyed(item) ? item._key : (item as Key) if (entries.length <= i) { const span = create_span_after(end) entries[i] = { _span: span, _part: create_child_part(span), _key: key } diff --git a/src/client/root.ts b/src/client/root.ts index 2541b8fc..ce69b9de 100644 --- a/src/client/root.ts +++ b/src/client/root.ts @@ -2,9 +2,11 @@ import { assert, is_html, is_iterable, + is_keyed, is_renderable, single_part_template, type Displayable, + type Key, type Renderable, } from '../shared.ts' import { @@ -16,7 +18,6 @@ import { PART_PROPERTY, type CompiledTemplate, } from './compiler.ts' -import { get_key, type Key } from './controller.ts' import { create_attribute_part, create_child_part, @@ -95,7 +96,7 @@ function hydrate_child_part(span: Span, value: unknown) { let end = span._start for (const item of value) { - const key = get_key(item) as Key + const key = is_keyed(item) ? item._key : (item as Key) const start = end.nextSibling assert(start && is_comment(start) && start.data === '?[') diff --git a/src/client/tests/hydration.test.ts b/src/client/tests/hydration.test.ts index e72648a4..814a7b80 100644 --- a/src/client/tests/hydration.test.ts +++ b/src/client/tests/hydration.test.ts @@ -1,5 +1,5 @@ -import { html, type Displayable, type Renderable } from 'dhtml' -import { attr, hydrate, invalidate, keyed, onMount, type Directive, type Root } from 'dhtml/client' +import { html, keyed, type Displayable, type Renderable } from 'dhtml' +import { attr, hydrate, invalidate, onMount, type Directive, type Root } from 'dhtml/client' import { renderToString } from 'dhtml/server' import { assert, assert_deep_eq, assert_eq, test } from '../../../scripts/test/test.ts' diff --git a/src/client/tests/lists.test.ts b/src/client/tests/lists.test.ts index cd7480b2..fa0a2c56 100644 --- a/src/client/tests/lists.test.ts +++ b/src/client/tests/lists.test.ts @@ -1,5 +1,5 @@ -import { html, type Displayable } from 'dhtml' -import { invalidate, keyed } from 'dhtml/client' +import { html, keyed, type Displayable } from 'dhtml' +import { invalidate } from 'dhtml/client' import { assert, assert_eq, test } from '../../../scripts/test/test.ts' import { setup } from './setup.ts' diff --git a/src/index.ts b/src/index.ts index eab6005b..b379622a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { html_tag, is_html } from './shared.ts' +import { html_tag, is_html, keyed_tag, type Displayable, type Key, type Keyed } from './shared.ts' export interface HTML { [html_tag]: true @@ -14,6 +14,14 @@ export function html(statics: TemplateStringsArray, ...dynamics: unknown[]): HTM } } +export function keyed(displayable: T, key: Key): Keyed { + return { + [keyed_tag]: true, + _key: key, + render: () => displayable, + } +} + if (__DEV__) { type JsonML = string | readonly [tag: string, attrs?: Record, ...children: JsonML[]] interface Formatter { diff --git a/src/shared.ts b/src/shared.ts index 4cc2d69a..5cee6fff 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -10,7 +10,7 @@ export interface ToString { toString(): string } -export type Displayable = null | undefined | ToString | Node | Renderable | Iterable | HTML +export type Displayable = null | undefined | ToString | Node | Renderable | Iterable | HTML | Keyed export interface Renderable { render(): Displayable } @@ -40,3 +40,14 @@ export function is_html(value: unknown): value is HTML { export function single_part_template(part: Displayable): HTML { return html`${part}` } + +export type Key = string | number | bigint | boolean | symbol | object | null +export interface Keyed extends Renderable { + [keyed_tag]: true + /** @internal */ _key: Key +} + +export const keyed_tag: unique symbol = Symbol() +export function is_keyed(value: any): value is Keyed { + return typeof value === 'object' && value !== null && keyed_tag in value +} From 9551c5b0a00978f682999501544b24b1a2dee0d9 Mon Sep 17 00:00:00 2001 From: Thomas Stokes Date: Sat, 2 Aug 2025 14:55:42 +0800 Subject: [PATCH 2/2] move everything into shared? --- src/index.ts | 26 ++------------------------ src/shared.ts | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/index.ts b/src/index.ts index b379622a..6ec16777 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,6 @@ -import { html_tag, is_html, keyed_tag, type Displayable, type Key, type Keyed } from './shared.ts' +export { html, keyed, type Displayable, type HTML, type Renderable } from './shared.ts' -export interface HTML { - [html_tag]: true - /* @internal */ _statics: TemplateStringsArray - /* @internal */ _dynamics: unknown[] -} - -export function html(statics: TemplateStringsArray, ...dynamics: unknown[]): HTML { - return { - [html_tag]: true, - _dynamics: dynamics, - _statics: statics, - } -} - -export function keyed(displayable: T, key: Key): Keyed { - return { - [keyed_tag]: true, - _key: key, - render: () => displayable, - } -} +import { is_html } from './shared.ts' if (__DEV__) { type JsonML = string | readonly [tag: string, attrs?: Record, ...children: JsonML[]] @@ -46,5 +26,3 @@ if (__DEV__) { }, }) } - -export type { Displayable, Renderable } from './shared.ts' diff --git a/src/shared.ts b/src/shared.ts index 5cee6fff..8f395ed9 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -1,4 +1,3 @@ -import { html, type HTML } from './index.ts' export * as lexer from './shared/lexer.ts' /** @internal */ @@ -32,7 +31,21 @@ export function assert(value: unknown, message?: string): asserts value { } } -export const html_tag: unique symbol = Symbol() +export interface HTML { + [html_tag]: true + /* @internal */ _statics: TemplateStringsArray + /* @internal */ _dynamics: unknown[] +} + +const html_tag: unique symbol = Symbol() +export function html(statics: TemplateStringsArray, ...dynamics: unknown[]): HTML { + return { + [html_tag]: true, + _dynamics: dynamics, + _statics: statics, + } +} + export function is_html(value: unknown): value is HTML { return typeof value === 'object' && value !== null && html_tag in value } @@ -47,7 +60,15 @@ export interface Keyed extends Renderable { /** @internal */ _key: Key } -export const keyed_tag: unique symbol = Symbol() +const keyed_tag: unique symbol = Symbol() +export function keyed(displayable: T, key: Key): Keyed { + return { + [keyed_tag]: true, + _key: key, + render: () => displayable, + } +} + export function is_keyed(value: any): value is Keyed { return typeof value === 'object' && value !== null && keyed_tag in value }