webspresso 0.0.109 → 0.0.111

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,365 @@
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 a custom element root was passed and is not inside a fragment, use it or doc.body
45
+ if (doc && doc.body && (root === doc.body || root === doc.documentElement)) {
46
+ return doc.body;
47
+ }
48
+ if (!doc || !doc.body) {
49
+ return root;
50
+ }
51
+ }
52
+ if (doc) {
53
+ return doc.body || doc.documentElement || null;
54
+ }
55
+ return root && root.nodeType === 1 ? root : null;
56
+ }
57
+
58
+ /**
59
+ * Create a runtime-scoped UI Host Manager.
60
+ *
61
+ * @param {object} [options]
62
+ * @param {Document|Element|null} [options.root]
63
+ * @param {object} [options.debug]
64
+ * @returns {object}
65
+ */
66
+ function createClientHostManager(options = {}) {
67
+ const root = options.root || null;
68
+ const debug = options.debug || null;
69
+ const activeHosts = new Map(); // key -> hostRecord
70
+ let disposed = false;
71
+
72
+ /**
73
+ * Mount or retrieve a shared UI host by key.
74
+ *
75
+ * @param {string|object} [keyOrOptions]
76
+ * @param {object} [maybeOptions]
77
+ * @param {string} [defaultKey]
78
+ */
79
+ function mount(keyOrOptions = {}, maybeOptions = {}, defaultKey = 'default') {
80
+ if (disposed) {
81
+ const err = new Error('Cannot mount UI host on a disposed client runtime.');
82
+ err.code = 'CLIENT_RUNTIME_DISPOSED';
83
+ throw err;
84
+ }
85
+
86
+ let opts = {};
87
+ let key = defaultKey;
88
+
89
+ if (typeof keyOrOptions === 'string') {
90
+ key = keyOrOptions.trim() || defaultKey;
91
+ opts = maybeOptions && typeof maybeOptions === 'object' ? maybeOptions : {};
92
+ } else if (keyOrOptions && typeof keyOrOptions === 'object') {
93
+ opts = keyOrOptions;
94
+ if (typeof opts.key === 'string' && opts.key.trim()) {
95
+ key = opts.key.trim();
96
+ } else if (typeof opts.name === 'string' && opts.name.trim()) {
97
+ key = opts.name.trim();
98
+ }
99
+ }
100
+
101
+ if (!key) {
102
+ throw new TypeError('mountHost() requires a non-empty host key.');
103
+ }
104
+
105
+ const existing = activeHosts.get(key);
106
+ if (existing && existing.mounted) {
107
+ // If the element is still connected (or in a headless/custom tree), reuse it
108
+ if (!existing.el || existing.el.isConnected !== false || !existing.created) {
109
+ return existing.handle;
110
+ }
111
+ // If a created host element was externally removed from DOM, clean up before re-creating
112
+ existing.handle.unmount();
113
+ }
114
+
115
+ const doc = resolveDocument(opts.root || root);
116
+ let el = null;
117
+ let created = false;
118
+ const prevAttributes = new Map();
119
+
120
+ // 1. Explicit target Element
121
+ if (opts.target && opts.target.nodeType === 1) {
122
+ el = opts.target;
123
+ }
124
+
125
+ // 2. Explicit CSS selector
126
+ if (!el && typeof opts.selector === 'string' && opts.selector.trim()) {
127
+ const searchRoot = opts.root || root || doc;
128
+ if (searchRoot && typeof searchRoot.querySelector === 'function') {
129
+ el = searchRoot.querySelector(opts.selector.trim());
130
+ }
131
+ if (!el && doc && searchRoot !== doc && typeof doc.querySelector === 'function') {
132
+ el = doc.querySelector(opts.selector.trim());
133
+ }
134
+ }
135
+
136
+ // 3. Existing layout placeholder `[data-w-host="<key>"]` not owned by another active host manager
137
+ if (!el && opts.createOnly !== true) {
138
+ const searchRoot = opts.root || root || doc;
139
+ if (searchRoot && typeof searchRoot.querySelectorAll === 'function') {
140
+ const candidates = searchRoot.querySelectorAll(`[data-w-host="${key}"]`);
141
+ for (let i = 0; i < candidates.length; i++) {
142
+ const cand = candidates[i];
143
+ if (!cand.__wHostManager || cand.__wHostManager === api) {
144
+ el = cand;
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ // 4. Create a new host container outside fragment regions
152
+ if (!el) {
153
+ if (!doc || typeof doc.createElement !== 'function') {
154
+ const err = new Error(`Cannot create UI host "${key}": no Document is available in the current environment.`);
155
+ err.code = 'CLIENT_HOST_NO_DOCUMENT';
156
+ throw err;
157
+ }
158
+ const tagName = typeof opts.tagName === 'string' && opts.tagName.trim() ? opts.tagName.trim() : 'div';
159
+ el = doc.createElement(tagName);
160
+ created = true;
161
+
162
+ const container =
163
+ (opts.container && opts.container.nodeType === 1 ? opts.container : null) ||
164
+ resolveDefaultContainer(opts.root || root, doc);
165
+
166
+ if (container && typeof container.appendChild === 'function') {
167
+ container.appendChild(el);
168
+ }
169
+ }
170
+
171
+ function recordAndSetAttr(attrName, attrVal) {
172
+ if (!el || typeof el.setAttribute !== 'function') return;
173
+ if (!created && !prevAttributes.has(attrName)) {
174
+ prevAttributes.set(attrName, el.hasAttribute(attrName) ? el.getAttribute(attrName) : null);
175
+ }
176
+ el.setAttribute(attrName, String(attrVal));
177
+ }
178
+
179
+ recordAndSetAttr('data-w-host', key);
180
+
181
+ if (typeof opts.id === 'string' && opts.id.trim()) {
182
+ recordAndSetAttr('id', opts.id.trim());
183
+ }
184
+
185
+ if (typeof opts.className === 'string' && opts.className.trim()) {
186
+ if (created) {
187
+ el.className = opts.className.trim();
188
+ } else {
189
+ recordAndSetAttr('class', opts.className.trim());
190
+ }
191
+ }
192
+
193
+ if (opts.attributes && typeof opts.attributes === 'object') {
194
+ for (const [attrName, attrVal] of Object.entries(opts.attributes)) {
195
+ if (attrVal === undefined || attrVal === null || attrVal === false) continue;
196
+ recordAndSetAttr(attrName, attrVal === true ? '' : String(attrVal));
197
+ }
198
+ }
199
+
200
+ try {
201
+ Object.defineProperty(el, '__wHostManager', {
202
+ value: api,
203
+ configurable: true,
204
+ enumerable: false,
205
+ writable: true,
206
+ });
207
+ } catch (_e) {
208
+ el.__wHostManager = api;
209
+ }
210
+
211
+ const decoratedEl = decorateElement(el);
212
+ const unmountCallbacks = [];
213
+ let isMounted = true;
214
+
215
+ function onUnmount(fn) {
216
+ if (typeof fn !== 'function') return () => {};
217
+ if (!isMounted) {
218
+ try {
219
+ fn();
220
+ } catch (_e) {}
221
+ return () => {};
222
+ }
223
+ unmountCallbacks.push(fn);
224
+ return () => {
225
+ const idx = unmountCallbacks.indexOf(fn);
226
+ if (idx !== -1) unmountCallbacks.splice(idx, 1);
227
+ };
228
+ }
229
+
230
+ function clear() {
231
+ if (el) {
232
+ el.innerHTML = '';
233
+ }
234
+ }
235
+
236
+ function unmount() {
237
+ if (!isMounted) return;
238
+ isMounted = false;
239
+ activeHosts.delete(key);
240
+
241
+ for (let i = unmountCallbacks.length - 1; i >= 0; i--) {
242
+ try {
243
+ unmountCallbacks[i](decoratedEl);
244
+ } catch (err) {
245
+ if (debug && typeof debug.trackCleanupError === 'function') {
246
+ debug.trackCleanupError(`host_${key}`, key, 'host:unmount', err);
247
+ }
248
+ }
249
+ }
250
+ unmountCallbacks.length = 0;
251
+
252
+ if (el) {
253
+ if (el.__wHostManager === api) {
254
+ delete el.__wHostManager;
255
+ }
256
+ if (created) {
257
+ if (el.parentNode && typeof el.parentNode.removeChild === 'function') {
258
+ el.parentNode.removeChild(el);
259
+ } else if (typeof el.remove === 'function') {
260
+ el.remove();
261
+ }
262
+ } else {
263
+ if (opts.clearOnUnmount !== false) {
264
+ el.innerHTML = '';
265
+ }
266
+ for (const [attrName, prevVal] of prevAttributes.entries()) {
267
+ if (prevVal === null) {
268
+ el.removeAttribute(attrName);
269
+ } else {
270
+ el.setAttribute(attrName, prevVal);
271
+ }
272
+ }
273
+ }
274
+ }
275
+
276
+ if (debug && typeof debug.recordEvent === 'function') {
277
+ debug.recordEvent('host:unmount', { key, created });
278
+ }
279
+ }
280
+
281
+ const handle = Object.freeze({
282
+ key,
283
+ el: decoratedEl,
284
+ created,
285
+ get mounted() {
286
+ return isMounted;
287
+ },
288
+ clear,
289
+ onUnmount,
290
+ unmount,
291
+ destroy: unmount,
292
+ });
293
+
294
+ activeHosts.set(key, {
295
+ key,
296
+ el: decoratedEl,
297
+ created,
298
+ get mounted() {
299
+ return isMounted;
300
+ },
301
+ handle,
302
+ });
303
+
304
+ if (debug && typeof debug.recordEvent === 'function') {
305
+ debug.recordEvent('host:mount', { key, created });
306
+ }
307
+
308
+ return handle;
309
+ }
310
+
311
+ function get(key) {
312
+ const record = activeHosts.get(String(key || '').trim());
313
+ return record && record.mounted ? record.handle : null;
314
+ }
315
+
316
+ function has(key) {
317
+ const record = activeHosts.get(String(key || '').trim());
318
+ return Boolean(record && record.mounted);
319
+ }
320
+
321
+ function unmount(key) {
322
+ const record = activeHosts.get(String(key || '').trim());
323
+ if (record && record.mounted) {
324
+ record.handle.unmount();
325
+ return true;
326
+ }
327
+ return false;
328
+ }
329
+
330
+ function dispose() {
331
+ if (disposed) return;
332
+ disposed = true;
333
+ const records = Array.from(activeHosts.values());
334
+ activeHosts.clear();
335
+ for (let i = records.length - 1; i >= 0; i--) {
336
+ try {
337
+ records[i].handle.unmount();
338
+ } catch (err) {
339
+ if (debug && typeof debug.trackCleanupError === 'function') {
340
+ debug.trackCleanupError(`host_${records[i].key}`, records[i].key, 'host:dispose', err);
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ const api = {
347
+ mount,
348
+ get,
349
+ has,
350
+ unmount,
351
+ dispose,
352
+ get isDisposed() {
353
+ return disposed;
354
+ },
355
+ };
356
+
357
+ return api;
358
+ }
359
+
360
+ module.exports = {
361
+ createClientHostManager,
362
+ resolveDocument,
363
+ resolveDefaultContainer,
364
+ };
365
+
@@ -3,20 +3,50 @@ export {
3
3
  ClientRequestErrorKind,
4
4
  ClientRequestError,
5
5
  ClientRequestCallOptions,
6
+ ClientRetryOptions,
6
7
  ClientApiClient,
7
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,
8
26
  ClientTask,
9
27
  ClientTaskOptions,
10
28
  ClientFormSubmitResult,
29
+ ClientFormHookMeta,
11
30
  ClientFormHandle,
12
31
  ClientFormFillOptions,
13
32
  ControllerFormsRegistry,
33
+ FragmentRefreshResult,
34
+ FragmentRefreshOptions,
14
35
  ControllerFragmentHandle,
15
36
  ControllerContext,
37
+ ControllerErrorMeta,
38
+ ControllerElement,
39
+ ControllerErrorHandler,
16
40
  ControllerActionHandler,
17
41
  ControllerComputedGetter,
18
42
  ControllerTaskFactory,
19
43
  ControllerDefinition,
44
+ WatchSource,
45
+ WatchCleanupCallback,
46
+ WatchCallback,
47
+ WatchOptions,
48
+ WatchDescriptor,
49
+ watch,
20
50
  defineController,
21
51
  deriveControllerNameFromPath,
22
52
  FragmentLoaderContext,
@@ -36,5 +66,12 @@ export {
36
66
  bootLiteClient,
37
67
  createDatastarAdapter,
38
68
  createClientDebugger,
69
+ StorageAdapter,
70
+ ClientStorage,
71
+ ControllerPersistConfig,
72
+ createClientStorage,
73
+ defaultStorage,
74
+ ClientDevtoolHandle,
75
+ mountDevtool,
39
76
  } from '../../index';
40
77