@bitstillery/mithril 3.0.2 → 3.1.2
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 -6
- package/api/mount-redraw.ts +130 -12
- package/index.ts +27 -7
- package/package.json +10 -2
- package/render/render.ts +21 -11
- package/render/ssrState.ts +14 -1
- package/signal.ts +11 -6
- package/state.ts +80 -217
- package/store.ts +12 -8
package/README.md
CHANGED
|
@@ -10,12 +10,13 @@ bun add @bitstillery/mithril
|
|
|
10
10
|
|
|
11
11
|
Mithril.js uses global `m.redraw()` — one state change re-renders **everything**. This fork adds component-level reactivity via signals so only affected components update.
|
|
12
12
|
|
|
13
|
-
| Feature | Original | This Fork
|
|
14
|
-
| ---------------- | ------------------- |
|
|
15
|
-
| Reactivity | Global `m.redraw()` | Fine-grained component updates
|
|
16
|
-
| State Management | Manual redraw calls | Signals with automatic tracking
|
|
17
|
-
| SSR | No hydration | State serialization + hydration
|
|
18
|
-
| TypeScript | Community types | Native
|
|
13
|
+
| Feature | Original | This Fork |
|
|
14
|
+
| ---------------- | ------------------- | -------------------------------- |
|
|
15
|
+
| Reactivity | Global `m.redraw()` | Fine-grained component updates |
|
|
16
|
+
| State Management | Manual redraw calls | Signals with automatic tracking |
|
|
17
|
+
| SSR | No hydration | State serialization + hydration |
|
|
18
|
+
| TypeScript | Community types | Native |
|
|
19
|
+
| Stream / request | Built-in | Removed (use signals, `fetch()`) |
|
|
19
20
|
|
|
20
21
|
Signals are opt-in. Existing Mithril code works unchanged.
|
|
21
22
|
|
package/api/mount-redraw.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Vnode from '../render/vnode'
|
|
2
2
|
import {getSignalComponents, type Signal} from '../signal'
|
|
3
|
+
import {getStateMaps} from '../render/render'
|
|
3
4
|
|
|
4
5
|
import type {ComponentType, Children, Vnode as VnodeType} from '../render/vnode'
|
|
5
6
|
|
|
@@ -54,11 +55,9 @@ export default function mountRedrawFactory(render: Render, schedule: Schedule, c
|
|
|
54
55
|
function redrawComponent(componentOrState: ComponentType) {
|
|
55
56
|
// componentOrState might be vnode.state (from signal tracking) or component object
|
|
56
57
|
// Try to find the actual component object if it's vnode.state
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
component = stateToComponentMap.get(componentOrState)!
|
|
61
|
-
}
|
|
58
|
+
const {stateToComponentMap, stateToDomMap, stateToVnodeMap} = getStateMaps()
|
|
59
|
+
const resolved = stateToComponentMap.get(componentOrState)
|
|
60
|
+
const component = resolved !== undefined ? resolved : componentOrState
|
|
62
61
|
|
|
63
62
|
// First try: find element in componentToElement (for m.mount components)
|
|
64
63
|
// Check this first to ensure synchronous redraws for m.mount components
|
|
@@ -74,14 +73,31 @@ export default function mountRedrawFactory(render: Render, schedule: Schedule, c
|
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
// Second try:
|
|
76
|
+
// Second try: targeted redraw for nested components (stateToDomMap path)
|
|
78
77
|
// Only check this if componentToElement didn't find anything (not an m.mount component)
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
const nestedElement = stateToDomMap.get(componentOrState)
|
|
79
|
+
if (nestedElement !== undefined) {
|
|
80
|
+
const vnodeInfo = stateToVnodeMap.get(componentOrState)
|
|
81
|
+
if (nestedElement?.isConnected && component != null && vnodeInfo != null) {
|
|
82
|
+
const parent = nestedElement.parentElement
|
|
83
|
+
const oldVnodes = parent != null ? (parent as any).vnodes : null
|
|
84
|
+
if (parent != null && Array.isArray(oldVnodes) && oldVnodes.length > 0) {
|
|
85
|
+
const i = oldVnodes.findIndex((v: any) => v?.state === componentOrState)
|
|
86
|
+
if (i >= 0) {
|
|
87
|
+
const {key, attrs} = vnodeInfo
|
|
88
|
+
const newVnodes = [...oldVnodes]
|
|
89
|
+
newVnodes[i] = Vnode(component, key ?? null, attrs ?? null, null, null, null)
|
|
90
|
+
try {
|
|
91
|
+
render(parent, newVnodes, redraw)
|
|
92
|
+
return
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error(e)
|
|
95
|
+
// Fall through to full sync on error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Fallback: full sync for RouterRoot correctness or when targeted path fails
|
|
85
101
|
if (!pending) {
|
|
86
102
|
pending = true
|
|
87
103
|
schedule(function () {
|
|
@@ -117,6 +133,107 @@ export default function mountRedrawFactory(render: Render, schedule: Schedule, c
|
|
|
117
133
|
}
|
|
118
134
|
}
|
|
119
135
|
|
|
136
|
+
function redrawComponents(components: Set<ComponentType>) {
|
|
137
|
+
const {stateToComponentMap, stateToDomMap, stateToVnodeMap} = getStateMaps()
|
|
138
|
+
const mountRoots: ComponentType[] = []
|
|
139
|
+
const nested = new Set<ComponentType>()
|
|
140
|
+
|
|
141
|
+
for (const c of components) {
|
|
142
|
+
const component = stateToComponentMap.get(c) ?? c
|
|
143
|
+
if (componentToElement.get(component)) {
|
|
144
|
+
mountRoots.push(c)
|
|
145
|
+
} else {
|
|
146
|
+
nested.add(c)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const c of mountRoots) {
|
|
151
|
+
redrawComponent(c)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (nested.size === 0) return
|
|
155
|
+
|
|
156
|
+
// Group nested components by parent element
|
|
157
|
+
const parentToStates = new Map<Element, Set<ComponentType>>()
|
|
158
|
+
for (const state of nested) {
|
|
159
|
+
const el = stateToDomMap.get(state)
|
|
160
|
+
const parent = el?.parentElement
|
|
161
|
+
if (!parent || !el?.isConnected) {
|
|
162
|
+
// Can't do targeted redraw, fall back to full sync
|
|
163
|
+
if (!pending) {
|
|
164
|
+
pending = true
|
|
165
|
+
schedule(function () {
|
|
166
|
+
pending = false
|
|
167
|
+
sync()
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
let set = parentToStates.get(parent)
|
|
173
|
+
if (!set) {
|
|
174
|
+
set = new Set()
|
|
175
|
+
parentToStates.set(parent, set)
|
|
176
|
+
}
|
|
177
|
+
set.add(state)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const [parent, states] of parentToStates) {
|
|
181
|
+
const oldVnodes = (parent as any).vnodes
|
|
182
|
+
if (!Array.isArray(oldVnodes) || oldVnodes.length === 0) {
|
|
183
|
+
if (!pending) {
|
|
184
|
+
pending = true
|
|
185
|
+
schedule(function () {
|
|
186
|
+
pending = false
|
|
187
|
+
sync()
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const newVnodes = [...oldVnodes]
|
|
194
|
+
let allFound = true
|
|
195
|
+
for (const state of states) {
|
|
196
|
+
const i = oldVnodes.findIndex((v: any) => v?.state === state)
|
|
197
|
+
if (i < 0) {
|
|
198
|
+
allFound = false
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
const component = stateToComponentMap.get(state) ?? state
|
|
202
|
+
const vnodeInfo = stateToVnodeMap.get(state)
|
|
203
|
+
if (!vnodeInfo) {
|
|
204
|
+
allFound = false
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
const {key, attrs} = vnodeInfo
|
|
208
|
+
newVnodes[i] = Vnode(component, key ?? null, attrs ?? null, null, null, null)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!allFound) {
|
|
212
|
+
if (!pending) {
|
|
213
|
+
pending = true
|
|
214
|
+
schedule(function () {
|
|
215
|
+
pending = false
|
|
216
|
+
sync()
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
render(parent, newVnodes, redraw)
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.error(e)
|
|
226
|
+
if (!pending) {
|
|
227
|
+
pending = true
|
|
228
|
+
schedule(function () {
|
|
229
|
+
pending = false
|
|
230
|
+
sync()
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
120
237
|
function redraw(component?: ComponentType) {
|
|
121
238
|
// Component-level redraw
|
|
122
239
|
if (component !== undefined) {
|
|
@@ -135,6 +252,7 @@ export default function mountRedrawFactory(render: Render, schedule: Schedule, c
|
|
|
135
252
|
}
|
|
136
253
|
|
|
137
254
|
redraw.sync = sync
|
|
255
|
+
;(redraw as any).redrawComponents = redrawComponents
|
|
138
256
|
|
|
139
257
|
// Export function to redraw components affected by signal changes
|
|
140
258
|
;(redraw as any).signal = function (signal: Signal<any>) {
|
package/index.ts
CHANGED
|
@@ -70,26 +70,46 @@ m.censor = censor
|
|
|
70
70
|
m.nextTick = nextTick
|
|
71
71
|
m.domFor = domFor
|
|
72
72
|
|
|
73
|
-
// Set up signal-to-component redraw integration
|
|
73
|
+
// Set up signal-to-component redraw integration with batching.
|
|
74
|
+
// Collects all components needing redraw in the current tick, then flushes once via queueMicrotask.
|
|
75
|
+
// Avoids N synchronous redraws when many signals fire (e.g. 50% of 800 rows).
|
|
76
|
+
let pendingRedrawComponents = new Set<any>()
|
|
77
|
+
let redrawScheduled = false
|
|
78
|
+
|
|
79
|
+
function flushPendingRedraws() {
|
|
80
|
+
const components = new Set(pendingRedrawComponents)
|
|
81
|
+
pendingRedrawComponents.clear()
|
|
82
|
+
redrawScheduled = false
|
|
83
|
+
if (components.size === 1) {
|
|
84
|
+
m.redraw(components.values().next().value)
|
|
85
|
+
} else if (components.size > 1) {
|
|
86
|
+
const fn = (m.redraw as any).redrawComponents
|
|
87
|
+
if (fn) fn(components)
|
|
88
|
+
else m.redraw()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
74
92
|
setSignalRedrawCallback((sig: Signal<any>) => {
|
|
75
93
|
const components = getSignalComponents(sig)
|
|
76
|
-
if (components) {
|
|
77
|
-
components.forEach((
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
if (components && components.size > 0) {
|
|
95
|
+
components.forEach((c) => pendingRedrawComponents.add(c))
|
|
96
|
+
if (!redrawScheduled) {
|
|
97
|
+
redrawScheduled = true
|
|
98
|
+
queueMicrotask(flushPendingRedraws)
|
|
99
|
+
}
|
|
81
100
|
}
|
|
82
101
|
})
|
|
83
102
|
|
|
84
103
|
// Export signals API
|
|
85
104
|
export {signal, computed, effect, Signal, ComputedSignal, state, watch, registerState, getRegisteredStates}
|
|
86
|
-
export type {State, StateOptions, Unwatch} from './state'
|
|
105
|
+
export type {State, StateOptions, StateSignals, Unwatch} from './state'
|
|
87
106
|
|
|
88
107
|
// Export Store class
|
|
89
108
|
export {Store} from './store'
|
|
90
109
|
|
|
91
110
|
// Export SSR utilities
|
|
92
111
|
export {serializeStore, deserializeStore, serializeAllStates, deserializeAllStates} from './render/ssrState'
|
|
112
|
+
export type {DeserializeOptions} from './render/ssrState'
|
|
93
113
|
|
|
94
114
|
// Export SSR request context (for per-request store and state registry)
|
|
95
115
|
export {getSSRContext, runWithContext, runWithContextAsync, cleanupWatchers} from './ssrContext'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitstillery/mithril",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Mithril + Signals, Store and SSR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Bitstillery",
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
"default": "./htm.ts"
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
47
50
|
"scripts": {
|
|
48
51
|
"lint": "bun run lint:ts",
|
|
49
52
|
"lint:ts": "bun run lint:ts-format && bun run lint:ts-syntax && bun run lint:ts-types",
|
|
@@ -52,7 +55,11 @@
|
|
|
52
55
|
"lint:ts-types": "tsgo --noEmit",
|
|
53
56
|
"test": "bun test",
|
|
54
57
|
"prepublishOnly": "bun run lint:ts",
|
|
55
|
-
"pack:dry-run": "npm pack --dry-run"
|
|
58
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
59
|
+
"release": "standard-version && npm publish",
|
|
60
|
+
"release:patch": "standard-version --release-as patch && npm publish",
|
|
61
|
+
"release:minor": "standard-version --release-as minor && npm publish",
|
|
62
|
+
"release:major": "standard-version --release-as major && npm publish"
|
|
56
63
|
},
|
|
57
64
|
"dependencies": {
|
|
58
65
|
"htm": "^3.1.1"
|
|
@@ -71,6 +78,7 @@
|
|
|
71
78
|
"marked-gfm-heading-id": "^4.0.0",
|
|
72
79
|
"oxfmt": "^0.32.0",
|
|
73
80
|
"oxlint": "^1.30.0",
|
|
81
|
+
"standard-version": "^9.5.0",
|
|
74
82
|
"tippy.js": "^6.3.7"
|
|
75
83
|
}
|
|
76
84
|
}
|
package/render/render.ts
CHANGED
|
@@ -9,6 +9,15 @@ import cachedAttrsIsStaticMap from './cachedAttrsIsStaticMap'
|
|
|
9
9
|
|
|
10
10
|
import type {Vnode as VnodeType, Children} from './vnode'
|
|
11
11
|
|
|
12
|
+
// Module-level maps for component/state-to-DOM tracking (used by mount-redraw for signal redraws)
|
|
13
|
+
const stateToDomMap = new WeakMap<any, Element>()
|
|
14
|
+
const stateToComponentMap = new WeakMap<any, any>()
|
|
15
|
+
const stateToVnodeMap = new WeakMap<any, {key?: string | number | null; attrs?: Record<string, any>}>()
|
|
16
|
+
|
|
17
|
+
export function getStateMaps() {
|
|
18
|
+
return {stateToDomMap, stateToComponentMap, stateToVnodeMap}
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export default function renderFactory() {
|
|
13
22
|
const nameSpace: Record<string, string> = {
|
|
14
23
|
svg: 'http://www.w3.org/2000/svg',
|
|
@@ -383,8 +392,7 @@ export default function renderFactory() {
|
|
|
383
392
|
// Track component for signal dependency tracking
|
|
384
393
|
// Store mapping from vnode.state to vnode.tag (component object) for redraw
|
|
385
394
|
if (vnode.state && vnode.tag && !isHydrating) {
|
|
386
|
-
|
|
387
|
-
;(globalThis as any).__mithrilStateToComponent.set(vnode.state, vnode.tag)
|
|
395
|
+
stateToComponentMap.set(vnode.state, vnode.tag)
|
|
388
396
|
}
|
|
389
397
|
// Always track component dependencies for signal tracking (even during hydration)
|
|
390
398
|
// This allows signals to know which components depend on them
|
|
@@ -418,10 +426,10 @@ export default function renderFactory() {
|
|
|
418
426
|
vnode.dom = vnode.instance.dom
|
|
419
427
|
vnode.domSize = vnode.instance.domSize
|
|
420
428
|
|
|
421
|
-
// Store component's DOM element for fine-grained redraw (not during hydration)
|
|
429
|
+
// Store component's DOM element and vnode info for fine-grained redraw (not during hydration)
|
|
422
430
|
if (vnode.state && vnode.dom && !isHydrating) {
|
|
423
|
-
|
|
424
|
-
|
|
431
|
+
stateToDomMap.set(vnode.state, vnode.dom)
|
|
432
|
+
stateToVnodeMap.set(vnode.state, {key: vnode.key, attrs: vnode.attrs})
|
|
425
433
|
}
|
|
426
434
|
} else {
|
|
427
435
|
vnode.domSize = 0
|
|
@@ -697,8 +705,7 @@ export default function renderFactory() {
|
|
|
697
705
|
// Track component for signal dependency tracking
|
|
698
706
|
// Store mapping from vnode.state to vnode.tag (component object) for redraw
|
|
699
707
|
if (vnode.state && vnode.tag && !isHydrating) {
|
|
700
|
-
|
|
701
|
-
;(globalThis as any).__mithrilStateToComponent.set(vnode.state, vnode.tag)
|
|
708
|
+
stateToComponentMap.set(vnode.state, vnode.tag)
|
|
702
709
|
}
|
|
703
710
|
// Always track component dependencies for signal tracking (even during hydration)
|
|
704
711
|
// This allows signals to know which components depend on them
|
|
@@ -723,10 +730,10 @@ export default function renderFactory() {
|
|
|
723
730
|
vnode.dom = vnode.instance.dom
|
|
724
731
|
vnode.domSize = vnode.instance.domSize
|
|
725
732
|
|
|
726
|
-
// Store component's DOM element for fine-grained redraw (not during hydration)
|
|
733
|
+
// Store component's DOM element and vnode info for fine-grained redraw (not during hydration)
|
|
727
734
|
if (vnode.state && vnode.dom && !isHydrating) {
|
|
728
|
-
|
|
729
|
-
|
|
735
|
+
stateToDomMap.set(vnode.state, vnode.dom)
|
|
736
|
+
stateToVnodeMap.set(vnode.state, {key: vnode.key, attrs: vnode.attrs})
|
|
730
737
|
}
|
|
731
738
|
} else {
|
|
732
739
|
if (old.instance != null) removeNode(parent, old.instance)
|
|
@@ -912,9 +919,12 @@ export default function renderFactory() {
|
|
|
912
919
|
}
|
|
913
920
|
|
|
914
921
|
function onremove(vnode: any) {
|
|
915
|
-
// Clean up signal dependencies when component is removed
|
|
922
|
+
// Clean up signal dependencies and state maps when component is removed
|
|
916
923
|
if (typeof vnode.tag !== 'string' && vnode.state != null) {
|
|
917
924
|
clearComponentDependencies(vnode.state)
|
|
925
|
+
stateToDomMap.delete(vnode.state)
|
|
926
|
+
stateToComponentMap.delete(vnode.state)
|
|
927
|
+
stateToVnodeMap.delete(vnode.state)
|
|
918
928
|
}
|
|
919
929
|
if (typeof vnode.tag !== 'string' && typeof vnode.state.onremove === 'function')
|
|
920
930
|
callHook.call(vnode.state.onremove, vnode)
|
package/render/ssrState.ts
CHANGED
|
@@ -326,12 +326,18 @@ function restoreComputedProperties(state: State<any>, initial: any): void {
|
|
|
326
326
|
restore(initial, state)
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
export
|
|
329
|
+
export interface DeserializeOptions {
|
|
330
|
+
/** Skip deserializing these state instances (e.g. Store state that should come from localStorage) */
|
|
331
|
+
skipStates?: Set<any>
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function deserializeAllStates(serialized: Record<string, any>, options?: DeserializeOptions): void {
|
|
330
335
|
if (!serialized || typeof serialized !== 'object') {
|
|
331
336
|
return
|
|
332
337
|
}
|
|
333
338
|
|
|
334
339
|
const registeredStates = getRegisteredStates()
|
|
340
|
+
const skipStates = options?.skipStates
|
|
335
341
|
|
|
336
342
|
// First, deserialize all states
|
|
337
343
|
for (const [name, serializedState] of Object.entries(serialized)) {
|
|
@@ -345,6 +351,10 @@ export function deserializeAllStates(serialized: Record<string, any>): void {
|
|
|
345
351
|
continue
|
|
346
352
|
}
|
|
347
353
|
|
|
354
|
+
if (skipStates?.has(entry.state)) {
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
|
|
348
358
|
try {
|
|
349
359
|
deserializeStore(entry.state, serializedState)
|
|
350
360
|
} catch (error) {
|
|
@@ -356,6 +366,9 @@ export function deserializeAllStates(serialized: Record<string, any>): void {
|
|
|
356
366
|
// After deserializing, restore computed properties from original initial states
|
|
357
367
|
// This ensures computed properties work after SSR deserialization
|
|
358
368
|
for (const [name, entry] of registeredStates.entries()) {
|
|
369
|
+
if (skipStates?.has(entry.state)) {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
359
372
|
try {
|
|
360
373
|
restoreComputedProperties(entry.state, entry.initial)
|
|
361
374
|
} catch (error) {
|
package/signal.ts
CHANGED
|
@@ -25,15 +25,20 @@ export function getCurrentComponent() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function trackComponentSignal(component: any, signal: Signal<any>) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
let set = componentSignalMap.get(component)
|
|
29
|
+
if (!set) {
|
|
30
|
+
set = new Set()
|
|
31
|
+
componentSignalMap.set(component, set)
|
|
30
32
|
}
|
|
31
|
-
|
|
33
|
+
if (set.has(signal)) return
|
|
34
|
+
set.add(signal)
|
|
32
35
|
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
let compSet = signalComponentMap.get(signal)
|
|
37
|
+
if (!compSet) {
|
|
38
|
+
compSet = new Set()
|
|
39
|
+
signalComponentMap.set(signal, compSet)
|
|
35
40
|
}
|
|
36
|
-
|
|
41
|
+
compSet.add(component)
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export function getComponentSignals(component: any): Set<Signal<any>> | undefined {
|
package/state.ts
CHANGED
|
@@ -204,6 +204,16 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
204
204
|
return stateCache.get(obj)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
const linkArrayParentSignal = (value: any, sig: Signal<any> | ComputedSignal<any>) => {
|
|
208
|
+
if (!value || typeof value !== 'object') return
|
|
209
|
+
if ((value as any).__isState === true && Array.isArray((value as any).__signals)) {
|
|
210
|
+
arrayParentSignalMap.set(value, sig as Signal<any>)
|
|
211
|
+
;(value as any)._parentSignal = sig as Signal<any>
|
|
212
|
+
} else if (Array.isArray(value)) {
|
|
213
|
+
arrayParentSignalMap.set(value, sig as Signal<any>)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
// Handle arrays
|
|
208
218
|
if (Array.isArray(obj)) {
|
|
209
219
|
// Arrays don't get their own signalMap - they use the parent's
|
|
@@ -294,7 +304,19 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
294
304
|
// Search methods also need unwrapped values for comparison
|
|
295
305
|
const searchMethods = ['includes', 'indexOf', 'lastIndexOf']
|
|
296
306
|
// Methods that return new arrays or strings need unwrapped values
|
|
297
|
-
const returnMethods = [
|
|
307
|
+
const returnMethods = [
|
|
308
|
+
'slice',
|
|
309
|
+
'concat',
|
|
310
|
+
'flat',
|
|
311
|
+
'flatMap',
|
|
312
|
+
'join',
|
|
313
|
+
'toString',
|
|
314
|
+
'toLocaleString',
|
|
315
|
+
// ES2023 copy-with-mutate methods - bind to proxy so iteration unwraps signals
|
|
316
|
+
'toSorted',
|
|
317
|
+
'toReversed',
|
|
318
|
+
'toSpliced',
|
|
319
|
+
]
|
|
298
320
|
// Iterator methods need unwrapped values
|
|
299
321
|
const iteratorMethods = ['entries', 'keys', 'values']
|
|
300
322
|
if (
|
|
@@ -554,7 +576,41 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
554
576
|
// Each nested state gets its own signalMap (unless parentSignalMap is explicitly provided)
|
|
555
577
|
// This prevents nested states from sharing the parent's signalMap
|
|
556
578
|
const nestedSignalMap = parentSignalMap || new Map<string, Signal<any> | ComputedSignal<any>>()
|
|
557
|
-
|
|
579
|
+
let wrapped: any
|
|
580
|
+
const getChildContext = (): InitContext | undefined =>
|
|
581
|
+
context ? {...context, rootState: stateRootMap.get(wrapped) ?? wrapped} : undefined
|
|
582
|
+
|
|
583
|
+
const createPropertySignal = (originalValue: any): Signal<any> | ComputedSignal<any> => {
|
|
584
|
+
if (typeof originalValue === 'function') {
|
|
585
|
+
return createStateComputed(wrapped, () => originalValue.call(wrapped), !!context?.deferComputed)
|
|
586
|
+
}
|
|
587
|
+
if (isGetSetDescriptor(originalValue)) {
|
|
588
|
+
if (typeof originalValue.get === 'function') {
|
|
589
|
+
return createStateComputed(wrapped, () => originalValue.get.call(wrapped), !!context?.deferComputed)
|
|
590
|
+
}
|
|
591
|
+
return signal(undefined)
|
|
592
|
+
}
|
|
593
|
+
if (typeof originalValue === 'object' && originalValue !== null) {
|
|
594
|
+
const nestedState = initializeSignals(originalValue, undefined, getChildContext())
|
|
595
|
+
const sig = signal(nestedState)
|
|
596
|
+
if (nestedState && (nestedState as any).__isState) {
|
|
597
|
+
stateRootMap.set(nestedState, stateRootMap.get(wrapped) ?? wrapped)
|
|
598
|
+
}
|
|
599
|
+
linkArrayParentSignal(nestedState, sig)
|
|
600
|
+
return sig
|
|
601
|
+
}
|
|
602
|
+
return toSignal(originalValue)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const ensurePropertySignal = (target: any, prop: string | symbol, key: string) => {
|
|
606
|
+
if (!nestedSignalMap.has(key)) {
|
|
607
|
+
const originalValue = Reflect.get(target, prop)
|
|
608
|
+
if (originalValue === undefined) return undefined
|
|
609
|
+
nestedSignalMap.set(key, createPropertySignal(originalValue))
|
|
610
|
+
}
|
|
611
|
+
return nestedSignalMap.get(key)
|
|
612
|
+
}
|
|
613
|
+
wrapped = new Proxy(obj, {
|
|
558
614
|
get(target, prop) {
|
|
559
615
|
if (prop === '__originalKeys') return originalKeys
|
|
560
616
|
if (prop === '__isState') return true
|
|
@@ -581,143 +637,25 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
581
637
|
if (propStr.startsWith('$') && propStr.length > 1) {
|
|
582
638
|
const key = propStr.slice(1) // Remove $ prefix
|
|
583
639
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const computedSig = createStateComputed(
|
|
592
|
-
wrapped,
|
|
593
|
-
() => originalValue.call(wrapped),
|
|
594
|
-
!!context?.deferComputed,
|
|
595
|
-
)
|
|
596
|
-
nestedSignalMap.set(key, computedSig)
|
|
597
|
-
} else if (isGetSetDescriptor(originalValue)) {
|
|
598
|
-
// Get/set descriptor -> computed signal from get function
|
|
599
|
-
if (typeof originalValue.get === 'function') {
|
|
600
|
-
const computedSig = createStateComputed(
|
|
601
|
-
wrapped,
|
|
602
|
-
() => originalValue.get.call(wrapped),
|
|
603
|
-
!!context?.deferComputed,
|
|
604
|
-
)
|
|
605
|
-
nestedSignalMap.set(key, computedSig)
|
|
606
|
-
} else {
|
|
607
|
-
// Only setter, no getter - treat as regular signal with undefined initial value
|
|
608
|
-
const sig = signal(undefined)
|
|
609
|
-
nestedSignalMap.set(key, sig)
|
|
610
|
-
}
|
|
611
|
-
} else if (typeof originalValue === 'object' && originalValue !== null) {
|
|
612
|
-
// Get the already-wrapped state from the wrapped object
|
|
613
|
-
// Don't call initializeSignals again as it would create a new wrapped array
|
|
614
|
-
const nestedState = (wrapped as any)[key]
|
|
615
|
-
if (nestedState === undefined) {
|
|
616
|
-
// Fallback: initialize if not already wrapped
|
|
617
|
-
const childContext = context
|
|
618
|
-
? {...context, rootState: stateRootMap.get(wrapped) ?? wrapped}
|
|
619
|
-
: undefined
|
|
620
|
-
const initialized = initializeSignals(originalValue, undefined, childContext)
|
|
621
|
-
const sig = signal(initialized)
|
|
622
|
-
if (Array.isArray(initialized)) {
|
|
623
|
-
arrayParentSignalMap.set(initialized, sig)
|
|
624
|
-
}
|
|
625
|
-
nestedSignalMap.set(key, sig)
|
|
626
|
-
} else {
|
|
627
|
-
const sig = signal(nestedState)
|
|
628
|
-
// Store parent signal reference for arrays
|
|
629
|
-
if (Array.isArray(nestedState)) {
|
|
630
|
-
arrayParentSignalMap.set(nestedState, sig)
|
|
631
|
-
}
|
|
632
|
-
nestedSignalMap.set(key, sig)
|
|
633
|
-
}
|
|
634
|
-
} else {
|
|
635
|
-
const sig = toSignal(originalValue)
|
|
636
|
-
nestedSignalMap.set(key, sig)
|
|
637
|
-
}
|
|
638
|
-
} else {
|
|
639
|
-
// Property doesn't exist - return undefined
|
|
640
|
-
return undefined
|
|
641
|
-
}
|
|
640
|
+
const sig = ensurePropertySignal(target, key, key)
|
|
641
|
+
if (!sig) return undefined
|
|
642
|
+
|
|
643
|
+
// Re-link array-backed values so mutations (splice, push) notify subscribers.
|
|
644
|
+
// Needed when $prop is accessed before .prop (e.g. watcher setup before render).
|
|
645
|
+
if (sig && !(sig instanceof ComputedSignal)) {
|
|
646
|
+
linkArrayParentSignal((sig as Signal<any>).peek(), sig)
|
|
642
647
|
}
|
|
643
648
|
|
|
644
649
|
// Return raw signal object (not the value)
|
|
645
|
-
return
|
|
650
|
+
return sig
|
|
646
651
|
}
|
|
647
652
|
|
|
648
653
|
const key = propStr
|
|
649
654
|
|
|
650
|
-
|
|
651
|
-
if (!nestedSignalMap.has(key)) {
|
|
652
|
-
// Try to get from target first (original object properties)
|
|
653
|
-
const originalValue = Reflect.get(target, prop)
|
|
654
|
-
if (originalValue !== undefined) {
|
|
655
|
-
// Initialize signal for this property
|
|
656
|
-
if (typeof originalValue === 'function') {
|
|
657
|
-
// Function property -> computed signal
|
|
658
|
-
const computedSig = createStateComputed(
|
|
659
|
-
wrapped,
|
|
660
|
-
() => originalValue.call(wrapped),
|
|
661
|
-
!!context?.deferComputed,
|
|
662
|
-
)
|
|
663
|
-
nestedSignalMap.set(key, computedSig)
|
|
664
|
-
} else if (isGetSetDescriptor(originalValue)) {
|
|
665
|
-
// Get/set descriptor -> computed signal from get function
|
|
666
|
-
if (typeof originalValue.get === 'function') {
|
|
667
|
-
const computedSig = createStateComputed(
|
|
668
|
-
wrapped,
|
|
669
|
-
() => originalValue.get.call(wrapped),
|
|
670
|
-
!!context?.deferComputed,
|
|
671
|
-
)
|
|
672
|
-
nestedSignalMap.set(key, computedSig)
|
|
673
|
-
} else {
|
|
674
|
-
// Only setter, no getter - treat as regular signal with undefined initial value
|
|
675
|
-
const sig = signal(undefined)
|
|
676
|
-
nestedSignalMap.set(key, sig)
|
|
677
|
-
}
|
|
678
|
-
} else if (typeof originalValue === 'object' && originalValue !== null) {
|
|
679
|
-
// Nested object -> recursive state with its own signalMap
|
|
680
|
-
const childContext = context
|
|
681
|
-
? {...context, rootState: stateRootMap.get(wrapped) ?? wrapped}
|
|
682
|
-
: undefined
|
|
683
|
-
const nestedState = initializeSignals(originalValue, undefined, childContext)
|
|
684
|
-
const sig = signal(nestedState)
|
|
685
|
-
// Store parent signal reference for arrays
|
|
686
|
-
if (Array.isArray(nestedState)) {
|
|
687
|
-
arrayParentSignalMap.set(nestedState, sig)
|
|
688
|
-
}
|
|
689
|
-
nestedSignalMap.set(key, sig)
|
|
690
|
-
} else {
|
|
691
|
-
// Primitive value -> signal
|
|
692
|
-
const sig = toSignal(originalValue)
|
|
693
|
-
nestedSignalMap.set(key, sig)
|
|
694
|
-
}
|
|
695
|
-
} else {
|
|
696
|
-
// Property doesn't exist in original object
|
|
697
|
-
// Check if it's a computed property that was added dynamically
|
|
698
|
-
// For now, return undefined
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const sig = nestedSignalMap.get(key)
|
|
655
|
+
const sig = ensurePropertySignal(target, prop, key)
|
|
703
656
|
if (sig) {
|
|
704
657
|
// Access signal.value to track component dependency
|
|
705
|
-
|
|
706
|
-
// Always ensure parent signal is stored for arrays (in case it wasn't stored during initialization)
|
|
707
|
-
// Check for wrapped arrays by looking for __isState and __signals properties
|
|
708
|
-
// Array.isArray() may return false for Proxies, so we check __isState instead
|
|
709
|
-
if (value && typeof value === 'object') {
|
|
710
|
-
if ((value as any).__isState === true && Array.isArray((value as any).__signals)) {
|
|
711
|
-
// This is a wrapped array - store parent signal
|
|
712
|
-
arrayParentSignalMap.set(value, sig as Signal<any>)
|
|
713
|
-
// Also store directly on the Proxy as a fallback
|
|
714
|
-
;(value as any)._parentSignal = sig as Signal<any>
|
|
715
|
-
} else if (Array.isArray(value)) {
|
|
716
|
-
// Regular array (shouldn't happen but just in case)
|
|
717
|
-
arrayParentSignalMap.set(value, sig as Signal<any>)
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return value
|
|
658
|
+
return sig.value
|
|
721
659
|
}
|
|
722
660
|
|
|
723
661
|
// Fallback to original property
|
|
@@ -786,47 +724,22 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
786
724
|
const sig = nestedSignalMap.get(key)
|
|
787
725
|
if (sig && !(sig instanceof ComputedSignal)) {
|
|
788
726
|
if (typeof value === 'object' && value !== null) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
: undefined
|
|
793
|
-
const nestedState = initializeSignals(value, undefined, childContext)
|
|
794
|
-
// Store parent signal reference for arrays
|
|
795
|
-
if (Array.isArray(nestedState)) {
|
|
796
|
-
arrayParentSignalMap.set(nestedState, sig as Signal<any>)
|
|
727
|
+
const nestedState = initializeSignals(value, undefined, getChildContext())
|
|
728
|
+
if (nestedState && (nestedState as any).__isState) {
|
|
729
|
+
stateRootMap.set(nestedState, stateRootMap.get(wrapped) ?? wrapped)
|
|
797
730
|
}
|
|
731
|
+
linkArrayParentSignal(nestedState, sig)
|
|
798
732
|
;(sig as Signal<any>).value = nestedState
|
|
799
733
|
} else {
|
|
800
734
|
;(sig as Signal<any>).value = value
|
|
801
735
|
}
|
|
802
736
|
} else {
|
|
803
737
|
// Replace computed with regular signal
|
|
804
|
-
|
|
805
|
-
const childContext = context
|
|
806
|
-
? {...context, rootState: stateRootMap.get(wrapped) ?? wrapped}
|
|
807
|
-
: undefined
|
|
808
|
-
const nestedState = initializeSignals(value, undefined, childContext)
|
|
809
|
-
const sig = signal(nestedState)
|
|
810
|
-
arrayParentSignalMap.set(nestedState, sig)
|
|
811
|
-
nestedSignalMap.set(key, sig)
|
|
812
|
-
} else {
|
|
813
|
-
nestedSignalMap.set(key, toSignal(value))
|
|
814
|
-
}
|
|
738
|
+
nestedSignalMap.set(key, createPropertySignal(value))
|
|
815
739
|
}
|
|
816
740
|
} else {
|
|
817
741
|
// Create new signal
|
|
818
|
-
|
|
819
|
-
const childContext = context ? {...context, rootState: stateRootMap.get(wrapped) ?? wrapped} : undefined
|
|
820
|
-
const nestedState = initializeSignals(value, undefined, childContext)
|
|
821
|
-
const sig = signal(nestedState)
|
|
822
|
-
// Store parent signal reference for arrays
|
|
823
|
-
if (Array.isArray(nestedState)) {
|
|
824
|
-
arrayParentSignalMap.set(nestedState, sig)
|
|
825
|
-
}
|
|
826
|
-
nestedSignalMap.set(key, sig)
|
|
827
|
-
} else {
|
|
828
|
-
nestedSignalMap.set(key, toSignal(value))
|
|
829
|
-
}
|
|
742
|
+
nestedSignalMap.set(key, createPropertySignal(value))
|
|
830
743
|
}
|
|
831
744
|
|
|
832
745
|
return true
|
|
@@ -890,6 +803,11 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
890
803
|
|
|
891
804
|
stateRootMap.set(wrapped, context?.rootState ?? wrapped)
|
|
892
805
|
stateCache.set(obj, wrapped)
|
|
806
|
+
for (const key of originalKeys) {
|
|
807
|
+
if (!nestedSignalMap.has(key)) {
|
|
808
|
+
nestedSignalMap.set(key, createPropertySignal((obj as any)[key]))
|
|
809
|
+
}
|
|
810
|
+
}
|
|
893
811
|
return wrapped
|
|
894
812
|
}
|
|
895
813
|
|
|
@@ -900,61 +818,6 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
900
818
|
stateDeferredFlags.set(wrapped, {allowed: false})
|
|
901
819
|
}
|
|
902
820
|
|
|
903
|
-
// Pre-initialize all signals from the initial object so they're available immediately
|
|
904
|
-
// This ensures $s.$property works even if $s.property hasn't been accessed yet
|
|
905
|
-
if (typeof initial === 'object' && initial !== null && !Array.isArray(initial)) {
|
|
906
|
-
for (const key in initial) {
|
|
907
|
-
if (Object.prototype.hasOwnProperty.call(initial, key)) {
|
|
908
|
-
if (!signalMap.has(key)) {
|
|
909
|
-
const value = initial[key]
|
|
910
|
-
if (typeof value === 'function') {
|
|
911
|
-
const computedSig = createStateComputed(wrapped, () => value.call(wrapped), deferComputed)
|
|
912
|
-
signalMap.set(key, computedSig)
|
|
913
|
-
} else if (isGetSetDescriptor(value)) {
|
|
914
|
-
// Get/set descriptor -> computed signal from get function
|
|
915
|
-
if (typeof value.get === 'function') {
|
|
916
|
-
const computedSig = createStateComputed(wrapped, () => value.get.call(wrapped), deferComputed)
|
|
917
|
-
signalMap.set(key, computedSig)
|
|
918
|
-
} else {
|
|
919
|
-
// Only setter, no getter - treat as regular signal with undefined initial value
|
|
920
|
-
const sig = signal(undefined)
|
|
921
|
-
signalMap.set(key, sig)
|
|
922
|
-
}
|
|
923
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
924
|
-
// Get the already-wrapped state from stateCache (bypass Proxy to get the actual wrapped value)
|
|
925
|
-
// This ensures we get the same wrapped array that was created during initializeSignals
|
|
926
|
-
const preInitContext = initContext ? {...initContext, rootState: wrapped} : undefined
|
|
927
|
-
const nestedState = stateCache.has(value)
|
|
928
|
-
? stateCache.get(value)
|
|
929
|
-
: initializeSignals(value, undefined, preInitContext)
|
|
930
|
-
if (nestedState && (nestedState as any).__isState) stateRootMap.set(nestedState, wrapped)
|
|
931
|
-
const sig = signal(nestedState)
|
|
932
|
-
// Always store parent signal reference for arrays
|
|
933
|
-
// Check for wrapped arrays by looking for __isState and __signals properties
|
|
934
|
-
// Array.isArray() may return false for Proxies, so we check __isState instead
|
|
935
|
-
if (
|
|
936
|
-
nestedState &&
|
|
937
|
-
typeof nestedState === 'object' &&
|
|
938
|
-
(nestedState as any).__isState === true &&
|
|
939
|
-
Array.isArray((nestedState as any).__signals)
|
|
940
|
-
) {
|
|
941
|
-
arrayParentSignalMap.set(nestedState, sig)
|
|
942
|
-
// Also store directly on the Proxy as a fallback
|
|
943
|
-
;(nestedState as any)._parentSignal = sig
|
|
944
|
-
} else if (Array.isArray(nestedState)) {
|
|
945
|
-
// Fallback for regular arrays (shouldn't happen but just in case)
|
|
946
|
-
arrayParentSignalMap.set(nestedState, sig)
|
|
947
|
-
}
|
|
948
|
-
signalMap.set(key, sig)
|
|
949
|
-
} else {
|
|
950
|
-
const sig = toSignal(value)
|
|
951
|
-
signalMap.set(key, sig)
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
821
|
// Register state for SSR serialization when name is provided (required for hydration)
|
|
959
822
|
if (name && typeof name === 'string' && name.trim() !== '') {
|
|
960
823
|
registerState(name, wrapped, initial)
|
|
@@ -969,7 +832,7 @@ export function state<T extends Record<string, any>>(initial: T, name?: string,
|
|
|
969
832
|
* - Nested objects: $prop => Signal<State<T[K]>>
|
|
970
833
|
* - Functions: $prop => ComputedSignal (computed from getter)
|
|
971
834
|
*/
|
|
972
|
-
type StateSignals<T extends Record<string, any>> = {
|
|
835
|
+
export type StateSignals<T extends Record<string, any>> = {
|
|
973
836
|
[K in keyof T as K extends string ? `$${K}` : never]: T[K] extends (...args: any[]) => any
|
|
974
837
|
? ComputedSignal<any>
|
|
975
838
|
: T[K] extends object
|
package/store.ts
CHANGED
|
@@ -144,9 +144,13 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
144
144
|
private lookup_verify_interval: number | null = null
|
|
145
145
|
private lookup_ttl: number
|
|
146
146
|
private computedPropertiesSetup?: () => void
|
|
147
|
+
private storageKey: string
|
|
148
|
+
private tabStorageKey: string
|
|
147
149
|
|
|
148
|
-
constructor(options: {lookup_ttl?: number} = {lookup_ttl: DEFAULT_LOOKUP_TTL}) {
|
|
150
|
+
constructor(options: {lookup_ttl?: number; storageKey?: string; tabStorageKey?: string} = {lookup_ttl: DEFAULT_LOOKUP_TTL}) {
|
|
149
151
|
this.lookup_ttl = options.lookup_ttl || DEFAULT_LOOKUP_TTL
|
|
152
|
+
this.storageKey = options.storageKey ?? 'store'
|
|
153
|
+
this.tabStorageKey = options.tabStorageKey ?? this.storageKey
|
|
150
154
|
// Initialize with empty state, will be loaded later (ADR-0013: defer computeds until ready() is called)
|
|
151
155
|
const instanceName = `store.instance.${storeInstanceCounter++}`
|
|
152
156
|
this.stateInstance = state({} as T, instanceName, {deferComputed: true})
|
|
@@ -271,8 +275,8 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
271
275
|
|
|
272
276
|
load(saved: Partial<T>, temporary: Partial<T>, tab: Partial<T> = {} as Partial<T>, session: Partial<T> = {} as Partial<T>) {
|
|
273
277
|
const restored_state = {
|
|
274
|
-
tab: this.get_tab_storage(
|
|
275
|
-
store: this.get(
|
|
278
|
+
tab: this.get_tab_storage(this.tabStorageKey),
|
|
279
|
+
store: this.get(this.storageKey),
|
|
276
280
|
}
|
|
277
281
|
|
|
278
282
|
this.templates = {
|
|
@@ -381,7 +385,7 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
381
385
|
|
|
382
386
|
// Save to localStorage (saved state)
|
|
383
387
|
if (saveSaved && this.templates.saved) {
|
|
384
|
-
this.set(
|
|
388
|
+
this.set(this.storageKey, this.blueprint(statePlain, copy_object(this.templates.saved)))
|
|
385
389
|
}
|
|
386
390
|
|
|
387
391
|
// Save to sessionStorage (tab state)
|
|
@@ -397,18 +401,18 @@ export class Store<T extends Record<string, any> = Record<string, any>> {
|
|
|
397
401
|
if (isState(tabState)) {
|
|
398
402
|
const tabPlain = serializeStore(tabState)
|
|
399
403
|
// blueprint expects both arguments to have the same structure
|
|
400
|
-
this.set_tab(
|
|
404
|
+
this.set_tab(this.tabStorageKey, this.blueprint(tabPlain, copy_object(tabTemplate)))
|
|
401
405
|
} else {
|
|
402
406
|
// Plain object tab
|
|
403
|
-
this.set_tab(
|
|
407
|
+
this.set_tab(this.tabStorageKey, this.blueprint(tabState, copy_object(tabTemplate)))
|
|
404
408
|
}
|
|
405
409
|
} else {
|
|
406
410
|
// No tab state - save empty tab based on template structure
|
|
407
411
|
const tabTemplate = (this.templates.tab as any).tab || this.templates.tab
|
|
408
412
|
if (tabTemplate && Object.keys(tabTemplate).length > 0) {
|
|
409
|
-
this.set_tab(
|
|
413
|
+
this.set_tab(this.tabStorageKey, this.blueprint({} as any, copy_object(tabTemplate)))
|
|
410
414
|
} else {
|
|
411
|
-
this.set_tab(
|
|
415
|
+
this.set_tab(this.tabStorageKey, {})
|
|
412
416
|
}
|
|
413
417
|
}
|
|
414
418
|
}
|