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.
Files changed (90) hide show
  1. package/package.json +6 -2
  2. package/BASELINE.md +0 -169
  3. package/CLAUDE.md +0 -518
  4. package/ROADMAP.md +0 -198
  5. package/build.mjs +0 -101
  6. package/client/control.ts +0 -247
  7. package/client/hydrate.ts +0 -37
  8. package/client/index.ts +0 -19
  9. package/client/jsx-runtime.ts +0 -209
  10. package/client/resource.ts +0 -122
  11. package/client/signal.ts +0 -211
  12. package/client/store.ts +0 -110
  13. package/client/useHead.ts +0 -63
  14. package/pka.config.json +0 -32
  15. package/src/build/config.ts +0 -42
  16. package/src/build/index.ts +0 -6
  17. package/src/build/plugins.ts +0 -118
  18. package/src/cli.ts +0 -502
  19. package/src/config.ts +0 -197
  20. package/src/create-multisite.ts +0 -310
  21. package/src/create.ts +0 -194
  22. package/src/dev/blueprints.ts +0 -75
  23. package/src/dev/components.ts +0 -108
  24. package/src/dev/insert.ts +0 -221
  25. package/src/dev/remove.ts +0 -677
  26. package/src/dev/watch.ts +0 -3098
  27. package/src/errors.ts +0 -64
  28. package/src/generate.ts +0 -228
  29. package/src/index.ts +0 -67
  30. package/src/island.ts +0 -47
  31. package/src/jsx-runtime.d.ts +0 -408
  32. package/src/jsx-runtime.d.ts.map +0 -1
  33. package/src/jsx-runtime.ts +0 -536
  34. package/src/link.ts +0 -49
  35. package/src/oml/fragment.ts +0 -54
  36. package/src/oml/index.ts +0 -21
  37. package/src/oml/jsx-runtime.ts +0 -121
  38. package/src/oml/jsx.ts +0 -151
  39. package/src/oml/page.ts +0 -13
  40. package/src/oml/render.ts +0 -181
  41. package/src/oml/types.ts +0 -159
  42. package/src/router/handler.ts +0 -515
  43. package/src/router/matcher.ts +0 -52
  44. package/src/router/scanner.ts +0 -272
  45. package/src/server/index.ts +0 -49
  46. package/src/signal.ts +0 -39
  47. package/src/ssg.ts +0 -253
  48. package/src/test/actions.test.ts +0 -40
  49. package/src/test/body-limits.test.ts +0 -83
  50. package/src/test/errors.test.ts +0 -53
  51. package/src/test/fixtures/routes/[id].page.ts +0 -3
  52. package/src/test/fixtures/routes/_error.ts +0 -6
  53. package/src/test/fixtures/routes/_global.ts +0 -8
  54. package/src/test/fixtures/routes/_layout-template.ts +0 -7
  55. package/src/test/fixtures/routes/_layout.ts +0 -7
  56. package/src/test/fixtures/routes/_layout_scripts.ts +0 -8
  57. package/src/test/fixtures/routes/_middleware.ts +0 -8
  58. package/src/test/fixtures/routes/_redirect301_mw.ts +0 -5
  59. package/src/test/fixtures/routes/_redirect_mw.ts +0 -5
  60. package/src/test/fixtures/routes/about.page.ts +0 -6
  61. package/src/test/fixtures/routes/action.page.ts +0 -11
  62. package/src/test/fixtures/routes/api/form-all.post.ts +0 -5
  63. package/src/test/fixtures/routes/api/form-limited.post.ts +0 -6
  64. package/src/test/fixtures/routes/api/response-obj.get.ts +0 -17
  65. package/src/test/fixtures/routes/api/upload.post.ts +0 -14
  66. package/src/test/fixtures/routes/api/users.get.ts +0 -3
  67. package/src/test/fixtures/routes/api/xml.get.ts +0 -5
  68. package/src/test/fixtures/routes/auth/_middleware.ts +0 -11
  69. package/src/test/fixtures/routes/auth/protected.page.ts +0 -3
  70. package/src/test/fixtures/routes/index.page.ts +0 -3
  71. package/src/test/fixtures/routes/oml.page.ts +0 -7
  72. package/src/test/fixtures/routes/redirect.page.ts +0 -3
  73. package/src/test/fixtures/routes/ssg/[slug].page.ts +0 -8
  74. package/src/test/fixtures/routes/ssg/server.page.ts +0 -5
  75. package/src/test/fixtures/routes/state.page.ts +0 -4
  76. package/src/test/fixtures/routes/throw.page.ts +0 -5
  77. package/src/test/fixtures/routes/wiki/[...slug].page.ts +0 -3
  78. package/src/test/helpers.ts +0 -132
  79. package/src/test/layouts.test.ts +0 -76
  80. package/src/test/middleware.test.ts +0 -69
  81. package/src/test/multipart.test.ts +0 -91
  82. package/src/test/oml-routing.test.ts +0 -59
  83. package/src/test/oml.test.ts +0 -429
  84. package/src/test/redirects.test.ts +0 -32
  85. package/src/test/routing.test.ts +0 -118
  86. package/src/test/ssg.test.ts +0 -273
  87. package/src/test/web-response.test.ts +0 -33
  88. package/src/types.ts +0 -670
  89. package/tsconfig.client.json +0 -17
  90. package/tsconfig.json +0 -20
@@ -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
- }
@@ -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
- }