@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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/api/mount-redraw.ts +173 -0
  4. package/api/router-server.ts +88 -0
  5. package/api/router.ts +670 -0
  6. package/browser.ts +10 -0
  7. package/htm.ts +7 -0
  8. package/hyperscript.ts +8 -0
  9. package/index.ts +134 -0
  10. package/jsx.d.ts +16 -0
  11. package/mount.ts +10 -0
  12. package/package.json +66 -0
  13. package/pathname/build.ts +40 -0
  14. package/pathname/compileTemplate.ts +49 -0
  15. package/pathname/parse.ts +19 -0
  16. package/querystring/build.ts +22 -0
  17. package/querystring/parse.ts +50 -0
  18. package/redraw.ts +10 -0
  19. package/render/cachedAttrsIsStaticMap.ts +10 -0
  20. package/render/delayedRemoval.ts +1 -0
  21. package/render/domFor.ts +23 -0
  22. package/render/emptyAttrs.ts +2 -0
  23. package/render/fragment.ts +11 -0
  24. package/render/hyperscript.ts +125 -0
  25. package/render/hyperscriptVnode.ts +21 -0
  26. package/render/render.ts +1307 -0
  27. package/render/renderToString.ts +494 -0
  28. package/render/ssrState.ts +366 -0
  29. package/render/trust.ts +6 -0
  30. package/render/vnode.ts +131 -0
  31. package/route.ts +11 -0
  32. package/server/logger.ts +223 -0
  33. package/server/session.ts +87 -0
  34. package/server/ssr.ts +205 -0
  35. package/server/ssrLogger.ts +13 -0
  36. package/server.ts +101 -0
  37. package/signal.ts +320 -0
  38. package/ssrContext.ts +87 -0
  39. package/state.ts +1026 -0
  40. package/store.ts +457 -0
  41. package/util/censor.ts +46 -0
  42. package/util/decodeURIComponentSafe.ts +34 -0
  43. package/util/hasOwn.ts +2 -0
  44. package/util/next_tick.ts +45 -0
  45. package/util/ssr.ts +385 -0
  46. package/util/uri.ts +126 -0
  47. 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
+ }