webspresso 0.0.110 → 0.0.112

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.
@@ -0,0 +1,369 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Client UI Host Lifecycle Helper (`core/client/host.js`)
5
+ *
6
+ * Manages optional, lazy DOM host containers for client services that need
7
+ * to render shared UI outside normal SSR fragment patch regions.
8
+ *
9
+ * Principles:
10
+ * - Zero DOM creation until a service explicitly requests a host (`mountHost`).
11
+ * - Idempotent per host key within a runtime: repeated calls return the same host handle
12
+ * and never create duplicate DOM containers.
13
+ * - Supports attaching to an existing layout element (`target` / `selector` / `[data-w-host]`)
14
+ * or lazily creating a container appended outside fragment areas (default: `document.body`).
15
+ * - Clean unmount and resource disposal contract when the owning service or runtime is destroyed.
16
+ * - Agnostic to UI rendering: does not impose any template language, virtual DOM, or widget type.
17
+ */
18
+
19
+ const { decorateElement } = require('./utils');
20
+
21
+ /**
22
+ * Resolve the owning Document from a root Document or Element.
23
+ *
24
+ * @param {Document|Element|null} [root]
25
+ * @returns {Document|null}
26
+ */
27
+ function resolveDocument(root) {
28
+ if (root) {
29
+ if (root.nodeType === 9) return root;
30
+ if (root.ownerDocument) return root.ownerDocument;
31
+ }
32
+ return typeof document !== 'undefined' ? document : null;
33
+ }
34
+
35
+ /**
36
+ * Resolve the default mount container outside `[data-w-fragment]` areas.
37
+ *
38
+ * @param {Document|Element|null} [root]
39
+ * @param {Document|null} [doc]
40
+ * @returns {Element|null}
41
+ */
42
+ function resolveDefaultContainer(root, doc) {
43
+ if (root && root.nodeType === 1 && root.tagName !== 'HTML') {
44
+ if (doc && doc.body && (root === doc.body || root === doc.documentElement)) {
45
+ return doc.body;
46
+ }
47
+ const insideFragment =
48
+ typeof root.closest === 'function' ? Boolean(root.closest('[data-w-fragment]')) : false;
49
+ if (!insideFragment) {
50
+ return root;
51
+ }
52
+ if (!doc || !doc.body) {
53
+ return root;
54
+ }
55
+ }
56
+ if (doc) {
57
+ return doc.body || doc.documentElement || null;
58
+ }
59
+ return root && root.nodeType === 1 ? root : null;
60
+ }
61
+
62
+ /**
63
+ * Create a runtime-scoped UI Host Manager.
64
+ *
65
+ * @param {object} [options]
66
+ * @param {Document|Element|null} [options.root]
67
+ * @param {object} [options.debug]
68
+ * @returns {object}
69
+ */
70
+ function createClientHostManager(options = {}) {
71
+ const root = options.root || null;
72
+ const debug = options.debug || null;
73
+ const activeHosts = new Map(); // key -> hostRecord
74
+ let disposed = false;
75
+
76
+ /**
77
+ * Mount or retrieve a shared UI host by key.
78
+ *
79
+ * @param {string|object} [keyOrOptions]
80
+ * @param {object} [maybeOptions]
81
+ * @param {string} [defaultKey]
82
+ */
83
+ function mount(keyOrOptions = {}, maybeOptions = {}, defaultKey = 'default') {
84
+ if (disposed) {
85
+ const err = new Error('Cannot mount UI host on a disposed client runtime.');
86
+ err.code = 'CLIENT_RUNTIME_DISPOSED';
87
+ throw err;
88
+ }
89
+
90
+ let opts = {};
91
+ let key = defaultKey;
92
+
93
+ if (typeof keyOrOptions === 'string') {
94
+ key = keyOrOptions.trim() || defaultKey;
95
+ opts = maybeOptions && typeof maybeOptions === 'object' ? maybeOptions : {};
96
+ } else if (keyOrOptions && typeof keyOrOptions === 'object') {
97
+ opts = keyOrOptions;
98
+ if (typeof opts.key === 'string' && opts.key.trim()) {
99
+ key = opts.key.trim();
100
+ } else if (typeof opts.name === 'string' && opts.name.trim()) {
101
+ key = opts.name.trim();
102
+ }
103
+ }
104
+
105
+ if (!key) {
106
+ throw new TypeError('mountHost() requires a non-empty host key.');
107
+ }
108
+
109
+ const existing = activeHosts.get(key);
110
+ if (existing && existing.mounted) {
111
+ // If the element is still connected (or in a headless/custom tree), reuse it
112
+ if (!existing.el || existing.el.isConnected !== false || !existing.created) {
113
+ return existing.handle;
114
+ }
115
+ // If a created host element was externally removed from DOM, clean up before re-creating
116
+ existing.handle.unmount();
117
+ }
118
+
119
+ const doc = resolveDocument(opts.root || root);
120
+ let el = null;
121
+ let created = false;
122
+ const prevAttributes = new Map();
123
+
124
+ // 1. Explicit target Element
125
+ if (opts.target && opts.target.nodeType === 1) {
126
+ el = opts.target;
127
+ }
128
+
129
+ // 2. Explicit CSS selector
130
+ if (!el && typeof opts.selector === 'string' && opts.selector.trim()) {
131
+ const searchRoot = opts.root || root || doc;
132
+ if (searchRoot && typeof searchRoot.querySelector === 'function') {
133
+ el = searchRoot.querySelector(opts.selector.trim());
134
+ }
135
+ if (!el && doc && searchRoot !== doc && typeof doc.querySelector === 'function') {
136
+ el = doc.querySelector(opts.selector.trim());
137
+ }
138
+ }
139
+
140
+ // 3. Existing layout placeholder `[data-w-host="<key>"]` not owned by another active host manager
141
+ if (!el && opts.createOnly !== true) {
142
+ const searchRoot = opts.root || root || doc;
143
+ if (searchRoot && typeof searchRoot.querySelectorAll === 'function') {
144
+ const candidates = searchRoot.querySelectorAll(`[data-w-host="${key}"]`);
145
+ for (let i = 0; i < candidates.length; i++) {
146
+ const cand = candidates[i];
147
+ if (!cand.__wHostManager || cand.__wHostManager === api) {
148
+ el = cand;
149
+ break;
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ // 4. Create a new host container outside fragment regions
156
+ if (!el) {
157
+ if (!doc || typeof doc.createElement !== 'function') {
158
+ const err = new Error(`Cannot create UI host "${key}": no Document is available in the current environment.`);
159
+ err.code = 'CLIENT_HOST_NO_DOCUMENT';
160
+ throw err;
161
+ }
162
+ const tagName = typeof opts.tagName === 'string' && opts.tagName.trim() ? opts.tagName.trim() : 'div';
163
+ el = doc.createElement(tagName);
164
+ created = true;
165
+
166
+ const container =
167
+ (opts.container && opts.container.nodeType === 1 ? opts.container : null) ||
168
+ resolveDefaultContainer(opts.root || root, doc);
169
+
170
+ if (container && typeof container.appendChild === 'function') {
171
+ container.appendChild(el);
172
+ }
173
+ }
174
+
175
+ function recordAndSetAttr(attrName, attrVal) {
176
+ if (!el || typeof el.setAttribute !== 'function') return;
177
+ if (!created && !prevAttributes.has(attrName)) {
178
+ prevAttributes.set(attrName, el.hasAttribute(attrName) ? el.getAttribute(attrName) : null);
179
+ }
180
+ el.setAttribute(attrName, String(attrVal));
181
+ }
182
+
183
+ recordAndSetAttr('data-w-host', key);
184
+
185
+ if (typeof opts.id === 'string' && opts.id.trim()) {
186
+ recordAndSetAttr('id', opts.id.trim());
187
+ }
188
+
189
+ if (typeof opts.className === 'string' && opts.className.trim()) {
190
+ if (created) {
191
+ el.className = opts.className.trim();
192
+ } else {
193
+ recordAndSetAttr('class', opts.className.trim());
194
+ }
195
+ }
196
+
197
+ if (opts.attributes && typeof opts.attributes === 'object') {
198
+ for (const [attrName, attrVal] of Object.entries(opts.attributes)) {
199
+ if (attrVal === undefined || attrVal === null || attrVal === false) continue;
200
+ recordAndSetAttr(attrName, attrVal === true ? '' : String(attrVal));
201
+ }
202
+ }
203
+
204
+ try {
205
+ Object.defineProperty(el, '__wHostManager', {
206
+ value: api,
207
+ configurable: true,
208
+ enumerable: false,
209
+ writable: true,
210
+ });
211
+ } catch (_e) {
212
+ el.__wHostManager = api;
213
+ }
214
+
215
+ const decoratedEl = decorateElement(el);
216
+ const unmountCallbacks = [];
217
+ let isMounted = true;
218
+
219
+ function onUnmount(fn) {
220
+ if (typeof fn !== 'function') return () => {};
221
+ if (!isMounted) {
222
+ try {
223
+ fn();
224
+ } catch (_e) {}
225
+ return () => {};
226
+ }
227
+ unmountCallbacks.push(fn);
228
+ return () => {
229
+ const idx = unmountCallbacks.indexOf(fn);
230
+ if (idx !== -1) unmountCallbacks.splice(idx, 1);
231
+ };
232
+ }
233
+
234
+ function clear() {
235
+ if (el) {
236
+ el.innerHTML = '';
237
+ }
238
+ }
239
+
240
+ function unmount() {
241
+ if (!isMounted) return;
242
+ isMounted = false;
243
+ activeHosts.delete(key);
244
+
245
+ for (let i = unmountCallbacks.length - 1; i >= 0; i--) {
246
+ try {
247
+ unmountCallbacks[i](decoratedEl);
248
+ } catch (err) {
249
+ if (debug && typeof debug.trackCleanupError === 'function') {
250
+ debug.trackCleanupError(`host_${key}`, key, 'host:unmount', err);
251
+ }
252
+ }
253
+ }
254
+ unmountCallbacks.length = 0;
255
+
256
+ if (el) {
257
+ if (el.__wHostManager === api) {
258
+ delete el.__wHostManager;
259
+ }
260
+ if (created) {
261
+ if (el.parentNode && typeof el.parentNode.removeChild === 'function') {
262
+ el.parentNode.removeChild(el);
263
+ } else if (typeof el.remove === 'function') {
264
+ el.remove();
265
+ }
266
+ } else {
267
+ if (opts.clearOnUnmount !== false) {
268
+ el.innerHTML = '';
269
+ }
270
+ for (const [attrName, prevVal] of prevAttributes.entries()) {
271
+ if (prevVal === null) {
272
+ el.removeAttribute(attrName);
273
+ } else {
274
+ el.setAttribute(attrName, prevVal);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ if (debug && typeof debug.recordEvent === 'function') {
281
+ debug.recordEvent('host:unmount', { key, created });
282
+ }
283
+ }
284
+
285
+ const handle = Object.freeze({
286
+ key,
287
+ el: decoratedEl,
288
+ created,
289
+ get mounted() {
290
+ return isMounted;
291
+ },
292
+ clear,
293
+ onUnmount,
294
+ unmount,
295
+ destroy: unmount,
296
+ });
297
+
298
+ activeHosts.set(key, {
299
+ key,
300
+ el: decoratedEl,
301
+ created,
302
+ get mounted() {
303
+ return isMounted;
304
+ },
305
+ handle,
306
+ });
307
+
308
+ if (debug && typeof debug.recordEvent === 'function') {
309
+ debug.recordEvent('host:mount', { key, created });
310
+ }
311
+
312
+ return handle;
313
+ }
314
+
315
+ function get(key) {
316
+ const record = activeHosts.get(String(key || '').trim());
317
+ return record && record.mounted ? record.handle : null;
318
+ }
319
+
320
+ function has(key) {
321
+ const record = activeHosts.get(String(key || '').trim());
322
+ return Boolean(record && record.mounted);
323
+ }
324
+
325
+ function unmount(key) {
326
+ const record = activeHosts.get(String(key || '').trim());
327
+ if (record && record.mounted) {
328
+ record.handle.unmount();
329
+ return true;
330
+ }
331
+ return false;
332
+ }
333
+
334
+ function dispose() {
335
+ if (disposed) return;
336
+ disposed = true;
337
+ const records = Array.from(activeHosts.values());
338
+ activeHosts.clear();
339
+ for (let i = records.length - 1; i >= 0; i--) {
340
+ try {
341
+ records[i].handle.unmount();
342
+ } catch (err) {
343
+ if (debug && typeof debug.trackCleanupError === 'function') {
344
+ debug.trackCleanupError(`host_${records[i].key}`, records[i].key, 'host:dispose', err);
345
+ }
346
+ }
347
+ }
348
+ }
349
+
350
+ const api = {
351
+ mount,
352
+ get,
353
+ has,
354
+ unmount,
355
+ dispose,
356
+ get isDisposed() {
357
+ return disposed;
358
+ },
359
+ };
360
+
361
+ return api;
362
+ }
363
+
364
+ module.exports = {
365
+ createClientHostManager,
366
+ resolveDocument,
367
+ resolveDefaultContainer,
368
+ };
369
+
@@ -6,9 +6,27 @@ export {
6
6
  ClientRetryOptions,
7
7
  ClientApiClient,
8
8
  AppApiRouteMap,
9
+ AppClientServices,
10
+ ClientServiceMountHostOptions,
11
+ ClientServiceHostHandle,
12
+ ClientServiceHostHelper,
13
+ ClientServiceContext,
14
+ ClientServiceDefinition,
15
+ InferClientServiceExport,
16
+ InferClientService,
17
+ InferClientServicesMap,
18
+ ResolvedClientServicesMap,
19
+ ClientServiceKey,
20
+ ClientServicesRegistry,
21
+ ClientRuntimeOptions,
22
+ ClientRuntime,
23
+ defineClientService,
24
+ createClientServiceRegistry,
25
+ createClientHostManager,
9
26
  ClientTask,
10
27
  ClientTaskOptions,
11
28
  ClientFormSubmitResult,
29
+ ClientFormHookMeta,
12
30
  ClientFormHandle,
13
31
  ClientFormFillOptions,
14
32
  ControllerFormsRegistry,
@@ -57,11 +57,6 @@ const {
57
57
  deduplicateParentChildFragments,
58
58
  } = require('./fragments');
59
59
 
60
- const {
61
- createClientDebugger,
62
- summarizePayloadSafely,
63
- } = require('./debug');
64
-
65
60
  const {
66
61
  MemoryStorage,
67
62
  StorageAdapter,
@@ -71,10 +66,62 @@ const {
71
66
  } = require('./storage');
72
67
 
73
68
  const {
74
- mountDevtool,
75
- highlightElement,
76
- setupReduxDevToolsBridge,
77
- } = require('./devtool');
69
+ defineClientService,
70
+ createClientServiceRegistry,
71
+ createServiceError,
72
+ } = require('./services');
73
+
74
+ const {
75
+ createClientHostManager,
76
+ } = require('./host');
77
+
78
+ const { isArray, isNumber, NOOP_DEBUGGER, randomId, uid } = require('./utils');
79
+
80
+ const NOOP_DEVTOOL_HANDLE = Object.freeze({
81
+ open() {},
82
+ close() {},
83
+ toggle() {},
84
+ refresh() {},
85
+ destroy() {},
86
+ });
87
+
88
+ let _debugMod = null;
89
+ function getDebugModule() {
90
+ if (process.env.NODE_ENV !== 'production') {
91
+ if (!_debugMod) {
92
+ _debugMod = require('./debug');
93
+ }
94
+ return _debugMod;
95
+ }
96
+ return {
97
+ createClientDebugger: () => NOOP_DEBUGGER,
98
+ summarizePayloadSafely: () => null,
99
+ };
100
+ }
101
+
102
+ let _devtoolMod = null;
103
+ function getDevtoolModule() {
104
+ if (process.env.NODE_ENV !== 'production') {
105
+ if (!_devtoolMod) {
106
+ _devtoolMod = require('./devtool');
107
+ }
108
+ return _devtoolMod;
109
+ }
110
+ return {
111
+ mountDevtool: () => NOOP_DEVTOOL_HANDLE,
112
+ highlightElement: () => {},
113
+ setupReduxDevToolsBridge: () => null,
114
+ };
115
+ }
116
+
117
+ let _liteMod = null;
118
+ function getLiteModule() {
119
+ if (!_liteMod) {
120
+ const reqFn = typeof module !== 'undefined' && module.require ? module.require.bind(module) : require;
121
+ _liteMod = reqFn('./lite');
122
+ }
123
+ return _liteMod;
124
+ }
78
125
 
79
126
  let _serverFragments = null;
80
127
  function getServerFragments() {
@@ -106,10 +153,12 @@ function createClientRuntime(options = {}) {
106
153
  options.debugger ||
107
154
  (options.debug && typeof options.debug === 'object' && typeof options.debug.getSnapshot === 'function'
108
155
  ? options.debug
109
- : createClientDebugger({
110
- enabled: Boolean(options.debug),
111
- logger: options.debugLogger,
112
- }));
156
+ : options.debug
157
+ ? getDebugModule().createClientDebugger({
158
+ enabled: true,
159
+ logger: options.debugLogger,
160
+ })
161
+ : NOOP_DEBUGGER);
113
162
 
114
163
  const datastar = createDatastarAdapter({
115
164
  datastar: options.datastar,
@@ -124,6 +173,14 @@ function createClientRuntime(options = {}) {
124
173
  endpoint: options.fragmentEndpoint || DEFAULT_FRAGMENT_ENDPOINT,
125
174
  datastar,
126
175
  debug,
176
+ staleTime: options.staleTime,
177
+ refetchOnWindowFocus: options.refetchOnWindowFocus,
178
+ refetchOnFocus: options.refetchOnFocus,
179
+ refetchOnReconnect: options.refetchOnReconnect,
180
+ refetchOnMount: options.refetchOnMount,
181
+ refetchOnVisit: options.refetchOnVisit,
182
+ refetchInterval: options.refetchInterval,
183
+ refetchIntervalInBackground: options.refetchIntervalInBackground,
127
184
  getCsrfToken: options.getCsrfToken || (() => discoverCsrfToken(rootOption)),
128
185
  onBeforeNodeRemove(removedEl) {
129
186
  if (controllers) {
@@ -153,11 +210,26 @@ function createClientRuntime(options = {}) {
153
210
 
154
211
  const api = createApiClient(apiConfig);
155
212
 
213
+ let runtime = null;
214
+
215
+ const services =
216
+ options.services && typeof options.services.get === 'function' && typeof options.services.register === 'function'
217
+ ? options.services
218
+ : createClientServiceRegistry({
219
+ root: rootOption,
220
+ api,
221
+ debug,
222
+ getRuntime: () => runtime,
223
+ services: options.services,
224
+ });
225
+
156
226
  controllers = createControllerRuntime({
227
+ root: rootOption,
157
228
  datastar,
158
229
  fragments,
159
230
  debug,
160
231
  apiConfig,
232
+ services,
161
233
  lazyLoader: options.lazyLoader,
162
234
  });
163
235
 
@@ -216,24 +288,37 @@ function createClientRuntime(options = {}) {
216
288
  observer = null;
217
289
  }
218
290
  controllers.destroyAll();
291
+ if (fragments && typeof fragments.dispose === 'function') {
292
+ fragments.dispose();
293
+ }
294
+ services.dispose();
295
+ if (runtime && runtime.devtool && typeof runtime.devtool.destroy === 'function') {
296
+ try {
297
+ runtime.devtool.destroy();
298
+ } catch (_e) {}
299
+ runtime.devtool = null;
300
+ }
219
301
  }
220
302
 
221
- const runtime = {
303
+ runtime = {
222
304
  api,
305
+ services,
223
306
  fragments,
224
307
  controllers,
225
308
  datastar,
226
309
  adapter: datastar,
227
310
  debug,
228
311
  register: (name, def, meta) => controllers.register(name, def, meta),
312
+ registerService: (name, def) => services.register(name, def),
229
313
  mount,
230
314
  unmount,
231
315
  destroy,
316
+ dispose: destroy,
232
317
  getControllerInstance: (el, name) => controllers.getInstanceForElement(el, name),
233
318
  getController: (el, name) => controllers.getInstanceForElement(el, name),
234
319
  invalidate: (tags, meta) => fragments.invalidate(tags, meta),
235
320
  refresh: (idOrName, input, opts) => fragments.refresh(idOrName, input, opts),
236
- mountDevtool: (opts) => mountDevtool(runtime, opts),
321
+ mountDevtool: (opts) => getDevtoolModule().mountDevtool(runtime, opts),
237
322
  forms: {
238
323
  parse: parseFormElement,
239
324
  create: (formEl, formOpts = {}) =>
@@ -241,6 +326,7 @@ function createClientRuntime(options = {}) {
241
326
  api,
242
327
  datastar,
243
328
  debug,
329
+ services,
244
330
  ...formOpts,
245
331
  }),
246
332
  },
@@ -257,7 +343,32 @@ function createClientRuntime(options = {}) {
257
343
  * Boot the client runtime on DOMContentLoaded (or immediately if ready).
258
344
  */
259
345
  function bootClient(options = {}) {
346
+ const bootRoot = options.root || options.document || (typeof document !== 'undefined' ? document : null);
347
+ if (
348
+ options.hmr !== false &&
349
+ bootRoot &&
350
+ bootRoot.__wBootedClientRuntime &&
351
+ typeof bootRoot.__wBootedClientRuntime.destroy === 'function'
352
+ ) {
353
+ try {
354
+ bootRoot.__wBootedClientRuntime.destroy();
355
+ } catch (_e) {}
356
+ }
357
+
260
358
  const runtime = createClientRuntime(options);
359
+ if (bootRoot && (typeof bootRoot === 'object' || typeof bootRoot === 'function')) {
360
+ try {
361
+ Object.defineProperty(bootRoot, '__wBootedClientRuntime', {
362
+ value: runtime,
363
+ configurable: true,
364
+ enumerable: false,
365
+ writable: true,
366
+ });
367
+ } catch (_e) {
368
+ bootRoot.__wBootedClientRuntime = runtime;
369
+ }
370
+ }
371
+
261
372
  const shouldMountDevtools =
262
373
  options.devtools === true ||
263
374
  (options.devtools !== false &&
@@ -271,7 +382,7 @@ function bootClient(options = {}) {
271
382
  const onReady = () => {
272
383
  runtime.mount();
273
384
  if (shouldMountDevtools) {
274
- runtime.devtool = mountDevtool(runtime, options.devtoolsOptions || {});
385
+ runtime.devtool = getDevtoolModule().mountDevtool(runtime, options.devtoolsOptions || {});
275
386
  }
276
387
  };
277
388
  if (document.readyState === 'loading') {
@@ -290,10 +401,10 @@ module.exports = {
290
401
  createClientRuntime,
291
402
  bootClient,
292
403
  get createLiteClientRuntime() {
293
- return require('./lite').createLiteClientRuntime;
404
+ return getLiteModule().createLiteClientRuntime;
294
405
  },
295
406
  get bootLiteClient() {
296
- return require('./lite').bootLiteClient;
407
+ return getLiteModule().bootLiteClient;
297
408
  },
298
409
  createControllerRuntime,
299
410
  queryScopedTargets,
@@ -369,17 +480,31 @@ module.exports = {
369
480
  createDatastarAdapter,
370
481
  createSignalsEngine,
371
482
  evaluateStateExpression,
483
+ isArray,
484
+ isNumber,
485
+ randomId,
486
+ uid,
372
487
  getByPath,
373
488
  setByPath,
374
489
  morphDomTrees,
375
490
  parseHtmlFragment,
376
491
 
377
492
  // Debug & Telemetry & DevTools
378
- createClientDebugger,
379
- summarizePayloadSafely,
380
- mountDevtool,
381
- highlightElement,
382
- setupReduxDevToolsBridge,
493
+ get createClientDebugger() {
494
+ return getDebugModule().createClientDebugger;
495
+ },
496
+ get summarizePayloadSafely() {
497
+ return getDebugModule().summarizePayloadSafely;
498
+ },
499
+ get mountDevtool() {
500
+ return getDevtoolModule().mountDevtool;
501
+ },
502
+ get highlightElement() {
503
+ return getDevtoolModule().highlightElement;
504
+ },
505
+ get setupReduxDevToolsBridge() {
506
+ return getDevtoolModule().setupReduxDevToolsBridge;
507
+ },
383
508
 
384
509
  // Client Storage & Persistence
385
510
  MemoryStorage,
@@ -387,5 +512,11 @@ module.exports = {
387
512
  getNativeStorageBackend,
388
513
  createClientStorage,
389
514
  defaultStorage,
515
+
516
+ // Client Services & Optional Lazy UI Hosts
517
+ defineClientService,
518
+ createClientServiceRegistry,
519
+ createServiceError,
520
+ createClientHostManager,
390
521
  };
391
522