@bitstillery/mithril 3.3.3 → 3.4.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/README.md +7 -3
- package/index.ts +2 -2
- package/package.json +8 -2
- package/render/ssrState.ts +4 -2
- package/state.ts +20 -7
- package/store.ts +36 -12
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ count(5) // Logs: 5 × 2 = 10
|
|
|
36
36
|
|
|
37
37
|
## Proxy State
|
|
38
38
|
|
|
39
|
-
`state()` creates reactive objects. Components track which properties they read and only re-render when those change.
|
|
39
|
+
`state()` creates reactive objects. Components track which properties they read and only re-render when those change. **Function properties become computed values**—they re-evaluate when their dependencies change.
|
|
40
40
|
|
|
41
41
|
```tsx
|
|
42
42
|
import m, {state, MithrilComponent} from '@bitstillery/mithril'
|
|
@@ -47,7 +47,9 @@ class Counter extends MithrilComponent {
|
|
|
47
47
|
view() {
|
|
48
48
|
return (
|
|
49
49
|
<div>
|
|
50
|
-
<p>
|
|
50
|
+
<p>
|
|
51
|
+
{$s.count} / {$s.totalTodos}
|
|
52
|
+
</p>
|
|
51
53
|
<button onclick={() => $s.count++}>+</button>
|
|
52
54
|
</div>
|
|
53
55
|
)
|
|
@@ -57,7 +59,9 @@ class Counter extends MithrilComponent {
|
|
|
57
59
|
m.mount(document.body, Counter)
|
|
58
60
|
```
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
- **Computed properties**: Any function in state (e.g. `totalTodos: () => $s.todos.length`) is a computed—read it like a normal property, it updates when dependencies change.
|
|
63
|
+
- **`$` prefix**: Use `$s.$count` for the raw signal (e.g. for `watch()`).
|
|
64
|
+
- The second argument is a name used for SSR serialization.
|
|
61
65
|
|
|
62
66
|
## Watchers
|
|
63
67
|
|
package/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ import censor from './util/censor'
|
|
|
12
12
|
import nextTick from './util/next_tick'
|
|
13
13
|
import domFor from './render/domFor'
|
|
14
14
|
import {signal, computed, effect, Signal, ComputedSignal, setSignalRedrawCallback, getSignalComponents} from './signal'
|
|
15
|
-
import {state, watch, registerState, getRegisteredStates, copyGlobalStatesToContext} from './state'
|
|
15
|
+
import {state, watch, registerState, getRegisteredStates, clearStateRegistry, copyGlobalStatesToContext} from './state'
|
|
16
16
|
|
|
17
17
|
import type {Vnode, Children, ComponentType} from './render/vnode'
|
|
18
18
|
import type {Hyperscript} from './render/hyperscript'
|
|
@@ -101,7 +101,7 @@ setSignalRedrawCallback((sig: Signal<any>) => {
|
|
|
101
101
|
})
|
|
102
102
|
|
|
103
103
|
// Export signals API
|
|
104
|
-
export {signal, computed, effect, Signal, ComputedSignal, state, watch, registerState, getRegisteredStates}
|
|
104
|
+
export {signal, computed, effect, Signal, ComputedSignal, state, watch, registerState, getRegisteredStates, clearStateRegistry}
|
|
105
105
|
export type {State, StateOptions, StateSignals, Unwatch} from './state'
|
|
106
106
|
|
|
107
107
|
// Export Store class
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitstillery/mithril",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.1",
|
|
4
4
|
"description": "Mithril + Signals, Store and SSR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bitstillery",
|
|
@@ -54,9 +54,14 @@
|
|
|
54
54
|
"lint:ts-syntax": "oxlint -c .oxlintrc.json",
|
|
55
55
|
"lint:ts-types": "tsgo --noEmit",
|
|
56
56
|
"test": "bun test",
|
|
57
|
+
"bench": "NODE_ENV=production bun run bench/index.ts",
|
|
58
|
+
"bench:hyperscript": "NODE_ENV=production bun run bench/index.ts hyperscript",
|
|
59
|
+
"bench:render": "NODE_ENV=production bun run bench/index.ts render",
|
|
60
|
+
"bench:signal": "NODE_ENV=production bun run bench/index.ts signal",
|
|
61
|
+
"bench:state": "NODE_ENV=production bun run bench/index.ts state",
|
|
62
|
+
"bench:profile": "bun --cpu-prof-md run bench/index.ts",
|
|
57
63
|
"prepublishOnly": "bun run lint:ts",
|
|
58
64
|
"pack:dry-run": "npm pack --dry-run",
|
|
59
|
-
"publish": "./scripts/release.sh",
|
|
60
65
|
"release": "./scripts/release.sh",
|
|
61
66
|
"release:patch": "./scripts/release.sh --release-as patch",
|
|
62
67
|
"release:minor": "./scripts/release.sh --release-as minor",
|
|
@@ -77,6 +82,7 @@
|
|
|
77
82
|
"codemirror": "^6.0.1",
|
|
78
83
|
"marked": "^15.0.4",
|
|
79
84
|
"marked-gfm-heading-id": "^4.0.0",
|
|
85
|
+
"mitata": "^1.0.34",
|
|
80
86
|
"oxfmt": "^0.32.0",
|
|
81
87
|
"oxlint": "^1.30.0",
|
|
82
88
|
"standard-version": "^9.5.0",
|
package/render/ssrState.ts
CHANGED
|
@@ -100,8 +100,10 @@ export function serializeStore(state: State<any>): any {
|
|
|
100
100
|
continue
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
// For nested states, only serialize keys that belong to this state (not parent keys)
|
|
104
|
-
|
|
103
|
+
// For nested states, only serialize keys that belong to this state (not parent keys).
|
|
104
|
+
// When originalKeys is empty (e.g. lookup: {}), include all keys so dynamically
|
|
105
|
+
// added entries (e.g. $s.lookup[path] = data) are serialized.
|
|
106
|
+
if (nestedOriginalKeys && nestedOriginalKeys.size > 0 && !nestedOriginalKeys.has(key)) {
|
|
105
107
|
continue
|
|
106
108
|
}
|
|
107
109
|
|
package/state.ts
CHANGED
|
@@ -27,14 +27,27 @@ function createStateComputed<T>(wrapped: any, computeFn: () => T, shouldDefer: b
|
|
|
27
27
|
function markAllComputedsDirty(stateObj: any): void {
|
|
28
28
|
if (!stateObj || !(stateObj as any).__isState) return
|
|
29
29
|
const signalMap = (stateObj as any).__signalMap
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
if (signalMap && signalMap instanceof Map) {
|
|
31
|
+
signalMap.forEach((sig: any) => {
|
|
32
|
+
if (sig instanceof ComputedSignal) {
|
|
33
|
+
sig.markDirty()
|
|
34
|
+
} else if (sig && typeof sig === 'object' && sig.value && (sig.value as any).__isState) {
|
|
35
|
+
markAllComputedsDirty(sig.value)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
// Arrays use __signals instead of __signalMap; recurse into nested state elements
|
|
40
|
+
const signals = (stateObj as any).__signals
|
|
41
|
+
if (Array.isArray(signals)) {
|
|
42
|
+
for (const sig of signals) {
|
|
43
|
+
if (sig && typeof sig === 'object' && (sig as any).__isState) {
|
|
44
|
+
markAllComputedsDirty(sig)
|
|
45
|
+
} else if (sig && typeof sig === 'object' && !isSignal(sig)) {
|
|
46
|
+
// Array elements that are Proxies (not signals) - they're nested states
|
|
47
|
+
markAllComputedsDirty(sig)
|
|
48
|
+
}
|
|
36
49
|
}
|
|
37
|
-
}
|
|
50
|
+
}
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
// Type guard to check if value is a Signal
|
package/store.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {state, State, updateStateRegistry} from './state'
|
|
2
2
|
import {serializeStore, deserializeStore} from './render/ssrState'
|
|
3
|
-
import {ComputedSignal} from './signal'
|
|
4
3
|
|
|
5
4
|
// Helper function to restore computed properties (same as in ssrState.ts)
|
|
6
5
|
function restoreComputedProperties(state: State<any>, initial: any): void {
|
|
@@ -374,22 +373,26 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
374
373
|
return
|
|
375
374
|
}
|
|
376
375
|
|
|
377
|
-
// Default to
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
376
|
+
// Default: write to localStorage and sessionStorage when no options; session is opt-in
|
|
377
|
+
const writeLocalStorage = options?.saved ?? options === undefined
|
|
378
|
+
const writeSessionStorage = options?.tab ?? options === undefined
|
|
379
|
+
const writeSessionApi = options?.session === true
|
|
381
380
|
|
|
382
|
-
// Use SSR serialization which properly handles State objects and skips ComputedSignal properties
|
|
383
|
-
// This is the same mechanism used for SSR, ensuring consistency
|
|
384
381
|
const statePlain = serializeStore(this.stateInstance)
|
|
385
382
|
|
|
386
|
-
//
|
|
387
|
-
if (
|
|
383
|
+
// Write to localStorage (persistent across browser restarts)
|
|
384
|
+
if (writeLocalStorage && this.templates.saved) {
|
|
388
385
|
this.set(this.storageKey, this.blueprint(statePlain, copy_object(this.templates.saved)))
|
|
389
386
|
}
|
|
390
387
|
|
|
391
|
-
//
|
|
392
|
-
|
|
388
|
+
// Lookup is always persisted to localStorage when present, regardless of writeLocalStorage.
|
|
389
|
+
// This ensures cached values (e.g. filter/sort state) are never lost when save() is called.
|
|
390
|
+
if ((statePlain as any).lookup) {
|
|
391
|
+
this.persist_lookup_to_local_storage(statePlain)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Write to sessionStorage (tab-scoped, cleared when tab closes)
|
|
395
|
+
if (writeSessionStorage && this.templates.tab) {
|
|
393
396
|
const tabState = (this.stateInstance as any).tab
|
|
394
397
|
if (tabState) {
|
|
395
398
|
// Get the tab template - unwrap if it's nested under a 'tab' key
|
|
@@ -420,7 +423,7 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
420
423
|
// Save to session API (session state) - async by nature
|
|
421
424
|
// Only save session on client side (not during SSR)
|
|
422
425
|
if (
|
|
423
|
-
|
|
426
|
+
writeSessionApi &&
|
|
424
427
|
this.templates.session &&
|
|
425
428
|
Object.keys(this.templates.session).length > 0 &&
|
|
426
429
|
typeof window !== 'undefined'
|
|
@@ -441,6 +444,27 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
441
444
|
}
|
|
442
445
|
}
|
|
443
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Merge lookup from state into localStorage and write. Ensures lookup is always persisted
|
|
449
|
+
* whenever save() is called and state contains lookup.
|
|
450
|
+
*/
|
|
451
|
+
private persist_lookup_to_local_storage(statePlain: Record<string, any>): void {
|
|
452
|
+
if (typeof window === 'undefined') return
|
|
453
|
+
try {
|
|
454
|
+
const existing = this.get(this.storageKey)
|
|
455
|
+
let storeData: Record<string, any> = {}
|
|
456
|
+
try {
|
|
457
|
+
storeData = JSON.parse(existing) || {}
|
|
458
|
+
} catch {
|
|
459
|
+
storeData = {}
|
|
460
|
+
}
|
|
461
|
+
storeData.lookup = copy_object(statePlain.lookup || {})
|
|
462
|
+
window.localStorage.setItem(this.storageKey, JSON.stringify(storeData))
|
|
463
|
+
} catch (err) {
|
|
464
|
+
console.error('Cannot persist lookup to Local Storage; continue without.', err)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
444
468
|
set(key: string, item: object): void {
|
|
445
469
|
if (typeof window === 'undefined') return
|
|
446
470
|
try {
|