davaux 0.8.0 → 0.8.1
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/package.json +6 -2
- package/BASELINE.md +0 -169
- package/CLAUDE.md +0 -518
- package/ROADMAP.md +0 -198
- package/build.mjs +0 -101
- package/client/control.ts +0 -247
- package/client/hydrate.ts +0 -37
- package/client/index.ts +0 -19
- package/client/jsx-runtime.ts +0 -209
- package/client/resource.ts +0 -122
- package/client/signal.ts +0 -211
- package/client/store.ts +0 -110
- package/client/useHead.ts +0 -63
- package/pka.config.json +0 -32
- package/src/build/config.ts +0 -42
- package/src/build/index.ts +0 -6
- package/src/build/plugins.ts +0 -118
- package/src/cli.ts +0 -502
- package/src/config.ts +0 -197
- package/src/create-multisite.ts +0 -310
- package/src/create.ts +0 -194
- package/src/dev/blueprints.ts +0 -75
- package/src/dev/components.ts +0 -108
- package/src/dev/insert.ts +0 -221
- package/src/dev/remove.ts +0 -677
- package/src/dev/watch.ts +0 -3098
- package/src/errors.ts +0 -64
- package/src/generate.ts +0 -228
- package/src/index.ts +0 -67
- package/src/island.ts +0 -47
- package/src/jsx-runtime.d.ts +0 -408
- package/src/jsx-runtime.d.ts.map +0 -1
- package/src/jsx-runtime.ts +0 -536
- package/src/link.ts +0 -49
- package/src/oml/fragment.ts +0 -54
- package/src/oml/index.ts +0 -21
- package/src/oml/jsx-runtime.ts +0 -121
- package/src/oml/jsx.ts +0 -151
- package/src/oml/page.ts +0 -13
- package/src/oml/render.ts +0 -181
- package/src/oml/types.ts +0 -159
- package/src/router/handler.ts +0 -515
- package/src/router/matcher.ts +0 -52
- package/src/router/scanner.ts +0 -272
- package/src/server/index.ts +0 -49
- package/src/signal.ts +0 -39
- package/src/ssg.ts +0 -253
- package/src/test/actions.test.ts +0 -40
- package/src/test/body-limits.test.ts +0 -83
- package/src/test/errors.test.ts +0 -53
- package/src/test/fixtures/routes/[id].page.ts +0 -3
- package/src/test/fixtures/routes/_error.ts +0 -6
- package/src/test/fixtures/routes/_global.ts +0 -8
- package/src/test/fixtures/routes/_layout-template.ts +0 -7
- package/src/test/fixtures/routes/_layout.ts +0 -7
- package/src/test/fixtures/routes/_layout_scripts.ts +0 -8
- package/src/test/fixtures/routes/_middleware.ts +0 -8
- package/src/test/fixtures/routes/_redirect301_mw.ts +0 -5
- package/src/test/fixtures/routes/_redirect_mw.ts +0 -5
- package/src/test/fixtures/routes/about.page.ts +0 -6
- package/src/test/fixtures/routes/action.page.ts +0 -11
- package/src/test/fixtures/routes/api/form-all.post.ts +0 -5
- package/src/test/fixtures/routes/api/form-limited.post.ts +0 -6
- package/src/test/fixtures/routes/api/response-obj.get.ts +0 -17
- package/src/test/fixtures/routes/api/upload.post.ts +0 -14
- package/src/test/fixtures/routes/api/users.get.ts +0 -3
- package/src/test/fixtures/routes/api/xml.get.ts +0 -5
- package/src/test/fixtures/routes/auth/_middleware.ts +0 -11
- package/src/test/fixtures/routes/auth/protected.page.ts +0 -3
- package/src/test/fixtures/routes/index.page.ts +0 -3
- package/src/test/fixtures/routes/oml.page.ts +0 -7
- package/src/test/fixtures/routes/redirect.page.ts +0 -3
- package/src/test/fixtures/routes/ssg/[slug].page.ts +0 -8
- package/src/test/fixtures/routes/ssg/server.page.ts +0 -5
- package/src/test/fixtures/routes/state.page.ts +0 -4
- package/src/test/fixtures/routes/throw.page.ts +0 -5
- package/src/test/fixtures/routes/wiki/[...slug].page.ts +0 -3
- package/src/test/helpers.ts +0 -132
- package/src/test/layouts.test.ts +0 -76
- package/src/test/middleware.test.ts +0 -69
- package/src/test/multipart.test.ts +0 -91
- package/src/test/oml-routing.test.ts +0 -59
- package/src/test/oml.test.ts +0 -429
- package/src/test/redirects.test.ts +0 -32
- package/src/test/routing.test.ts +0 -118
- package/src/test/ssg.test.ts +0 -273
- package/src/test/web-response.test.ts +0 -33
- package/src/types.ts +0 -670
- package/tsconfig.client.json +0 -17
- package/tsconfig.json +0 -20
package/client/jsx-runtime.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
// Client JSX runtime — creates live DOM nodes instead of HTML strings.
|
|
2
|
-
// When a signal is read during prop/child evaluation, a subscription is
|
|
3
|
-
// created so that DOM node updates automatically when the signal changes.
|
|
4
|
-
// No virtual DOM: changes are applied directly and surgically.
|
|
5
|
-
|
|
6
|
-
import { createEffect } from './signal.js'
|
|
7
|
-
|
|
8
|
-
type Child = Node | string | number | boolean | null | undefined | (() => Child) | Child[]
|
|
9
|
-
type StyleObject = Record<string, string | number>
|
|
10
|
-
|
|
11
|
-
export type Props = {
|
|
12
|
-
children?: Child | Child[]
|
|
13
|
-
key?: string | number
|
|
14
|
-
ref?: ((el: Element) => void) | { current: Element | null }
|
|
15
|
-
style?: string | StyleObject
|
|
16
|
-
className?: string
|
|
17
|
-
htmlFor?: string
|
|
18
|
-
[prop: string]: unknown
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type ComponentType<P extends Props = Props> = (props: P) => Node
|
|
22
|
-
|
|
23
|
-
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
|
|
24
|
-
const SVG_TAGS = new Set([
|
|
25
|
-
'svg',
|
|
26
|
-
'circle',
|
|
27
|
-
'rect',
|
|
28
|
-
'path',
|
|
29
|
-
'line',
|
|
30
|
-
'polyline',
|
|
31
|
-
'polygon',
|
|
32
|
-
'ellipse',
|
|
33
|
-
'g',
|
|
34
|
-
'text',
|
|
35
|
-
'defs',
|
|
36
|
-
'use',
|
|
37
|
-
'symbol',
|
|
38
|
-
'clipPath',
|
|
39
|
-
'mask',
|
|
40
|
-
'filter',
|
|
41
|
-
'linearGradient',
|
|
42
|
-
'radialGradient',
|
|
43
|
-
'stop',
|
|
44
|
-
'pattern',
|
|
45
|
-
'image',
|
|
46
|
-
'foreignObject',
|
|
47
|
-
])
|
|
48
|
-
|
|
49
|
-
function normalizeAttrName(key: string): string {
|
|
50
|
-
if (key === 'className') return 'class'
|
|
51
|
-
if (key === 'htmlFor') return 'for'
|
|
52
|
-
if (key === 'tabIndex') return 'tabindex'
|
|
53
|
-
return key
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function applyStyle(el: HTMLElement, style: string | StyleObject): void {
|
|
57
|
-
if (typeof style === 'string') {
|
|
58
|
-
el.style.cssText = style
|
|
59
|
-
} else {
|
|
60
|
-
for (const [k, v] of Object.entries(style)) {
|
|
61
|
-
// camelCase → kebab-case for CSS custom properties and standard props
|
|
62
|
-
el.style.setProperty(
|
|
63
|
-
k.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`),
|
|
64
|
-
String(v),
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function applyProp(el: Element, key: string, value: unknown): void {
|
|
71
|
-
if (key === 'ref') {
|
|
72
|
-
if (typeof value === 'function') (value as (el: Element) => void)(el)
|
|
73
|
-
else if (value && typeof value === 'object' && 'current' in value) {
|
|
74
|
-
;(value as { current: Element | null }).current = el
|
|
75
|
-
}
|
|
76
|
-
return
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (key === 'style') {
|
|
80
|
-
applyStyle(el as HTMLElement, value as string | StyleObject)
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Event handlers
|
|
85
|
-
if (key.startsWith('on') && key[2] === key[2]?.toUpperCase()) {
|
|
86
|
-
const event = key.slice(2).toLowerCase()
|
|
87
|
-
el.addEventListener(event, value as EventListener)
|
|
88
|
-
return
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const attr = normalizeAttrName(key)
|
|
92
|
-
|
|
93
|
-
if (value === false || value == null) {
|
|
94
|
-
el.removeAttribute(attr)
|
|
95
|
-
} else if (value === true) {
|
|
96
|
-
el.setAttribute(attr, '')
|
|
97
|
-
} else {
|
|
98
|
-
el.setAttribute(attr, String(value))
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function toNode(child: Child): Node | Node[] {
|
|
103
|
-
if (child == null || child === false) return document.createTextNode('')
|
|
104
|
-
if (Array.isArray(child)) return child.flatMap((c) => toNode(c))
|
|
105
|
-
if (child instanceof Node) return child
|
|
106
|
-
if (typeof child === 'function') {
|
|
107
|
-
// Reactive child — re-evaluates when signals it reads change.
|
|
108
|
-
// The effect runs synchronously before `anchor` has a parent, so we
|
|
109
|
-
// capture the initial nodes here and return them in a fragment so they
|
|
110
|
-
// are inserted into the DOM immediately. Updates insert before `anchor`.
|
|
111
|
-
const anchor = document.createTextNode('')
|
|
112
|
-
let prevNodes: Node[] = []
|
|
113
|
-
let initialized = false
|
|
114
|
-
|
|
115
|
-
createEffect(() => {
|
|
116
|
-
const newNodes = [toNode((child as () => Child)())].flat()
|
|
117
|
-
if (!initialized) {
|
|
118
|
-
initialized = true
|
|
119
|
-
prevNodes = newNodes
|
|
120
|
-
} else {
|
|
121
|
-
const parent = anchor.parentNode
|
|
122
|
-
if (parent) {
|
|
123
|
-
for (const n of newNodes) parent.insertBefore(n, anchor)
|
|
124
|
-
for (const n of prevNodes) n.parentNode?.removeChild(n)
|
|
125
|
-
prevNodes = newNodes
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const frag = document.createDocumentFragment()
|
|
131
|
-
for (const n of prevNodes) frag.append(n)
|
|
132
|
-
frag.append(anchor)
|
|
133
|
-
return frag as unknown as Node
|
|
134
|
-
}
|
|
135
|
-
return document.createTextNode(String(child))
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function mountChildren(el: Element, children: Child | Child[] | undefined): void {
|
|
139
|
-
if (children == null) return
|
|
140
|
-
|
|
141
|
-
const flat = Array.isArray(children) ? children : [children]
|
|
142
|
-
for (const child of flat) {
|
|
143
|
-
const nodes = [toNode(child)].flat()
|
|
144
|
-
el.append(...nodes)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function jsx(type: string | ComponentType, props: Props, _key?: string | number): Node {
|
|
149
|
-
if (typeof type === 'function') {
|
|
150
|
-
return type(props)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (type === Fragment) {
|
|
154
|
-
const frag = document.createDocumentFragment()
|
|
155
|
-
mountChildren(frag as unknown as Element, props.children)
|
|
156
|
-
return frag
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const el = SVG_TAGS.has(type)
|
|
160
|
-
? document.createElementNS(SVG_NAMESPACE, type)
|
|
161
|
-
: document.createElement(type)
|
|
162
|
-
|
|
163
|
-
for (const [key, value] of Object.entries(props)) {
|
|
164
|
-
if (key === 'children') continue
|
|
165
|
-
|
|
166
|
-
if (typeof value === 'function' && !key.startsWith('on')) {
|
|
167
|
-
// Reactive prop — re-run when the signal it reads changes
|
|
168
|
-
createEffect(() => applyProp(el, key, (value as () => unknown)()))
|
|
169
|
-
} else {
|
|
170
|
-
applyProp(el, key, value)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
mountChildren(el, props.children)
|
|
175
|
-
|
|
176
|
-
return el
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export const jsxs = jsx
|
|
180
|
-
export const jsxDEV = jsx
|
|
181
|
-
|
|
182
|
-
export const Fragment = '__Fragment__'
|
|
183
|
-
|
|
184
|
-
// Client-side JSX namespace. JSX.Element is Node (a live DOM node),
|
|
185
|
-
// not Promise<string> — the two runtimes are intentionally separate.
|
|
186
|
-
export namespace JSX {
|
|
187
|
-
export type Element = Node
|
|
188
|
-
|
|
189
|
-
/** Reactive child: a signal getter (or any zero-arg fn) re-runs on signal change. */
|
|
190
|
-
export type ReactiveChild = () => Child
|
|
191
|
-
export type Child = Node | string | number | boolean | null | undefined | ReactiveChild | Child[]
|
|
192
|
-
|
|
193
|
-
export interface ElementChildrenAttribute {
|
|
194
|
-
children: object
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export interface IntrinsicAttributes {
|
|
198
|
-
key?: string | number
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
interface DOMAttributes {
|
|
202
|
-
children?: Child | Child[]
|
|
203
|
-
[attr: string]: unknown
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export interface IntrinsicElements {
|
|
207
|
-
[tag: string]: DOMAttributes
|
|
208
|
-
}
|
|
209
|
-
}
|
package/client/resource.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { createEffect, createSignal, onCleanup, untrack } from './signal.js'
|
|
2
|
-
|
|
3
|
-
// ─── createResource ───────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
/** A reactive async data accessor. Call it like a signal to read the resolved
|
|
6
|
-
* value. Read `.loading` and `.error` inside effects or JSX to react to state changes. */
|
|
7
|
-
export type Resource<T> = (() => T | undefined) & {
|
|
8
|
-
readonly loading: boolean
|
|
9
|
-
readonly error: Error | undefined
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/** Options for `createResource`. */
|
|
13
|
-
export interface ResourceOptions {
|
|
14
|
-
/** Poll the fetcher on this interval (ms). Cleared when the reactive scope is disposed. */
|
|
15
|
-
refetchInterval?: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type ResourceReturn<T> = [resource: Resource<T>, actions: { refetch: () => void }]
|
|
19
|
-
|
|
20
|
-
/** No-source: fetcher is called immediately and on every `refetch()`. */
|
|
21
|
-
export function createResource<T>(
|
|
22
|
-
fetcher: () => Promise<T>,
|
|
23
|
-
options?: ResourceOptions,
|
|
24
|
-
): ResourceReturn<T>
|
|
25
|
-
|
|
26
|
-
/** Reactive source: re-fetches whenever the source signal changes.
|
|
27
|
-
* Skips fetching when source returns `false`, `null`, or `undefined`. */
|
|
28
|
-
export function createResource<T, S>(
|
|
29
|
-
source: () => S | false | null | undefined,
|
|
30
|
-
fetcher: (src: S) => Promise<T>,
|
|
31
|
-
options?: ResourceOptions,
|
|
32
|
-
): ResourceReturn<T>
|
|
33
|
-
|
|
34
|
-
export function createResource<T, S = never>(
|
|
35
|
-
fetcherOrSource: (() => Promise<T>) | (() => S | false | null | undefined),
|
|
36
|
-
fetcherOrOptions?: ((src: S) => Promise<T>) | ResourceOptions,
|
|
37
|
-
maybeOptions?: ResourceOptions,
|
|
38
|
-
): ResourceReturn<T> {
|
|
39
|
-
const hasSource = typeof fetcherOrOptions === 'function'
|
|
40
|
-
const options: ResourceOptions = hasSource
|
|
41
|
-
? (maybeOptions ?? {})
|
|
42
|
-
: ((fetcherOrOptions as ResourceOptions | undefined) ?? {})
|
|
43
|
-
|
|
44
|
-
// When using a source, don't start in loading=true if source is initially falsy.
|
|
45
|
-
const initialSrc = hasSource ? untrack(fetcherOrSource as () => unknown) : null
|
|
46
|
-
const startLoading = !hasSource || (initialSrc != null && initialSrc !== false)
|
|
47
|
-
|
|
48
|
-
const [data, setData] = createSignal<T | undefined>(undefined)
|
|
49
|
-
const [loading, setLoading] = createSignal(startLoading)
|
|
50
|
-
const [error, setError] = createSignal<Error | undefined>(undefined)
|
|
51
|
-
|
|
52
|
-
// Version counter discards results from superseded in-flight requests.
|
|
53
|
-
let version = 0
|
|
54
|
-
|
|
55
|
-
async function execute(call: () => Promise<T>): Promise<void> {
|
|
56
|
-
const v = ++version
|
|
57
|
-
setLoading(true)
|
|
58
|
-
setError(undefined)
|
|
59
|
-
try {
|
|
60
|
-
const result = await untrack(call)
|
|
61
|
-
if (v !== version) return
|
|
62
|
-
setData(result)
|
|
63
|
-
} catch (e) {
|
|
64
|
-
if (v !== version) return
|
|
65
|
-
setError(e instanceof Error ? e : new Error(String(e)))
|
|
66
|
-
} finally {
|
|
67
|
-
if (v === version) setLoading(false)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let refetch: () => void = () => {}
|
|
72
|
-
|
|
73
|
-
if (hasSource) {
|
|
74
|
-
const source = fetcherOrSource as () => S | false | null | undefined
|
|
75
|
-
const fetcher = fetcherOrOptions as (src: S) => Promise<T>
|
|
76
|
-
|
|
77
|
-
refetch = () => {
|
|
78
|
-
const src = untrack(source)
|
|
79
|
-
if (src != null && src !== false) execute(() => fetcher(src as S))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
createEffect(() => {
|
|
83
|
-
const src = source()
|
|
84
|
-
if (src == null || src === false) return
|
|
85
|
-
execute(() => fetcher(src))
|
|
86
|
-
})
|
|
87
|
-
} else {
|
|
88
|
-
const fetcher = fetcherOrSource as () => Promise<T>
|
|
89
|
-
refetch = () => execute(fetcher)
|
|
90
|
-
refetch()
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (options.refetchInterval) {
|
|
94
|
-
const id = setInterval(refetch, options.refetchInterval)
|
|
95
|
-
onCleanup(() => clearInterval(id))
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const resource = data as Resource<T>
|
|
99
|
-
Object.defineProperty(resource, 'loading', { get: loading, enumerable: true })
|
|
100
|
-
Object.defineProperty(resource, 'error', { get: error, enumerable: true })
|
|
101
|
-
|
|
102
|
-
return [resource, { refetch }]
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── createEventSource ────────────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Subscribe to a [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)
|
|
109
|
-
* endpoint. Returns a signal getter that holds the latest event's `data` string
|
|
110
|
-
* (`undefined` until the first message arrives) and the raw `EventSource` instance.
|
|
111
|
-
* The connection is closed when the reactive scope is disposed.
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* const [message] = createEventSource('/api/events')
|
|
115
|
-
* <p>Latest: {() => message() ?? '—'}</p>
|
|
116
|
-
*/
|
|
117
|
-
export function createEventSource(url: string): [() => string | undefined, EventSource] {
|
|
118
|
-
const [message, setMessage] = createSignal<string | undefined>(undefined)
|
|
119
|
-
const es = new EventSource(url)
|
|
120
|
-
es.onmessage = (e: MessageEvent) => setMessage(String(e.data))
|
|
121
|
-
return [message, es]
|
|
122
|
-
}
|
package/client/signal.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// Fine-grained reactivity for browser islands.
|
|
2
|
-
// Signal changes update only the DOM nodes that read them — no VDOM diffing.
|
|
3
|
-
|
|
4
|
-
type Subscriber = () => void
|
|
5
|
-
// biome-ignore lint/suspicious/noConfusingVoidType: void in this union is intentional — it lets createEffect accept functions that return nothing without an explicit `undefined` return
|
|
6
|
-
type Cleanup = (() => void) | void
|
|
7
|
-
|
|
8
|
-
let currentSubscriber: Subscriber | null = null
|
|
9
|
-
let currentCleanups: (() => void)[] | null = null
|
|
10
|
-
let batchDepth = 0
|
|
11
|
-
const pendingEffects = new Set<Subscriber>()
|
|
12
|
-
|
|
13
|
-
export type Signal<T> = [get: () => T, set: (value: T) => void]
|
|
14
|
-
export type ReadonlySignal<T> = () => T
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Create a reactive signal — a getter/setter pair.
|
|
18
|
-
* Reading the getter inside an effect or reactive JSX child subscribes that
|
|
19
|
-
* context to future updates; calling the setter notifies all subscribers.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* const [count, setCount] = createSignal(0)
|
|
23
|
-
* count() // read: 0
|
|
24
|
-
* setCount(1) // write
|
|
25
|
-
* setCount(n => n + 1) // updater function
|
|
26
|
-
*/
|
|
27
|
-
export function createSignal<T>(initialValue: T): Signal<T> {
|
|
28
|
-
let value = initialValue
|
|
29
|
-
const subscribers = new Set<Subscriber>()
|
|
30
|
-
|
|
31
|
-
const get = (): T => {
|
|
32
|
-
if (currentSubscriber) subscribers.add(currentSubscriber)
|
|
33
|
-
return value
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const set = (next: T): void => {
|
|
37
|
-
if (Object.is(value, next)) return
|
|
38
|
-
value = next
|
|
39
|
-
if (batchDepth > 0) {
|
|
40
|
-
for (const sub of subscribers) pendingEffects.add(sub)
|
|
41
|
-
} else {
|
|
42
|
-
for (const sub of [...subscribers]) sub()
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return [get, set]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Run a side effect immediately and re-run it whenever its reactive
|
|
51
|
-
* dependencies change. The effect automatically tracks which signals are
|
|
52
|
-
* read during execution and subscribes to each of them.
|
|
53
|
-
*
|
|
54
|
-
* Return a cleanup function (or nothing) — it runs before the next execution
|
|
55
|
-
* and when the enclosing scope is disposed.
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* createEffect(() => {
|
|
59
|
-
* document.title = name() // re-runs whenever `name` changes
|
|
60
|
-
* })
|
|
61
|
-
*/
|
|
62
|
-
export function createEffect(fn: () => Cleanup): void {
|
|
63
|
-
// Use an object wrapper so the parent-scope disposal closure always reads
|
|
64
|
-
// the current cleanups array, not a stale reference from an earlier run.
|
|
65
|
-
const state = { cleanups: [] as (() => void)[] }
|
|
66
|
-
|
|
67
|
-
// If created inside a parent scope (createRoot or another createEffect),
|
|
68
|
-
// register disposal so this effect's cleanups run when the parent disposes.
|
|
69
|
-
if (currentCleanups) {
|
|
70
|
-
currentCleanups.push(() => {
|
|
71
|
-
for (const c of state.cleanups) c()
|
|
72
|
-
state.cleanups = []
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const run: Subscriber = () => {
|
|
77
|
-
for (const c of state.cleanups) c()
|
|
78
|
-
state.cleanups = []
|
|
79
|
-
|
|
80
|
-
const prev = currentSubscriber
|
|
81
|
-
const prevCleanups = currentCleanups
|
|
82
|
-
currentSubscriber = run
|
|
83
|
-
currentCleanups = state.cleanups
|
|
84
|
-
try {
|
|
85
|
-
const cleanup = fn()
|
|
86
|
-
if (cleanup) state.cleanups.push(cleanup)
|
|
87
|
-
} finally {
|
|
88
|
-
currentSubscriber = prev
|
|
89
|
-
currentCleanups = prevCleanups
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
run()
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Create a memoized derived value. Recomputes only when its reactive
|
|
98
|
-
* dependencies change; all readers share the same cached result.
|
|
99
|
-
*
|
|
100
|
-
* @example
|
|
101
|
-
* const fullName = createMemo(() => `${firstName()} ${lastName()}`)
|
|
102
|
-
* <h1>{() => fullName()}</h1>
|
|
103
|
-
*/
|
|
104
|
-
export function createMemo<T>(fn: () => T): ReadonlySignal<T> {
|
|
105
|
-
const [get, set] = createSignal<T>(undefined as T)
|
|
106
|
-
createEffect(() => {
|
|
107
|
-
set(fn())
|
|
108
|
-
})
|
|
109
|
-
return get
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Register a teardown tied to the current reactive scope.
|
|
114
|
-
* Called before the enclosing effect re-runs, or when the root is disposed.
|
|
115
|
-
* No-op if called outside any reactive scope.
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* createEffect(() => {
|
|
119
|
-
* const id = setInterval(tick, 1000)
|
|
120
|
-
* onCleanup(() => clearInterval(id))
|
|
121
|
-
* })
|
|
122
|
-
*/
|
|
123
|
-
export function onCleanup(fn: () => void): void {
|
|
124
|
-
currentCleanups?.push(fn)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Create a reactive scope with an explicit lifetime. The `dispose` function
|
|
129
|
-
* tears down all effects and cleanups registered within the scope.
|
|
130
|
-
* Useful for managing reactive state outside the normal component lifecycle.
|
|
131
|
-
*
|
|
132
|
-
* @example
|
|
133
|
-
* const dispose = createRoot((d) => {
|
|
134
|
-
* createEffect(() => console.log(count()))
|
|
135
|
-
* return d
|
|
136
|
-
* })
|
|
137
|
-
* dispose() // tear everything down
|
|
138
|
-
*/
|
|
139
|
-
export function createRoot<T>(fn: (dispose: () => void) => T): T {
|
|
140
|
-
const cleanups: (() => void)[] = []
|
|
141
|
-
const dispose = (): void => {
|
|
142
|
-
for (const c of cleanups) c()
|
|
143
|
-
cleanups.length = 0
|
|
144
|
-
}
|
|
145
|
-
const prev = currentCleanups
|
|
146
|
-
currentCleanups = cleanups
|
|
147
|
-
try {
|
|
148
|
-
return fn(dispose)
|
|
149
|
-
} finally {
|
|
150
|
-
currentCleanups = prev
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Read signals inside `fn` without registering reactive dependencies.
|
|
156
|
-
* The enclosing effect will not re-run when those signals change.
|
|
157
|
-
*
|
|
158
|
-
* @example
|
|
159
|
-
* createEffect(() => {
|
|
160
|
-
* const a = a() // tracked — effect re-runs when `a` changes
|
|
161
|
-
* const b = untrack(() => b()) // not tracked
|
|
162
|
-
* })
|
|
163
|
-
*/
|
|
164
|
-
export function untrack<T>(fn: () => T): T {
|
|
165
|
-
const prev = currentSubscriber
|
|
166
|
-
currentSubscriber = null
|
|
167
|
-
try {
|
|
168
|
-
return fn()
|
|
169
|
-
} finally {
|
|
170
|
-
currentSubscriber = prev
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Batch multiple signal writes into a single downstream notification pass.
|
|
176
|
-
* Without batching, each setter triggers its own update cycle.
|
|
177
|
-
*
|
|
178
|
-
* @example
|
|
179
|
-
* batch(() => {
|
|
180
|
-
* setX(1)
|
|
181
|
-
* setY(2) // effects that read both x and y run once, not twice
|
|
182
|
-
* })
|
|
183
|
-
*/
|
|
184
|
-
export function batch<T>(fn: () => T): T {
|
|
185
|
-
batchDepth++
|
|
186
|
-
try {
|
|
187
|
-
return fn()
|
|
188
|
-
} finally {
|
|
189
|
-
batchDepth--
|
|
190
|
-
if (batchDepth === 0) {
|
|
191
|
-
const toRun = [...pendingEffects]
|
|
192
|
-
pendingEffects.clear()
|
|
193
|
-
for (const sub of toRun) sub()
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ─── Store integration hooks ──────────────────────────────────────────────────
|
|
199
|
-
// Used by store.ts to participate in the same subscriber graph and batching.
|
|
200
|
-
|
|
201
|
-
export function trackRead(subs: Set<Subscriber>): void {
|
|
202
|
-
if (currentSubscriber) subs.add(currentSubscriber)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function notifySubs(subs: Set<Subscriber>): void {
|
|
206
|
-
if (batchDepth > 0) {
|
|
207
|
-
for (const sub of subs) pendingEffects.add(sub)
|
|
208
|
-
} else {
|
|
209
|
-
for (const sub of [...subs]) sub()
|
|
210
|
-
}
|
|
211
|
-
}
|
package/client/store.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { notifySubs, trackRead } from './signal.js'
|
|
2
|
-
|
|
3
|
-
type PlainObj = Record<string | symbol, unknown>
|
|
4
|
-
type Subscriber = () => void
|
|
5
|
-
|
|
6
|
-
// Per-property subscriber sets, keyed on the raw (unwrapped) target object.
|
|
7
|
-
const nodeMap = new WeakMap<object, Map<string | symbol, Set<Subscriber>>>()
|
|
8
|
-
|
|
9
|
-
function getSubs(target: object, key: string | symbol): Set<Subscriber> {
|
|
10
|
-
let keyMap = nodeMap.get(target)
|
|
11
|
-
if (!keyMap) {
|
|
12
|
-
keyMap = new Map()
|
|
13
|
-
nodeMap.set(target, keyMap)
|
|
14
|
-
}
|
|
15
|
-
let s = keyMap.get(key)
|
|
16
|
-
if (!s) {
|
|
17
|
-
s = new Set()
|
|
18
|
-
keyMap.set(key, s)
|
|
19
|
-
}
|
|
20
|
-
return s
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function wrap<T extends object>(raw: T): T {
|
|
24
|
-
return new Proxy(raw, {
|
|
25
|
-
get(target, key, receiver) {
|
|
26
|
-
const val = Reflect.get(target, key, receiver)
|
|
27
|
-
// Only track string-keyed data reads (not Symbol builtins or methods)
|
|
28
|
-
if (typeof key === 'string' && typeof val !== 'function') {
|
|
29
|
-
trackRead(getSubs(target, key))
|
|
30
|
-
}
|
|
31
|
-
if (val !== null && typeof val === 'object') return wrap(val as object)
|
|
32
|
-
return val
|
|
33
|
-
},
|
|
34
|
-
set() {
|
|
35
|
-
throw new Error('[davaux] Store is read-only — use setStore() to update')
|
|
36
|
-
},
|
|
37
|
-
deleteProperty() {
|
|
38
|
-
throw new Error('[davaux] Store is read-only — use setStore() to update')
|
|
39
|
-
},
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function setPath(raw: PlainObj, path: (string | number)[], value: unknown): void {
|
|
44
|
-
const [head, ...tail] = path
|
|
45
|
-
const key = String(head)
|
|
46
|
-
if (tail.length === 0) {
|
|
47
|
-
const prev = raw[key]
|
|
48
|
-
const next = typeof value === 'function' ? (value as (p: unknown) => unknown)(prev) : value
|
|
49
|
-
if (Object.is(prev, next)) return
|
|
50
|
-
raw[key] = next
|
|
51
|
-
notifySubs(getSubs(raw, key))
|
|
52
|
-
} else {
|
|
53
|
-
const child = raw[key]
|
|
54
|
-
if (child === null || typeof child !== 'object') {
|
|
55
|
-
throw new Error(`[davaux] setStore: "${key}" is not an object`)
|
|
56
|
-
}
|
|
57
|
-
setPath(child as PlainObj, tail, value)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** A value or a function that receives the previous value and returns the next. */
|
|
62
|
-
export type Updater<T> = T | ((prev: T) => T)
|
|
63
|
-
|
|
64
|
-
/** Typed path-based setter for a store. Supports 1–3 level deep key paths;
|
|
65
|
-
* deeper paths fall through to an untyped escape hatch. */
|
|
66
|
-
export interface SetStoreFn<T extends object> {
|
|
67
|
-
<K extends keyof T>(k: K, v: Updater<T[K]>): void
|
|
68
|
-
<K1 extends keyof T, K2 extends keyof NonNullable<T[K1]>>(
|
|
69
|
-
k1: K1,
|
|
70
|
-
k2: K2,
|
|
71
|
-
v: Updater<NonNullable<T[K1]>[K2]>,
|
|
72
|
-
): void
|
|
73
|
-
<
|
|
74
|
-
K1 extends keyof T,
|
|
75
|
-
K2 extends keyof NonNullable<T[K1]>,
|
|
76
|
-
K3 extends keyof NonNullable<NonNullable<T[K1]>[K2]>,
|
|
77
|
-
>(
|
|
78
|
-
k1: K1,
|
|
79
|
-
k2: K2,
|
|
80
|
-
k3: K3,
|
|
81
|
-
v: Updater<NonNullable<NonNullable<T[K1]>[K2]>[K3]>,
|
|
82
|
-
): void
|
|
83
|
-
(...args: unknown[]): void
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Create a reactive store for shared inter-island state. Returns a
|
|
88
|
-
* `[store, setStore]` pair. `store` is a deeply reactive readonly proxy —
|
|
89
|
-
* reads inside effects and JSX are tracked at individual property granularity,
|
|
90
|
-
* so only the effects that read a changed key re-run.
|
|
91
|
-
*
|
|
92
|
-
* Use `setStore` with dot-path key segments; functional updaters are supported
|
|
93
|
-
* at every level. Arrays must be updated via functional updaters (not `.push()`).
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* const [cart, setCart] = createStore({ items: [] as Item[], total: 0 })
|
|
97
|
-
* setCart('items', items => [...items, newItem])
|
|
98
|
-
* setCart('total', t => t + newItem.price)
|
|
99
|
-
*/
|
|
100
|
-
export function createStore<T extends object>(init: T): [store: T, setStore: SetStoreFn<T>] {
|
|
101
|
-
const raw = structuredClone(init) as PlainObj
|
|
102
|
-
const store = wrap(raw) as T
|
|
103
|
-
|
|
104
|
-
const setStore = (...args: unknown[]): void => {
|
|
105
|
-
if (args.length < 2) throw new Error('[davaux] setStore requires a path and a value')
|
|
106
|
-
setPath(raw, args.slice(0, -1) as (string | number)[], args[args.length - 1])
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return [store, setStore as unknown as SetStoreFn<T>]
|
|
110
|
-
}
|