@braidlabs/core 0.1.1 → 0.3.0
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/package.json +2 -2
- package/src/adapters/adapter.d.ts +13 -0
- package/src/adapters/adapter.js.map +1 -1
- package/src/adapters/compat-adapter.d.ts +7 -0
- package/src/adapters/compat-adapter.js +24 -16
- package/src/adapters/compat-adapter.js.map +1 -1
- package/src/adapters/contract-adapter.d.ts +21 -0
- package/src/adapters/contract-adapter.js +71 -0
- package/src/adapters/contract-adapter.js.map +1 -0
- package/src/config.d.ts +29 -0
- package/src/config.js +16 -0
- package/src/config.js.map +1 -1
- package/src/context/context-bus.d.ts +63 -3
- package/src/context/context-bus.js +147 -61
- package/src/context/context-bus.js.map +1 -1
- package/src/elements/fragment-slot.d.ts +114 -1
- package/src/elements/fragment-slot.js +743 -60
- package/src/elements/fragment-slot.js.map +1 -1
- package/src/env/create-env.d.ts +20 -8
- package/src/env/create-env.js +48 -16
- package/src/env/create-env.js.map +1 -1
- package/src/env/define-fragment.d.ts +43 -0
- package/src/env/define-fragment.js +23 -0
- package/src/env/define-fragment.js.map +1 -0
- package/src/env/fragment-env.d.ts +20 -1
- package/src/env/fragment-env.js +0 -12
- package/src/env/fragment-env.js.map +1 -1
- package/src/errors.d.ts +1 -1
- package/src/errors.js.map +1 -1
- package/src/index.d.ts +14 -1
- package/src/index.js +7 -1
- package/src/index.js.map +1 -1
- package/src/protocol.d.ts +20 -4
- package/src/protocol.js +23 -7
- package/src/protocol.js.map +1 -1
- package/src/realm/realm-manager.d.ts +19 -2
- package/src/realm/realm-manager.js +32 -7
- package/src/realm/realm-manager.js.map +1 -1
- package/src/realm/sandbox-realm.d.ts +95 -0
- package/src/realm/sandbox-realm.js +194 -0
- package/src/realm/sandbox-realm.js.map +1 -0
- package/src/weave/capabilities.d.ts +71 -0
- package/src/weave/capabilities.js +61 -0
- package/src/weave/capabilities.js.map +1 -0
- package/src/weave/channel.d.ts +63 -0
- package/src/weave/channel.js +189 -0
- package/src/weave/channel.js.map +1 -0
- package/src/weave/closing.d.ts +99 -0
- package/src/weave/closing.js +130 -0
- package/src/weave/closing.js.map +1 -0
- package/src/weave/context-bridge.d.ts +81 -0
- package/src/weave/context-bridge.js +145 -0
- package/src/weave/context-bridge.js.map +1 -0
- package/src/weave/contract.d.ts +89 -0
- package/src/weave/contract.js +143 -0
- package/src/weave/contract.js.map +1 -0
- package/src/weave/envelope.d.ts +80 -0
- package/src/weave/envelope.js +49 -0
- package/src/weave/envelope.js.map +1 -0
- package/src/weave/guest.d.ts +59 -0
- package/src/weave/guest.js +120 -0
- package/src/weave/guest.js.map +1 -0
- package/src/weave/handshake.d.ts +124 -0
- package/src/weave/handshake.js +109 -0
- package/src/weave/handshake.js.map +1 -0
- package/src/weave/liveness.d.ts +90 -0
- package/src/weave/liveness.js +189 -0
- package/src/weave/liveness.js.map +1 -0
- package/src/weave/messages.d.ts +27 -0
- package/src/weave/messages.js +13 -0
- package/src/weave/messages.js.map +1 -0
- package/src/weave/port-backing.d.ts +19 -0
- package/src/weave/port-backing.js +54 -0
- package/src/weave/port-backing.js.map +1 -0
- package/src/weave/same-realm-backing.d.ts +21 -0
- package/src/weave/same-realm-backing.js +44 -0
- package/src/weave/same-realm-backing.js.map +1 -0
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
import { BraidError } from '../errors.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
/**
|
|
3
|
+
* A context bus.
|
|
4
|
+
*
|
|
5
|
+
* State moved from module scope to instance fields so that a page can hold more than one — see
|
|
6
|
+
* `createContextBus`. Every method body below is unchanged by that move; the semantics that make
|
|
7
|
+
* this a bus rather than an event emitter are the versioning ones, and they are per-bus by nature.
|
|
8
|
+
*/
|
|
9
|
+
class ContextBus {
|
|
10
|
+
#values = new Map();
|
|
11
|
+
#subscriptions = new Map();
|
|
12
|
+
#schemas = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Firehose listeners: notified of *which* key changed, never of the value.
|
|
15
|
+
*
|
|
16
|
+
* This exists for the boundary router, which mirrors the whole bus into a fragment realm and
|
|
17
|
+
* therefore has to learn about keys that did not exist when it attached. It carries no value
|
|
18
|
+
* because every observer needs the value projected to a different version, so handing one out here
|
|
19
|
+
* would be handing out the wrong one.
|
|
20
|
+
*/
|
|
21
|
+
#observers = new Set();
|
|
22
|
+
#notifyObservers(key) {
|
|
23
|
+
for (const observer of [...this.#observers]) {
|
|
24
|
+
try {
|
|
25
|
+
observer(key);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`braid: a context observer threw for "${key}"`, error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
6
32
|
/**
|
|
7
33
|
* Declares the contract a context key carries.
|
|
8
34
|
*
|
|
@@ -11,12 +37,17 @@ export const braidContext = {
|
|
|
11
37
|
* real page.
|
|
12
38
|
*/
|
|
13
39
|
register(key, schema) {
|
|
14
|
-
schemas.set(key, schema);
|
|
15
|
-
|
|
40
|
+
this.#schemas.set(key, schema);
|
|
41
|
+
// A schema registered after a value was published changes how that value projects, and a
|
|
42
|
+
// mirror holding the pre-registration shape would stay wrong until the next `set` — which for
|
|
43
|
+
// a slow-moving key like the signed-in user may be never.
|
|
44
|
+
if (this.#values.has(key))
|
|
45
|
+
this.#notifyObservers(key);
|
|
46
|
+
}
|
|
16
47
|
/** The schema registered for a key, if any. */
|
|
17
48
|
schemaFor(key) {
|
|
18
|
-
return schemas.get(key);
|
|
19
|
-
}
|
|
49
|
+
return this.#schemas.get(key);
|
|
50
|
+
}
|
|
20
51
|
/**
|
|
21
52
|
* Publishes a context value to every fragment (and host code) subscribed to the key.
|
|
22
53
|
*
|
|
@@ -26,10 +57,10 @@ export const braidContext = {
|
|
|
26
57
|
* failing to update, which is the least debuggable shape a bug can take on a composed page.
|
|
27
58
|
*/
|
|
28
59
|
set(key, value) {
|
|
29
|
-
values.set(key, value);
|
|
30
|
-
for (const subscription of [...(subscriptions.get(key) ?? [])]) {
|
|
60
|
+
this.#values.set(key, value);
|
|
61
|
+
for (const subscription of [...(this.#subscriptions.get(key) ?? [])]) {
|
|
31
62
|
try {
|
|
32
|
-
subscription.listener(project(key, value, subscription.as));
|
|
63
|
+
subscription.listener(this.#project(key, value, subscription.as));
|
|
33
64
|
}
|
|
34
65
|
catch (error) {
|
|
35
66
|
// Reported rather than swallowed: a subscriber that never hears anything again is worth
|
|
@@ -37,23 +68,24 @@ export const braidContext = {
|
|
|
37
68
|
console.error(`braid: a "${key}" context subscriber threw; other subscribers were unaffected`, error);
|
|
38
69
|
}
|
|
39
70
|
}
|
|
40
|
-
|
|
71
|
+
this.#notifyObservers(key);
|
|
72
|
+
}
|
|
41
73
|
get(key, options) {
|
|
42
|
-
const value = values.get(key);
|
|
74
|
+
const value = this.#values.get(key);
|
|
43
75
|
if (value === undefined)
|
|
44
76
|
return undefined;
|
|
45
77
|
// A read at an unreachable version is the same error as a subscription at one, and is raised
|
|
46
78
|
// rather than quietly answered at the publisher's version: handing a fragment a shape it cannot
|
|
47
79
|
// parse is how a context bus produces a stack trace in someone else's code.
|
|
48
|
-
assertReachable(key, options?.as, options?.fragmentId);
|
|
49
|
-
return project(key, value, options?.as);
|
|
50
|
-
}
|
|
80
|
+
this.#assertReachable(key, options?.as, options?.fragmentId);
|
|
81
|
+
return this.#project(key, value, options?.as);
|
|
82
|
+
}
|
|
51
83
|
subscribe(key, listener, options) {
|
|
52
|
-
assertReachable(key, options?.as, options?.fragmentId);
|
|
53
|
-
let keySubscriptions = subscriptions.get(key);
|
|
84
|
+
this.#assertReachable(key, options?.as, options?.fragmentId);
|
|
85
|
+
let keySubscriptions = this.#subscriptions.get(key);
|
|
54
86
|
if (!keySubscriptions) {
|
|
55
87
|
keySubscriptions = new Set();
|
|
56
|
-
subscriptions.set(key, keySubscriptions);
|
|
88
|
+
this.#subscriptions.set(key, keySubscriptions);
|
|
57
89
|
}
|
|
58
90
|
const subscription = {
|
|
59
91
|
listener,
|
|
@@ -63,55 +95,109 @@ export const braidContext = {
|
|
|
63
95
|
const unsubscribe = () => void keySubscriptions.delete(subscription);
|
|
64
96
|
options?.signal?.addEventListener('abort', unsubscribe, { once: true });
|
|
65
97
|
return unsubscribe;
|
|
66
|
-
}
|
|
98
|
+
}
|
|
99
|
+
/** Every key that currently holds a value. Used to seed a fragment's mirror at handshake. */
|
|
100
|
+
keys() {
|
|
101
|
+
return [...this.#values.keys()];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Observes *which* keys change, including keys that do not exist yet.
|
|
105
|
+
*
|
|
106
|
+
* `subscribe` cannot serve this: it binds to one key by name, so a mirror built from it would
|
|
107
|
+
* silently miss every key first published after the fragment mounted.
|
|
108
|
+
*/
|
|
109
|
+
observe(listener) {
|
|
110
|
+
this.#observers.add(listener);
|
|
111
|
+
return () => void this.#observers.delete(listener);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Raises if `as` cannot be projected to from the published version.
|
|
115
|
+
*
|
|
116
|
+
* The check `subscribe` runs, exposed so it can be run *earlier* — at the boundary handshake,
|
|
117
|
+
* before a fragment has been handed any state. Decided from the declared chain rather than from
|
|
118
|
+
* a stored value, so a fragment that mounts before the first broadcast gets the same answer as
|
|
119
|
+
* one that mounts after it.
|
|
120
|
+
*/
|
|
121
|
+
assertReachable(key, as, fragmentId) {
|
|
122
|
+
this.#assertReachable(key, as, fragmentId);
|
|
123
|
+
}
|
|
67
124
|
/** Exposed for tests. */
|
|
68
125
|
clear() {
|
|
69
|
-
values.clear();
|
|
70
|
-
subscriptions.clear();
|
|
71
|
-
schemas.clear();
|
|
72
|
-
|
|
73
|
-
}
|
|
126
|
+
this.#values.clear();
|
|
127
|
+
this.#subscriptions.clear();
|
|
128
|
+
this.#schemas.clear();
|
|
129
|
+
this.#observers.clear();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Projects a published value to one subscriber's version.
|
|
133
|
+
*
|
|
134
|
+
* `write({ as })` down-migrates and returns the older envelope — the polite move when the reader is
|
|
135
|
+
* known to be older than the publisher. Subscribers receive the payload rather than the envelope:
|
|
136
|
+
* the version they asked for is the version they get, so handing them a wrapper to unwrap would be
|
|
137
|
+
* ceremony without information.
|
|
138
|
+
*/
|
|
139
|
+
#project(key, value, as) {
|
|
140
|
+
const schema = this.#schemas.get(key);
|
|
141
|
+
if (!schema)
|
|
142
|
+
return structuredClone(value);
|
|
143
|
+
const envelope = as === undefined || as === schema.version ? schema.write(value) : schema.write(value, { as });
|
|
144
|
+
return structuredClone(envelope.payload);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Refuses a subscriber whose version cannot be reached from the publisher's.
|
|
148
|
+
*
|
|
149
|
+
* Decided from the declared chain rather than by attempting a migration, so the answer does not
|
|
150
|
+
* depend on a value happening to have been published yet — a fragment that mounts before the first
|
|
151
|
+
* broadcast deserves the same error as one that mounts after it.
|
|
152
|
+
*/
|
|
153
|
+
#assertReachable(key, as, fragmentId) {
|
|
154
|
+
const schema = this.#schemas.get(key);
|
|
155
|
+
if (!schema || as === undefined || as === schema.version)
|
|
156
|
+
return;
|
|
157
|
+
const raise = (message, fixHint) => {
|
|
158
|
+
throw new BraidError(message, { fragmentId: fragmentId ?? key, stage: 'context-version', fixHint });
|
|
159
|
+
};
|
|
160
|
+
if (as > schema.version) {
|
|
161
|
+
raise(`context "${key}" is published at v${schema.version}, and this subscriber asked for v${as}`, 'the publisher is older than the subscriber — deploy the host, or subscribe at a version it can produce');
|
|
162
|
+
}
|
|
163
|
+
if (as < 1)
|
|
164
|
+
raise(`context "${key}" has no v${as}`, 'contract versions start at 1');
|
|
165
|
+
// Every step between the subscriber's version and the publisher's has to walk back down.
|
|
166
|
+
const missing = schema.steps
|
|
167
|
+
.filter((step) => step.to > as && step.to <= schema.version && !step.down)
|
|
168
|
+
.map((step) => `v${step.to} (${step.description})`);
|
|
169
|
+
if (missing.length > 0) {
|
|
170
|
+
raise(`context "${key}" cannot be projected from v${schema.version} down to v${as}: ` +
|
|
171
|
+
`${missing.join(', ')} ${missing.length === 1 ? 'declares' : 'declare'} no down migration`, `add a down migration to the step that introduced ${missing[0]}, or subscribe at v${schema.version}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
74
175
|
/**
|
|
75
|
-
*
|
|
176
|
+
* The page-level context bus: what `<fragment-slot>` routes to unless a fragment is scoped elsewhere.
|
|
76
177
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* ceremony without information.
|
|
178
|
+
* Still a singleton, still the default, and every existing import of it behaves exactly as before.
|
|
179
|
+
* What changed is that it is now *an instance of something*, which is what makes a second bus
|
|
180
|
+
* possible at all.
|
|
81
181
|
*/
|
|
82
|
-
|
|
83
|
-
const schema = schemas.get(key);
|
|
84
|
-
if (!schema)
|
|
85
|
-
return structuredClone(value);
|
|
86
|
-
const envelope = as === undefined || as === schema.version ? schema.write(value) : schema.write(value, { as });
|
|
87
|
-
return structuredClone(envelope.payload);
|
|
88
|
-
}
|
|
182
|
+
export const braidContext = new ContextBus();
|
|
89
183
|
/**
|
|
90
|
-
*
|
|
184
|
+
* Creates a bus a fragment owns, rather than one it merely subscribes to.
|
|
185
|
+
*
|
|
186
|
+
* Keys published here never reach the page bus and are never visible to any other fragment — not
|
|
187
|
+
* their values, not their names, not the timing of their changes. Two uses, and they are the two
|
|
188
|
+
* readings the plan left open:
|
|
189
|
+
*
|
|
190
|
+
* - **A private namespace.** A fragment that wants durable, versioned, cross-realm state that is
|
|
191
|
+
* nobody else's business gets it without inventing a second storage mechanism, and without the
|
|
192
|
+
* host having to grant or withhold anything.
|
|
193
|
+
* - **A subtree bus**, once nested fragments exist: a parent fragment hands its own bus to the
|
|
194
|
+
* fragments it composes, and they coordinate below the host's notice.
|
|
91
195
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
196
|
+
* The first works today. The second needs nested fragments to be a first-class concept, which is a
|
|
197
|
+
* larger commitment and is not made here — but nothing about this signature has to change when it
|
|
198
|
+
* is, because the routing already addresses buses by `scope` rather than by position.
|
|
95
199
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
if (!schema || as === undefined || as === schema.version)
|
|
99
|
-
return;
|
|
100
|
-
const raise = (message, fixHint) => {
|
|
101
|
-
throw new BraidError(message, { fragmentId: fragmentId ?? key, stage: 'context-version', fixHint });
|
|
102
|
-
};
|
|
103
|
-
if (as > schema.version) {
|
|
104
|
-
raise(`context "${key}" is published at v${schema.version}, and this subscriber asked for v${as}`, 'the publisher is older than the subscriber — deploy the host, or subscribe at a version it can produce');
|
|
105
|
-
}
|
|
106
|
-
if (as < 1)
|
|
107
|
-
raise(`context "${key}" has no v${as}`, 'contract versions start at 1');
|
|
108
|
-
// Every step between the subscriber's version and the publisher's has to walk back down.
|
|
109
|
-
const missing = schema.steps
|
|
110
|
-
.filter((step) => step.to > as && step.to <= schema.version && !step.down)
|
|
111
|
-
.map((step) => `v${step.to} (${step.description})`);
|
|
112
|
-
if (missing.length > 0) {
|
|
113
|
-
raise(`context "${key}" cannot be projected from v${schema.version} down to v${as}: ` +
|
|
114
|
-
`${missing.join(', ')} ${missing.length === 1 ? 'declares' : 'declare'} no down migration`, `add a down migration to the step that introduced ${missing[0]}, or subscribe at v${schema.version}`);
|
|
115
|
-
}
|
|
200
|
+
export function createContextBus() {
|
|
201
|
+
return new ContextBus();
|
|
116
202
|
}
|
|
117
203
|
//# sourceMappingURL=context-bus.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-bus.js","sourceRoot":"","sources":["../../../../../libs/core/src/context/context-bus.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAmD1C,MAAM,
|
|
1
|
+
{"version":3,"file":"context-bus.js","sourceRoot":"","sources":["../../../../../libs/core/src/context/context-bus.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
import { BraidError } from '../errors.js';
|
|
2
|
+
import { LivenessState } from '../weave/liveness.js';
|
|
3
|
+
/**
|
|
4
|
+
* What the slot reports about its fragment.
|
|
5
|
+
*
|
|
6
|
+
* The original four are all still here and still mean what they meant — `ready` in particular is
|
|
7
|
+
* kept as the name for "mounted, first beat not yet observed", which the liveness machine calls
|
|
8
|
+
* `connecting`. Renaming it would have broken every host checking `slot.state === 'ready'` in
|
|
9
|
+
* exchange for a word.
|
|
10
|
+
*
|
|
11
|
+
* The new three are the ones `ready` used to have to cover on its own:
|
|
12
|
+
*
|
|
13
|
+
* - `healthy` — beating.
|
|
14
|
+
* - `unobservable` — the tab is hidden, so the host has stopped being able to tell. Not a fault.
|
|
15
|
+
* - `suspect` → `gone` — beats stopped while the fragment should have been running.
|
|
16
|
+
*/
|
|
17
|
+
export type FragmentSlotState = 'idle' | 'loading' | 'ready' | 'error' | 'closing' | 'healthy' | 'unobservable' | 'suspect' | 'gone';
|
|
18
|
+
/** `connecting` is reported as `ready`; every other liveness state carries its own name. */
|
|
19
|
+
export declare function slotStateFor(liveness: LivenessState): FragmentSlotState;
|
|
2
20
|
/**
|
|
3
21
|
* Finds server-rendered fragment content the gateway pierced into a slot.
|
|
4
22
|
*
|
|
@@ -8,6 +26,28 @@ export type FragmentSlotState = 'idle' | 'loading' | 'ready' | 'error';
|
|
|
8
26
|
* and replaces identical content — so it is worth a named helper and a test.
|
|
9
27
|
*/
|
|
10
28
|
export declare function findPiercedContentRoot(shadowRoot: ShadowRoot | null): HTMLElement | null;
|
|
29
|
+
/**
|
|
30
|
+
* The slot's own styles, including the fallback rules.
|
|
31
|
+
*
|
|
32
|
+
* `gone` and `error` hide the fragment's content and reveal whatever the host put in the fallback
|
|
33
|
+
* slot. Note what is *not* hidden: `suspect`. A fragment that is merely slow keeps its rendered
|
|
34
|
+
* content on screen, because replacing a working-if-sluggish UI with an apology is a worse outcome
|
|
35
|
+
* than a moment of unresponsiveness — and suspicion is designed to be recoverable.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SLOT_STYLES: string;
|
|
38
|
+
/**
|
|
39
|
+
* Ensures the shadow root can present host-provided fallback content:
|
|
40
|
+
*
|
|
41
|
+
* ```html
|
|
42
|
+
* <fragment-slot name="checkout"><p slot="fallback">Checkout is unavailable.</p></fragment-slot>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Added separately from the styles because pierced fragments arrive with a shadow root the gateway
|
|
46
|
+
* wrote, which has content but no fallback slot — and a fallback that only worked for
|
|
47
|
+
* client-rendered fragments would be missing from exactly the pages that server-render because
|
|
48
|
+
* their first paint matters.
|
|
49
|
+
*/
|
|
50
|
+
export declare function ensureFallbackSlot(shadowRoot: ShadowRoot): void;
|
|
11
51
|
/**
|
|
12
52
|
* `<fragment-slot>` — the custom element a host renders to mount a fragment.
|
|
13
53
|
*
|
|
@@ -30,6 +70,19 @@ export declare class FragmentSlot extends HTMLElement {
|
|
|
30
70
|
static get observedAttributes(): string[];
|
|
31
71
|
get state(): FragmentSlotState;
|
|
32
72
|
get name(): string;
|
|
73
|
+
/**
|
|
74
|
+
* The fragment's unsaved-work declaration, or null.
|
|
75
|
+
*
|
|
76
|
+
* Readable synchronously because the only place a host can act on it — a `beforeunload`
|
|
77
|
+
* handler — cannot await anything. Braid never installs that handler itself:
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* addEventListener('beforeunload', (event) => {
|
|
81
|
+
* if (slot.dirty) event.preventDefault();
|
|
82
|
+
* });
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
get dirty(): string | null;
|
|
33
86
|
get props(): Record<string, unknown>;
|
|
34
87
|
set props(value: Record<string, unknown>);
|
|
35
88
|
attributeChangedCallback(attribute: string, oldValue: string | null, newValue: string | null): void;
|
|
@@ -37,3 +90,63 @@ export declare class FragmentSlot extends HTMLElement {
|
|
|
37
90
|
disconnectedCallback(): void;
|
|
38
91
|
reload(): Promise<void>;
|
|
39
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Fetches the fragment's document from the gateway's document namespace.
|
|
95
|
+
*
|
|
96
|
+
* Redirects are refused rather than followed. Followed, a cross-origin one (an auth redirect to an
|
|
97
|
+
* identity provider, say) dies as an opaque CORS error that hides the status that caused it, and a
|
|
98
|
+
* relative one resolves against the gateway's origin — outside the namespace — so the shell's or a
|
|
99
|
+
* login page's html would be injected as the fragment's document without any error at all.
|
|
100
|
+
*
|
|
101
|
+
* A browser hands back an `opaqueredirect` for `redirect: 'manual'` (status 0, no readable
|
|
102
|
+
* `Location`); other runtimes expose the real 3xx, so the target is named when it can be read.
|
|
103
|
+
*
|
|
104
|
+
* A fragment that opted in to `redirect: 'navigate'` is the exception: the gateway answers with a
|
|
105
|
+
* `409` carrying the target, which becomes a {@link FragmentRedirectError} for the slot to follow.
|
|
106
|
+
*
|
|
107
|
+
* Resolves with the document and whether it arrived as HTML — which, through the gateway, is the one
|
|
108
|
+
* kind of document it prepares (scripts inert, handlers stripped). Anything else is not safe to put
|
|
109
|
+
* on screen ahead of the adapter.
|
|
110
|
+
*/
|
|
111
|
+
export declare function fetchFragmentHtml(fragmentId: string, routeSrcUrl: URL, signal: AbortSignal): Promise<{
|
|
112
|
+
html: string;
|
|
113
|
+
isHtml: boolean;
|
|
114
|
+
}>;
|
|
115
|
+
/**
|
|
116
|
+
* The gateway relayed a redirect from the fragment and vouched for its target. Carries the full
|
|
117
|
+
* target; the message does not, because a login's query string is where its tokens live and an
|
|
118
|
+
* error message ends up in the console and in the `braid:error` event.
|
|
119
|
+
*/
|
|
120
|
+
export declare class FragmentRedirectError extends BraidError {
|
|
121
|
+
readonly target: string;
|
|
122
|
+
constructor(fragmentId: string, target: string, documentUrl: string);
|
|
123
|
+
}
|
|
124
|
+
/** How long a second redirect from the same fragment is treated as a loop rather than a new one. */
|
|
125
|
+
export declare const REDIRECT_LOOP_WINDOW_MS = 30000;
|
|
126
|
+
export interface RedirectDeps {
|
|
127
|
+
/** Navigates the top-level page. */
|
|
128
|
+
assign(url: string): void;
|
|
129
|
+
/** `sessionStorage`, as a thunk: merely reaching for it throws in some privacy modes. */
|
|
130
|
+
storage(): Pick<Storage, 'getItem' | 'setItem'>;
|
|
131
|
+
now(): number;
|
|
132
|
+
/** What a relative target is relative to. */
|
|
133
|
+
baseHref: string;
|
|
134
|
+
/**
|
|
135
|
+
* Fragments whose redirect this page is already following. Two slots for one fragment hear the
|
|
136
|
+
* same redirect in the same tick; the second is not a loop, it is the same navigation.
|
|
137
|
+
*/
|
|
138
|
+
inFlight: Set<string>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Sends the page where the fragment's redirect asked, once.
|
|
142
|
+
*
|
|
143
|
+
* @returns `null` when the page is navigating, or the error to report when it is not.
|
|
144
|
+
*
|
|
145
|
+
* Two things stop it. The scheme must be http(s) — the gateway already checks, and this is the one
|
|
146
|
+
* line between a `javascript:` URL and `location.assign`. And a fragment that redirects again
|
|
147
|
+
* within {@link REDIRECT_LOOP_WINDOW_MS} is a loop: login succeeded, the user came back, and the
|
|
148
|
+
* fragment still cannot tell who they are. A page reload loses all in-memory state, so the only
|
|
149
|
+
* thing that can remember the last redirect is `sessionStorage`; if that is unavailable, the answer
|
|
150
|
+
* is no — an unbounded loop is worse than an error.
|
|
151
|
+
*/
|
|
152
|
+
export declare function followFragmentRedirect(error: FragmentRedirectError, deps: RedirectDeps): BraidError | null;
|