@bitstillery/mithril 3.0.0-AA
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/LICENSE +21 -0
- package/README.md +185 -0
- package/api/mount-redraw.ts +173 -0
- package/api/router-server.ts +88 -0
- package/api/router.ts +670 -0
- package/browser.ts +10 -0
- package/htm.ts +7 -0
- package/hyperscript.ts +8 -0
- package/index.ts +134 -0
- package/jsx.d.ts +16 -0
- package/mount.ts +10 -0
- package/package.json +66 -0
- package/pathname/build.ts +40 -0
- package/pathname/compileTemplate.ts +49 -0
- package/pathname/parse.ts +19 -0
- package/querystring/build.ts +22 -0
- package/querystring/parse.ts +50 -0
- package/redraw.ts +10 -0
- package/render/cachedAttrsIsStaticMap.ts +10 -0
- package/render/delayedRemoval.ts +1 -0
- package/render/domFor.ts +23 -0
- package/render/emptyAttrs.ts +2 -0
- package/render/fragment.ts +11 -0
- package/render/hyperscript.ts +125 -0
- package/render/hyperscriptVnode.ts +21 -0
- package/render/render.ts +1307 -0
- package/render/renderToString.ts +494 -0
- package/render/ssrState.ts +366 -0
- package/render/trust.ts +6 -0
- package/render/vnode.ts +131 -0
- package/route.ts +11 -0
- package/server/logger.ts +223 -0
- package/server/session.ts +87 -0
- package/server/ssr.ts +205 -0
- package/server/ssrLogger.ts +13 -0
- package/server.ts +101 -0
- package/signal.ts +320 -0
- package/ssrContext.ts +87 -0
- package/state.ts +1026 -0
- package/store.ts +457 -0
- package/util/censor.ts +46 -0
- package/util/decodeURIComponentSafe.ts +34 -0
- package/util/hasOwn.ts +2 -0
- package/util/next_tick.ts +45 -0
- package/util/ssr.ts +385 -0
- package/util/uri.ts +126 -0
- package/version.ts +3 -0
package/signal.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Core signal primitive for fine-grained reactivity
|
|
2
|
+
|
|
3
|
+
import {getSSRContext, runWithContext} from './ssrContext'
|
|
4
|
+
|
|
5
|
+
// Current effect context for dependency tracking
|
|
6
|
+
let currentEffect: (() => void) | null = null
|
|
7
|
+
|
|
8
|
+
// Component-to-signal dependency tracking
|
|
9
|
+
const componentSignalMap = new WeakMap<any, Set<Signal<any>>>()
|
|
10
|
+
const signalComponentMap = new WeakMap<Signal<any>, Set<any>>()
|
|
11
|
+
|
|
12
|
+
// Current component context for component-to-signal dependency tracking
|
|
13
|
+
let currentComponent: any = null
|
|
14
|
+
|
|
15
|
+
export function setCurrentComponent(component: any) {
|
|
16
|
+
currentComponent = component
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function clearCurrentComponent() {
|
|
20
|
+
currentComponent = null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getCurrentComponent() {
|
|
24
|
+
return currentComponent
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function trackComponentSignal(component: any, signal: Signal<any>) {
|
|
28
|
+
if (!componentSignalMap.has(component)) {
|
|
29
|
+
componentSignalMap.set(component, new Set())
|
|
30
|
+
}
|
|
31
|
+
componentSignalMap.get(component)!.add(signal)
|
|
32
|
+
|
|
33
|
+
if (!signalComponentMap.has(signal)) {
|
|
34
|
+
signalComponentMap.set(signal, new Set())
|
|
35
|
+
}
|
|
36
|
+
signalComponentMap.get(signal)!.add(component)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getComponentSignals(component: any): Set<Signal<any>> | undefined {
|
|
40
|
+
return componentSignalMap.get(component)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getSignalComponents(signal: Signal<any>): Set<any> | undefined {
|
|
44
|
+
return signalComponentMap.get(signal)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clearComponentDependencies(component: any) {
|
|
48
|
+
const signals = componentSignalMap.get(component)
|
|
49
|
+
if (signals) {
|
|
50
|
+
signals.forEach((signal) => {
|
|
51
|
+
const components = signalComponentMap.get(signal)
|
|
52
|
+
if (components) {
|
|
53
|
+
components.delete(component)
|
|
54
|
+
if (components.size === 0) {
|
|
55
|
+
signalComponentMap.delete(signal)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
componentSignalMap.delete(component)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set up callback for signal-to-component redraw integration
|
|
64
|
+
export function setSignalRedrawCallback(callback: (signal: Signal<any>) => void) {
|
|
65
|
+
;(signal as any).__redrawCallback = callback
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Signal class - reactive primitive that tracks subscribers
|
|
70
|
+
*/
|
|
71
|
+
export class Signal<T> {
|
|
72
|
+
private _value: T
|
|
73
|
+
private _subscribers: Set<() => void> = new Set()
|
|
74
|
+
|
|
75
|
+
constructor(initial: T) {
|
|
76
|
+
this._value = initial
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get value(): T {
|
|
80
|
+
// Ensure _subscribers is initialized (defensive check)
|
|
81
|
+
if (!this._subscribers) {
|
|
82
|
+
this._subscribers = new Set()
|
|
83
|
+
}
|
|
84
|
+
// Track access during render/effect
|
|
85
|
+
if (currentEffect) {
|
|
86
|
+
this._subscribers.add(currentEffect)
|
|
87
|
+
}
|
|
88
|
+
// Track component dependency
|
|
89
|
+
if (currentComponent) {
|
|
90
|
+
trackComponentSignal(currentComponent, this)
|
|
91
|
+
}
|
|
92
|
+
return this._value
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
set value(newValue: T) {
|
|
96
|
+
if (this._value !== newValue) {
|
|
97
|
+
this._value = newValue
|
|
98
|
+
// Ensure _subscribers is initialized (defensive check)
|
|
99
|
+
if (!this._subscribers) {
|
|
100
|
+
this._subscribers = new Set()
|
|
101
|
+
}
|
|
102
|
+
// Notify all subscribers
|
|
103
|
+
const context = getSSRContext()
|
|
104
|
+
this._subscribers.forEach((fn) => {
|
|
105
|
+
try {
|
|
106
|
+
// Always run watchers - wrap in SSR context if available
|
|
107
|
+
if (context) {
|
|
108
|
+
// Run watcher inside SSR context, similar to events
|
|
109
|
+
runWithContext(context, () => {
|
|
110
|
+
fn()
|
|
111
|
+
})
|
|
112
|
+
} else {
|
|
113
|
+
fn()
|
|
114
|
+
}
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error('Error in signal subscriber:', e)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
// Trigger component redraws for affected components
|
|
120
|
+
// This is set up in index.ts after m.redraw is created
|
|
121
|
+
if ((signal as any).__redrawCallback) {
|
|
122
|
+
;(signal as any).__redrawCallback(this)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Subscribe to signal changes
|
|
129
|
+
*/
|
|
130
|
+
subscribe(callback: () => void): () => void {
|
|
131
|
+
// Ensure _subscribers is initialized (defensive check)
|
|
132
|
+
if (!this._subscribers) {
|
|
133
|
+
this._subscribers = new Set()
|
|
134
|
+
}
|
|
135
|
+
this._subscribers.add(callback)
|
|
136
|
+
return () => {
|
|
137
|
+
if (this._subscribers) {
|
|
138
|
+
this._subscribers.delete(callback)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Watch signal changes (convenience method)
|
|
145
|
+
*/
|
|
146
|
+
watch(callback: (newValue: T, oldValue: T) => void): () => void {
|
|
147
|
+
let oldValue = this._value
|
|
148
|
+
const unsubscribe = this.subscribe(() => {
|
|
149
|
+
const newValue = this._value
|
|
150
|
+
callback(newValue, oldValue)
|
|
151
|
+
oldValue = newValue
|
|
152
|
+
})
|
|
153
|
+
return unsubscribe
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Peek at value without subscribing
|
|
158
|
+
*/
|
|
159
|
+
peek(): T {
|
|
160
|
+
return this._value
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Computed signal - automatically recomputes when dependencies change
|
|
166
|
+
*/
|
|
167
|
+
export class ComputedSignal<T> extends Signal<T> {
|
|
168
|
+
private _compute: () => T
|
|
169
|
+
private _dependencies: Set<Signal<any>> = new Set()
|
|
170
|
+
private _isDirty = true
|
|
171
|
+
private _cachedValue!: T
|
|
172
|
+
|
|
173
|
+
constructor(compute: () => T) {
|
|
174
|
+
super(null as any) // Will be computed on first access
|
|
175
|
+
this._compute = compute
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get value(): T {
|
|
179
|
+
// Track access by other computed signals - this enables computed-to-computed dependency chains
|
|
180
|
+
// When computed B accesses computed A, A should notify B when A's dependencies change
|
|
181
|
+
if (currentEffect) {
|
|
182
|
+
// Ensure _subscribers is initialized (defensive check)
|
|
183
|
+
if (!(this as any)._subscribers) {
|
|
184
|
+
;(this as any)._subscribers = new Set()
|
|
185
|
+
}
|
|
186
|
+
;(this as any)._subscribers.add(currentEffect)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (this._isDirty) {
|
|
190
|
+
// Clear old dependencies
|
|
191
|
+
this._dependencies.forEach((dep) => {
|
|
192
|
+
dep.subscribe(() => this._markDirty())?.() // Unsubscribe old
|
|
193
|
+
})
|
|
194
|
+
this._dependencies.clear()
|
|
195
|
+
|
|
196
|
+
// Track dependencies during computation
|
|
197
|
+
const previousEffect = currentEffect
|
|
198
|
+
currentEffect = () => {
|
|
199
|
+
this._markDirty()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
this._cachedValue = this._compute()
|
|
204
|
+
// Re-subscribe to new dependencies
|
|
205
|
+
// Dependencies are tracked via the compute function accessing signals
|
|
206
|
+
} finally {
|
|
207
|
+
currentEffect = previousEffect
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this._isDirty = false
|
|
211
|
+
}
|
|
212
|
+
return this._cachedValue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private _markDirty() {
|
|
216
|
+
if (!this._isDirty) {
|
|
217
|
+
this._isDirty = true
|
|
218
|
+
// Ensure _subscribers is initialized (defensive check)
|
|
219
|
+
if (!(this as any)._subscribers) {
|
|
220
|
+
;(this as any)._subscribers = new Set()
|
|
221
|
+
}
|
|
222
|
+
// Notify subscribers that computed value changed
|
|
223
|
+
const context = getSSRContext()
|
|
224
|
+
;(this as any)._subscribers.forEach((fn: () => void) => {
|
|
225
|
+
try {
|
|
226
|
+
if (context) {
|
|
227
|
+
// Run watcher inside SSR context, similar to events
|
|
228
|
+
runWithContext(context, () => {
|
|
229
|
+
fn()
|
|
230
|
+
})
|
|
231
|
+
} else {
|
|
232
|
+
fn()
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
console.error('Error in computed signal subscriber:', e)
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Mark this computed as dirty so it will recompute on next value access.
|
|
243
|
+
* Used by the state layer when opening the deferred-computed gate (allowComputed).
|
|
244
|
+
*/
|
|
245
|
+
markDirty(): void {
|
|
246
|
+
this._markDirty()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
set value(_newValue: T) {
|
|
250
|
+
throw new Error('Computed signals are read-only')
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create a signal
|
|
256
|
+
*/
|
|
257
|
+
export function signal<T>(initial: T): Signal<T> {
|
|
258
|
+
return new Signal(initial)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a computed signal
|
|
263
|
+
*/
|
|
264
|
+
export function computed<T>(compute: () => T): ComputedSignal<T> {
|
|
265
|
+
return new ComputedSignal(compute)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create an effect that runs when dependencies change
|
|
270
|
+
*/
|
|
271
|
+
export function effect(fn: () => void): () => void {
|
|
272
|
+
const previousEffect = currentEffect
|
|
273
|
+
let cleanup: (() => void) | null = null
|
|
274
|
+
let isActive = true
|
|
275
|
+
|
|
276
|
+
const effectFn = () => {
|
|
277
|
+
if (!isActive) return
|
|
278
|
+
|
|
279
|
+
// Run cleanup if exists
|
|
280
|
+
if (cleanup) {
|
|
281
|
+
try {
|
|
282
|
+
cleanup()
|
|
283
|
+
} catch (e) {
|
|
284
|
+
console.error('Error in effect cleanup:', e)
|
|
285
|
+
}
|
|
286
|
+
cleanup = null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Track dependencies
|
|
290
|
+
currentEffect = effectFn
|
|
291
|
+
try {
|
|
292
|
+
const result = fn()
|
|
293
|
+
// If fn returns a cleanup function, store it
|
|
294
|
+
if (typeof result === 'function') {
|
|
295
|
+
cleanup = result
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.error('Error in effect:', e)
|
|
299
|
+
} finally {
|
|
300
|
+
currentEffect = previousEffect
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Run effect immediately
|
|
305
|
+
effectFn()
|
|
306
|
+
|
|
307
|
+
// Return cleanup function
|
|
308
|
+
return () => {
|
|
309
|
+
isActive = false
|
|
310
|
+
if (cleanup) {
|
|
311
|
+
try {
|
|
312
|
+
cleanup()
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error('Error in effect cleanup:', e)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Note: We can't unsubscribe from signals here because we don't track them
|
|
318
|
+
// This is a limitation - in a full implementation, we'd track signal subscriptions
|
|
319
|
+
}
|
|
320
|
+
}
|
package/ssrContext.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-scoped SSR context using AsyncLocalStorage (Node/Bun).
|
|
3
|
+
* Each SSR request runs inside runWithContext(); code that needs the current
|
|
4
|
+
* request's store or state registry calls getSSRContext() and gets that
|
|
5
|
+
* request's context. No globals, safe under concurrent requests.
|
|
6
|
+
* In the browser, getSSRContext() returns undefined and runWithContext just runs fn.
|
|
7
|
+
*/
|
|
8
|
+
type StorageLike = {
|
|
9
|
+
getStore(): SSRAccessContext | undefined
|
|
10
|
+
run<T>(context: SSRAccessContext, fn: () => T): T
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let ssrStorage: StorageLike
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const {AsyncLocalStorage} = require('node:async_hooks') as {AsyncLocalStorage: new () => StorageLike}
|
|
17
|
+
ssrStorage = new AsyncLocalStorage()
|
|
18
|
+
} catch {
|
|
19
|
+
// Browser or environment without node:async_hooks; no request context
|
|
20
|
+
ssrStorage = {
|
|
21
|
+
getStore: () => undefined,
|
|
22
|
+
run: (_context, fn) => fn(),
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Data for a single SSR request. Created per request; only visible to code
|
|
28
|
+
* that runs inside the same runWithContext() call.
|
|
29
|
+
*/
|
|
30
|
+
export interface SSRAccessContext {
|
|
31
|
+
store?: any
|
|
32
|
+
/** Per-request state registry for serialization; fresh Map per request. */
|
|
33
|
+
stateRegistry: Map<string, {state: any; initial: any}>
|
|
34
|
+
sessionId?: string
|
|
35
|
+
sessionData?: any
|
|
36
|
+
/** Per-request EventEmitter; prevents event listeners from persisting between requests. */
|
|
37
|
+
events?: any
|
|
38
|
+
/** Per-request watcher cleanup functions; prevents watchers from persisting between requests. */
|
|
39
|
+
watchers?: Array<() => void>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the current SSR request context, or undefined if we're not inside
|
|
44
|
+
* a runWithContext() call (e.g. on the client or outside SSR).
|
|
45
|
+
*/
|
|
46
|
+
export function getSSRContext(): SSRAccessContext | undefined {
|
|
47
|
+
return ssrStorage.getStore()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Runs fn with context as the current SSR context. Used by the server so that
|
|
52
|
+
* getSSRContext() returns this request's context for the duration of fn.
|
|
53
|
+
*/
|
|
54
|
+
export function runWithContext<T>(context: SSRAccessContext, fn: () => T): T {
|
|
55
|
+
return ssrStorage.run(context, fn)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clean up all watchers registered in the current SSR context.
|
|
60
|
+
* Called automatically at the end of runWithContextAsync, but can be called manually if needed.
|
|
61
|
+
*/
|
|
62
|
+
export function cleanupWatchers(context?: SSRAccessContext): void {
|
|
63
|
+
const ctx = context || getSSRContext()
|
|
64
|
+
if (ctx && ctx.watchers && ctx.watchers.length > 0) {
|
|
65
|
+
ctx.watchers.forEach((unwatch) => {
|
|
66
|
+
try {
|
|
67
|
+
unwatch()
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error('Error cleaning up watcher:', e)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
ctx.watchers.length = 0
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Same as runWithContext but for async functions.
|
|
78
|
+
* Automatically cleans up watchers at the end of the request.
|
|
79
|
+
*/
|
|
80
|
+
export async function runWithContextAsync<T>(context: SSRAccessContext, fn: () => Promise<T>): Promise<T> {
|
|
81
|
+
try {
|
|
82
|
+
return await ssrStorage.run(context, fn)
|
|
83
|
+
} finally {
|
|
84
|
+
// Clean up watchers at the end of SSR request
|
|
85
|
+
cleanupWatchers(context)
|
|
86
|
+
}
|
|
87
|
+
}
|