dalila 1.9.16 → 1.9.17

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.
@@ -322,6 +322,14 @@ export function getSnapshot() {
322
322
  export function registerScope(scopeRef, parentScopeRef, name) {
323
323
  if (!enabled)
324
324
  return;
325
+ const scopeId = getNodeId(scopeRef);
326
+ const existingNode = nodes.get(scopeId);
327
+ if (existingNode) {
328
+ if (name) {
329
+ existingNode.label = name;
330
+ }
331
+ return;
332
+ }
325
333
  createNode(scopeRef, "scope", name || "scope", {
326
334
  parentScopeRef,
327
335
  });
@@ -49,7 +49,7 @@ export interface PersistedSignal<T> extends Signal<T> {
49
49
  /**
50
50
  * Create a persisted signal that automatically syncs with storage.
51
51
  */
52
- export declare function persist<T>(baseSignal: Signal<T>, options: PersistOptions<T>): Signal<T> | PersistedSignal<T>;
52
+ export declare function persist<T>(baseSignal: Signal<T>, options: PersistOptions<T>): PersistedSignal<T>;
53
53
  /**
54
54
  * Helper to create JSON storage wrapper
55
55
  */
@@ -47,6 +47,13 @@ export function persist(baseSignal, options) {
47
47
  }
48
48
  const storageKey = name;
49
49
  const versionKey = version !== undefined ? `${name}:version` : undefined;
50
+ const handleError = (err, ctx) => {
51
+ const e = toError(err);
52
+ if (onError)
53
+ onError(e);
54
+ else
55
+ console.error(`[Dalila] persist(): ${ctx} "${name}"`, e);
56
+ };
50
57
  // ---- Tab Sync (BroadcastChannel) ----
51
58
  const { syncTabs = false } = options;
52
59
  let channel = null;
@@ -57,39 +64,20 @@ export function persist(baseSignal, options) {
57
64
  try {
58
65
  channel = new BroadcastChannel(`dalila:${name}`);
59
66
  channel.onmessage = (event) => {
60
- const { type, value, version: remoteVersion } = event.data;
61
67
  // Ignore our own messages (prevent echo loop)
62
68
  if (isHandlingRemoteChange)
63
69
  return;
64
- // Only handle update events
65
- if (type !== 'update')
66
- return;
67
- // Mark as handling remote to prevent echo
68
- isHandlingRemoteChange = true;
69
70
  try {
70
- // Deserialize and apply the remote value
71
- const deserialized = serializer.deserialize(value);
72
- // Apply with merge strategy
73
- if (merge === 'shallow' && isPlainObject(deserialized)) {
74
- const current = baseSignal.peek();
75
- if (isPlainObject(current)) {
76
- baseSignal.set({ ...current, ...deserialized });
77
- }
78
- else {
79
- baseSignal.set(deserialized);
80
- }
81
- }
82
- else {
83
- baseSignal.set(deserialized);
84
- }
85
- // Update local tracking
86
- lastSaved = value;
87
- pendingSaved = null;
88
- // Handle version sync
89
- if (versionKey && remoteVersion !== undefined) {
90
- lastSavedVersion = String(remoteVersion);
91
- pendingSavedVersion = null;
92
- }
71
+ const { type, value, version: remoteVersion } = event.data;
72
+ // Only handle update events
73
+ if (type !== 'update')
74
+ return;
75
+ // Mark as handling remote to prevent echo
76
+ isHandlingRemoteChange = true;
77
+ applyRemoteStoredValue(value, versionKey && remoteVersion !== undefined ? String(remoteVersion) : null);
78
+ }
79
+ catch (err) {
80
+ handleError(err, 'failed to apply remote sync payload');
93
81
  }
94
82
  finally {
95
83
  isHandlingRemoteChange = false;
@@ -110,23 +98,17 @@ export function persist(baseSignal, options) {
110
98
  // Ignore our own changes
111
99
  if (isHandlingRemoteChange)
112
100
  return;
113
- isHandlingRemoteChange = true;
114
101
  try {
115
- const deserialized = serializer.deserialize(e.newValue);
116
- if (merge === 'shallow' && isPlainObject(deserialized)) {
117
- const current = baseSignal.peek();
118
- if (isPlainObject(current)) {
119
- baseSignal.set({ ...current, ...deserialized });
120
- }
121
- else {
122
- baseSignal.set(deserialized);
123
- }
124
- }
125
- else {
126
- baseSignal.set(deserialized);
102
+ isHandlingRemoteChange = true;
103
+ let remoteVersionRaw = null;
104
+ if (versionKey) {
105
+ const v = storage.getItem(versionKey);
106
+ remoteVersionRaw = isPromiseLike(v) ? null : typeof v === 'string' ? v : null;
127
107
  }
128
- lastSaved = e.newValue;
129
- pendingSaved = null;
108
+ applyRemoteStoredValue(e.newValue, remoteVersionRaw);
109
+ }
110
+ catch (err) {
111
+ handleError(err, 'failed to apply remote sync payload');
130
112
  }
131
113
  finally {
132
114
  isHandlingRemoteChange = false;
@@ -165,14 +147,10 @@ export function persist(baseSignal, options) {
165
147
  // Same idea for version key (when enabled)
166
148
  let lastSavedVersion = null;
167
149
  let pendingSavedVersion = null;
168
- const handleError = (err, ctx) => {
169
- const e = toError(err);
170
- if (onError)
171
- onError(e);
172
- else
173
- console.error(`[Dalila] persist(): ${ctx} "${name}"`, e);
174
- };
150
+ let disposed = false;
175
151
  const applyHydration = (value) => {
152
+ if (disposed)
153
+ return;
176
154
  // Ensure dirty listener does not treat hydration as user mutation.
177
155
  const setHydratedValue = (next) => {
178
156
  isHydrationWrite = true;
@@ -273,7 +251,7 @@ export function persist(baseSignal, options) {
273
251
  };
274
252
  const persistValue = (value) => {
275
253
  // Don't write until hydration finishes (prevents overwriting stored state).
276
- if (!hydrated)
254
+ if (disposed || !hydrated)
277
255
  return;
278
256
  let serialized;
279
257
  try {
@@ -319,6 +297,9 @@ export function persist(baseSignal, options) {
319
297
  }
320
298
  applyHydration(deserialized);
321
299
  };
300
+ const applyRemoteStoredValue = (storedValue, storedVersionRaw) => {
301
+ hydrateFromStored(storedValue, storedVersionRaw);
302
+ };
322
303
  const finalizeHydration = (didUserChangeBefore) => {
323
304
  hydrated = true;
324
305
  // Remove temporary dirty listener (no longer needed after hydration)
@@ -326,6 +307,8 @@ export function persist(baseSignal, options) {
326
307
  removeDirtyListener();
327
308
  removeDirtyListener = null;
328
309
  }
310
+ if (disposed)
311
+ return;
329
312
  // If user changed before hydrate finished, we must persist current value at least once,
330
313
  // because the change already happened while hydrated=false and won't re-trigger.
331
314
  if (didUserChangeBefore) {
@@ -447,7 +430,12 @@ export function persist(baseSignal, options) {
447
430
  // Add dispose method for manual cleanup
448
431
  const persistedSignal = baseSignal;
449
432
  persistedSignal.dispose = () => {
433
+ disposed = true;
450
434
  removeSubscription();
435
+ if (removeDirtyListener) {
436
+ removeDirtyListener();
437
+ removeDirtyListener = null;
438
+ }
451
439
  cleanup();
452
440
  };
453
441
  return persistedSignal;
@@ -43,7 +43,7 @@ export declare function isScopeDisposed(scope: Scope): boolean;
43
43
  export declare function createScope(): Scope;
44
44
  export declare function createScope(parentOverride?: Scope | null): Scope;
45
45
  export declare function createScope(options: CreateScopeOptions): Scope;
46
- export declare function createScope(parentOverride: Scope | null, options?: CreateScopeOptions): Scope;
46
+ export declare function createScope(parentOverride: Scope | null | undefined, options?: CreateScopeOptions): Scope;
47
47
  /** Returns the current active scope (or null if none). */
48
48
  export declare function getCurrentScope(): Scope | null;
49
49
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.9.16",
3
+ "version": "1.9.17",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",