@techninja/clearstack 0.3.27 → 0.3.28

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.
@@ -381,6 +381,54 @@ This is especially important when render depends on external mutable state
381
381
  (e.g. an in-memory cache object) that the property change is meant to
382
382
  signal. Without the reset, the component renders with stale external state.
383
383
 
384
+ #### Router Cache Poisons Boolean Flags Across Reconnects
385
+
386
+ When a routed view has a boolean property used as a fallback/error flag,
387
+ the router cache can poison it across reconnects. If the flag is ever set
388
+ to `true` (e.g. a failed fetch), the cached value persists. On the next
389
+ navigation to that view, `connect` runs but the property is already `true`
390
+ from cache — even though the default is `false`:
391
+
392
+ ```javascript
393
+ // ❌ BAD — if fallback was ever true, it stays true on reconnect
394
+ export default define({
395
+ tag: 'my-view',
396
+ data: {
397
+ value: undefined,
398
+ connect(host, _key, invalidate) {
399
+ loadData(host).then(() => invalidate());
400
+ },
401
+ },
402
+ fallback: false, // default is false, but cache may hold true
403
+ render: ({ data, fallback }) => {
404
+ if (fallback) return html`<p>Error</p>`; // stuck here forever
405
+ // ...
406
+ },
407
+ });
408
+
409
+ // ✅ GOOD — reset the flag in connect before the async load
410
+ export default define({
411
+ tag: 'my-view',
412
+ data: {
413
+ value: undefined,
414
+ connect(host, _key, invalidate) {
415
+ host.fallback = false; // clear stale cache
416
+ loadData(host).then(() => invalidate());
417
+ },
418
+ },
419
+ fallback: false,
420
+ render: ({ data, fallback }) => {
421
+ if (fallback) return html`<p>Error</p>`;
422
+ // ...
423
+ },
424
+ });
425
+ ```
426
+
427
+ The general rule: any boolean flag that gates render output and is set
428
+ by an async `connect` flow should be explicitly reset at the top of
429
+ `connect`. This is especially common with fetch-or-fallback patterns
430
+ where the fallback path sets the flag to `true` on network errors.
431
+
384
432
  #### `router.backUrl()` Serializes All Parent Properties
385
433
 
386
434
  `router.backUrl()` encodes the parent view's property values into query
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {
@@ -381,6 +381,54 @@ This is especially important when render depends on external mutable state
381
381
  (e.g. an in-memory cache object) that the property change is meant to
382
382
  signal. Without the reset, the component renders with stale external state.
383
383
 
384
+ #### Router Cache Poisons Boolean Flags Across Reconnects
385
+
386
+ When a routed view has a boolean property used as a fallback/error flag,
387
+ the router cache can poison it across reconnects. If the flag is ever set
388
+ to `true` (e.g. a failed fetch), the cached value persists. On the next
389
+ navigation to that view, `connect` runs but the property is already `true`
390
+ from cache — even though the default is `false`:
391
+
392
+ ```javascript
393
+ // ❌ BAD — if fallback was ever true, it stays true on reconnect
394
+ export default define({
395
+ tag: 'my-view',
396
+ data: {
397
+ value: undefined,
398
+ connect(host, _key, invalidate) {
399
+ loadData(host).then(() => invalidate());
400
+ },
401
+ },
402
+ fallback: false, // default is false, but cache may hold true
403
+ render: ({ data, fallback }) => {
404
+ if (fallback) return html`<p>Error</p>`; // stuck here forever
405
+ // ...
406
+ },
407
+ });
408
+
409
+ // ✅ GOOD — reset the flag in connect before the async load
410
+ export default define({
411
+ tag: 'my-view',
412
+ data: {
413
+ value: undefined,
414
+ connect(host, _key, invalidate) {
415
+ host.fallback = false; // clear stale cache
416
+ loadData(host).then(() => invalidate());
417
+ },
418
+ },
419
+ fallback: false,
420
+ render: ({ data, fallback }) => {
421
+ if (fallback) return html`<p>Error</p>`;
422
+ // ...
423
+ },
424
+ });
425
+ ```
426
+
427
+ The general rule: any boolean flag that gates render output and is set
428
+ by an async `connect` flow should be explicitly reset at the top of
429
+ `connect`. This is especially common with fetch-or-fallback patterns
430
+ where the fallback path sets the flag to `true` on network errors.
431
+
384
432
  #### `router.backUrl()` Serializes All Parent Properties
385
433
 
386
434
  `router.backUrl()` encodes the parent view's property values into query
@@ -2,10 +2,11 @@
2
2
 
3
3
  /**
4
4
  * Copies third-party ES module sources into src/vendor/ so the browser
5
- * can load them directly via import map. Runs on `npm postinstall`.
5
+ * can load them directly via import map. Runs on `npm run setup`.
6
+ * Applies local patches after copy.
6
7
  */
7
8
 
8
- import { cpSync, mkdirSync } from 'node:fs';
9
+ import { cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
10
  import { resolve, dirname } from 'node:path';
10
11
  import { fileURLToPath } from 'node:url';
11
12
 
@@ -23,3 +24,24 @@ for (const dep of DEPS) {
23
24
  cpSync(src, dest, { recursive: true });
24
25
  console.log(`✓ Vendored: ${dep.name} → src/vendor/${dep.name}/`);
25
26
  }
27
+
28
+ // Patch: fix router bootstrap URL rewrite (hybrids#router deep-link bug)
29
+ const routerPath = resolve(VENDOR_DIR, 'hybrids/router.js');
30
+ let routerSrc = readFileSync(routerPath, 'utf8');
31
+ const original = ` globalThis.history.replaceState([entry], "", options.url);`;
32
+ const patched = ` let nestedEntry = entry;
33
+ while (nestedEntry.nested) nestedEntry = nestedEntry.nested;
34
+ const entryConfig = getConfigById(nestedEntry.id);
35
+ const bootstrapUrl = entryConfig && entryConfig.browserUrl
36
+ ? entryConfig.url(nestedEntry.params, true)
37
+ : options.url;
38
+
39
+ globalThis.history.replaceState([entry], "", bootstrapUrl);`;
40
+
41
+ if (routerSrc.includes(original)) {
42
+ routerSrc = routerSrc.replace(original, patched);
43
+ writeFileSync(routerPath, routerSrc);
44
+ console.log('✓ Patched: hybrids/router.js (bootstrap deep-link URL fix)');
45
+ } else if (!routerSrc.includes('bootstrapUrl')) {
46
+ console.warn('⚠ Could not apply router patch — source changed upstream');
47
+ }