webspresso 0.0.110 → 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.
@@ -24,7 +24,7 @@
24
24
  * external package is not installed in `node_modules`.
25
25
  */
26
26
 
27
- const { getByPath, setByPath } = require('../utils');
27
+ const { isArray, getByPath, setByPath } = require('../utils');
28
28
 
29
29
  const PluginType = {
30
30
  Attribute: 1,
@@ -507,11 +507,15 @@ function createIsolatedControllerState(
507
507
  return Reflect.get(obj, prop, receiver);
508
508
  }
509
509
 
510
- // Allow accessing tasks on root state
510
+ // Allow accessing tasks and busy status on root state
511
511
  if (!basePath && prop === 'tasks') {
512
512
  const ctx = getContext();
513
513
  return ctx && ctx.tasks ? ctx.tasks : undefined;
514
514
  }
515
+ if (!basePath && (prop === '$busy' || prop === 'isBusy') && !Object.prototype.hasOwnProperty.call(obj, prop)) {
516
+ const ctx = getContext();
517
+ return ctx ? Boolean(ctx.isBusy) : false;
518
+ }
515
519
 
516
520
  // Check if accessing a root-level computed property
517
521
  if (!basePath && computedDefs && Object.prototype.hasOwnProperty.call(computedDefs, prop)) {
@@ -523,7 +527,7 @@ function createIsolatedControllerState(
523
527
  // Track signal dependency in Datastar signals store
524
528
  engine.signals.value(signalKeyFor(path));
525
529
  // If accessing array length or methods, also subscribe to the array path itself
526
- if (Array.isArray(obj) && basePath) {
530
+ if (isArray(obj) && basePath) {
527
531
  engine.signals.value(signalKeyFor(basePath));
528
532
  }
529
533
  const val = Reflect.get(obj, prop, receiver);
@@ -562,21 +566,21 @@ function createIsolatedControllerState(
562
566
  const prevValue = obj[prop];
563
567
  const path = basePath ? `${basePath}.${String(prop)}` : String(prop);
564
568
 
565
- if (Object.is(prevValue, rawNext) && !Array.isArray(obj)) {
569
+ if (Object.is(prevValue, rawNext) && !isArray(obj)) {
566
570
  return true;
567
571
  }
568
572
 
569
573
  Reflect.set(obj, prop, rawNext, receiver);
570
574
 
571
575
  // Seed nested keys if setting an object
572
- if (rawNext !== null && typeof rawNext === 'object' && !Array.isArray(rawNext)) {
576
+ if (rawNext !== null && typeof rawNext === 'object' && !isArray(rawNext)) {
573
577
  for (const [k, v] of Object.entries(rawNext)) {
574
578
  engine.signals.setValue(signalKeyFor(`${path}.${k}`), v);
575
579
  }
576
580
  }
577
581
  engine.signals.setValue(signalKeyFor(path), rawNext);
578
582
  if (basePath) {
579
- engine.signals.setValue(signalKeyFor(basePath), Array.isArray(obj) ? [...obj] : { ...obj });
583
+ engine.signals.setValue(signalKeyFor(basePath), isArray(obj) ? [...obj] : { ...obj });
580
584
  }
581
585
  return true;
582
586
  },
@@ -609,13 +613,13 @@ function createIsolatedControllerState(
609
613
  for (const [key, val] of Object.entries(initialState)) {
610
614
  const cloned =
611
615
  val !== null && typeof val === 'object'
612
- ? Array.isArray(val)
616
+ ? isArray(val)
613
617
  ? [...val]
614
618
  : { ...val }
615
619
  : val;
616
620
  rawStore[key] = cloned;
617
621
  engine.signals.setValue(signalKeyFor(key), cloned);
618
- if (cloned !== null && typeof cloned === 'object' && !Array.isArray(cloned)) {
622
+ if (cloned !== null && typeof cloned === 'object' && !isArray(cloned)) {
619
623
  for (const [subKey, subVal] of Object.entries(cloned)) {
620
624
  engine.signals.setValue(signalKeyFor(`${key}.${subKey}`), subVal);
621
625
  }
@@ -734,6 +738,8 @@ function getMorphNodeKey(node) {
734
738
  if (node.id) return `id:${node.id}`;
735
739
  const wKey = node.getAttribute('data-w-key');
736
740
  if (wKey) return `wkey:${wKey}`;
741
+ const hostKey = node.getAttribute('data-w-host');
742
+ if (hostKey) return `host:${hostKey}`;
737
743
  const fragId = node.getAttribute('data-w-fragment-id');
738
744
  if (fragId) return `frag:${fragId}`;
739
745
  const formName = node.getAttribute('data-w-form');
@@ -850,6 +856,11 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
850
856
  }
851
857
  }
852
858
 
859
+ // If `toEl` is a client service UI host (`data-w-host`), the service owns its DOM contents
860
+ if (toEl.hasAttribute && toEl.hasAttribute('data-w-host')) {
861
+ return toEl;
862
+ }
863
+
853
864
  // If `toEl` has `data-w-text`, client state owns its textContent; do not overwrite from server HTML
854
865
  if (ownership.text) {
855
866
  return toEl;
@@ -951,10 +962,13 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
951
962
  }
952
963
  }
953
964
 
954
- // Remove leftover existing children that were not matched
965
+ // Remove leftover existing children that were not matched (preserve client service UI hosts)
955
966
  for (let i = 0; i < existingChildren.length; i++) {
956
967
  const oldChild = existingChildren[i];
957
968
  if (!usedExisting.has(oldChild) && oldChild.parentNode === toEl) {
969
+ if (oldChild.nodeType === 1 && oldChild.hasAttribute && oldChild.hasAttribute('data-w-host')) {
970
+ continue;
971
+ }
958
972
  if (oldChild.nodeType === 1 && typeof ctx.onBeforeNodeRemove === 'function') {
959
973
  ctx.onBeforeNodeRemove(oldChild);
960
974
  }
@@ -1124,10 +1138,31 @@ function createDatastarAdapter(options = {}) {
1124
1138
  return bindings;
1125
1139
  }
1126
1140
 
1141
+ function toggleElementVisibility(el, visible, initialDisplay) {
1142
+ if (visible) {
1143
+ el.removeAttribute('hidden');
1144
+ if (el.style && el.style.display === 'none') {
1145
+ el.style.display = initialDisplay;
1146
+ }
1147
+ } else {
1148
+ el.setAttribute('hidden', '');
1149
+ if (el.style) {
1150
+ el.style.display = 'none';
1151
+ }
1152
+ }
1153
+ }
1154
+
1127
1155
  /**
1128
1156
  * Bind V1 declarative attributes inside `rootEl` for a controller instance.
1129
1157
  */
1130
- function bindControllerDom({ instanceId, rootEl, state, invokeAction, isConnected }) {
1158
+ function bindControllerDom({
1159
+ instanceId,
1160
+ rootEl,
1161
+ state,
1162
+ invokeAction,
1163
+ isConnected,
1164
+ isLoadingTargetActive,
1165
+ }) {
1131
1166
  if (!boundElementsByInstance.has(instanceId)) {
1132
1167
  boundElementsByInstance.set(instanceId, new WeakSet());
1133
1168
  }
@@ -1161,16 +1196,31 @@ function createDatastarAdapter(options = {}) {
1161
1196
  const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
1162
1197
  const stop = engine.effect(() => {
1163
1198
  if (!isConnected()) return;
1164
- const visible = Boolean(evaluateStateExpression(state, expr));
1165
- if (visible) {
1166
- el.removeAttribute('hidden');
1167
- if (el.style && el.style.display === 'none') {
1168
- el.style.display = initialDisplay;
1169
- }
1170
- } else {
1171
- el.setAttribute('hidden', '');
1172
- if (el.style) {
1173
- el.style.display = 'none';
1199
+ toggleElementVisibility(el, Boolean(evaluateStateExpression(state, expr)), initialDisplay);
1200
+ });
1201
+ disposers.push(stop);
1202
+ }
1203
+
1204
+ // 2b. data-w-loading & data-w-loading-hide (declarative action/task busy indicators)
1205
+ if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
1206
+ const isHide = el.hasAttribute('data-w-loading-hide');
1207
+ const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
1208
+ const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
1209
+ const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
1210
+ .trim()
1211
+ .split(/\s+/)
1212
+ .filter(Boolean);
1213
+ const stop = engine.effect(() => {
1214
+ if (!isConnected()) return;
1215
+ const active =
1216
+ typeof isLoadingTargetActive === 'function'
1217
+ ? Boolean(isLoadingTargetActive(rawSpec))
1218
+ : Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
1219
+ toggleElementVisibility(el, isHide ? !active : active, initialDisplay);
1220
+ if (loadingClasses.length > 0 && el.classList) {
1221
+ for (let c = 0; c < loadingClasses.length; c++) {
1222
+ if (active) el.classList.add(loadingClasses[c]);
1223
+ else el.classList.remove(loadingClasses[c]);
1174
1224
  }
1175
1225
  }
1176
1226
  });
@@ -1249,7 +1299,7 @@ function createDatastarAdapter(options = {}) {
1249
1299
  const currentVal = getByPath(state, modelPath);
1250
1300
 
1251
1301
  if (type === 'checkbox') {
1252
- if (Array.isArray(currentVal)) {
1302
+ if (isArray(currentVal)) {
1253
1303
  el.checked = currentVal.map(String).includes(String(el.value));
1254
1304
  } else {
1255
1305
  el.checked = Boolean(currentVal);
@@ -1257,7 +1307,7 @@ function createDatastarAdapter(options = {}) {
1257
1307
  } else if (type === 'radio') {
1258
1308
  el.checked = String(currentVal) === String(el.value);
1259
1309
  } else if (el.tagName === 'SELECT' && el.multiple) {
1260
- const arr = Array.isArray(currentVal) ? currentVal.map(String) : [];
1310
+ const arr = isArray(currentVal) ? currentVal.map(String) : [];
1261
1311
  const opts = el.options || [];
1262
1312
  for (let j = 0; j < opts.length; j++) {
1263
1313
  opts[j].selected = arr.includes(String(opts[j].value));
@@ -1282,7 +1332,7 @@ function createDatastarAdapter(options = {}) {
1282
1332
 
1283
1333
  if (type === 'checkbox') {
1284
1334
  const cur = getByPath(state, modelPath);
1285
- if (Array.isArray(cur)) {
1335
+ if (isArray(cur)) {
1286
1336
  const next = el.checked
1287
1337
  ? Array.from(new Set([...cur, el.value]))
1288
1338
  : cur.filter((item) => String(item) !== String(el.value));
@@ -13,6 +13,8 @@
13
13
  * - Raw & streaming escape hatches (`api.raw`, `api.stream`)
14
14
  */
15
15
 
16
+ const { isArray, isNumber } = require('./utils');
17
+
16
18
  const VALID_POLICIES = new Set(['latest', 'drop', 'queue', 'parallel']);
17
19
  const MUTATION_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
18
20
  const RESERVED_OPTION_KEYS = new Set([
@@ -68,7 +70,7 @@ class ClientRequestError extends Error {
68
70
  this.key = details.key || '';
69
71
  this.fieldErrors = details.fieldErrors || {};
70
72
  this.fieldErrorList = details.fieldErrorList || {};
71
- this.issues = Array.isArray(details.issues) ? details.issues : [];
73
+ this.issues = isArray(details.issues) ? details.issues : [];
72
74
  this.formError = details.formError || null;
73
75
  this.data = details.data !== undefined ? details.data : null;
74
76
  this.isCanceled = this.kind === 'canceled';
@@ -176,7 +178,7 @@ function buildRequestUrl(urlTemplate, params, query) {
176
178
  const searchParams = new URLSearchParams();
177
179
  for (const [key, rawVal] of Object.entries(query)) {
178
180
  if (rawVal === undefined) continue;
179
- if (Array.isArray(rawVal)) {
181
+ if (isArray(rawVal)) {
180
182
  for (const item of rawVal) {
181
183
  if (item === undefined) continue;
182
184
  searchParams.append(key, item instanceof Date ? item.toISOString() : String(item));
@@ -205,7 +207,7 @@ function containsBinaryValue(value) {
205
207
  if (typeof Blob !== 'undefined' && value instanceof Blob) return true;
206
208
  if (typeof File !== 'undefined' && value instanceof File) return true;
207
209
  if (typeof FormData !== 'undefined' && value instanceof FormData) return true;
208
- if (Array.isArray(value)) {
210
+ if (isArray(value)) {
209
211
  return value.some(containsBinaryValue);
210
212
  }
211
213
  for (const v of Object.values(value)) {
@@ -221,7 +223,7 @@ function objectToFormData(obj) {
221
223
  const fd = new FormData();
222
224
  for (const [key, val] of Object.entries(obj)) {
223
225
  if (val === undefined) continue;
224
- if (Array.isArray(val)) {
226
+ if (isArray(val)) {
225
227
  for (const item of val) {
226
228
  if (item === undefined) continue;
227
229
  if (typeof Blob !== 'undefined' && item instanceof Blob) {
@@ -307,9 +309,9 @@ function extractValidationErrors(payload) {
307
309
 
308
310
  // 1. Direct fieldErrors map (e.g. { title: 'Required' } or { title: ['Required'] })
309
311
  const rawFieldErrors = errObj.fieldErrors || payload.fieldErrors || errObj.fields || null;
310
- if (rawFieldErrors && typeof rawFieldErrors === 'object' && !Array.isArray(rawFieldErrors)) {
312
+ if (rawFieldErrors && typeof rawFieldErrors === 'object' && !isArray(rawFieldErrors)) {
311
313
  for (const [field, msgOrArr] of Object.entries(rawFieldErrors)) {
312
- const list = Array.isArray(msgOrArr) ? msgOrArr.map(String) : [String(msgOrArr)];
314
+ const list = isArray(msgOrArr) ? msgOrArr.map(String) : [String(msgOrArr)];
313
315
  if (list.length > 0) {
314
316
  fieldErrors[field] = list[0];
315
317
  fieldErrorList[field] = list;
@@ -320,17 +322,17 @@ function extractValidationErrors(payload) {
320
322
 
321
323
  // 2. Zod / Webspresso `issues` or `details` array
322
324
  const rawIssues =
323
- (Array.isArray(errObj.issues) && errObj.issues) ||
324
- (Array.isArray(payload.issues) && payload.issues) ||
325
- (Array.isArray(errObj.details) && errObj.details) ||
326
- (Array.isArray(payload.details) && payload.details) ||
327
- (Array.isArray(errObj.errors) && errObj.errors) ||
325
+ (isArray(errObj.issues) && errObj.issues) ||
326
+ (isArray(payload.issues) && payload.issues) ||
327
+ (isArray(errObj.details) && errObj.details) ||
328
+ (isArray(payload.details) && payload.details) ||
329
+ (isArray(errObj.errors) && errObj.errors) ||
328
330
  [];
329
331
 
330
332
  for (const issue of rawIssues) {
331
333
  if (!issue || typeof issue !== 'object') continue;
332
334
  let rawPath = '';
333
- if (Array.isArray(issue.path)) {
335
+ if (isArray(issue.path)) {
334
336
  // Strip leading 'body' / 'query' / 'params' prefix from route validation paths
335
337
  const cleanParts =
336
338
  issue.path.length > 1 &&
@@ -393,8 +395,8 @@ function unwrapApiResultAndInvalidate(parsedBody, responseHeaders) {
393
395
  if (
394
396
  parsedBody &&
395
397
  typeof parsedBody === 'object' &&
396
- !Array.isArray(parsedBody) &&
397
- Array.isArray(parsedBody.invalidate) &&
398
+ !isArray(parsedBody) &&
399
+ isArray(parsedBody.invalidate) &&
398
400
  'data' in parsedBody
399
401
  ) {
400
402
  for (const tag of parsedBody.invalidate) {
@@ -495,18 +497,18 @@ function normalizeRetryConfig(retryOpt, upperMethod, retryMutations = false) {
495
497
  let statusCodes = new Set([408, 429, 502, 503, 504]);
496
498
  let onRetry = null;
497
499
 
498
- if (typeof retryOpt === 'number') {
500
+ if (isNumber(retryOpt)) {
499
501
  count = Math.max(0, Math.floor(retryOpt));
500
502
  } else if (retryOpt === true) {
501
503
  count = 2;
502
504
  } else if (typeof retryOpt === 'object' && retryOpt !== null) {
503
- count = typeof retryOpt.count === 'number' ? Math.max(0, Math.floor(retryOpt.count)) : 2;
504
- if (typeof retryOpt.delay === 'number') delay = Math.max(0, retryOpt.delay);
505
- if (typeof retryOpt.maxDelay === 'number') maxDelay = Math.max(delay, retryOpt.maxDelay);
505
+ count = isNumber(retryOpt.count) ? Math.max(0, Math.floor(retryOpt.count)) : 2;
506
+ if (isNumber(retryOpt.delay)) delay = Math.max(0, retryOpt.delay);
507
+ if (isNumber(retryOpt.maxDelay)) maxDelay = Math.max(delay, retryOpt.maxDelay);
506
508
  if (retryOpt.backoff === 'linear' || retryOpt.backoff === 'exponential') {
507
509
  backoff = retryOpt.backoff;
508
510
  }
509
- if (Array.isArray(retryOpt.statusCodes)) {
511
+ if (isArray(retryOpt.statusCodes)) {
510
512
  statusCodes = new Set(retryOpt.statusCodes);
511
513
  }
512
514
  if (typeof retryOpt.onRetry === 'function') {
@@ -536,7 +538,7 @@ function isRetryableError(err, statusCodes) {
536
538
  // Network failures and timeouts are retryable
537
539
  if (err.kind === 'network' || err.kind === 'timeout' || err.isTimedOut) return true;
538
540
  // Transient HTTP status codes
539
- if (typeof err.status === 'number' && statusCodes && statusCodes.has(err.status)) {
541
+ if (isNumber(err.status) && statusCodes && statusCodes.has(err.status)) {
540
542
  return true;
541
543
  }
542
544
  return false;
@@ -797,7 +799,7 @@ function createApiClient(config = {}) {
797
799
  let timeoutTimer = null;
798
800
  let currentSeq = 1;
799
801
 
800
- if (typeof options.timeout === 'number' && options.timeout > 0) {
802
+ if (isNumber(options.timeout) && options.timeout > 0) {
801
803
  timeoutTimer = setTimeout(() => {
802
804
  timedOut = true;
803
805
  abortController.abort();