@techninja/clearstack 0.3.28 → 0.3.30
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.
|
@@ -102,21 +102,14 @@ default values, so the model initializes cleanly:
|
|
|
102
102
|
|
|
103
103
|
```javascript
|
|
104
104
|
[store.connect]: {
|
|
105
|
-
// ✅ GOOD — returns empty object, hybrids merges with defaults
|
|
106
105
|
get: () => {
|
|
107
106
|
const raw = localStorage.getItem('appState');
|
|
108
|
-
return raw ? JSON.parse(raw) : {};
|
|
107
|
+
return raw ? JSON.parse(raw) : {}; // ✅ — {} merges with defaults
|
|
108
|
+
// return raw ? JSON.parse(raw) : undefined; // ❌ — triggers error state
|
|
109
109
|
},
|
|
110
|
-
// ❌ BAD — undefined triggers error state
|
|
111
|
-
// get: () => {
|
|
112
|
-
// const raw = localStorage.getItem('appState');
|
|
113
|
-
// return raw ? JSON.parse(raw) : undefined;
|
|
114
|
-
// },
|
|
115
110
|
}
|
|
116
111
|
```
|
|
117
112
|
|
|
118
|
-
This applies to all localStorage-backed singletons (`AppState`, `UserPrefs`).
|
|
119
|
-
|
|
120
113
|
Consume in any component:
|
|
121
114
|
|
|
122
115
|
```javascript
|
|
@@ -227,56 +220,28 @@ non-array during the pending phase.
|
|
|
227
220
|
Always guard with `Array.isArray()` before calling array methods:
|
|
228
221
|
|
|
229
222
|
```javascript
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
invalidate();
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
render: ({ items }) => html`
|
|
242
|
-
// ❌ BAD — items may not be an array yet when render fires
|
|
243
|
-
${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)} // ✅ GOOD — guard
|
|
244
|
-
before calling array methods
|
|
245
|
-
${Array.isArray(items) && items.length > 0
|
|
246
|
-
? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
|
|
247
|
-
: html``}
|
|
248
|
-
`,
|
|
249
|
-
});
|
|
223
|
+
// ❌ BAD — items may not be an array yet when render fires
|
|
224
|
+
${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)}
|
|
225
|
+
|
|
226
|
+
// ✅ GOOD — guard before calling array methods
|
|
227
|
+
${Array.isArray(items) && items.length > 0
|
|
228
|
+
? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
|
|
229
|
+
: html``}
|
|
250
230
|
```
|
|
251
231
|
|
|
252
|
-
|
|
253
|
-
The `connect` callback sets the value and calls `invalidate()`, but the
|
|
254
|
-
first render happens before that resolves.
|
|
232
|
+
The first render fires before `connect`'s async callback resolves.
|
|
255
233
|
|
|
256
234
|
#### Async Init with Child Component Props
|
|
257
235
|
|
|
258
|
-
When a parent
|
|
259
|
-
|
|
260
|
-
detection requires observing old → new. If the prop is set before the
|
|
261
|
-
child mounts, there's no "old" value to compare against.
|
|
236
|
+
When a parent sets a child prop during `connect`, the child may never
|
|
237
|
+
see the change (no old → new transition). Defer to after first render:
|
|
262
238
|
|
|
263
239
|
```javascript
|
|
264
|
-
// ❌ BAD —
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
connect: (host, _key, invalidate) => {
|
|
268
|
-
loadData(host).then(() => {
|
|
269
|
-
host.resultCount = 14;
|
|
270
|
-
invalidate(); // child mounts with 14, never sees a change
|
|
271
|
-
});
|
|
272
|
-
},
|
|
240
|
+
// ❌ BAD — child mounts with 14, never sees a change
|
|
241
|
+
connect: (host, _key, invalidate) => {
|
|
242
|
+
loadData(host).then(() => { host.resultCount = 14; invalidate(); });
|
|
273
243
|
},
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
The fix: defer the prop update to after the first render frame so the
|
|
277
|
-
child is mounted and can observe the transition:
|
|
278
244
|
|
|
279
|
-
```javascript
|
|
280
245
|
// ✅ GOOD — child mounts with default (0), then sees 0 → 14
|
|
281
246
|
connect: (host, _key, invalidate) => {
|
|
282
247
|
loadData(host).then(() => {
|
|
@@ -286,10 +251,6 @@ connect: (host, _key, invalidate) => {
|
|
|
286
251
|
},
|
|
287
252
|
```
|
|
288
253
|
|
|
289
|
-
Pre-populate external caches during `loadData` so the child has data
|
|
290
|
-
ready when the deferred update triggers. The 0 → 14 transition happens
|
|
291
|
-
within a single frame — no flicker.
|
|
292
|
-
|
|
293
254
|
---
|
|
294
255
|
|
|
295
256
|
## Routing
|
|
@@ -346,40 +307,21 @@ export default define({
|
|
|
346
307
|
|
|
347
308
|
#### Router Property Cache: Same-Value Writes Are No-Ops
|
|
348
309
|
|
|
349
|
-
The router
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
This means if a `connect` callback loads data asynchronously and then sets
|
|
354
|
-
a property to the same value the cache already holds, hybrids sees no
|
|
355
|
-
change and skips the re-render:
|
|
310
|
+
The router restores cached property values on reconnect. If `connect`
|
|
311
|
+
sets a property to the same value the cache holds, hybrids skips re-render.
|
|
312
|
+
Reset to a sentinel first:
|
|
356
313
|
|
|
357
314
|
```javascript
|
|
358
|
-
// ❌ BAD —
|
|
359
|
-
|
|
360
|
-
connect: (host, _key, invalidate) => {
|
|
361
|
-
loadFromDB(host.userId).then((data) => {
|
|
362
|
-
populateMemoryCache(data); // side effect: fills an external object
|
|
363
|
-
host.resultCount = data.length; // may equal cached value → no re-render
|
|
364
|
-
invalidate();
|
|
365
|
-
});
|
|
366
|
-
},
|
|
315
|
+
// ❌ BAD — may equal cached value → no re-render
|
|
316
|
+
host.resultCount = data.length;
|
|
367
317
|
|
|
368
|
-
// ✅ GOOD —
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
loadFromDB(host.userId).then((data) => {
|
|
372
|
-
populateMemoryCache(data);
|
|
373
|
-
host.resultCount = 0; // force a change
|
|
374
|
-
host.resultCount = data.length; // now hybrids sees a real change
|
|
375
|
-
invalidate();
|
|
376
|
-
});
|
|
377
|
-
},
|
|
318
|
+
// ✅ GOOD — force a change so hybrids sees 0 → 22
|
|
319
|
+
host.resultCount = 0;
|
|
320
|
+
host.resultCount = data.length;
|
|
378
321
|
```
|
|
379
322
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
signal. Without the reset, the component renders with stale external state.
|
|
323
|
+
Important when render depends on external mutable state that the property
|
|
324
|
+
change is meant to signal.
|
|
383
325
|
|
|
384
326
|
#### Router Cache Poisons Boolean Flags Across Reconnects
|
|
385
327
|
|
package/lib/update.js
CHANGED
|
@@ -14,7 +14,7 @@ import { detectPlatforms, updatePlatform } from './platform.js';
|
|
|
14
14
|
* @param {string} src
|
|
15
15
|
* @param {string} dest
|
|
16
16
|
* @param {string} label
|
|
17
|
-
* @param {{ skipExisting?: boolean }} [opts]
|
|
17
|
+
* @param {{ skipExisting?: boolean, onlyExisting?: boolean }} [opts]
|
|
18
18
|
* @returns {number} count of updated files
|
|
19
19
|
*/
|
|
20
20
|
function syncDir(src, dest, label, opts = {}) {
|
|
@@ -25,6 +25,7 @@ function syncDir(src, dest, label, opts = {}) {
|
|
|
25
25
|
|
|
26
26
|
for (const file of files) {
|
|
27
27
|
const destPath = join(dest, file);
|
|
28
|
+
if (opts.onlyExisting && !existsSync(destPath)) continue;
|
|
28
29
|
if (opts.skipExisting && existsSync(destPath)) {
|
|
29
30
|
console.log(` ⏭ Skipped: ${label}/${file} (exists, use --force to overwrite)`);
|
|
30
31
|
continue;
|
|
@@ -82,6 +83,14 @@ export async function update(pkgRoot, opts = {}) {
|
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// Scripts: sync clearstack-owned scripts (only update existing files)
|
|
87
|
+
total += syncDir(
|
|
88
|
+
resolve(templateShared, 'scripts'),
|
|
89
|
+
resolve(process.cwd(), 'scripts'),
|
|
90
|
+
'scripts',
|
|
91
|
+
{ onlyExisting: true },
|
|
92
|
+
);
|
|
93
|
+
|
|
85
94
|
// Write spec version marker
|
|
86
95
|
const pkg = JSON.parse(readFileSync(resolve(pkgRoot, 'package.json'), 'utf-8'));
|
|
87
96
|
const versionPath = resolve(process.cwd(), 'docs/clearstack/.specversion');
|
package/package.json
CHANGED
|
@@ -102,21 +102,14 @@ default values, so the model initializes cleanly:
|
|
|
102
102
|
|
|
103
103
|
```javascript
|
|
104
104
|
[store.connect]: {
|
|
105
|
-
// ✅ GOOD — returns empty object, hybrids merges with defaults
|
|
106
105
|
get: () => {
|
|
107
106
|
const raw = localStorage.getItem('appState');
|
|
108
|
-
return raw ? JSON.parse(raw) : {};
|
|
107
|
+
return raw ? JSON.parse(raw) : {}; // ✅ — {} merges with defaults
|
|
108
|
+
// return raw ? JSON.parse(raw) : undefined; // ❌ — triggers error state
|
|
109
109
|
},
|
|
110
|
-
// ❌ BAD — undefined triggers error state
|
|
111
|
-
// get: () => {
|
|
112
|
-
// const raw = localStorage.getItem('appState');
|
|
113
|
-
// return raw ? JSON.parse(raw) : undefined;
|
|
114
|
-
// },
|
|
115
110
|
}
|
|
116
111
|
```
|
|
117
112
|
|
|
118
|
-
This applies to all localStorage-backed singletons (`AppState`, `UserPrefs`).
|
|
119
|
-
|
|
120
113
|
Consume in any component:
|
|
121
114
|
|
|
122
115
|
```javascript
|
|
@@ -227,56 +220,28 @@ non-array during the pending phase.
|
|
|
227
220
|
Always guard with `Array.isArray()` before calling array methods:
|
|
228
221
|
|
|
229
222
|
```javascript
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
invalidate();
|
|
238
|
-
});
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
render: ({ items }) => html`
|
|
242
|
-
// ❌ BAD — items may not be an array yet when render fires
|
|
243
|
-
${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)} // ✅ GOOD — guard
|
|
244
|
-
before calling array methods
|
|
245
|
-
${Array.isArray(items) && items.length > 0
|
|
246
|
-
? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
|
|
247
|
-
: html``}
|
|
248
|
-
`,
|
|
249
|
-
});
|
|
223
|
+
// ❌ BAD — items may not be an array yet when render fires
|
|
224
|
+
${items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)}
|
|
225
|
+
|
|
226
|
+
// ✅ GOOD — guard before calling array methods
|
|
227
|
+
${Array.isArray(items) && items.length > 0
|
|
228
|
+
? items.filter((i) => i.active).map((i) => html`<span>${i.name}</span>`)
|
|
229
|
+
: html``}
|
|
250
230
|
```
|
|
251
231
|
|
|
252
|
-
|
|
253
|
-
The `connect` callback sets the value and calls `invalidate()`, but the
|
|
254
|
-
first render happens before that resolves.
|
|
232
|
+
The first render fires before `connect`'s async callback resolves.
|
|
255
233
|
|
|
256
234
|
#### Async Init with Child Component Props
|
|
257
235
|
|
|
258
|
-
When a parent
|
|
259
|
-
|
|
260
|
-
detection requires observing old → new. If the prop is set before the
|
|
261
|
-
child mounts, there's no "old" value to compare against.
|
|
236
|
+
When a parent sets a child prop during `connect`, the child may never
|
|
237
|
+
see the change (no old → new transition). Defer to after first render:
|
|
262
238
|
|
|
263
239
|
```javascript
|
|
264
|
-
// ❌ BAD —
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
connect: (host, _key, invalidate) => {
|
|
268
|
-
loadData(host).then(() => {
|
|
269
|
-
host.resultCount = 14;
|
|
270
|
-
invalidate(); // child mounts with 14, never sees a change
|
|
271
|
-
});
|
|
272
|
-
},
|
|
240
|
+
// ❌ BAD — child mounts with 14, never sees a change
|
|
241
|
+
connect: (host, _key, invalidate) => {
|
|
242
|
+
loadData(host).then(() => { host.resultCount = 14; invalidate(); });
|
|
273
243
|
},
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
The fix: defer the prop update to after the first render frame so the
|
|
277
|
-
child is mounted and can observe the transition:
|
|
278
244
|
|
|
279
|
-
```javascript
|
|
280
245
|
// ✅ GOOD — child mounts with default (0), then sees 0 → 14
|
|
281
246
|
connect: (host, _key, invalidate) => {
|
|
282
247
|
loadData(host).then(() => {
|
|
@@ -286,10 +251,6 @@ connect: (host, _key, invalidate) => {
|
|
|
286
251
|
},
|
|
287
252
|
```
|
|
288
253
|
|
|
289
|
-
Pre-populate external caches during `loadData` so the child has data
|
|
290
|
-
ready when the deferred update triggers. The 0 → 14 transition happens
|
|
291
|
-
within a single frame — no flicker.
|
|
292
|
-
|
|
293
254
|
---
|
|
294
255
|
|
|
295
256
|
## Routing
|
|
@@ -346,40 +307,21 @@ export default define({
|
|
|
346
307
|
|
|
347
308
|
#### Router Property Cache: Same-Value Writes Are No-Ops
|
|
348
309
|
|
|
349
|
-
The router
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
This means if a `connect` callback loads data asynchronously and then sets
|
|
354
|
-
a property to the same value the cache already holds, hybrids sees no
|
|
355
|
-
change and skips the re-render:
|
|
310
|
+
The router restores cached property values on reconnect. If `connect`
|
|
311
|
+
sets a property to the same value the cache holds, hybrids skips re-render.
|
|
312
|
+
Reset to a sentinel first:
|
|
356
313
|
|
|
357
314
|
```javascript
|
|
358
|
-
// ❌ BAD —
|
|
359
|
-
|
|
360
|
-
connect: (host, _key, invalidate) => {
|
|
361
|
-
loadFromDB(host.userId).then((data) => {
|
|
362
|
-
populateMemoryCache(data); // side effect: fills an external object
|
|
363
|
-
host.resultCount = data.length; // may equal cached value → no re-render
|
|
364
|
-
invalidate();
|
|
365
|
-
});
|
|
366
|
-
},
|
|
315
|
+
// ❌ BAD — may equal cached value → no re-render
|
|
316
|
+
host.resultCount = data.length;
|
|
367
317
|
|
|
368
|
-
// ✅ GOOD —
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
loadFromDB(host.userId).then((data) => {
|
|
372
|
-
populateMemoryCache(data);
|
|
373
|
-
host.resultCount = 0; // force a change
|
|
374
|
-
host.resultCount = data.length; // now hybrids sees a real change
|
|
375
|
-
invalidate();
|
|
376
|
-
});
|
|
377
|
-
},
|
|
318
|
+
// ✅ GOOD — force a change so hybrids sees 0 → 22
|
|
319
|
+
host.resultCount = 0;
|
|
320
|
+
host.resultCount = data.length;
|
|
378
321
|
```
|
|
379
322
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
signal. Without the reset, the component renders with stale external state.
|
|
323
|
+
Important when render depends on external mutable state that the property
|
|
324
|
+
change is meant to signal.
|
|
383
325
|
|
|
384
326
|
#### Router Cache Poisons Boolean Flags Across Reconnects
|
|
385
327
|
|