@webjsdev/core 0.8.0 → 0.9.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.
Files changed (81) hide show
  1. package/README.md +5 -0
  2. package/dist/webjs-core-browser.js +6 -5
  3. package/dist/webjs-core-browser.js.map +3 -3
  4. package/dist/webjs-core-testing.js +20 -19
  5. package/dist/webjs-core-testing.js.map +3 -3
  6. package/dist/webjs-core.js +7 -6
  7. package/dist/webjs-core.js.map +3 -3
  8. package/index-browser.js +14 -9
  9. package/index.d.ts +22 -10
  10. package/index.js +17 -9
  11. package/package.json +15 -2
  12. package/src/action-effects-client.d.ts +5 -0
  13. package/src/action-effects-client.js +84 -0
  14. package/src/action-seed-client.js +5 -5
  15. package/src/asset-url.js +12 -13
  16. package/src/async-directive.d.ts +18 -0
  17. package/src/async-directive.js +88 -0
  18. package/src/attribute-reader.js +197 -23
  19. package/src/binding-prefixes.js +0 -12
  20. package/src/component/lifecycle.js +760 -209
  21. package/src/component/reactive.js +2 -10
  22. package/src/component/server-element.js +23 -1
  23. package/src/component.d.ts +151 -41
  24. package/src/context.d.ts +94 -18
  25. package/src/context.js +341 -217
  26. package/src/converters.d.ts +7 -0
  27. package/src/converters.js +38 -0
  28. package/src/css.d.ts +32 -6
  29. package/src/css.js +178 -14
  30. package/src/directive.d.ts +58 -0
  31. package/src/directive.js +119 -0
  32. package/src/directives.d.ts +38 -1
  33. package/src/directives.js +158 -1
  34. package/src/form-action.d.ts +13 -0
  35. package/src/form-action.js +139 -4
  36. package/src/hot.js +23 -0
  37. package/src/html.d.ts +4 -0
  38. package/src/html.js +35 -3
  39. package/src/is-server.d.ts +2 -0
  40. package/src/is-server.js +18 -0
  41. package/src/jsx-guard.js +130 -0
  42. package/src/metadata.d.ts +22 -3
  43. package/src/nav.d.ts +4 -1
  44. package/src/nav.js +44 -11
  45. package/src/optimistic.js +2 -2
  46. package/src/refresh-header.js +52 -0
  47. package/src/registry.js +10 -1
  48. package/src/render-client/adopt-slots.js +8 -3
  49. package/src/render-client/parts.js +745 -31
  50. package/src/render-client/reconciler.js +56 -11
  51. package/src/render-client/template-compiler.js +139 -44
  52. package/src/render-client.d.ts +5 -1
  53. package/src/render-server/attrs.js +2 -2
  54. package/src/render-server/compile.js +34 -0
  55. package/src/render-server/errors.js +37 -0
  56. package/src/render-server/expand.js +61 -8
  57. package/src/render-server/served-attrs.js +1 -2
  58. package/src/render-server/walk.js +135 -3
  59. package/src/render-server.d.ts +3 -0
  60. package/src/render-server.js +2 -0
  61. package/src/repeat.d.ts +7 -2
  62. package/src/repeat.js +20 -6
  63. package/src/router-client/dom-differ.js +17 -0
  64. package/src/router-client/fetch-apply.js +86 -5
  65. package/src/router-client/navigator.js +45 -9
  66. package/src/router-client/prefetch.js +3 -4
  67. package/src/router-client/root-attrs.js +43 -0
  68. package/src/router-client/stream.js +7 -0
  69. package/src/router-client/swap.js +4 -1
  70. package/src/routes.d.ts +75 -18
  71. package/src/serialize.d.ts +2 -0
  72. package/src/serialize.js +35 -4
  73. package/src/signal.d.ts +7 -0
  74. package/src/signal.js +26 -6
  75. package/src/slot/polyfills.js +3 -3
  76. package/src/slot.js +4 -8
  77. package/src/static-html.d.ts +22 -0
  78. package/src/static-html.js +144 -0
  79. package/src/task.d.ts +63 -20
  80. package/src/task.js +326 -293
  81. package/src/webjs-config.d.ts +1 -8
package/src/context.js CHANGED
@@ -1,28 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Portions derived from Lit (`@lit/context`), Copyright Google LLC, BSD-3-Clause.
4
+ * https://github.com/lit/lit/blob/main/packages/context/LICENSE
5
+ */
6
+
1
7
  /**
2
8
  * Context Protocol: cross-component data sharing without prop drilling.
3
9
  *
4
- * Implements the W3C Community Group Context Protocol so any web component
5
- * (not just WebJs components) can participate as a provider or consumer.
10
+ * A port of Lit's `@lit/context`, which implements the Web Components
11
+ * Community Group Context Protocol, so any web component (not only a WebJs
12
+ * one) can take part, and a component written from Lit memory behaves the
13
+ * same under the `@webjsdev/core/context` import.
6
14
  *
7
15
  * ## When to use
8
16
  *
9
- * Use Context when you need to share data (theme, auth state, locale,
10
- * config, feature flags) across deeply nested components without passing
11
- * props through every intermediate level.
12
- *
13
- * Prefer Context over module-level globals when the shared data is **scoped
14
- * to a subtree**: e.g. a specific page, panel, or dialog: and different
15
- * subtrees may hold different values for the same context key.
16
- *
17
- * ## When NOT to use
18
- *
19
- * - For page-level data loading, use async page functions or server actions
20
- * instead. Context is a client-side primitive.
21
- * - If only a parent and its direct child need to communicate, plain
22
- * properties/attributes are simpler.
23
- * - If every component in the app needs the same single value (e.g. a
24
- * singleton API client), a module-level export may be simpler than
25
- * context.
17
+ * Share data (theme, auth state, locale, config) with deeply nested
18
+ * components without passing it through every level. Prefer it over a
19
+ * module-level signal when the value is SCOPED to a subtree, so different
20
+ * subtrees can hold different values for the same context.
26
21
  *
27
22
  * ## Quick example
28
23
  *
@@ -30,15 +25,17 @@
30
25
  * import { WebComponent, html } from '@webjsdev/core';
31
26
  * import { createContext, ContextProvider, ContextConsumer } from '@webjsdev/core/context';
32
27
  *
33
- * const ThemeCtx = createContext('theme');
28
+ * // The KEY is the identity: the same string (or the same Symbol) in two
29
+ * // modules is the same context.
30
+ * export const themeContext = createContext('theme');
34
31
  *
35
32
  * class MyApp extends WebComponent {
36
- * _theme = new ContextProvider(this, { context: ThemeCtx, initialValue: 'light' });
33
+ * _theme = new ContextProvider(this, { context: themeContext, initialValue: 'light' });
37
34
  * render() {
38
35
  * return html`
39
- * <button @click=${() => this._theme.setValue(
40
- * this._theme.value === 'light' ? 'dark' : 'light'
41
- * )}>Toggle</button>
36
+ * <button @click=${() => this._theme.setValue(this._theme.value === 'light' ? 'dark' : 'light')}>
37
+ * Toggle
38
+ * </button>
42
39
  * <slot></slot>
43
40
  * `;
44
41
  * }
@@ -46,7 +43,7 @@
46
43
  * MyApp.register('my-app');
47
44
  *
48
45
  * class ThemedCard extends WebComponent {
49
- * _theme = new ContextConsumer(this, { context: ThemeCtx, subscribe: true });
46
+ * _theme = new ContextConsumer(this, { context: themeContext, subscribe: true });
50
47
  * render() {
51
48
  * return html`<div class=${this._theme.value}>Card content</div>`;
52
49
  * }
@@ -54,176 +51,237 @@
54
51
  * ThemedCard.register('themed-card');
55
52
  * ```
56
53
  *
57
- * `<themed-card>` can live at any depth under `<my-app>`: it finds the
58
- * provider automatically via a bubbling `context-request` event that
59
- * crosses shadow-DOM boundaries (`composed: true`).
60
- *
61
54
  * @module
62
55
  */
63
56
 
64
57
  // ---------------------------------------------------------------------------
65
- // Context key
58
+ // createContext
66
59
  // ---------------------------------------------------------------------------
67
60
 
68
61
  /**
69
- * @template T
70
- * @typedef {{ __context__: typeof CONTEXT_KEY, name: string }} Context
71
- */
72
-
73
- const CONTEXT_KEY = Symbol.for('webjs.context');
74
-
75
- /**
76
- * Create a typed context key.
77
- *
78
- * The returned object is used as an identity token: two calls to
79
- * `createContext('theme')` produce **different** contexts even though
80
- * the debug name is the same. Store the key in a shared module and
81
- * import it from both provider and consumer.
62
+ * Create a context key. It returns `key` itself, typed with the value type,
63
+ * exactly as Lit does: the key IS the identity, so `createContext('theme')`
64
+ * in two modules produces the same context. Use a `Symbol('theme')` exported
65
+ * from one module when the context must not collide with any other.
82
66
  *
83
- * @template T
84
- * @param {string} name Human-readable name (used in error messages and
85
- * devtools, not for matching).
86
- * @returns {Context<T>}
67
+ * @template ValueType
68
+ * @template [K=unknown]
69
+ * @param {K} key
70
+ * @returns {K}
87
71
  */
88
- export function createContext(name) {
89
- return { __context__: CONTEXT_KEY, name };
72
+ export function createContext(key) {
73
+ return key;
90
74
  }
91
75
 
92
76
  // ---------------------------------------------------------------------------
93
- // ContextRequestEvent
77
+ // Events
94
78
  // ---------------------------------------------------------------------------
95
79
 
96
80
  /**
97
- * Fired by a consumer to locate a provider higher in the DOM.
98
- *
99
- * - `bubbles: true` so it walks up the DOM tree.
100
- * - `composed: true` so it crosses shadow-DOM boundaries.
101
- *
102
- * Providers listen for this event on their host element and respond by
103
- * calling the event's `callback` with the current value.
104
- *
105
- * @template T
81
+ * Fired by a consumer to ask for a value. It bubbles and is composed, so it
82
+ * crosses shadow roots on its way to the nearest provider.
106
83
  */
107
84
  export class ContextRequestEvent extends Event {
108
85
  /**
109
- * @param {Context<T>} context The context key to request.
110
- * @param {(value: T, unsubscribe?: () => void) => void} callback
111
- * Called by the provider with the current value. If `subscribe` is
112
- * true the provider retains the callback and calls it again whenever
113
- * the value changes (passing an `unsubscribe` function on each call).
114
- * @param {boolean} subscribe Whether the consumer wants ongoing updates.
86
+ * @param {unknown} context The context key.
87
+ * @param {Element} contextTarget The element asking (the consumer's host).
88
+ * @param {(value: any, unsubscribe?: () => void) => void} callback
89
+ * Called by the provider with the value. A subscribing request also gets
90
+ * an `unsubscribe` function, and is called again on every change.
91
+ * @param {boolean} [subscribe=false] Whether to keep receiving changes.
115
92
  */
116
- constructor(context, callback, subscribe = false) {
93
+ constructor(context, contextTarget, callback, subscribe) {
117
94
  super('context-request', { bubbles: true, composed: true });
118
- /** @type {Context<T>} */
95
+ /** @readonly */
119
96
  this.context = context;
120
- /** @type {(value: T, unsubscribe?: () => void) => void} */
97
+ /** @readonly */
98
+ this.contextTarget = contextTarget;
99
+ /** @readonly */
121
100
  this.callback = callback;
122
- /** @type {boolean} */
123
- this.subscribe = subscribe;
101
+ /** @readonly */
102
+ this.subscribe = subscribe ?? false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Fired by a provider when it connects, so a provider ABOVE it can hand over
108
+ * the subscriptions that now belong to the nearer provider, and so a
109
+ * `ContextRoot` can replay requests that arrived before any provider existed.
110
+ */
111
+ export class ContextProviderEvent extends Event {
112
+ /**
113
+ * @param {unknown} context The context key.
114
+ * @param {Element} contextTarget The provider's host.
115
+ */
116
+ constructor(context, contextTarget) {
117
+ super('context-provider', { bubbles: true, composed: true });
118
+ /** @readonly */
119
+ this.context = context;
120
+ /** @readonly */
121
+ this.contextTarget = contextTarget;
124
122
  }
125
123
  }
126
124
 
127
125
  // ---------------------------------------------------------------------------
128
- // ContextProvider
126
+ // ValueNotifier (the provider's value + subscriber list)
129
127
  // ---------------------------------------------------------------------------
130
128
 
131
129
  /**
130
+ * Holds a value and the callbacks subscribed to it. The base of
131
+ * {@link ContextProvider}, as in Lit.
132
132
  * @template T
133
- * @typedef {{ callback: (value: T, unsubscribe?: () => void) => void }} Subscription
134
133
  */
134
+ export class ValueNotifier {
135
+ /** @param {T} [defaultValue] */
136
+ constructor(defaultValue) {
137
+ /**
138
+ * Subscribed callbacks, each with its disposer and the consumer host it
139
+ * came from (used to re-dispatch when a nearer provider appears).
140
+ * @type {Map<Function, { disposer: () => void, consumerHost: Element }>}
141
+ */
142
+ this.subscriptions = new Map();
143
+ /** @type {T} */
144
+ this._value = /** @type {T} */ (undefined);
145
+ if (defaultValue !== undefined) this.value = defaultValue;
146
+ }
147
+
148
+ /** @returns {T} */
149
+ get value() {
150
+ return this._value;
151
+ }
152
+
153
+ /** @param {T} v */
154
+ set value(v) {
155
+ this.setValue(v);
156
+ }
157
+
158
+ /**
159
+ * Set the value and, when it changed (or `force` is true), call every
160
+ * subscriber with it.
161
+ * @param {T} v
162
+ * @param {boolean} [force=false]
163
+ */
164
+ setValue(v, force = false) {
165
+ const update = force || !Object.is(v, this._value);
166
+ this._value = v;
167
+ if (update) this.updateObservers();
168
+ }
169
+
170
+ /** Call every subscriber with the current value. */
171
+ updateObservers = () => {
172
+ for (const [callback, { disposer }] of this.subscriptions) {
173
+ callback(this._value, disposer);
174
+ }
175
+ };
176
+
177
+ /**
178
+ * Answer a request: call `callback` with the value, and keep it for later
179
+ * changes when `subscribe` is true.
180
+ * @param {(value: T, unsubscribe?: () => void) => void} callback
181
+ * @param {Element} consumerHost
182
+ * @param {boolean} [subscribe]
183
+ */
184
+ addCallback(callback, consumerHost, subscribe) {
185
+ if (!subscribe) {
186
+ callback(this.value);
187
+ return;
188
+ }
189
+ if (!this.subscriptions.has(callback)) {
190
+ this.subscriptions.set(callback, {
191
+ disposer: () => {
192
+ this.subscriptions.delete(callback);
193
+ },
194
+ consumerHost,
195
+ });
196
+ }
197
+ const { disposer } = /** @type {{ disposer: () => void }} */ (this.subscriptions.get(callback));
198
+ callback(this.value, disposer);
199
+ }
200
+
201
+ /** Drop every subscriber. */
202
+ clearCallbacks() {
203
+ this.subscriptions.clear();
204
+ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // ContextProvider
209
+ // ---------------------------------------------------------------------------
135
210
 
136
211
  /**
137
- * A ReactiveController that provides a context value to descendant
138
- * components.
212
+ * A ReactiveController that provides a value to descendants. It listens on
213
+ * its host from construction, answers matching `context-request` events from
214
+ * descendants (never its own host's), and on connect fires
215
+ * `context-provider` so an outer provider of the same context hands over the
216
+ * subscribers that are now nearer to this one.
139
217
  *
140
- * The provider listens for `context-request` events on its host element.
141
- * When a matching request arrives it delivers the current value (and,
142
- * for subscribing consumers, retains the callback for future updates).
143
- *
144
- * ## AI guidance
145
- *
146
- * - Create one `ContextProvider` per context key per component.
147
- * - Call `setValue()` to push updates to all subscribing consumers.
148
- * - The provider **must** be a DOM ancestor of its consumers (shadow-DOM
149
- * depth does not matter thanks to `composed: true`).
218
+ * `new ContextProvider(host, { context, initialValue })` or
219
+ * `new ContextProvider(host, context, initialValue)`.
150
220
  *
151
221
  * @template T
222
+ * @extends {ValueNotifier<T>}
152
223
  */
153
- export class ContextProvider {
224
+ export class ContextProvider extends ValueNotifier {
154
225
  /**
155
- * @param {import('./component.js').WebComponent} host
156
- * The host component that owns this provider.
157
- * @param {{ context: Context<T>, initialValue?: T }} options
226
+ * @param {any} host The host element (a WebComponent or any element).
227
+ * @param {any} contextOrOptions `{ context, initialValue }`, or the context.
228
+ * @param {T} [initialValue] The value, positional form only.
158
229
  */
159
- constructor(host, { context, initialValue }) {
160
- /** @type {import('./component.js').WebComponent} */
161
- this._host = host;
162
- /** @type {Context<T>} */
163
- this._context = context;
164
- /** @type {T} */
165
- this._value = /** @type {T} */ (initialValue);
166
- /** @type {Set<Subscription<T>>} */
167
- this._subscriptions = new Set();
168
-
169
- /** @type {(e: Event) => void} */
170
- this._onRequest = (e) => {
171
- const evt = /** @type {ContextRequestEvent<T>} */ (e);
172
- if (evt.context !== this._context) return;
173
-
174
- // Prevent further providers higher in the tree from also responding.
175
- e.stopPropagation();
176
-
177
- if (evt.subscribe) {
178
- /** @type {Subscription<T>} */
179
- const sub = { callback: evt.callback };
180
- const unsubscribe = () => { this._subscriptions.delete(sub); };
181
- this._subscriptions.add(sub);
182
- evt.callback(this._value, unsubscribe);
183
- } else {
184
- evt.callback(this._value);
185
- }
186
- };
187
-
188
- // Follow the ReactiveController protocol.
189
- if (typeof host.addController === 'function') {
190
- host.addController(this);
191
- }
230
+ constructor(host, contextOrOptions, initialValue) {
231
+ const isOptions = contextOrOptions != null && contextOrOptions.context !== undefined;
232
+ super(isOptions ? contextOrOptions.initialValue : initialValue);
233
+ /** @protected */
234
+ this.host = host;
235
+ /** @private */
236
+ this.context = isOptions ? contextOrOptions.context : contextOrOptions;
237
+ this.attachListeners();
238
+ this.host.addController?.(this);
192
239
  }
193
240
 
194
- /** @returns {T} The current provided value. */
195
- get value() {
196
- return this._value;
197
- }
241
+ /** @param {Event} e */
242
+ onContextRequest = (e) => {
243
+ const ev = /** @type {ContextRequestEvent} */ (e);
244
+ if (ev.context !== this.context) return;
245
+ const consumerHost = ev.contextTarget ?? ev.composedPath()[0];
246
+ // A host never provides to itself: a component that both provides and
247
+ // consumes a context reads it from an ancestor.
248
+ if (consumerHost === this.host) return;
249
+ ev.stopPropagation();
250
+ this.addCallback(ev.callback, consumerHost, ev.subscribe);
251
+ };
198
252
 
199
253
  /**
200
- * Update the provided value and notify all subscribers.
201
- *
202
- * Every subscribing consumer's callback is invoked synchronously with
203
- * the new value. Each consumer then calls `host.requestUpdate()` to
204
- * schedule a re-render: so one `setValue` batches all downstream
205
- * re-renders via the microtask queue.
206
- *
207
- * @param {T} newValue
254
+ * A nearer provider of the same context connected below this one: re-send
255
+ * every subscriber's request from its consumer, so the nearest provider
256
+ * answers it. The consumer drops this provider's subscription when the new
257
+ * one calls back with a different disposer.
258
+ * @param {Event} e
208
259
  */
209
- setValue(newValue) {
210
- if (Object.is(this._value, newValue)) return;
211
- this._value = newValue;
212
- for (const sub of this._subscriptions) {
213
- const unsubscribe = () => { this._subscriptions.delete(sub); };
214
- sub.callback(newValue, unsubscribe);
260
+ onProviderRequest = (e) => {
261
+ const ev = /** @type {ContextProviderEvent} */ (e);
262
+ if (ev.context !== this.context) return;
263
+ const childProviderHost = ev.contextTarget ?? ev.composedPath()[0];
264
+ if (childProviderHost === this.host) return;
265
+ const seen = new Set();
266
+ for (const [callback, { consumerHost }] of this.subscriptions) {
267
+ if (seen.has(callback)) continue;
268
+ seen.add(callback);
269
+ consumerHost.dispatchEvent(
270
+ new ContextRequestEvent(this.context, consumerHost, /** @type {any} */ (callback), true),
271
+ );
215
272
  }
216
- }
273
+ ev.stopPropagation();
274
+ };
217
275
 
218
- /** Called by the host's controller lifecycle when the element connects. */
219
- hostConnected() {
220
- this._host.addEventListener('context-request', this._onRequest);
276
+ /** @private */
277
+ attachListeners() {
278
+ this.host.addEventListener('context-request', this.onContextRequest);
279
+ this.host.addEventListener('context-provider', this.onProviderRequest);
221
280
  }
222
281
 
223
- /** Called by the host's controller lifecycle when the element disconnects. */
224
- hostDisconnected() {
225
- this._host.removeEventListener('context-request', this._onRequest);
226
- this._subscriptions.clear();
282
+ /** ReactiveController hook: announce this provider to the tree above. */
283
+ hostConnected() {
284
+ this.host.dispatchEvent(new ContextProviderEvent(this.context, this.host));
227
285
  }
228
286
  }
229
287
 
@@ -232,96 +290,162 @@ export class ContextProvider {
232
290
  // ---------------------------------------------------------------------------
233
291
 
234
292
  /**
235
- * A ReactiveController that consumes a context value from an ancestor
236
- * provider.
237
- *
238
- * On `hostConnected` it dispatches a `ContextRequestEvent`. If a provider
239
- * for the matching context key exists higher in the DOM tree, the consumer
240
- * receives the current value immediately (and, if `subscribe: true`, all
241
- * future updates as well).
242
- *
243
- * ## AI guidance
293
+ * A ReactiveController that reads a context value from the nearest provider
294
+ * above its host. It requests the value on connect and re-renders the host
295
+ * on delivery. With `subscribe: true` it keeps receiving changes; the
296
+ * default, as in Lit, is a one-time read per connection.
244
297
  *
245
- * - Use `subscribe: true` (the default) when you want the consumer to
246
- * update automatically whenever the provider calls `setValue()`. This is
247
- * the common case for reactive data like theme, auth, or locale.
248
- * - Use `subscribe: false` for one-shot reads where you only need the
249
- * value at connection time (e.g. reading a static config once).
250
- * - Access the value via `consumer.value`. It is `undefined` until a
251
- * provider responds.
252
- * - If no provider exists in the ancestor chain, `value` stays
253
- * `undefined`: design your render method to handle that case.
298
+ * `new ContextConsumer(host, { context, callback, subscribe })` or
299
+ * `new ContextConsumer(host, context, callback, subscribe)`.
254
300
  *
255
301
  * @template T
256
302
  */
257
303
  export class ContextConsumer {
258
304
  /**
259
- * @param {import('./component.js').WebComponent} host
260
- * The host component that owns this consumer.
261
- * @param {{ context: Context<T>, subscribe?: boolean }} options
262
- * `subscribe` defaults to `true`.
305
+ * @param {any} host The host element.
306
+ * @param {any} contextOrOptions `{ context, callback, subscribe }`, or the context.
307
+ * @param {(value: T, dispose?: () => void) => void} [callback] Positional form only.
308
+ * @param {boolean} [subscribe] Positional form only.
263
309
  */
264
- constructor(host, { context, subscribe = true }) {
265
- /** @type {import('./component.js').WebComponent} */
266
- this._host = host;
267
- /** @type {Context<T>} */
268
- this._context = context;
269
- /** @type {boolean} */
270
- this._subscribe = subscribe;
310
+ constructor(host, contextOrOptions, callback, subscribe) {
311
+ /** @protected */
312
+ this.host = host;
313
+ /** @private */
314
+ this.provided = false;
271
315
  /** @type {T | undefined} */
272
- this._value = undefined;
273
- /** @type {(() => void) | undefined} */
274
- this._unsubscribe = undefined;
275
-
276
- if (typeof host.addController === 'function') {
277
- host.addController(this);
316
+ this.value = undefined;
317
+ /** @private @type {(() => void) | undefined} */
318
+ this.unsubscribe = undefined;
319
+ if (contextOrOptions != null && contextOrOptions.context !== undefined) {
320
+ /** @private */
321
+ this.context = contextOrOptions.context;
322
+ /** @private */
323
+ this.callback = contextOrOptions.callback;
324
+ /** @private */
325
+ this.subscribe = contextOrOptions.subscribe ?? false;
326
+ } else {
327
+ this.context = contextOrOptions;
328
+ this.callback = callback;
329
+ this.subscribe = subscribe ?? false;
278
330
  }
331
+ this.host.addController(this);
279
332
  }
280
333
 
281
- /**
282
- * The current context value. `undefined` if no provider has responded
283
- * yet.
284
- * @returns {T | undefined}
285
- */
286
- get value() {
287
- return this._value;
288
- }
289
-
290
- /** Called by the host's controller lifecycle when the element connects. */
334
+ /** ReactiveController hook: request the value. */
291
335
  hostConnected() {
292
- this._dispatchRequest();
336
+ this.dispatchRequest();
293
337
  }
294
338
 
295
- /** Called by the host's controller lifecycle when the element disconnects. */
339
+ /** ReactiveController hook: stop receiving changes. */
296
340
  hostDisconnected() {
297
- if (this._unsubscribe) {
298
- this._unsubscribe();
299
- this._unsubscribe = undefined;
341
+ if (this.unsubscribe) {
342
+ this.unsubscribe();
343
+ this.unsubscribe = undefined;
300
344
  }
301
345
  }
302
346
 
303
347
  /** @private */
304
- _dispatchRequest() {
305
- const event = new ContextRequestEvent(
306
- this._context,
307
- (value, unsubscribe) => {
308
- // Guard against duplicate updates with the same value.
309
- const old = this._value;
310
- this._value = value;
311
- if (unsubscribe) {
312
- this._unsubscribe = unsubscribe;
313
- }
314
- // Trigger a re-render of the consuming component when the value
315
- // changes (skip on the very first delivery during connection,
316
- // since the host will render after connectedCallback anyway -
317
- // but we still update to be safe with various host lifecycles).
318
- if (!Object.is(old, value) && typeof this._host.requestUpdate === 'function') {
319
- this._host.requestUpdate();
320
- }
321
- },
322
- this._subscribe,
348
+ dispatchRequest() {
349
+ this.host.dispatchEvent(
350
+ new ContextRequestEvent(this.context, this.host, this._callback, this.subscribe),
323
351
  );
352
+ }
353
+
354
+ /**
355
+ * One stable callback per consumer, so a provider recognises a repeat
356
+ * request and a hand-over between providers.
357
+ * @private
358
+ * @param {T} value
359
+ * @param {() => void} [unsubscribe]
360
+ */
361
+ _callback = (value, unsubscribe) => {
362
+ if (this.unsubscribe) {
363
+ // A different disposer means a different (nearer) provider answered:
364
+ // leave the old one.
365
+ if (this.unsubscribe !== unsubscribe) {
366
+ this.provided = false;
367
+ this.unsubscribe();
368
+ }
369
+ if (!this.subscribe) this.unsubscribe();
370
+ }
371
+ this.value = value;
372
+ this.host.requestUpdate();
373
+ if (!this.provided || this.subscribe) {
374
+ this.provided = true;
375
+ if (this.callback) this.callback(value, unsubscribe);
376
+ }
377
+ this.unsubscribe = unsubscribe;
378
+ };
379
+ }
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // ContextRoot
383
+ // ---------------------------------------------------------------------------
384
+
385
+ /**
386
+ * Buffers SUBSCRIBING requests that reach it unanswered (no provider above
387
+ * the consumer yet, typically because the provider's element upgrades
388
+ * later), and replays them when a provider for that context connects.
389
+ * Attach one near the root, before components upgrade:
390
+ *
391
+ * ```js
392
+ * new ContextRoot().attach(document.body);
393
+ * ```
394
+ */
395
+ export class ContextRoot {
396
+ constructor() {
397
+ /**
398
+ * @private
399
+ * @type {Map<unknown, { callbacks: WeakMap<Element, WeakSet<Function>>, requests: Array<{ elementRef: WeakRef<Element>, callbackRef: WeakRef<Function> }> }>}
400
+ */
401
+ this.pendingContextRequests = new Map();
402
+ }
324
403
 
325
- this._host.dispatchEvent(event);
404
+ /** @param {Element} element */
405
+ attach(element) {
406
+ element.addEventListener('context-request', this.onContextRequest);
407
+ element.addEventListener('context-provider', this.onContextProvider);
326
408
  }
409
+
410
+ /** @param {Element} element */
411
+ detach(element) {
412
+ element.removeEventListener('context-request', this.onContextRequest);
413
+ element.removeEventListener('context-provider', this.onContextProvider);
414
+ }
415
+
416
+ /** @private @param {Event} e */
417
+ onContextProvider = (e) => {
418
+ const event = /** @type {ContextProviderEvent} */ (e);
419
+ const pending = this.pendingContextRequests.get(event.context);
420
+ if (pending === undefined) return;
421
+ this.pendingContextRequests.delete(event.context);
422
+ for (const { elementRef, callbackRef } of pending.requests) {
423
+ const element = elementRef.deref();
424
+ const callback = callbackRef.deref();
425
+ if (element === undefined || callback === undefined) continue;
426
+ element.dispatchEvent(
427
+ new ContextRequestEvent(event.context, element, /** @type {any} */ (callback), true),
428
+ );
429
+ }
430
+ };
431
+
432
+ /** @private @param {Event} e */
433
+ onContextRequest = (e) => {
434
+ const event = /** @type {ContextRequestEvent} */ (e);
435
+ // Only a subscribing request can be answered later.
436
+ if (event.subscribe !== true) return;
437
+ const element = event.contextTarget ?? /** @type {Element} */ (event.composedPath()[0]);
438
+ const callback = event.callback;
439
+ let pending = this.pendingContextRequests.get(event.context);
440
+ if (pending === undefined) {
441
+ this.pendingContextRequests.set(event.context, (pending = { callbacks: new WeakMap(), requests: [] }));
442
+ }
443
+ let callbacks = pending.callbacks.get(element);
444
+ if (callbacks === undefined) {
445
+ pending.callbacks.set(element, (callbacks = new WeakSet()));
446
+ }
447
+ if (callbacks.has(callback)) return;
448
+ callbacks.add(callback);
449
+ pending.requests.push({ elementRef: new WeakRef(element), callbackRef: new WeakRef(callback) });
450
+ };
327
451
  }
@@ -0,0 +1,7 @@
1
+ /** Lit's default attribute converter, as `lit` exports it. */
2
+ export declare const defaultConverter: {
3
+ toAttribute(value: unknown, type?: unknown): unknown;
4
+ fromAttribute(value: string | null, type?: unknown): unknown;
5
+ };
6
+ /** Lit's default `hasChanged`: true when `value` and `old` differ by `Object.is`. */
7
+ export declare const notEqual: (value: unknown, old: unknown) => boolean;