@fullstack-webapp/document-shell 0.1.0-beta.3 → 0.1.0-beta.4
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 +17 -1
- package/dist/client.d.ts +10 -0
- package/dist/client.js +80 -1
- package/docs/integration.md +121 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -155,6 +155,18 @@ The commit waits for that gate, marks `<html data-app-ready="true">`, removes
|
|
|
155
155
|
the element carrying `data-document-shell-static`, and always fails open so a
|
|
156
156
|
broken stylesheet cannot leave a permanent overlay.
|
|
157
157
|
|
|
158
|
+
A consumer whose static projection reveals delayed content (for example a
|
|
159
|
+
content placeholder that only appears after a short race with the runtime and
|
|
160
|
+
then stays visible for a minimum duration) can declare an absolute
|
|
161
|
+
reveal-not-before deadline on the projection element via the exported
|
|
162
|
+
`documentShellRevealNotBeforeAttribute` at the moment that content becomes
|
|
163
|
+
visible. The loaded handoff then waits until the deadline before writing
|
|
164
|
+
`data-app-ready` and removing the projection. Only a finite, future deadline
|
|
165
|
+
that is still inside both the package's bounded hold horizon and the runtime
|
|
166
|
+
stylesheet gate's recorded fail-open deadline is honored; absent, invalid, or
|
|
167
|
+
expired declarations leave the loaded handoff immediate, and stylesheet `error`, `timeout`, and absence always reveal
|
|
168
|
+
immediately. See the integration guide for the declaration contract.
|
|
169
|
+
|
|
158
170
|
For the full file-by-file procedure, safe-area effect example, verification
|
|
159
171
|
checklist, and failure guide, see [Integration guide](docs/integration.md).
|
|
160
172
|
|
|
@@ -164,7 +176,7 @@ checklist, and failure guide, see [Integration guide](docs/integration.md).
|
|
|
164
176
|
| --- | --- |
|
|
165
177
|
| `@fullstack-webapp/document-shell` | Typed HTML/CSS/script boundaries, document composition types, compiler, structural validation, and the shared-default safe-area bridge. |
|
|
166
178
|
| `@fullstack-webapp/document-shell/vite` | The Vite HTML producer, optional runtime-stylesheet gate, and final emitted-document validator. |
|
|
167
|
-
| `@fullstack-webapp/document-shell/client` | The framework-neutral, one-shot runtime handoff. |
|
|
179
|
+
| `@fullstack-webapp/document-shell/client` | The framework-neutral, one-shot runtime handoff and its package-owned DOM contracts, including the optional reveal-not-before deadline. |
|
|
168
180
|
| `@fullstack-webapp/document-shell/reference` | Reference-application safe-area rollout used to retain existing evidence while profiles mature; it is not a consumer profile selector. |
|
|
169
181
|
|
|
170
182
|
The current beta catalog projects all four accepted iOS standalone portrait
|
|
@@ -213,6 +225,10 @@ an arbitrary callback with `Function#toString()`.
|
|
|
213
225
|
- The current safe-area runtime matches observable browser geometry and
|
|
214
226
|
platform signals, not marketing device names. Unsupported geometry fails
|
|
215
227
|
open and receives no reserve.
|
|
228
|
+
- A reveal-not-before declaration can delay only a stylesheet `loaded`
|
|
229
|
+
handoff, and never past the runtime stylesheet gate's recorded fail-open
|
|
230
|
+
deadline; `error`, `timeout`, and `absent` outcomes and missing, invalid, or
|
|
231
|
+
expired declarations keep the immediate fail-open behavior.
|
|
216
232
|
|
|
217
233
|
## Development
|
|
218
234
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export declare const documentShellReadyAttribute = "data-app-ready";
|
|
2
2
|
export declare const documentShellStaticAttribute = "data-document-shell-static";
|
|
3
3
|
export declare const documentShellRuntimeStylesheetId = "runtime-stylesheet";
|
|
4
|
+
/**
|
|
5
|
+
* Runtime-declared reveal-not-before deadline. The consumer's parser startup
|
|
6
|
+
* effect writes an absolute epoch-millisecond deadline into this attribute on
|
|
7
|
+
* the element carrying {@link documentShellStaticAttribute} at the moment the
|
|
8
|
+
* projection's delayed content actually becomes visible. A loaded stylesheet
|
|
9
|
+
* handoff waits until that deadline before writing `data-app-ready` and
|
|
10
|
+
* removing the projection; absent, invalid, or expired deadlines leave the
|
|
11
|
+
* loaded handoff immediate, and `error`/`timeout`/`absent` always fail open.
|
|
12
|
+
*/
|
|
13
|
+
export declare const documentShellRevealNotBeforeAttribute = "data-document-shell-reveal-not-before";
|
|
4
14
|
export type DocumentShellHandoffResult = Readonly<{
|
|
5
15
|
status: 'revealed';
|
|
6
16
|
stylesheet: 'loaded' | 'error' | 'timeout' | 'absent';
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
export const documentShellReadyAttribute = 'data-app-ready';
|
|
2
2
|
export const documentShellStaticAttribute = 'data-document-shell-static';
|
|
3
3
|
export const documentShellRuntimeStylesheetId = 'runtime-stylesheet';
|
|
4
|
+
/**
|
|
5
|
+
* Runtime-declared reveal-not-before deadline. The consumer's parser startup
|
|
6
|
+
* effect writes an absolute epoch-millisecond deadline into this attribute on
|
|
7
|
+
* the element carrying {@link documentShellStaticAttribute} at the moment the
|
|
8
|
+
* projection's delayed content actually becomes visible. A loaded stylesheet
|
|
9
|
+
* handoff waits until that deadline before writing `data-app-ready` and
|
|
10
|
+
* removing the projection; absent, invalid, or expired deadlines leave the
|
|
11
|
+
* loaded handoff immediate, and `error`/`timeout`/`absent` always fail open.
|
|
12
|
+
*/
|
|
13
|
+
export const documentShellRevealNotBeforeAttribute = 'data-document-shell-reveal-not-before';
|
|
4
14
|
const handoffs = new WeakMap();
|
|
15
|
+
/**
|
|
16
|
+
* Maximum wall-clock horizon accepted for an optional reveal hold. The
|
|
17
|
+
* stylesheet bootstrap normally supplies the same three-second gate, while
|
|
18
|
+
* this independent bound also survives wall-clock adjustments between the
|
|
19
|
+
* declaration and the loaded handoff.
|
|
20
|
+
*/
|
|
21
|
+
const revealHoldHorizonMs = 3_000;
|
|
5
22
|
export function commitDocumentShellRuntime() {
|
|
6
23
|
if (typeof document === 'undefined') {
|
|
7
24
|
return Promise.reject(new Error('Document shell runtime handoff requires a browser document'));
|
|
@@ -20,6 +37,10 @@ export function commitDocumentShellRuntime() {
|
|
|
20
37
|
const pending = {};
|
|
21
38
|
root.setAttribute('data-document-shell-runtime-committed', 'true');
|
|
22
39
|
const cleanup = () => {
|
|
40
|
+
if (pending.holdTimer !== undefined) {
|
|
41
|
+
window.clearTimeout(pending.holdTimer);
|
|
42
|
+
pending.holdTimer = undefined;
|
|
43
|
+
}
|
|
23
44
|
if (pending.fallbackTimer !== undefined)
|
|
24
45
|
window.clearTimeout(pending.fallbackTimer);
|
|
25
46
|
if (pending.revealFrame !== undefined)
|
|
@@ -27,7 +48,7 @@ export function commitDocumentShellRuntime() {
|
|
|
27
48
|
stylesheet?.removeEventListener('load', handleLoad);
|
|
28
49
|
stylesheet?.removeEventListener('error', handleError);
|
|
29
50
|
};
|
|
30
|
-
const
|
|
51
|
+
const commitReveal = (stylesheetStatus) => {
|
|
31
52
|
if (finished)
|
|
32
53
|
return;
|
|
33
54
|
finished = true;
|
|
@@ -36,6 +57,18 @@ export function commitDocumentShellRuntime() {
|
|
|
36
57
|
document.querySelector(`[${documentShellStaticAttribute}]`)?.remove();
|
|
37
58
|
resolveHandoff({ status: 'revealed', stylesheet: stylesheetStatus });
|
|
38
59
|
};
|
|
60
|
+
const reveal = (stylesheetStatus) => {
|
|
61
|
+
if (finished)
|
|
62
|
+
return;
|
|
63
|
+
if (stylesheetStatus === 'loaded') {
|
|
64
|
+
const holdUntil = readRevealNotBeforeDeadline();
|
|
65
|
+
if (holdUntil !== null) {
|
|
66
|
+
scheduleRevealHold(holdUntil);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
commitReveal(stylesheetStatus);
|
|
71
|
+
};
|
|
39
72
|
const revealAfterStylesApply = () => {
|
|
40
73
|
if (finished || pending.revealFrame !== undefined)
|
|
41
74
|
return;
|
|
@@ -75,4 +108,50 @@ export function commitDocumentShellRuntime() {
|
|
|
75
108
|
: 0;
|
|
76
109
|
pending.fallbackTimer = window.setTimeout(() => reveal('timeout'), fallbackDelay);
|
|
77
110
|
return handoff;
|
|
111
|
+
/**
|
|
112
|
+
* Reads the reveal-not-before declaration at the moment the loaded handoff
|
|
113
|
+
* is about to reveal, so a deadline declared while the stylesheet was still
|
|
114
|
+
* loading is honored. Only a finite absolute deadline that is strictly in
|
|
115
|
+
* the future and still inside the runtime stylesheet gate's recorded
|
|
116
|
+
* fail-open deadline is honored; every other value is treated as absent or
|
|
117
|
+
* expired, keeping the loaded handoff immediate and guaranteeing that the
|
|
118
|
+
* projection can never outlive the stylesheet gate.
|
|
119
|
+
*/
|
|
120
|
+
function readRevealNotBeforeDeadline() {
|
|
121
|
+
const projection = document.querySelector(`[${documentShellStaticAttribute}]`);
|
|
122
|
+
const raw = projection?.getAttribute(documentShellRevealNotBeforeAttribute);
|
|
123
|
+
if (!raw)
|
|
124
|
+
return null;
|
|
125
|
+
const deadline = Number(raw);
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (!Number.isFinite(deadline) || deadline <= now)
|
|
128
|
+
return null;
|
|
129
|
+
const gateDeadline = Number(stylesheet?.dataset.failureDeadline);
|
|
130
|
+
const ceiling = Math.min(now + revealHoldHorizonMs, Number.isFinite(gateDeadline) ? gateDeadline : Number.POSITIVE_INFINITY);
|
|
131
|
+
return deadline <= ceiling ? deadline : null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Defers the loaded reveal until the declared deadline. Wall-clock time
|
|
135
|
+
* preserves the absolute deadline during normal operation, while a monotonic
|
|
136
|
+
* ceiling limits the wait to the delay accepted when it was scheduled. The
|
|
137
|
+
* fired timer is cleared before re-evaluation so an early fire reschedules;
|
|
138
|
+
* a backward wall-clock adjustment cannot extend the inert projection beyond
|
|
139
|
+
* the original bounded delay. The timer shares the handoff cleanup lifecycle.
|
|
140
|
+
*/
|
|
141
|
+
function scheduleRevealHold(deadline) {
|
|
142
|
+
if (pending.holdTimer !== undefined)
|
|
143
|
+
return;
|
|
144
|
+
const maximumDelay = Math.min(revealHoldHorizonMs, Math.max(0, deadline - Date.now()));
|
|
145
|
+
const monotonicDeadline = window.performance.now() + maximumDelay;
|
|
146
|
+
const wait = () => {
|
|
147
|
+
pending.holdTimer = undefined;
|
|
148
|
+
const remaining = Math.min(deadline - Date.now(), monotonicDeadline - window.performance.now());
|
|
149
|
+
if (remaining > 0) {
|
|
150
|
+
pending.holdTimer = window.setTimeout(wait, remaining);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
commitReveal('loaded');
|
|
154
|
+
};
|
|
155
|
+
pending.holdTimer = window.setTimeout(wait, maximumDelay);
|
|
156
|
+
}
|
|
78
157
|
}
|
package/docs/integration.md
CHANGED
|
@@ -179,6 +179,110 @@ The resolved result records whether the runtime stylesheet was `loaded`,
|
|
|
179
179
|
`error`, `timeout`, or `absent`. Every result is a revealed state. Error and
|
|
180
180
|
timeout are fail-open outcomes, not thrown errors.
|
|
181
181
|
|
|
182
|
+
### Declare a reveal-not-before deadline (optional)
|
|
183
|
+
|
|
184
|
+
A projection may reveal part of its content only after a short race with the
|
|
185
|
+
runtime, then keep that delayed content visible for a minimum duration. CSS can
|
|
186
|
+
delay the delayed content's appearance, but it cannot stop the loaded handoff
|
|
187
|
+
from removing the whole projection mid-duration. For that surface the consumer
|
|
188
|
+
declares an absolute reveal-not-before deadline on the static projection at the
|
|
189
|
+
moment the delayed content actually becomes visible; the loaded handoff then
|
|
190
|
+
waits until that deadline before writing the readiness attribute and removing
|
|
191
|
+
the projection.
|
|
192
|
+
|
|
193
|
+
The deadline is a package-owned DOM contract, not a hand-written attribute:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import {
|
|
197
|
+
commitDocumentShellRuntime,
|
|
198
|
+
documentShellRevealNotBeforeAttribute,
|
|
199
|
+
} from '@fullstack-webapp/document-shell/client'
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The consumer's parser startup effect writes an absolute epoch-millisecond
|
|
203
|
+
deadline (`Date.now() + minimumVisibleMs`) into
|
|
204
|
+
`documentShellRevealNotBeforeAttribute` on the element carrying
|
|
205
|
+
`data-document-shell-static`. Declare it only when the delayed content is
|
|
206
|
+
actually visible: when the runtime won the race and the projection was already
|
|
207
|
+
revealed, there is nothing to hold, and a later declaration is simply too late.
|
|
208
|
+
A minimal runtime effect, composed by the renderer, can look like this:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import {
|
|
212
|
+
cssText,
|
|
213
|
+
htmlFragment,
|
|
214
|
+
inlineScript,
|
|
215
|
+
type DocumentShellComposition,
|
|
216
|
+
} from '@fullstack-webapp/document-shell'
|
|
217
|
+
import { documentShellRevealNotBeforeAttribute } from '@fullstack-webapp/document-shell/client'
|
|
218
|
+
|
|
219
|
+
const revealDelayMs = 150 // surface policy, chosen by the consumer
|
|
220
|
+
const minimumVisibleMs = 250 // surface policy, chosen by the consumer
|
|
221
|
+
|
|
222
|
+
const composition: DocumentShellComposition = {
|
|
223
|
+
// ...
|
|
224
|
+
startupEffects: {
|
|
225
|
+
beforePaint: [
|
|
226
|
+
{
|
|
227
|
+
marker: 'data-reveal-declarer',
|
|
228
|
+
script: inlineScript(`
|
|
229
|
+
(() => {
|
|
230
|
+
const revealDelay = ${revealDelayMs}
|
|
231
|
+
const minimumVisible = ${minimumVisibleMs}
|
|
232
|
+
const selector = '[data-document-shell-static]'
|
|
233
|
+
setTimeout(() => {
|
|
234
|
+
const projection = document.querySelector(selector)
|
|
235
|
+
// Still projected at the reveal-delay boundary means the delayed
|
|
236
|
+
// content is becoming visible now: keep it for its minimum
|
|
237
|
+
// visible duration before the handoff may reveal.
|
|
238
|
+
if (projection) {
|
|
239
|
+
projection.setAttribute(
|
|
240
|
+
'${documentShellRevealNotBeforeAttribute}',
|
|
241
|
+
String(Date.now() + minimumVisible),
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}, revealDelay)
|
|
245
|
+
})()
|
|
246
|
+
`),
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
The values shown are examples; the delay and the minimum visible duration are
|
|
254
|
+
consumer surface policy and belong to the surface that owns the pending
|
|
255
|
+
experience, not to Document Shell.
|
|
256
|
+
|
|
257
|
+
Semantics:
|
|
258
|
+
|
|
259
|
+
- Only a normal stylesheet `loaded` handoff can wait for a deadline.
|
|
260
|
+
Stylesheet `error`, `timeout`, and absence keep their immediate fail-open
|
|
261
|
+
reveal and are never blocked by a declaration.
|
|
262
|
+
- The package honors a declaration only when it is a finite absolute timestamp,
|
|
263
|
+
strictly after the current time, within the package's three-second defensive
|
|
264
|
+
hold horizon, and no later than the runtime stylesheet gate's recorded
|
|
265
|
+
fail-open deadline (the same absolute deadline that already bounds the whole
|
|
266
|
+
handoff). Every other value — absent, non-numeric, zero,
|
|
267
|
+
negative, `Infinity`, expired, or too far in the future — is treated as
|
|
268
|
+
expired, and the loaded handoff reveals immediately exactly as without a
|
|
269
|
+
declaration.
|
|
270
|
+
- The deadline is sampled when the loaded reveal is about to run. A
|
|
271
|
+
declaration that lands while the stylesheet is still loading or between the
|
|
272
|
+
load event and the apply frame is honored; a declaration that lands after the
|
|
273
|
+
reveal already ran cannot resurrect the projection.
|
|
274
|
+
- The wait is a package timer with the same cleanup lifecycle as the rest of
|
|
275
|
+
the handoff. Repeated `commitDocumentShellRuntime()` calls return the same
|
|
276
|
+
document-level promise and never create a second timer, and the projection is
|
|
277
|
+
still removed exactly once. The absolute wall-clock deadline is also bounded
|
|
278
|
+
by the monotonic delay accepted when scheduling, so moving the system clock
|
|
279
|
+
backward cannot extend the hold indefinitely.
|
|
280
|
+
|
|
281
|
+
The bounded rule keeps the existing fail-open guarantee intact: an inert
|
|
282
|
+
projection can never outlive the runtime stylesheet gate, so a malformed or
|
|
283
|
+
mistaken declaration cannot pin the overlay past the point the package would
|
|
284
|
+
otherwise have released it.
|
|
285
|
+
|
|
182
286
|
## 6. Keep static and runtime geometry identical
|
|
183
287
|
|
|
184
288
|
The most common integration defect is not lifecycle; it is two shells using
|
|
@@ -323,6 +427,23 @@ The commit hook is mounted too early, or a consumer reimplemented the handoff.
|
|
|
323
427
|
Place it inside the persistent runtime shell and call only
|
|
324
428
|
`commitDocumentShellRuntime()`.
|
|
325
429
|
|
|
430
|
+
### A delayed projection disappears before its minimum visible duration ends
|
|
431
|
+
|
|
432
|
+
The delayed content's appearance is timed with CSS, but the loaded handoff
|
|
433
|
+
still removes the projection as soon as the stylesheet applied. Declare a
|
|
434
|
+
reveal-not-before deadline (see the optional seam above) at the moment the
|
|
435
|
+
delayed content becomes visible instead of adding a competing timer in the
|
|
436
|
+
framework hook.
|
|
437
|
+
|
|
438
|
+
### A declared reveal-not-before deadline never seems to hold
|
|
439
|
+
|
|
440
|
+
The declaration must live on the element carrying `data-document-shell-static`
|
|
441
|
+
as `documentShellRevealNotBeforeAttribute`, hold an absolute epoch-millisecond
|
|
442
|
+
timestamp inside both the bounded hold horizon and the runtime stylesheet gate,
|
|
443
|
+
and land before the loaded reveal runs. A static far-future value in the emitted
|
|
444
|
+
HTML is ignored by design so it cannot pin the overlay; declare it from the startup effect when
|
|
445
|
+
the delayed content is actually visible.
|
|
446
|
+
|
|
326
447
|
### Icons, labels, active state, or tab-bar height still move
|
|
327
448
|
|
|
328
449
|
The static and runtime shells do not share all geometry inputs. Compare font
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fullstack-webapp/document-shell",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "Parser-visible startup shell compiler, Vite HTML pipeline, and framework-neutral runtime handoff for web applications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|