@adukiorg/anza 0.2.0 → 0.2.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/CHANGELOG.md +81 -4
- package/README.md +97 -133
- package/bin/anza/anza +0 -0
- package/bin/anza/anza.exe +0 -0
- package/bin/anza/find.js +35 -0
- package/bin/anza/index.js +34 -0
- package/bin/anza/launch.js +19 -0
- package/bin/common/index.js +7 -0
- package/bin/common/logs.js +62 -0
- package/bin/create/copy.js +18 -0
- package/bin/create/index.js +45 -0
- package/bin/create/run.js +210 -0
- package/bin/create/write.js +19 -0
- package/importmap.json +4 -0
- package/package.json +16 -10
- package/src/core/offline/{usage.md → notes/usage.md} +11 -1
- package/src/core/router/boot.js +82 -0
- package/src/core/router/cascade.js +76 -0
- package/src/core/router/container.js +63 -72
- package/src/core/router/graph.js +144 -0
- package/src/core/router/index.js +12 -2
- package/src/core/router/intercept.js +26 -7
- package/src/core/router/lca.js +58 -0
- package/src/core/router/match.js +49 -36
- package/src/core/router/notes/audit-old.md +887 -0
- package/src/core/router/notes/audti.md +773 -0
- package/src/core/router/notes/tasks.md +473 -0
- package/src/core/router/{usage.md → notes/usage.md} +57 -35
- package/src/core/router/sync/tab.js +6 -4
- package/src/core/router/transitions.js +35 -8
- package/src/core/router/trie.js +130 -0
- package/src/core/security/{usage.md → notes/usage.md} +1 -2
- package/src/core/storage/{usage.md → notes/usage.md} +6 -6
- package/src/core/theme/index.js +78 -0
- package/src/core/ui/define/index.js +2 -1
- package/src/core/ui/define/orchestrator.js +10 -4
- package/src/core/ui/defs/dock.js +134 -0
- package/src/core/ui/defs/index.js +20 -0
- package/src/core/ui/defs/page.js +89 -0
- package/src/core/ui/defs/part.js +28 -0
- package/src/core/ui/defs/spec.js +96 -0
- package/src/core/ui/defs/view.js +23 -0
- package/src/core/ui/index.js +16 -3
- package/src/core/ui/notes/definations.md +979 -0
- package/src/tokens/index.css +1 -0
- package/src/tokens/semantic/contrast.css +18 -0
- package/src/tokens/semantic/transitions.css +32 -0
- package/types/core/platform/index.d.ts +39 -10
- package/types/core/router/index.d.ts +9 -0
- package/types/core/theme/index.d.ts +18 -0
- package/types/core/ui/index.d.ts +11 -0
- package/types/index.d.ts +1 -0
- package/bin/anza.js +0 -63
- package/bin/create.js +0 -150
- package/src/core/api/plan.md +0 -209
- package/src/core/events/missing.md +0 -103
- package/src/core/events/plan.md +0 -177
- package/src/core/offline/missing.md +0 -89
- package/src/core/offline/plan.md +0 -143
- package/src/core/platform/missing.md +0 -119
- package/src/core/platform/platform.d.ts +0 -88
- package/src/core/router/missing.md +0 -716
- package/src/core/router/outlet.js +0 -139
- package/src/core/router/plan.md +0 -370
- package/src/core/security/missing.md +0 -97
- package/src/core/state/missing.md +0 -165
- package/src/core/storage/missing.md +0 -165
- package/src/core/storage/plan.md +0 -69
- package/src/core/ui/implementation.md +0 -170
- package/src/core/ui/plan.md +0 -510
- package/src/core/ui/ui.types.md +0 -890
- /package/src/core/animations/{usage.md → notes/usage.md} +0 -0
- /package/src/core/api/{usage.md → notes/usage.md} +0 -0
- /package/src/core/events/{usage.md → notes/usage.md} +0 -0
- /package/src/core/platform/{usage.md → notes/usage.md} +0 -0
- /package/src/core/state/{usage.md → notes/usage.md} +0 -0
- /package/src/core/ui/{usage.md → notes/usage.md} +0 -0
- /package/src/core/ui/{watch.md → notes/watch.md} +0 -0
- /package/src/core/workers/{plan.md → notes/plan.md} +0 -0
- /package/src/core/workers/{usage.md → notes/usage.md} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/create/run.js
|
|
3
|
+
*
|
|
4
|
+
* Scaffold logic. Generates anza app files from templates.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
9
|
+
import * as logs from '../common/index.js';
|
|
10
|
+
import * as copy from './copy.js';
|
|
11
|
+
import * as write from './write.js';
|
|
12
|
+
|
|
13
|
+
const DIRS = [
|
|
14
|
+
'src',
|
|
15
|
+
'src/pages',
|
|
16
|
+
'src/pages/index',
|
|
17
|
+
'src/docks',
|
|
18
|
+
'src/views',
|
|
19
|
+
'src/parts',
|
|
20
|
+
'src/elements',
|
|
21
|
+
'src/tokens',
|
|
22
|
+
'src/styles',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const MAP = JSON.stringify({ imports: {} }, null, 2) + '\n';
|
|
26
|
+
|
|
27
|
+
const HTML = (name) => `<!DOCTYPE html>
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<meta charset="utf-8" />
|
|
31
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
32
|
+
<title>${name}</title>
|
|
33
|
+
|
|
34
|
+
<script type="importmap" src="/importmap.json"></script>
|
|
35
|
+
|
|
36
|
+
<link rel="stylesheet" href="/dist/tokens/index.css" />
|
|
37
|
+
<link rel="stylesheet" href="/dist/styles/index.css" />
|
|
38
|
+
|
|
39
|
+
<script type="module" src="/dist/app.js"></script>
|
|
40
|
+
</head>
|
|
41
|
+
<body></body>
|
|
42
|
+
</html>
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const APP = `/**
|
|
46
|
+
* src/app.js — app entry point
|
|
47
|
+
*/
|
|
48
|
+
import '@adukiorg/anza/ui';
|
|
49
|
+
import { dock } from '@adukiorg/anza/ui';
|
|
50
|
+
import '@adukiorg/anza/theme';
|
|
51
|
+
|
|
52
|
+
// Service Worker
|
|
53
|
+
navigator.serviceWorker.register('/dist/sw.js');
|
|
54
|
+
|
|
55
|
+
// Layout shell
|
|
56
|
+
dock('main', { parent: 'body' });
|
|
57
|
+
|
|
58
|
+
// Pages
|
|
59
|
+
import './pages/index/index.js';
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const SW = `/**
|
|
63
|
+
* src/sw.js — Service Worker entry
|
|
64
|
+
*/
|
|
65
|
+
import { precache, router, CacheFirst, NetworkFirst, pruneStale, claim } from '@adukiorg/anza/sw';
|
|
66
|
+
|
|
67
|
+
const SHELL = 'shell-v1';
|
|
68
|
+
const API = 'api-v1';
|
|
69
|
+
|
|
70
|
+
self.addEventListener('install', (e) => {
|
|
71
|
+
e.waitUntil(precache(SHELL, ['/dist/index.html', '/dist/app.js', '/dist/tokens/index.css', '/dist/styles/index.css']));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
self.addEventListener('activate', (e) => {
|
|
75
|
+
e.waitUntil(Promise.all([pruneStale(SHELL), claim()]));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const r = router();
|
|
79
|
+
r.register('/dist/*', new CacheFirst(SHELL));
|
|
80
|
+
r.register('/api/*', new NetworkFirst(API, { timeout: 3000 }));
|
|
81
|
+
|
|
82
|
+
self.addEventListener('fetch', (e) => {
|
|
83
|
+
if (r.handle(e)) return;
|
|
84
|
+
e.respondWith(fetch(e.request));
|
|
85
|
+
});
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const PAGE = `/**
|
|
89
|
+
* src/pages/index/index.js — welcome page
|
|
90
|
+
*/
|
|
91
|
+
import { page } from '@adukiorg/anza/ui';
|
|
92
|
+
|
|
93
|
+
page('/', {
|
|
94
|
+
tag: 'page-welcome',
|
|
95
|
+
via: ['main'],
|
|
96
|
+
template: { html: './index.html', css: './index.css' }
|
|
97
|
+
}, import.meta.url);
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const MARKUP = (name) => `<article class="welcome">
|
|
101
|
+
<h1>Welcome to ${name}</h1>
|
|
102
|
+
<p>Your anza app is running.</p>
|
|
103
|
+
<nav>
|
|
104
|
+
<a href="https://github.com/aduki-org/anza" target="_blank" rel="noopener">Docs</a>
|
|
105
|
+
<a href="https://github.com/aduki-org/anza/issues" target="_blank" rel="noopener">Issues</a>
|
|
106
|
+
</nav>
|
|
107
|
+
</article>
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const STYLE = `.welcome {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
min-height: 100vh;
|
|
116
|
+
padding: var(--space-8);
|
|
117
|
+
text-align: center;
|
|
118
|
+
gap: var(--space-4);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.welcome h1 {
|
|
122
|
+
font-size: var(--font-size-3xl);
|
|
123
|
+
color: var(--color-content-primary);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.welcome p {
|
|
127
|
+
font-size: var(--font-size-lg);
|
|
128
|
+
color: var(--color-content-secondary);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.welcome nav {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: var(--space-4);
|
|
134
|
+
margin-top: var(--space-4);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.welcome nav a {
|
|
138
|
+
color: var(--color-content-link);
|
|
139
|
+
font-weight: var(--font-weight-medium);
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const IGNORE = 'node_modules/\ndist/\n.anzacache.json\n';
|
|
144
|
+
|
|
145
|
+
export function run(target, name, library) {
|
|
146
|
+
if (existsSync(target)) {
|
|
147
|
+
logs.error(`Target directory already exists: ${target}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
logs.info(`Scaffolding anza app: ${name}`);
|
|
152
|
+
|
|
153
|
+
for (const dir of DIRS) {
|
|
154
|
+
const path = join(target, dir);
|
|
155
|
+
try {
|
|
156
|
+
mkdirSync(path, { recursive: true });
|
|
157
|
+
} catch (e) {
|
|
158
|
+
logs.error(`Failed to create ${path}: ${e.message}`);
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const libTokens = join(library, 'src', 'tokens');
|
|
164
|
+
const libStyles = join(library, 'src', 'styles');
|
|
165
|
+
const appTokens = join(target, 'src', 'tokens');
|
|
166
|
+
const appStyles = join(target, 'src', 'styles');
|
|
167
|
+
|
|
168
|
+
if (existsSync(libTokens)) {
|
|
169
|
+
if (copy.copy(libTokens, appTokens)) {
|
|
170
|
+
logs.compiler('Copied library tokens -> src/tokens/');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (existsSync(libStyles)) {
|
|
175
|
+
if (copy.copy(libStyles, appStyles)) {
|
|
176
|
+
logs.compiler('Copied library styles -> src/styles/');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
write.write(join(target, 'importmap.json'), MAP);
|
|
181
|
+
write.write(join(target, 'src', 'index.html'), HTML(name));
|
|
182
|
+
write.write(join(target, 'src', 'app.js'), APP);
|
|
183
|
+
write.write(join(target, 'src', 'sw.js'), SW);
|
|
184
|
+
write.write(join(target, 'src', 'pages', 'index', 'index.js'), PAGE);
|
|
185
|
+
write.write(join(target, 'src', 'pages', 'index', 'index.html'), MARKUP(name));
|
|
186
|
+
write.write(join(target, 'src', 'pages', 'index', 'index.css'), STYLE);
|
|
187
|
+
|
|
188
|
+
const manifest = JSON.stringify({
|
|
189
|
+
name,
|
|
190
|
+
version: '0.1.0',
|
|
191
|
+
private: true,
|
|
192
|
+
type: 'module',
|
|
193
|
+
scripts: {
|
|
194
|
+
dev: 'anza dev',
|
|
195
|
+
build: 'anza build',
|
|
196
|
+
},
|
|
197
|
+
devDependencies: {
|
|
198
|
+
'@adukiorg/anza': 'latest',
|
|
199
|
+
}
|
|
200
|
+
}, null, 2) + '\n';
|
|
201
|
+
|
|
202
|
+
write.write(join(target, 'package.json'), manifest);
|
|
203
|
+
write.write(join(target, '.gitignore'), IGNORE);
|
|
204
|
+
|
|
205
|
+
logs.success(`Created ${target}`);
|
|
206
|
+
logs.info('Next steps:');
|
|
207
|
+
logs.info(` cd ${name}`);
|
|
208
|
+
logs.info(' npm install');
|
|
209
|
+
logs.info(' npm run dev');
|
|
210
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bin/create/write.js
|
|
3
|
+
*
|
|
4
|
+
* Writes a file, creating parent directories as needed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
import * as logs from '../common/index.js';
|
|
10
|
+
|
|
11
|
+
export function write(path, content) {
|
|
12
|
+
try {
|
|
13
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
14
|
+
writeFileSync(path, content);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
logs.error(`Failed to write ${path}: ${e.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
package/importmap.json
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"@adukiorg/anza/security": "/dist/core/security/index.js",
|
|
14
14
|
"@adukiorg/anza/platform": "/dist/core/platform/index.js",
|
|
15
15
|
"@adukiorg/anza/ui": "/dist/core/ui/index.js",
|
|
16
|
+
"@adukiorg/anza/defs": "/dist/core/ui/defs/index.js",
|
|
17
|
+
"@adukiorg/anza/theme": "/dist/core/theme/index.js",
|
|
18
|
+
|
|
19
|
+
"@adukiorg/anza/sw": "/dist/sw/index.js",
|
|
16
20
|
|
|
17
21
|
"@adukiorg/anza/elements": "/dist/elements/index.js",
|
|
18
22
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adukiorg/anza",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Anza web platform library — reactive state, networking, offline, animations, custom elements. Zero build step. Pure browser ESM.",
|
|
5
5
|
"author": "fescii",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"shadow-dom"
|
|
23
23
|
],
|
|
24
24
|
"bin": {
|
|
25
|
-
"anza": "./bin/anza.js",
|
|
26
|
-
"anza-create": "./bin/create.js"
|
|
25
|
+
"anza": "./bin/anza/index.js",
|
|
26
|
+
"anza-create": "./bin/create/index.js"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"src/",
|
|
@@ -83,18 +83,24 @@
|
|
|
83
83
|
"types": "./types/core/ui/index.d.ts",
|
|
84
84
|
"default": "./src/core/ui/index.js"
|
|
85
85
|
},
|
|
86
|
+
"./defs": {
|
|
87
|
+
"default": "./src/core/ui/defs/index.js"
|
|
88
|
+
},
|
|
86
89
|
"./elements": {
|
|
87
90
|
"types": "./types/elements/index.d.ts",
|
|
88
91
|
"default": "./src/elements/index.js"
|
|
92
|
+
},
|
|
93
|
+
"./sw": {
|
|
94
|
+
"default": "./src/sw/index.js"
|
|
95
|
+
},
|
|
96
|
+
"./theme": {
|
|
97
|
+
"types": "./types/core/theme/index.d.ts",
|
|
98
|
+
"default": "./src/core/theme/index.js"
|
|
89
99
|
}
|
|
90
100
|
},
|
|
91
101
|
"scripts": {
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"check": "tsc --noEmit -p tsconfig.json"
|
|
96
|
-
},
|
|
97
|
-
"devDependencies": {
|
|
98
|
-
"esbuild": "^0.25.0"
|
|
102
|
+
"test": "npx @web/test-runner",
|
|
103
|
+
"serve": "npx @web/dev-server --port 8080 --node-resolve false",
|
|
104
|
+
"check": "tsc --noEmit -p tsconfig.json"
|
|
99
105
|
}
|
|
100
106
|
}
|
|
@@ -19,7 +19,7 @@ import { check, subscribe, queue, sync, bridge, state, clock } from '@adukiorg/a
|
|
|
19
19
|
## 1. Choosing an API
|
|
20
20
|
|
|
21
21
|
| Need | Use |
|
|
22
|
-
|
|
22
|
+
| --- | --- |
|
|
23
23
|
| Query network connectivity | `check` |
|
|
24
24
|
| Subscribe to network status events | `subscribe` |
|
|
25
25
|
| Get current network/queue state | `state.get` or `state.snapshot` |
|
|
@@ -44,6 +44,7 @@ import { check, subscribe, queue, sync, bridge, state, clock } from '@adukiorg/a
|
|
|
44
44
|
The connectivity module handles network status monitoring and reachability checks.
|
|
45
45
|
|
|
46
46
|
### Checking Connectivity
|
|
47
|
+
|
|
47
48
|
The `check(force?)` function checks network reachability. It automatically rate-limits HEAD probes to `/favicon.ico` (cached for 10 seconds) to avoid thrashing the network. Pass `true` to bypass throttling.
|
|
48
49
|
|
|
49
50
|
```javascript
|
|
@@ -55,6 +56,7 @@ const absoluteOnline = await check(true);
|
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
### Event Subscriptions
|
|
59
|
+
|
|
58
60
|
Use `subscribe(callback, signal?)` to listen to connectivity status changes. You can pass an optional `AbortSignal` for automated unsubscription.
|
|
59
61
|
|
|
60
62
|
```javascript
|
|
@@ -72,7 +74,9 @@ controller.abort();
|
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
### Reactive State Store
|
|
77
|
+
|
|
75
78
|
The `state` store is a fine-grained `ReactiveStore` keeping track of network connectivity metrics and task queue length:
|
|
79
|
+
|
|
76
80
|
* `online` (boolean): Whether the client is currently online.
|
|
77
81
|
* `status` (string): `'online' | 'offline' | 'unknown'`.
|
|
78
82
|
* `pending` (number): Monitored count of uncompleted tasks in the queue.
|
|
@@ -95,6 +99,7 @@ console.log(`Status: ${snapshot.status}, Online: ${snapshot.online}`);
|
|
|
95
99
|
The offline queue is backed by IndexedDB (`platform-offline-queue` / `tasks`). It preserves causal chronological sequencing (FIFO) and is protected by write-ahead journaling.
|
|
96
100
|
|
|
97
101
|
### Enqueuing Tasks
|
|
102
|
+
|
|
98
103
|
Use `queue.push(taskName, payload?, options?)` to add tasks to the offline queue. If local client storage usage exceeds 80%, the write is blocked and throws a `QuotaExceededError`.
|
|
99
104
|
|
|
100
105
|
```javascript
|
|
@@ -116,6 +121,7 @@ try {
|
|
|
116
121
|
```
|
|
117
122
|
|
|
118
123
|
### Managing Tasks
|
|
124
|
+
|
|
119
125
|
Retrieve, update, and delete tasks from the queue as they are processed:
|
|
120
126
|
|
|
121
127
|
```javascript
|
|
@@ -153,6 +159,7 @@ await queue.clear();
|
|
|
153
159
|
The `sync` manager coordinates queue replays using native browser Background Sync or event-driven fallbacks.
|
|
154
160
|
|
|
155
161
|
### Registering Sync Events
|
|
162
|
+
|
|
156
163
|
Register sync tags with the Service Worker Background Sync API. On browsers without Background Sync support (e.g. Safari, Firefox), the manager registers a custom window-level online fallback listener.
|
|
157
164
|
|
|
158
165
|
```javascript
|
|
@@ -165,6 +172,7 @@ if (registered) {
|
|
|
165
172
|
```
|
|
166
173
|
|
|
167
174
|
### Web Lock Coordinated Fallback
|
|
175
|
+
|
|
168
176
|
When a fallback online event fires in multi-tab sessions, all tabs simultaneously receive the event. To prevent concurrent database access and duplicate HTTP requests, the callback is automatically coordinated under an exclusive Web Lock named `"offline:sync"`.
|
|
169
177
|
|
|
170
178
|
Only one active tab will acquire the lock and execute the sync handler:
|
|
@@ -226,7 +234,9 @@ const stamp = await clock.stamp();
|
|
|
226
234
|
```
|
|
227
235
|
|
|
228
236
|
### Deterministic LWW Resolution
|
|
237
|
+
|
|
229
238
|
Compare logical stamps during data conflict merges:
|
|
239
|
+
|
|
230
240
|
* The stamp with the higher `clock` counter wins.
|
|
231
241
|
* If `clock` counters are equal, the lexicographically greater `actor` UUID acts as the tiebreaker.
|
|
232
242
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/router/boot.js
|
|
3
|
+
*
|
|
4
|
+
* Deferred boot gate. Holds the router's initial route match until the DOM is
|
|
5
|
+
* parsed and every registered prerequisite (custom element definitions, etc.)
|
|
6
|
+
* has resolved. This is what makes a hard refresh on a deep route work: the
|
|
7
|
+
* first match no longer races ahead of element registration.
|
|
8
|
+
*
|
|
9
|
+
* Source: tasks.md Phase 1
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Outstanding prerequisites the initial match must wait for.
|
|
13
|
+
const gates = new Set();
|
|
14
|
+
|
|
15
|
+
// Resolved once the boot sequence has fired.
|
|
16
|
+
let booted = false;
|
|
17
|
+
|
|
18
|
+
// Pending trigger captured before boot ran, if any.
|
|
19
|
+
let trigger = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers a prerequisite promise. The initial route match will not run until
|
|
23
|
+
* all gates registered before boot have settled. Gates added after boot has
|
|
24
|
+
* already fired are ignored (the page is already live).
|
|
25
|
+
*
|
|
26
|
+
* @param {Promise<any>} promise - prerequisite to await (e.g. whenDefined).
|
|
27
|
+
* @returns {Promise<any>} the same promise, for chaining.
|
|
28
|
+
*/
|
|
29
|
+
export function gate(promise) {
|
|
30
|
+
if (booted || !promise || typeof promise.then !== 'function') return promise;
|
|
31
|
+
gates.add(promise);
|
|
32
|
+
// Drop the gate once it settles so the set never grows unbounded.
|
|
33
|
+
promise.then(() => gates.delete(promise), () => gates.delete(promise));
|
|
34
|
+
return promise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fires the initial route match once the document is interactive and all gates
|
|
39
|
+
* have settled. Idempotent — only the first call wins.
|
|
40
|
+
*
|
|
41
|
+
* @param {() => any | Promise<any>} emitFn - runs the initial match + emit.
|
|
42
|
+
*/
|
|
43
|
+
export function boot(emitFn) {
|
|
44
|
+
if (booted) return;
|
|
45
|
+
trigger = emitFn;
|
|
46
|
+
|
|
47
|
+
const launch = async () => {
|
|
48
|
+
if (booted) return;
|
|
49
|
+
// Snapshot current gates; settle them all (failures are non-fatal — a
|
|
50
|
+
// single element that fails to define must not wedge the whole router).
|
|
51
|
+
const pending = Array.from(gates);
|
|
52
|
+
if (pending.length) {
|
|
53
|
+
await Promise.allSettled(pending);
|
|
54
|
+
}
|
|
55
|
+
booted = true;
|
|
56
|
+
const fn = trigger;
|
|
57
|
+
trigger = null;
|
|
58
|
+
if (fn) await fn();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (typeof document !== 'undefined' && document.readyState === 'loading') {
|
|
62
|
+
document.addEventListener('DOMContentLoaded', () => { launch(); }, { once: true });
|
|
63
|
+
} else {
|
|
64
|
+
launch();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @returns {boolean} true once the boot sequence has completed.
|
|
70
|
+
*/
|
|
71
|
+
export function ready() {
|
|
72
|
+
return booted;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resets boot state. Intended for test isolation and SSR teardown.
|
|
77
|
+
*/
|
|
78
|
+
export function reset() {
|
|
79
|
+
gates.clear();
|
|
80
|
+
booted = false;
|
|
81
|
+
trigger = null;
|
|
82
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/router/cascade.js
|
|
3
|
+
*
|
|
4
|
+
* Sequential container mounting. When a navigation targets a container that is
|
|
5
|
+
* not yet in the DOM, walk the graph path from the nearest live ancestor down
|
|
6
|
+
* to the target, creating and mounting each missing level in order and yielding
|
|
7
|
+
* a frame between each so connectedCallback (and self-registration) can run.
|
|
8
|
+
*
|
|
9
|
+
* Source: tasks.md Phase 5
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { get, element as resolve, root } from './graph.js';
|
|
13
|
+
import { path } from './lca.js';
|
|
14
|
+
|
|
15
|
+
/** Yields one frame so a freshly-connected element can register itself. */
|
|
16
|
+
function frame() {
|
|
17
|
+
if (typeof requestAnimationFrame === 'undefined') return Promise.resolve();
|
|
18
|
+
return new Promise((r) => requestAnimationFrame(() => r()));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Ensures `target` is mounted, cascading through any missing intermediate
|
|
23
|
+
* containers from the deepest currently-mounted ancestor downward.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} target - container name that must end up in the DOM.
|
|
26
|
+
* @param {string} [current='body'] - the source container to path from.
|
|
27
|
+
* @returns {Promise<Element|null>} the resolved target element.
|
|
28
|
+
*/
|
|
29
|
+
export async function ensure(target, current = 'body') {
|
|
30
|
+
// Already mounted — nothing to do.
|
|
31
|
+
const live = resolve(target);
|
|
32
|
+
if (live) return live;
|
|
33
|
+
|
|
34
|
+
const segments = path(current, target);
|
|
35
|
+
if (!segments) throw new Error(`CascadeError: no path '${current}' → '${target}'`);
|
|
36
|
+
|
|
37
|
+
// Find the deepest node on the path that is currently connected.
|
|
38
|
+
let mounted = null;
|
|
39
|
+
for (const node of segments) {
|
|
40
|
+
const el = node.ref?.deref();
|
|
41
|
+
if (el && el.isConnected) mounted = node;
|
|
42
|
+
else break;
|
|
43
|
+
}
|
|
44
|
+
if (!mounted) mounted = root;
|
|
45
|
+
|
|
46
|
+
// Mount sequentially from the first unmounted node down to the target.
|
|
47
|
+
const start = segments.indexOf(mounted) + 1;
|
|
48
|
+
for (let i = start; i < segments.length; i++) {
|
|
49
|
+
const node = segments[i];
|
|
50
|
+
const parentEl = node.parent?.ref?.deref() ?? (node.parent === root ? document.body : null);
|
|
51
|
+
if (!parentEl || !parentEl.isConnected) {
|
|
52
|
+
throw new Error(`CascadeError: parent '${node.parent?.name}' is disconnected while mounting '${node.name}'`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const tag = node.name;
|
|
56
|
+
if (tag.includes('-') && typeof customElements !== 'undefined' && !customElements.get(tag)) {
|
|
57
|
+
await customElements.whenDefined(tag);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const el = document.createElement(tag);
|
|
61
|
+
if (typeof parentEl.swap === 'function') {
|
|
62
|
+
await parentEl.swap(el, { direction: 'push' });
|
|
63
|
+
} else {
|
|
64
|
+
parentEl.replaceChildren(el);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Yield so connectedCallback fires and the dock self-registers in the graph.
|
|
68
|
+
await frame();
|
|
69
|
+
|
|
70
|
+
if (!resolve(node.name)) {
|
|
71
|
+
throw new Error(`CascadeError: container '${node.name}' failed to register after mount`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolve(target);
|
|
76
|
+
}
|