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.
@@ -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,
@@ -218,6 +218,22 @@ function createSignalsEngine() {
218
218
  }
219
219
  return out;
220
220
  }
221
+
222
+ getSignalCount() {
223
+ return this._nodes.size;
224
+ }
225
+
226
+ getSubscriberCount() {
227
+ const runners = new Set();
228
+ for (const node of this._nodes.values()) {
229
+ for (const sub of node._subs) {
230
+ if (!sub.disposed) {
231
+ runners.add(sub);
232
+ }
233
+ }
234
+ }
235
+ return runners.size;
236
+ }
221
237
  }
222
238
 
223
239
  return {
@@ -507,11 +523,15 @@ function createIsolatedControllerState(
507
523
  return Reflect.get(obj, prop, receiver);
508
524
  }
509
525
 
510
- // Allow accessing tasks on root state
526
+ // Allow accessing tasks and busy status on root state
511
527
  if (!basePath && prop === 'tasks') {
512
528
  const ctx = getContext();
513
529
  return ctx && ctx.tasks ? ctx.tasks : undefined;
514
530
  }
531
+ if (!basePath && (prop === '$busy' || prop === 'isBusy') && !Object.prototype.hasOwnProperty.call(obj, prop)) {
532
+ const ctx = getContext();
533
+ return ctx ? Boolean(ctx.isBusy) : false;
534
+ }
515
535
 
516
536
  // Check if accessing a root-level computed property
517
537
  if (!basePath && computedDefs && Object.prototype.hasOwnProperty.call(computedDefs, prop)) {
@@ -523,7 +543,7 @@ function createIsolatedControllerState(
523
543
  // Track signal dependency in Datastar signals store
524
544
  engine.signals.value(signalKeyFor(path));
525
545
  // If accessing array length or methods, also subscribe to the array path itself
526
- if (Array.isArray(obj) && basePath) {
546
+ if (isArray(obj) && basePath) {
527
547
  engine.signals.value(signalKeyFor(basePath));
528
548
  }
529
549
  const val = Reflect.get(obj, prop, receiver);
@@ -562,21 +582,21 @@ function createIsolatedControllerState(
562
582
  const prevValue = obj[prop];
563
583
  const path = basePath ? `${basePath}.${String(prop)}` : String(prop);
564
584
 
565
- if (Object.is(prevValue, rawNext) && !Array.isArray(obj)) {
585
+ if (Object.is(prevValue, rawNext) && !isArray(obj)) {
566
586
  return true;
567
587
  }
568
588
 
569
589
  Reflect.set(obj, prop, rawNext, receiver);
570
590
 
571
591
  // Seed nested keys if setting an object
572
- if (rawNext !== null && typeof rawNext === 'object' && !Array.isArray(rawNext)) {
592
+ if (rawNext !== null && typeof rawNext === 'object' && !isArray(rawNext)) {
573
593
  for (const [k, v] of Object.entries(rawNext)) {
574
594
  engine.signals.setValue(signalKeyFor(`${path}.${k}`), v);
575
595
  }
576
596
  }
577
597
  engine.signals.setValue(signalKeyFor(path), rawNext);
578
598
  if (basePath) {
579
- engine.signals.setValue(signalKeyFor(basePath), Array.isArray(obj) ? [...obj] : { ...obj });
599
+ engine.signals.setValue(signalKeyFor(basePath), isArray(obj) ? [...obj] : { ...obj });
580
600
  }
581
601
  return true;
582
602
  },
@@ -609,13 +629,13 @@ function createIsolatedControllerState(
609
629
  for (const [key, val] of Object.entries(initialState)) {
610
630
  const cloned =
611
631
  val !== null && typeof val === 'object'
612
- ? Array.isArray(val)
632
+ ? isArray(val)
613
633
  ? [...val]
614
634
  : { ...val }
615
635
  : val;
616
636
  rawStore[key] = cloned;
617
637
  engine.signals.setValue(signalKeyFor(key), cloned);
618
- if (cloned !== null && typeof cloned === 'object' && !Array.isArray(cloned)) {
638
+ if (cloned !== null && typeof cloned === 'object' && !isArray(cloned)) {
619
639
  for (const [subKey, subVal] of Object.entries(cloned)) {
620
640
  engine.signals.setValue(signalKeyFor(`${key}.${subKey}`), subVal);
621
641
  }
@@ -734,6 +754,8 @@ function getMorphNodeKey(node) {
734
754
  if (node.id) return `id:${node.id}`;
735
755
  const wKey = node.getAttribute('data-w-key');
736
756
  if (wKey) return `wkey:${wKey}`;
757
+ const hostKey = node.getAttribute('data-w-host');
758
+ if (hostKey) return `host:${hostKey}`;
737
759
  const fragId = node.getAttribute('data-w-fragment-id');
738
760
  if (fragId) return `frag:${fragId}`;
739
761
  const formName = node.getAttribute('data-w-form');
@@ -754,7 +776,7 @@ function getMorphNodeKey(node) {
754
776
  * Sync attributes from `fromEl` onto `toEl` while respecting client-owned properties
755
777
  * and preserving active focus / form draft states.
756
778
  */
757
- function syncElementAttributes(toEl, fromEl, ownership) {
779
+ function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false) {
758
780
  const fromAttrs = fromEl.attributes;
759
781
  const toAttrs = Array.from(toEl.attributes);
760
782
 
@@ -781,6 +803,9 @@ function syncElementAttributes(toEl, fromEl, ownership) {
781
803
  const attr = fromAttrs[i];
782
804
  if (ownership.disabled && attr.name === 'disabled') continue;
783
805
  if (ownership.show && attr.name === 'hidden') continue;
806
+ if (attr.name === 'action' && toEl.tagName === 'FORM' && (toEl.__wCustomAction || insideDirtyForm)) {
807
+ continue;
808
+ }
784
809
  if (toEl.getAttribute(attr.name) !== attr.value) {
785
810
  toEl.setAttribute(attr.name, attr.value);
786
811
  }
@@ -841,7 +866,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
841
866
  const isForm = toEl.tagName === 'FORM';
842
867
  const insideDirtyForm = Boolean(ctx.insideDirtyForm || (isForm && isFormDirtyOrPreserved(toEl)));
843
868
 
844
- syncElementAttributes(toEl, fromEl, ownership);
869
+ syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm);
845
870
 
846
871
  if (toEl.tagName === 'INPUT' || toEl.tagName === 'TEXTAREA' || toEl.tagName === 'SELECT') {
847
872
  syncFormControlValue(toEl, fromEl, ownership, insideDirtyForm);
@@ -850,6 +875,11 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
850
875
  }
851
876
  }
852
877
 
878
+ // If `toEl` is a client service UI host (`data-w-host`), the service owns its DOM contents
879
+ if (toEl.hasAttribute && toEl.hasAttribute('data-w-host')) {
880
+ return toEl;
881
+ }
882
+
853
883
  // If `toEl` has `data-w-text`, client state owns its textContent; do not overwrite from server HTML
854
884
  if (ownership.text) {
855
885
  return toEl;
@@ -885,7 +915,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
885
915
  }
886
916
  }
887
917
 
888
- if (!match) {
918
+ if (!match && !key) {
889
919
  // Look ahead for a compatible unkeyed element with the same tagName and controller attribute
890
920
  for (let j = unkeyedCursor; j < existingChildren.length; j++) {
891
921
  const cand = existingChildren[j];
@@ -951,10 +981,13 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
951
981
  }
952
982
  }
953
983
 
954
- // Remove leftover existing children that were not matched
984
+ // Remove leftover existing children that were not matched (preserve client service UI hosts)
955
985
  for (let i = 0; i < existingChildren.length; i++) {
956
986
  const oldChild = existingChildren[i];
957
987
  if (!usedExisting.has(oldChild) && oldChild.parentNode === toEl) {
988
+ if (oldChild.nodeType === 1 && oldChild.hasAttribute && oldChild.hasAttribute('data-w-host')) {
989
+ continue;
990
+ }
958
991
  if (oldChild.nodeType === 1 && typeof ctx.onBeforeNodeRemove === 'function') {
959
992
  ctx.onBeforeNodeRemove(oldChild);
960
993
  }
@@ -1049,7 +1082,7 @@ function createDatastarAdapter(options = {}) {
1049
1082
  * - Single binding per element/attribute per instance (`WeakMap` guard)
1050
1083
  * - All effects & listeners returned in a single cleanup function
1051
1084
  */
1052
- const boundElementsByInstance = new Map(); // instanceId -> WeakSet<Element>
1085
+ const boundElementsByInstance = new Map(); // instanceId -> Map<Element, Array<() => void>>
1053
1086
 
1054
1087
  function isWithinControllerScope(rootEl, candidateEl) {
1055
1088
  if (candidateEl === rootEl) return true;
@@ -1124,35 +1157,72 @@ function createDatastarAdapter(options = {}) {
1124
1157
  return bindings;
1125
1158
  }
1126
1159
 
1160
+ function toggleElementVisibility(el, visible, initialDisplay) {
1161
+ if (visible) {
1162
+ el.removeAttribute('hidden');
1163
+ if (el.style && el.style.display === 'none') {
1164
+ el.style.display = initialDisplay;
1165
+ }
1166
+ } else {
1167
+ el.setAttribute('hidden', '');
1168
+ if (el.style) {
1169
+ el.style.display = 'none';
1170
+ }
1171
+ }
1172
+ }
1173
+
1127
1174
  /**
1128
1175
  * Bind V1 declarative attributes inside `rootEl` for a controller instance.
1129
1176
  */
1130
- function bindControllerDom({ instanceId, rootEl, state, invokeAction, isConnected }) {
1177
+ function bindControllerDom({
1178
+ instanceId,
1179
+ rootEl,
1180
+ state,
1181
+ invokeAction,
1182
+ isConnected,
1183
+ isLoadingTargetActive,
1184
+ }) {
1131
1185
  if (!boundElementsByInstance.has(instanceId)) {
1132
- boundElementsByInstance.set(instanceId, new WeakSet());
1186
+ boundElementsByInstance.set(instanceId, new Map());
1187
+ }
1188
+ const elementDisposersMap = boundElementsByInstance.get(instanceId);
1189
+
1190
+ // Prune any previously bound elements that were detached/replaced by fragment patches
1191
+ for (const [prevEl, prevDisposers] of Array.from(elementDisposersMap.entries())) {
1192
+ const detached =
1193
+ !prevEl ||
1194
+ prevEl.isConnected === false ||
1195
+ (rootEl && typeof rootEl.contains === 'function' && !rootEl.contains(prevEl));
1196
+ if (detached) {
1197
+ for (let d = 0; d < prevDisposers.length; d++) {
1198
+ try {
1199
+ prevDisposers[d]();
1200
+ } catch (_e) {}
1201
+ }
1202
+ elementDisposersMap.delete(prevEl);
1203
+ }
1133
1204
  }
1134
- const boundSet = boundElementsByInstance.get(instanceId);
1135
- const disposers = [];
1136
1205
 
1137
1206
  const elements = collectScopedElements(rootEl);
1138
1207
 
1139
1208
  for (let i = 0; i < elements.length; i++) {
1140
1209
  const el = elements[i];
1141
- if (boundSet.has(el)) continue;
1142
- boundSet.add(el);
1210
+ if (elementDisposersMap.has(el)) continue;
1211
+ const elDisposers = [];
1212
+ elementDisposersMap.set(el, elDisposers);
1143
1213
 
1144
1214
  // 1. data-w-text
1145
1215
  if (el.hasAttribute('data-w-text')) {
1146
1216
  const expr = el.getAttribute('data-w-text');
1147
1217
  const stop = engine.effect(() => {
1148
- if (!isConnected()) return;
1218
+ if (!isConnected() || el.isConnected === false) return;
1149
1219
  const val = evaluateStateExpression(state, expr);
1150
1220
  const text = val === null || val === undefined ? '' : String(val);
1151
1221
  if (el.textContent !== text) {
1152
1222
  el.textContent = text;
1153
1223
  }
1154
1224
  });
1155
- disposers.push(stop);
1225
+ elDisposers.push(stop);
1156
1226
  }
1157
1227
 
1158
1228
  // 2. data-w-show
@@ -1160,28 +1230,43 @@ function createDatastarAdapter(options = {}) {
1160
1230
  const expr = el.getAttribute('data-w-show');
1161
1231
  const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
1162
1232
  const stop = engine.effect(() => {
1163
- 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';
1233
+ if (!isConnected() || el.isConnected === false) return;
1234
+ toggleElementVisibility(el, Boolean(evaluateStateExpression(state, expr)), initialDisplay);
1235
+ });
1236
+ elDisposers.push(stop);
1237
+ }
1238
+
1239
+ // 2b. data-w-loading & data-w-loading-hide (declarative action/task busy indicators)
1240
+ if (el.hasAttribute('data-w-loading') || el.hasAttribute('data-w-loading-hide')) {
1241
+ const isHide = el.hasAttribute('data-w-loading-hide');
1242
+ const rawSpec = el.getAttribute(isHide ? 'data-w-loading-hide' : 'data-w-loading') || '';
1243
+ const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
1244
+ const loadingClasses = (el.getAttribute('data-w-loading-class') || '')
1245
+ .trim()
1246
+ .split(/\s+/)
1247
+ .filter(Boolean);
1248
+ const stop = engine.effect(() => {
1249
+ if (!isConnected() || el.isConnected === false) return;
1250
+ const active =
1251
+ typeof isLoadingTargetActive === 'function'
1252
+ ? Boolean(isLoadingTargetActive(rawSpec))
1253
+ : Boolean(evaluateStateExpression(state, rawSpec || '$busy'));
1254
+ toggleElementVisibility(el, isHide ? !active : active, initialDisplay);
1255
+ if (loadingClasses.length > 0 && el.classList) {
1256
+ for (let c = 0; c < loadingClasses.length; c++) {
1257
+ if (active) el.classList.add(loadingClasses[c]);
1258
+ else el.classList.remove(loadingClasses[c]);
1174
1259
  }
1175
1260
  }
1176
1261
  });
1177
- disposers.push(stop);
1262
+ elDisposers.push(stop);
1178
1263
  }
1179
1264
 
1180
1265
  // 3. data-w-disabled
1181
1266
  if (el.hasAttribute('data-w-disabled')) {
1182
1267
  const expr = el.getAttribute('data-w-disabled');
1183
1268
  const stop = engine.effect(() => {
1184
- if (!isConnected()) return;
1269
+ if (!isConnected() || el.isConnected === false) return;
1185
1270
  const disabled = Boolean(evaluateStateExpression(state, expr));
1186
1271
  if ('disabled' in el) {
1187
1272
  el.disabled = disabled;
@@ -1194,7 +1279,7 @@ function createDatastarAdapter(options = {}) {
1194
1279
  el.removeAttribute('aria-disabled');
1195
1280
  }
1196
1281
  });
1197
- disposers.push(stop);
1282
+ elDisposers.push(stop);
1198
1283
  }
1199
1284
 
1200
1285
  // 4. data-w-model (two-way binding between input/select/textarea and controller state)
@@ -1236,7 +1321,7 @@ function createDatastarAdapter(options = {}) {
1236
1321
  };
1237
1322
  el.addEventListener('compositionstart', onCompositionStart);
1238
1323
  el.addEventListener('compositionend', onCompositionEnd);
1239
- disposers.push(() => {
1324
+ elDisposers.push(() => {
1240
1325
  try {
1241
1326
  el.removeEventListener('compositionstart', onCompositionStart);
1242
1327
  el.removeEventListener('compositionend', onCompositionEnd);
@@ -1245,11 +1330,11 @@ function createDatastarAdapter(options = {}) {
1245
1330
 
1246
1331
  // State -> DOM effect (synchronizes changes in state to the DOM control)
1247
1332
  const stopModelEffect = engine.effect(() => {
1248
- if (!isConnected() || isComposing) return;
1333
+ if (!isConnected() || el.isConnected === false || isComposing) return;
1249
1334
  const currentVal = getByPath(state, modelPath);
1250
1335
 
1251
1336
  if (type === 'checkbox') {
1252
- if (Array.isArray(currentVal)) {
1337
+ if (isArray(currentVal)) {
1253
1338
  el.checked = currentVal.map(String).includes(String(el.value));
1254
1339
  } else {
1255
1340
  el.checked = Boolean(currentVal);
@@ -1257,7 +1342,7 @@ function createDatastarAdapter(options = {}) {
1257
1342
  } else if (type === 'radio') {
1258
1343
  el.checked = String(currentVal) === String(el.value);
1259
1344
  } else if (el.tagName === 'SELECT' && el.multiple) {
1260
- const arr = Array.isArray(currentVal) ? currentVal.map(String) : [];
1345
+ const arr = isArray(currentVal) ? currentVal.map(String) : [];
1261
1346
  const opts = el.options || [];
1262
1347
  for (let j = 0; j < opts.length; j++) {
1263
1348
  opts[j].selected = arr.includes(String(opts[j].value));
@@ -1269,7 +1354,7 @@ function createDatastarAdapter(options = {}) {
1269
1354
  }
1270
1355
  }
1271
1356
  });
1272
- disposers.push(stopModelEffect);
1357
+ elDisposers.push(stopModelEffect);
1273
1358
 
1274
1359
  // DOM -> State listener (synchronizes user interactions to state)
1275
1360
  const evtType =
@@ -1278,11 +1363,11 @@ function createDatastarAdapter(options = {}) {
1278
1363
  : 'input';
1279
1364
 
1280
1365
  const onModelInput = () => {
1281
- if (!isConnected() || isComposing) return;
1366
+ if (!isConnected() || el.isConnected === false || isComposing) return;
1282
1367
 
1283
1368
  if (type === 'checkbox') {
1284
1369
  const cur = getByPath(state, modelPath);
1285
- if (Array.isArray(cur)) {
1370
+ if (isArray(cur)) {
1286
1371
  const next = el.checked
1287
1372
  ? Array.from(new Set([...cur, el.value]))
1288
1373
  : cur.filter((item) => String(item) !== String(el.value));
@@ -1310,7 +1395,7 @@ function createDatastarAdapter(options = {}) {
1310
1395
  };
1311
1396
 
1312
1397
  el.addEventListener(evtType, onModelInput);
1313
- disposers.push(() => el.removeEventListener(evtType, onModelInput));
1398
+ elDisposers.push(() => el.removeEventListener(evtType, onModelInput));
1314
1399
  }
1315
1400
  }
1316
1401
 
@@ -1318,23 +1403,30 @@ function createDatastarAdapter(options = {}) {
1318
1403
  const eventBindings = extractEventBindings(el);
1319
1404
  for (const binding of eventBindings) {
1320
1405
  const listener = (event) => {
1321
- if (!isConnected()) return;
1406
+ if (!isConnected() || el.isConnected === false) return;
1322
1407
  if (binding.modifiers.has('prevent')) event.preventDefault();
1323
1408
  if (binding.modifiers.has('stop')) event.stopPropagation();
1324
1409
  invokeAction(binding.actionName, event, el);
1325
1410
  };
1326
1411
  el.addEventListener(binding.eventName, listener);
1327
- disposers.push(() => el.removeEventListener(binding.eventName, listener));
1412
+ elDisposers.push(() => el.removeEventListener(binding.eventName, listener));
1328
1413
  }
1329
1414
  }
1330
1415
 
1331
1416
  return function unbindInstanceDom() {
1332
- for (let i = 0; i < disposers.length; i++) {
1333
- try {
1334
- disposers[i]();
1335
- } catch (_err) {
1336
- // Ignore individual unbind errors
1417
+ const map = boundElementsByInstance.get(instanceId);
1418
+ if (map) {
1419
+ for (const elDisposers of map.values()) {
1420
+ for (let i = 0; i < elDisposers.length; i++) {
1421
+ try {
1422
+ elDisposers[i]();
1423
+ } catch (_err) {
1424
+ // Ignore individual unbind errors
1425
+ }
1426
+ }
1337
1427
  }
1428
+ map.clear();
1429
+ boundElementsByInstance.delete(instanceId);
1338
1430
  }
1339
1431
  };
1340
1432
  }
@@ -1344,14 +1436,53 @@ function createDatastarAdapter(options = {}) {
1344
1436
  */
1345
1437
  function patchFragmentElement(targetEl, htmlString, lifecycleCallbacks = {}) {
1346
1438
  if (!targetEl || typeof htmlString !== 'string') return targetEl;
1347
- const parsed = parseHtmlFragment(htmlString, targetEl.ownerDocument);
1439
+ const doc = targetEl.ownerDocument || (typeof document !== 'undefined' ? document : null);
1440
+ const parsed = parseHtmlFragment(htmlString, doc);
1348
1441
  if (!parsed) return targetEl;
1349
1442
 
1443
+ // Capture active focus & text selection if inside targetEl before morphing
1444
+ let focusedEl = null;
1445
+ let selStart = null;
1446
+ let selEnd = null;
1447
+ if (
1448
+ doc &&
1449
+ doc.activeElement &&
1450
+ typeof targetEl.contains === 'function' &&
1451
+ targetEl.contains(doc.activeElement)
1452
+ ) {
1453
+ focusedEl = doc.activeElement;
1454
+ try {
1455
+ if (typeof focusedEl.selectionStart === 'number') selStart = focusedEl.selectionStart;
1456
+ if (typeof focusedEl.selectionEnd === 'number') selEnd = focusedEl.selectionEnd;
1457
+ } catch (_e) {}
1458
+ }
1459
+
1350
1460
  const morphed = morphDomTrees(targetEl, parsed, {
1351
1461
  onBeforeNodeRemove: lifecycleCallbacks.onBeforeNodeRemove,
1352
1462
  onNodeAdded: lifecycleCallbacks.onNodeAdded,
1353
1463
  });
1354
1464
 
1465
+ // Restore focus & selection range if the focused element was preserved inside targetEl
1466
+ if (
1467
+ focusedEl &&
1468
+ focusedEl.isConnected !== false &&
1469
+ typeof targetEl.contains === 'function' &&
1470
+ targetEl.contains(focusedEl)
1471
+ ) {
1472
+ try {
1473
+ if (doc && doc.activeElement !== focusedEl && typeof focusedEl.focus === 'function') {
1474
+ focusedEl.focus();
1475
+ }
1476
+ if (
1477
+ typeof selStart === 'number' &&
1478
+ typeof selEnd === 'number' &&
1479
+ typeof focusedEl.setSelectionRange === 'function'
1480
+ ) {
1481
+ focusedEl.setSelectionRange(selStart, selEnd);
1482
+ }
1483
+ } catch (_e) {}
1484
+ }
1485
+
1355
1486
  // If external @starfederation/datastar is active, notify apply() on updated subtree
1356
1487
  if (externalDatastar && typeof externalDatastar.apply === 'function') {
1357
1488
  try {
@@ -1382,6 +1513,14 @@ function createDatastarAdapter(options = {}) {
1382
1513
  setByPath,
1383
1514
  isWithinControllerScope,
1384
1515
  collectScopedElements,
1516
+ getBoundInstanceCount: () => boundElementsByInstance.size,
1517
+ getBoundElementCount: () => {
1518
+ let total = 0;
1519
+ for (const m of boundElementsByInstance.values()) {
1520
+ total += m.size;
1521
+ }
1522
+ return total;
1523
+ },
1385
1524
  };
1386
1525
  }
1387
1526
 
@@ -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();