@thelacanians/vue-native-runtime 0.2.0 → 0.4.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.
package/dist/index.cjs CHANGED
@@ -160,7 +160,7 @@ function createCommentNode(_text) {
160
160
  }
161
161
 
162
162
  // src/bridge.ts
163
- var NativeBridgeImpl = class {
163
+ var _NativeBridgeImpl = class _NativeBridgeImpl {
164
164
  constructor() {
165
165
  /** Pending operations waiting to be flushed to native */
166
166
  this.pendingOps = [];
@@ -170,7 +170,8 @@ var NativeBridgeImpl = class {
170
170
  this.eventHandlers = /* @__PURE__ */ new Map();
171
171
  /** Pending async callbacks from native module invocations */
172
172
  this.pendingCallbacks = /* @__PURE__ */ new Map();
173
- /** Auto-incrementing callback ID for async native module calls */
173
+ /** Auto-incrementing callback ID for async native module calls.
174
+ * Wraps around at MAX_SAFE_CALLBACK_ID to prevent overflow. */
174
175
  this.nextCallbackId = 1;
175
176
  /** Global event listeners: eventName -> Set of callbacks */
176
177
  this.globalEventHandlers = /* @__PURE__ */ new Map();
@@ -202,10 +203,14 @@ var NativeBridgeImpl = class {
202
203
  const json = JSON.stringify(ops);
203
204
  const flushFn = globalThis.__VN_flushOperations;
204
205
  if (typeof flushFn === "function") {
205
- flushFn(json);
206
- } else if (__DEV__) {
206
+ try {
207
+ flushFn(json);
208
+ } catch (err) {
209
+ console.error("[VueNative] Error in __VN_flushOperations:", err);
210
+ }
211
+ } else {
207
212
  console.warn(
208
- "[VueNative] __VN_flushOperations is not registered. Make sure the Swift runtime has been initialized."
213
+ "[VueNative] __VN_flushOperations is not registered. Make sure the native runtime has been initialized."
209
214
  );
210
215
  }
211
216
  }
@@ -365,7 +370,12 @@ var NativeBridgeImpl = class {
365
370
  */
366
371
  invokeNativeModule(moduleName, methodName, args = [], timeoutMs = 3e4) {
367
372
  return new Promise((resolve, reject) => {
368
- const callbackId = this.nextCallbackId++;
373
+ const callbackId = this.nextCallbackId;
374
+ if (this.nextCallbackId >= _NativeBridgeImpl.MAX_CALLBACK_ID) {
375
+ this.nextCallbackId = 1;
376
+ } else {
377
+ this.nextCallbackId++;
378
+ }
369
379
  const timeoutId = setTimeout(() => {
370
380
  if (this.pendingCallbacks.has(callbackId)) {
371
381
  this.pendingCallbacks.delete(callbackId);
@@ -374,6 +384,17 @@ var NativeBridgeImpl = class {
374
384
  ));
375
385
  }
376
386
  }, timeoutMs);
387
+ if (this.pendingCallbacks.size >= _NativeBridgeImpl.MAX_PENDING_CALLBACKS) {
388
+ const oldestKey = this.pendingCallbacks.keys().next().value;
389
+ if (oldestKey !== void 0) {
390
+ const oldest = this.pendingCallbacks.get(oldestKey);
391
+ if (oldest) {
392
+ clearTimeout(oldest.timeoutId);
393
+ oldest.reject(new Error("Callback queue full, evicting oldest pending callback"));
394
+ this.pendingCallbacks.delete(oldestKey);
395
+ }
396
+ }
397
+ }
377
398
  this.pendingCallbacks.set(callbackId, { resolve, reject, timeoutId });
378
399
  this.enqueue("invokeNativeModule", [moduleName, methodName, args, callbackId]);
379
400
  });
@@ -393,11 +414,9 @@ var NativeBridgeImpl = class {
393
414
  resolveCallback(callbackId, result, error) {
394
415
  const pending = this.pendingCallbacks.get(callbackId);
395
416
  if (!pending) {
396
- if (__DEV__) {
397
- console.warn(
398
- `[VueNative] Received callback for unknown callbackId: ${callbackId}`
399
- );
400
- }
417
+ console.warn(
418
+ `[VueNative] Received callback for unknown callbackId: ${callbackId}. This likely means the callback already timed out or was evicted. The late response has been discarded.`
419
+ );
401
420
  return;
402
421
  }
403
422
  clearTimeout(pending.timeoutId);
@@ -460,6 +479,11 @@ var NativeBridgeImpl = class {
460
479
  this.globalEventHandlers.clear();
461
480
  }
462
481
  };
482
+ /** Maximum callback ID before wraparound (safe for 32-bit signed int) */
483
+ _NativeBridgeImpl.MAX_CALLBACK_ID = 2147483647;
484
+ /** Maximum number of pending callbacks before evicting the oldest */
485
+ _NativeBridgeImpl.MAX_PENDING_CALLBACKS = 1e3;
486
+ var NativeBridgeImpl = _NativeBridgeImpl;
463
487
  if (typeof globalThis.__DEV__ === "undefined") {
464
488
  ;
465
489
  globalThis.__DEV__ = true;
@@ -478,17 +502,21 @@ function toEventName(key) {
478
502
  return key.slice(2).toLowerCase();
479
503
  }
480
504
  function patchStyle(nodeId, prevStyle, nextStyle) {
481
- const prev = prevStyle || {};
482
- const next = nextStyle || {};
483
- for (const key in next) {
484
- if (next[key] !== prev[key]) {
485
- NativeBridge.updateStyle(nodeId, key, next[key]);
505
+ try {
506
+ const prev = prevStyle || {};
507
+ const next = nextStyle || {};
508
+ for (const key in next) {
509
+ if (next[key] !== prev[key]) {
510
+ NativeBridge.updateStyle(nodeId, key, next[key]);
511
+ }
486
512
  }
487
- }
488
- for (const key in prev) {
489
- if (!(key in next)) {
490
- NativeBridge.updateStyle(nodeId, key, null);
513
+ for (const key in prev) {
514
+ if (!(key in next)) {
515
+ NativeBridge.updateStyle(nodeId, key, null);
516
+ }
491
517
  }
518
+ } catch (err) {
519
+ console.error(`[VueNative] Error patching style on node ${nodeId}:`, err);
492
520
  }
493
521
  }
494
522
  var nodeOps = {
@@ -541,22 +569,26 @@ var nodeOps = {
541
569
  * - all else -> updateProp
542
570
  */
543
571
  patchProp(el, key, prevValue, nextValue) {
544
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase()) {
545
- const eventName = toEventName(key);
546
- if (prevValue) {
547
- NativeBridge.removeEventListener(el.id, eventName);
572
+ try {
573
+ if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase()) {
574
+ const eventName = toEventName(key);
575
+ if (prevValue) {
576
+ NativeBridge.removeEventListener(el.id, eventName);
577
+ }
578
+ if (nextValue) {
579
+ NativeBridge.addEventListener(el.id, eventName, nextValue);
580
+ }
581
+ return;
548
582
  }
549
- if (nextValue) {
550
- NativeBridge.addEventListener(el.id, eventName, nextValue);
583
+ if (key === "style") {
584
+ patchStyle(el.id, prevValue, nextValue);
585
+ return;
551
586
  }
552
- return;
553
- }
554
- if (key === "style") {
555
- patchStyle(el.id, prevValue, nextValue);
556
- return;
587
+ el.props[key] = nextValue;
588
+ NativeBridge.updateProp(el.id, key, nextValue);
589
+ } catch (err) {
590
+ console.error(`[VueNative] Error patching prop "${key}" on node ${el.id}:`, err);
557
591
  }
558
- el.props[key] = nextValue;
559
- NativeBridge.updateProp(el.id, key, nextValue);
560
592
  },
561
593
  /**
562
594
  * Insert a child node into a parent, optionally before an anchor node.
@@ -571,30 +603,34 @@ var nodeOps = {
571
603
  }
572
604
  }
573
605
  child.parent = parent;
574
- if (anchor) {
575
- const anchorIdx = parent.children.indexOf(anchor);
576
- if (anchorIdx !== -1) {
577
- parent.children.splice(anchorIdx, 0, child);
578
- } else {
579
- parent.children.push(child);
580
- }
581
- if (child.type !== "__COMMENT__") {
582
- if (anchor.type !== "__COMMENT__") {
583
- NativeBridge.insertBefore(parent.id, child.id, anchor.id);
606
+ try {
607
+ if (anchor) {
608
+ const anchorIdx = parent.children.indexOf(anchor);
609
+ if (anchorIdx !== -1) {
610
+ parent.children.splice(anchorIdx, 0, child);
584
611
  } else {
585
- const realAnchor = findNextNonComment(parent, anchor);
586
- if (realAnchor) {
587
- NativeBridge.insertBefore(parent.id, child.id, realAnchor.id);
612
+ parent.children.push(child);
613
+ }
614
+ if (child.type !== "__COMMENT__") {
615
+ if (anchor.type !== "__COMMENT__") {
616
+ NativeBridge.insertBefore(parent.id, child.id, anchor.id);
588
617
  } else {
589
- NativeBridge.appendChild(parent.id, child.id);
618
+ const realAnchor = findNextNonComment(parent, anchor);
619
+ if (realAnchor) {
620
+ NativeBridge.insertBefore(parent.id, child.id, realAnchor.id);
621
+ } else {
622
+ NativeBridge.appendChild(parent.id, child.id);
623
+ }
590
624
  }
591
625
  }
626
+ } else {
627
+ parent.children.push(child);
628
+ if (child.type !== "__COMMENT__") {
629
+ NativeBridge.appendChild(parent.id, child.id);
630
+ }
592
631
  }
593
- } else {
594
- parent.children.push(child);
595
- if (child.type !== "__COMMENT__") {
596
- NativeBridge.appendChild(parent.id, child.id);
597
- }
632
+ } catch (err) {
633
+ console.error(`[VueNative] Error inserting node ${child.id} into ${parent.id}:`, err);
598
634
  }
599
635
  },
600
636
  /**
@@ -608,8 +644,12 @@ var nodeOps = {
608
644
  parent.children.splice(idx, 1);
609
645
  }
610
646
  child.parent = null;
611
- if (child.type !== "__COMMENT__") {
612
- NativeBridge.removeChild(parent.id, child.id);
647
+ try {
648
+ if (child.type !== "__COMMENT__") {
649
+ NativeBridge.removeChild(parent.id, child.id);
650
+ }
651
+ } catch (err) {
652
+ console.error(`[VueNative] Error removing node ${child.id}:`, err);
613
653
  }
614
654
  }
615
655
  },
@@ -757,7 +797,17 @@ var VInput = (0, import_runtime_core5.defineComponent)({
757
797
  },
758
798
  emits: ["update:modelValue", "focus", "blur", "submit"],
759
799
  setup(props, { emit }) {
800
+ const isComposing = (0, import_runtime_core5.ref)(false);
801
+ const onCompositionstart = () => {
802
+ isComposing.value = true;
803
+ };
804
+ const onCompositionend = (payload) => {
805
+ isComposing.value = false;
806
+ const text = typeof payload === "string" ? payload : payload?.text ?? "";
807
+ emit("update:modelValue", text);
808
+ };
760
809
  const onChangetext = (payload) => {
810
+ if (isComposing.value) return;
761
811
  const text = typeof payload === "string" ? payload : payload?.text ?? "";
762
812
  emit("update:modelValue", text);
763
813
  };
@@ -786,6 +836,8 @@ var VInput = (0, import_runtime_core5.defineComponent)({
786
836
  accessibilityHint: props.accessibilityHint,
787
837
  accessibilityState: props.accessibilityState,
788
838
  onChangetext,
839
+ onCompositionstart,
840
+ onCompositionend,
789
841
  onFocus,
790
842
  onBlur,
791
843
  onSubmit
@@ -898,6 +950,11 @@ var VScrollView = (0, import_runtime_core8.defineComponent)({
898
950
  default: false
899
951
  },
900
952
  contentContainerStyle: Object,
953
+ /** Minimum interval in ms between scroll event emissions. Default: 16 (~60fps) */
954
+ scrollEventThrottle: {
955
+ type: Number,
956
+ default: 16
957
+ },
901
958
  /** Whether the pull-to-refresh indicator is active */
902
959
  refreshing: {
903
960
  type: Boolean,
@@ -911,8 +968,13 @@ var VScrollView = (0, import_runtime_core8.defineComponent)({
911
968
  },
912
969
  emits: ["scroll", "refresh"],
913
970
  setup(props, { slots, emit }) {
971
+ let lastScrollEmit = 0;
914
972
  const onScroll = (payload) => {
915
- emit("scroll", payload);
973
+ const now = Date.now();
974
+ if (now - lastScrollEmit >= props.scrollEventThrottle) {
975
+ lastScrollEmit = now;
976
+ emit("scroll", payload);
977
+ }
916
978
  };
917
979
  const onRefresh = () => {
918
980
  emit("refresh");
@@ -959,13 +1021,29 @@ var VImage = (0, import_runtime_core9.defineComponent)({
959
1021
  accessibilityState: Object
960
1022
  },
961
1023
  emits: ["load", "error"],
962
- setup(props, { emit }) {
1024
+ setup(props, { emit, expose }) {
1025
+ const loading = (0, import_runtime_core9.ref)(true);
1026
+ (0, import_runtime_core9.watch)(
1027
+ () => props.source?.uri,
1028
+ () => {
1029
+ loading.value = true;
1030
+ }
1031
+ );
1032
+ const onLoad = () => {
1033
+ loading.value = false;
1034
+ emit("load");
1035
+ };
1036
+ const onError = (e) => {
1037
+ loading.value = false;
1038
+ emit("error", e);
1039
+ };
1040
+ expose({ loading });
963
1041
  return () => (0, import_runtime_core9.h)(
964
1042
  "VImage",
965
1043
  {
966
1044
  ...props,
967
- onLoad: () => emit("load"),
968
- onError: (e) => emit("error", e)
1045
+ onLoad,
1046
+ onError
969
1047
  }
970
1048
  );
971
1049
  }
@@ -1071,8 +1149,29 @@ var VList = (0, import_runtime_core13.defineComponent)({
1071
1149
  },
1072
1150
  emits: ["scroll", "endReached"],
1073
1151
  setup(props, { slots, emit }) {
1152
+ let lastScrollEmit = 0;
1153
+ const onScroll = (e) => {
1154
+ const now = Date.now();
1155
+ if (now - lastScrollEmit >= 16) {
1156
+ lastScrollEmit = now;
1157
+ emit("scroll", e);
1158
+ }
1159
+ };
1074
1160
  return () => {
1075
1161
  const items = props.data ?? [];
1162
+ if (typeof __DEV__ !== "undefined" && __DEV__ && items.length > 0) {
1163
+ const keys = /* @__PURE__ */ new Set();
1164
+ for (let index = 0; index < items.length; index++) {
1165
+ const key = props.keyExtractor(items[index], index);
1166
+ if (keys.has(key)) {
1167
+ console.warn(
1168
+ `[VueNative] VList: Duplicate key "${key}" at index ${index}. Each item must have a unique key for correct reconciliation.`
1169
+ );
1170
+ break;
1171
+ }
1172
+ keys.add(key);
1173
+ }
1174
+ }
1076
1175
  const children = [];
1077
1176
  if (slots.header) {
1078
1177
  children.push(
@@ -1110,7 +1209,7 @@ var VList = (0, import_runtime_core13.defineComponent)({
1110
1209
  showsScrollIndicator: props.showsScrollIndicator,
1111
1210
  bounces: props.bounces,
1112
1211
  horizontal: props.horizontal,
1113
- onScroll: (e) => emit("scroll", e),
1212
+ onScroll,
1114
1213
  onEndReached: () => emit("endReached")
1115
1214
  },
1116
1215
  children
@@ -1135,10 +1234,24 @@ var VModal = (0, import_runtime_core14.defineComponent)({
1135
1234
  },
1136
1235
  emits: ["dismiss"],
1137
1236
  setup(props, { slots, emit }) {
1237
+ const debouncedVisible = (0, import_runtime_core14.ref)(props.visible);
1238
+ let visibleTimer;
1239
+ (0, import_runtime_core14.watch)(
1240
+ () => props.visible,
1241
+ (val) => {
1242
+ if (visibleTimer) clearTimeout(visibleTimer);
1243
+ visibleTimer = setTimeout(() => {
1244
+ debouncedVisible.value = val;
1245
+ }, 50);
1246
+ }
1247
+ );
1248
+ (0, import_runtime_core14.onUnmounted)(() => {
1249
+ if (visibleTimer) clearTimeout(visibleTimer);
1250
+ });
1138
1251
  return () => (0, import_runtime_core14.h)(
1139
1252
  "VModal",
1140
1253
  {
1141
- visible: props.visible,
1254
+ visible: debouncedVisible.value,
1142
1255
  style: props.style,
1143
1256
  onDismiss: () => emit("dismiss")
1144
1257
  },
@@ -1159,8 +1272,22 @@ var VAlertDialog = (0, import_runtime_core15.defineComponent)({
1159
1272
  },
1160
1273
  emits: ["confirm", "cancel", "action"],
1161
1274
  setup(props, { emit }) {
1275
+ const debouncedVisible = (0, import_runtime_core15.ref)(props.visible);
1276
+ let visibleTimer;
1277
+ (0, import_runtime_core15.watch)(
1278
+ () => props.visible,
1279
+ (val) => {
1280
+ if (visibleTimer) clearTimeout(visibleTimer);
1281
+ visibleTimer = setTimeout(() => {
1282
+ debouncedVisible.value = val;
1283
+ }, 50);
1284
+ }
1285
+ );
1286
+ (0, import_runtime_core15.onUnmounted)(() => {
1287
+ if (visibleTimer) clearTimeout(visibleTimer);
1288
+ });
1162
1289
  return () => (0, import_runtime_core15.h)("VAlertDialog", {
1163
- visible: props.visible,
1290
+ visible: debouncedVisible.value,
1164
1291
  title: props.title,
1165
1292
  message: props.message,
1166
1293
  buttons: props.buttons,
@@ -1200,8 +1327,18 @@ var VWebView = (0, import_runtime_core17.defineComponent)({
1200
1327
  },
1201
1328
  emits: ["load", "error", "message"],
1202
1329
  setup(props, { emit }) {
1330
+ const sanitizedSource = (0, import_runtime_core17.computed)(() => {
1331
+ const source = props.source;
1332
+ if (!source?.uri) return source;
1333
+ const lower = source.uri.toLowerCase().trim();
1334
+ if (lower.startsWith("javascript:") || lower.startsWith("data:text/html")) {
1335
+ console.warn("[VueNative] VWebView: Blocked potentially unsafe URI scheme");
1336
+ return { ...source, uri: void 0 };
1337
+ }
1338
+ return source;
1339
+ });
1203
1340
  return () => (0, import_runtime_core17.h)("VWebView", {
1204
- source: props.source,
1341
+ source: sanitizedSource.value,
1205
1342
  style: props.style,
1206
1343
  javaScriptEnabled: props.javaScriptEnabled,
1207
1344
  onLoad: (e) => emit("load", e),
@@ -1847,15 +1984,33 @@ function useHaptics() {
1847
1984
  }
1848
1985
 
1849
1986
  // src/composables/useAsyncStorage.ts
1987
+ var writeQueues = /* @__PURE__ */ new Map();
1988
+ function queueWrite(key, fn) {
1989
+ const prev = writeQueues.get(key) ?? Promise.resolve();
1990
+ const next = prev.then(fn, fn);
1991
+ writeQueues.set(key, next);
1992
+ next.then(() => {
1993
+ if (writeQueues.get(key) === next) {
1994
+ writeQueues.delete(key);
1995
+ }
1996
+ });
1997
+ return next;
1998
+ }
1850
1999
  function useAsyncStorage() {
1851
2000
  function getItem(key) {
1852
2001
  return NativeBridge.invokeNativeModule("AsyncStorage", "getItem", [key]);
1853
2002
  }
1854
2003
  function setItem(key, value) {
1855
- return NativeBridge.invokeNativeModule("AsyncStorage", "setItem", [key, value]).then(() => void 0);
2004
+ return queueWrite(
2005
+ key,
2006
+ () => NativeBridge.invokeNativeModule("AsyncStorage", "setItem", [key, value]).then(() => void 0)
2007
+ );
1856
2008
  }
1857
2009
  function removeItem(key) {
1858
- return NativeBridge.invokeNativeModule("AsyncStorage", "removeItem", [key]).then(() => void 0);
2010
+ return queueWrite(
2011
+ key,
2012
+ () => NativeBridge.invokeNativeModule("AsyncStorage", "removeItem", [key]).then(() => void 0)
2013
+ );
1859
2014
  }
1860
2015
  function getAllKeys() {
1861
2016
  return NativeBridge.invokeNativeModule("AsyncStorage", "getAllKeys", []);
@@ -1992,15 +2147,20 @@ var import_runtime_core33 = require("@vue/runtime-core");
1992
2147
  function useNetwork() {
1993
2148
  const isConnected = (0, import_runtime_core33.ref)(true);
1994
2149
  const connectionType = (0, import_runtime_core33.ref)("unknown");
1995
- NativeBridge.invokeNativeModule("Network", "getStatus").then((status) => {
1996
- isConnected.value = status.isConnected;
1997
- connectionType.value = status.connectionType;
1998
- }).catch(() => {
1999
- });
2150
+ let lastEventTime = 0;
2000
2151
  const unsubscribe = NativeBridge.onGlobalEvent("network:change", (payload) => {
2152
+ lastEventTime = Date.now();
2001
2153
  isConnected.value = payload.isConnected;
2002
2154
  connectionType.value = payload.connectionType;
2003
2155
  });
2156
+ const initTime = Date.now();
2157
+ NativeBridge.invokeNativeModule("Network", "getStatus").then((status) => {
2158
+ if (lastEventTime <= initTime) {
2159
+ isConnected.value = status.isConnected;
2160
+ connectionType.value = status.connectionType;
2161
+ }
2162
+ }).catch(() => {
2163
+ });
2004
2164
  (0, import_runtime_core33.onUnmounted)(unsubscribe);
2005
2165
  return { isConnected, connectionType };
2006
2166
  }
@@ -2057,21 +2217,39 @@ function useGeolocation() {
2057
2217
  const error = (0, import_runtime_core35.ref)(null);
2058
2218
  let watchId = null;
2059
2219
  async function getCurrentPosition() {
2060
- const result = await NativeBridge.invokeNativeModule("Geolocation", "getCurrentPosition");
2061
- coords.value = result;
2062
- return result;
2220
+ try {
2221
+ error.value = null;
2222
+ const result = await NativeBridge.invokeNativeModule("Geolocation", "getCurrentPosition");
2223
+ coords.value = result;
2224
+ return result;
2225
+ } catch (e) {
2226
+ const msg = e instanceof Error ? e.message : String(e);
2227
+ error.value = msg;
2228
+ throw e;
2229
+ }
2063
2230
  }
2064
2231
  async function watchPosition() {
2065
- const id = await NativeBridge.invokeNativeModule("Geolocation", "watchPosition");
2066
- watchId = id;
2067
- const unsubscribe = NativeBridge.onGlobalEvent("location:update", (payload) => {
2068
- coords.value = payload;
2069
- });
2070
- (0, import_runtime_core35.onUnmounted)(() => {
2071
- unsubscribe();
2072
- if (watchId !== null) clearWatch(watchId);
2073
- });
2074
- return id;
2232
+ try {
2233
+ error.value = null;
2234
+ const id = await NativeBridge.invokeNativeModule("Geolocation", "watchPosition");
2235
+ watchId = id;
2236
+ const unsubscribe = NativeBridge.onGlobalEvent("location:update", (payload) => {
2237
+ coords.value = payload;
2238
+ });
2239
+ const unsubscribeError = NativeBridge.onGlobalEvent("location:error", (payload) => {
2240
+ error.value = payload.message;
2241
+ });
2242
+ (0, import_runtime_core35.onUnmounted)(() => {
2243
+ unsubscribe();
2244
+ unsubscribeError();
2245
+ if (watchId !== null) clearWatch(watchId);
2246
+ });
2247
+ return id;
2248
+ } catch (e) {
2249
+ const msg = e instanceof Error ? e.message : String(e);
2250
+ error.value = msg;
2251
+ throw e;
2252
+ }
2075
2253
  }
2076
2254
  async function clearWatch(id) {
2077
2255
  await NativeBridge.invokeNativeModule("Geolocation", "clearWatch", [id]);
@@ -2204,6 +2382,10 @@ function useHttp(config = {}) {
2204
2382
  }
2205
2383
  const loading = (0, import_runtime_core38.ref)(false);
2206
2384
  const error = (0, import_runtime_core38.ref)(null);
2385
+ let isMounted = true;
2386
+ (0, import_runtime_core38.onUnmounted)(() => {
2387
+ isMounted = false;
2388
+ });
2207
2389
  async function request(method, url, options = {}) {
2208
2390
  const fullUrl = config.baseURL ? `${config.baseURL}${url}` : url;
2209
2391
  loading.value = true;
@@ -2223,6 +2405,9 @@ function useHttp(config = {}) {
2223
2405
  }
2224
2406
  const response = await fetch(fullUrl, fetchOptions);
2225
2407
  const data = await response.json();
2408
+ if (!isMounted) {
2409
+ return { data, status: response.status, ok: response.ok, headers: {} };
2410
+ }
2226
2411
  return {
2227
2412
  data,
2228
2413
  status: response.status,
@@ -2231,10 +2416,14 @@ function useHttp(config = {}) {
2231
2416
  };
2232
2417
  } catch (e) {
2233
2418
  const msg = e instanceof Error ? e.message : String(e);
2234
- error.value = msg;
2419
+ if (isMounted) {
2420
+ error.value = msg;
2421
+ }
2235
2422
  throw e;
2236
2423
  } finally {
2237
- loading.value = false;
2424
+ if (isMounted) {
2425
+ loading.value = false;
2426
+ }
2238
2427
  }
2239
2428
  }
2240
2429
  return {
@@ -2270,7 +2459,11 @@ function useBackHandler(handler) {
2270
2459
  let unsubscribe = null;
2271
2460
  (0, import_runtime_core40.onMounted)(() => {
2272
2461
  unsubscribe = NativeBridge.onGlobalEvent("hardware:backPress", () => {
2273
- handler();
2462
+ const handled = handler();
2463
+ if (!handled) {
2464
+ NativeBridge.invokeNativeModule("BackHandler", "exitApp", []).catch(() => {
2465
+ });
2466
+ }
2274
2467
  });
2275
2468
  });
2276
2469
  (0, import_runtime_core40.onUnmounted)(() => {
@@ -2360,6 +2553,8 @@ function useWebSocket(url, options = {}) {
2360
2553
  const error = (0, import_runtime_core43.ref)(null);
2361
2554
  let reconnectAttempts = 0;
2362
2555
  let reconnectTimer = null;
2556
+ const MAX_PENDING_MESSAGES = 100;
2557
+ const pendingMessages = [];
2363
2558
  const unsubscribers = [];
2364
2559
  unsubscribers.push(
2365
2560
  NativeBridge.onGlobalEvent("websocket:open", (payload) => {
@@ -2367,6 +2562,12 @@ function useWebSocket(url, options = {}) {
2367
2562
  status.value = "OPEN";
2368
2563
  error.value = null;
2369
2564
  reconnectAttempts = 0;
2565
+ while (pendingMessages.length > 0) {
2566
+ const msg = pendingMessages.shift();
2567
+ NativeBridge.invokeNativeModule("WebSocket", "send", [connectionId, msg]).catch((err) => {
2568
+ error.value = err.message;
2569
+ });
2570
+ }
2370
2571
  })
2371
2572
  );
2372
2573
  unsubscribers.push(
@@ -2381,9 +2582,10 @@ function useWebSocket(url, options = {}) {
2381
2582
  status.value = "CLOSED";
2382
2583
  if (autoReconnect && reconnectAttempts < maxReconnectAttempts && payload.code !== 1e3) {
2383
2584
  reconnectAttempts++;
2585
+ const backoffMs = reconnectInterval * Math.pow(2, reconnectAttempts - 1);
2384
2586
  reconnectTimer = setTimeout(() => {
2385
2587
  open();
2386
- }, reconnectInterval);
2588
+ }, backoffMs);
2387
2589
  }
2388
2590
  })
2389
2591
  );
@@ -2403,8 +2605,17 @@ function useWebSocket(url, options = {}) {
2403
2605
  });
2404
2606
  }
2405
2607
  function send(data) {
2406
- if (status.value !== "OPEN") return;
2407
2608
  const message = typeof data === "string" ? data : JSON.stringify(data);
2609
+ if (status.value !== "OPEN") {
2610
+ if (pendingMessages.length >= MAX_PENDING_MESSAGES) {
2611
+ pendingMessages.shift();
2612
+ if (__DEV__) {
2613
+ console.warn("[VueNative] WebSocket pending message queue full, dropping oldest message");
2614
+ }
2615
+ }
2616
+ pendingMessages.push(message);
2617
+ return;
2618
+ }
2408
2619
  NativeBridge.invokeNativeModule("WebSocket", "send", [connectionId, message]).catch((err) => {
2409
2620
  error.value = err.message;
2410
2621
  });
@@ -2688,22 +2899,22 @@ function useDatabase(name = "default") {
2688
2899
  }
2689
2900
  async function transaction(callback) {
2690
2901
  await ensureOpen();
2691
- const statements = [];
2692
- const ctx = {
2693
- execute: async (sql, params) => {
2694
- statements.push({ sql, params: params ?? [] });
2695
- return { rowsAffected: 0 };
2696
- },
2697
- query: async (sql, params) => {
2698
- if (statements.length > 0) {
2699
- await NativeBridge.invokeNativeModule("Database", "executeTransaction", [name, statements.splice(0)]);
2902
+ await NativeBridge.invokeNativeModule("Database", "execute", [name, "BEGIN TRANSACTION", []]);
2903
+ try {
2904
+ const ctx = {
2905
+ execute: async (sql, params) => {
2906
+ return NativeBridge.invokeNativeModule("Database", "execute", [name, sql, params ?? []]);
2907
+ },
2908
+ query: async (sql, params) => {
2909
+ return NativeBridge.invokeNativeModule("Database", "query", [name, sql, params ?? []]);
2700
2910
  }
2701
- return NativeBridge.invokeNativeModule("Database", "query", [name, sql, params ?? []]);
2702
- }
2703
- };
2704
- await callback(ctx);
2705
- if (statements.length > 0) {
2706
- await NativeBridge.invokeNativeModule("Database", "executeTransaction", [name, statements]);
2911
+ };
2912
+ await callback(ctx);
2913
+ await NativeBridge.invokeNativeModule("Database", "execute", [name, "COMMIT", []]);
2914
+ } catch (err) {
2915
+ await NativeBridge.invokeNativeModule("Database", "execute", [name, "ROLLBACK", []]).catch(() => {
2916
+ });
2917
+ throw err;
2707
2918
  }
2708
2919
  }
2709
2920
  async function close() {
@@ -3092,6 +3303,8 @@ function useOTAUpdate(serverUrl) {
3092
3303
  await NativeBridge.invokeNativeModule("OTA", "downloadUpdate", [downloadUrl, expectedHash || ""]);
3093
3304
  status.value = "ready";
3094
3305
  } catch (err) {
3306
+ await NativeBridge.invokeNativeModule("OTA", "cleanupPartialDownload", []).catch(() => {
3307
+ });
3095
3308
  error.value = err?.message || String(err);
3096
3309
  status.value = "error";
3097
3310
  throw err;
@@ -3100,7 +3313,17 @@ function useOTAUpdate(serverUrl) {
3100
3313
  }
3101
3314
  }
3102
3315
  async function applyUpdate() {
3316
+ if (status.value !== "ready") {
3317
+ throw new Error("No update ready to apply. Call downloadUpdate() first.");
3318
+ }
3103
3319
  error.value = null;
3320
+ try {
3321
+ await NativeBridge.invokeNativeModule("OTA", "verifyBundle", []);
3322
+ } catch (err) {
3323
+ status.value = "error";
3324
+ error.value = "Bundle verification failed: " + (err?.message || String(err));
3325
+ throw err;
3326
+ }
3104
3327
  try {
3105
3328
  await NativeBridge.invokeNativeModule("OTA", "applyUpdate", []);
3106
3329
  const info = await NativeBridge.invokeNativeModule("OTA", "getCurrentVersion", []);