@thelacanians/vue-native-runtime 0.3.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
@@ -384,6 +384,17 @@ var _NativeBridgeImpl = class _NativeBridgeImpl {
384
384
  ));
385
385
  }
386
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
+ }
387
398
  this.pendingCallbacks.set(callbackId, { resolve, reject, timeoutId });
388
399
  this.enqueue("invokeNativeModule", [moduleName, methodName, args, callbackId]);
389
400
  });
@@ -403,11 +414,9 @@ var _NativeBridgeImpl = class _NativeBridgeImpl {
403
414
  resolveCallback(callbackId, result, error) {
404
415
  const pending = this.pendingCallbacks.get(callbackId);
405
416
  if (!pending) {
406
- if (__DEV__) {
407
- console.warn(
408
- `[VueNative] Received callback for unknown callbackId: ${callbackId}`
409
- );
410
- }
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
+ );
411
420
  return;
412
421
  }
413
422
  clearTimeout(pending.timeoutId);
@@ -472,6 +481,8 @@ var _NativeBridgeImpl = class _NativeBridgeImpl {
472
481
  };
473
482
  /** Maximum callback ID before wraparound (safe for 32-bit signed int) */
474
483
  _NativeBridgeImpl.MAX_CALLBACK_ID = 2147483647;
484
+ /** Maximum number of pending callbacks before evicting the oldest */
485
+ _NativeBridgeImpl.MAX_PENDING_CALLBACKS = 1e3;
475
486
  var NativeBridgeImpl = _NativeBridgeImpl;
476
487
  if (typeof globalThis.__DEV__ === "undefined") {
477
488
  ;
@@ -786,7 +797,17 @@ var VInput = (0, import_runtime_core5.defineComponent)({
786
797
  },
787
798
  emits: ["update:modelValue", "focus", "blur", "submit"],
788
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
+ };
789
809
  const onChangetext = (payload) => {
810
+ if (isComposing.value) return;
790
811
  const text = typeof payload === "string" ? payload : payload?.text ?? "";
791
812
  emit("update:modelValue", text);
792
813
  };
@@ -815,6 +836,8 @@ var VInput = (0, import_runtime_core5.defineComponent)({
815
836
  accessibilityHint: props.accessibilityHint,
816
837
  accessibilityState: props.accessibilityState,
817
838
  onChangetext,
839
+ onCompositionstart,
840
+ onCompositionend,
818
841
  onFocus,
819
842
  onBlur,
820
843
  onSubmit
@@ -927,6 +950,11 @@ var VScrollView = (0, import_runtime_core8.defineComponent)({
927
950
  default: false
928
951
  },
929
952
  contentContainerStyle: Object,
953
+ /** Minimum interval in ms between scroll event emissions. Default: 16 (~60fps) */
954
+ scrollEventThrottle: {
955
+ type: Number,
956
+ default: 16
957
+ },
930
958
  /** Whether the pull-to-refresh indicator is active */
931
959
  refreshing: {
932
960
  type: Boolean,
@@ -940,8 +968,13 @@ var VScrollView = (0, import_runtime_core8.defineComponent)({
940
968
  },
941
969
  emits: ["scroll", "refresh"],
942
970
  setup(props, { slots, emit }) {
971
+ let lastScrollEmit = 0;
943
972
  const onScroll = (payload) => {
944
- emit("scroll", payload);
973
+ const now = Date.now();
974
+ if (now - lastScrollEmit >= props.scrollEventThrottle) {
975
+ lastScrollEmit = now;
976
+ emit("scroll", payload);
977
+ }
945
978
  };
946
979
  const onRefresh = () => {
947
980
  emit("refresh");
@@ -988,13 +1021,29 @@ var VImage = (0, import_runtime_core9.defineComponent)({
988
1021
  accessibilityState: Object
989
1022
  },
990
1023
  emits: ["load", "error"],
991
- 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 });
992
1041
  return () => (0, import_runtime_core9.h)(
993
1042
  "VImage",
994
1043
  {
995
1044
  ...props,
996
- onLoad: () => emit("load"),
997
- onError: (e) => emit("error", e)
1045
+ onLoad,
1046
+ onError
998
1047
  }
999
1048
  );
1000
1049
  }
@@ -1100,8 +1149,29 @@ var VList = (0, import_runtime_core13.defineComponent)({
1100
1149
  },
1101
1150
  emits: ["scroll", "endReached"],
1102
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
+ };
1103
1160
  return () => {
1104
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
+ }
1105
1175
  const children = [];
1106
1176
  if (slots.header) {
1107
1177
  children.push(
@@ -1139,7 +1209,7 @@ var VList = (0, import_runtime_core13.defineComponent)({
1139
1209
  showsScrollIndicator: props.showsScrollIndicator,
1140
1210
  bounces: props.bounces,
1141
1211
  horizontal: props.horizontal,
1142
- onScroll: (e) => emit("scroll", e),
1212
+ onScroll,
1143
1213
  onEndReached: () => emit("endReached")
1144
1214
  },
1145
1215
  children
@@ -1164,10 +1234,24 @@ var VModal = (0, import_runtime_core14.defineComponent)({
1164
1234
  },
1165
1235
  emits: ["dismiss"],
1166
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
+ });
1167
1251
  return () => (0, import_runtime_core14.h)(
1168
1252
  "VModal",
1169
1253
  {
1170
- visible: props.visible,
1254
+ visible: debouncedVisible.value,
1171
1255
  style: props.style,
1172
1256
  onDismiss: () => emit("dismiss")
1173
1257
  },
@@ -1188,8 +1272,22 @@ var VAlertDialog = (0, import_runtime_core15.defineComponent)({
1188
1272
  },
1189
1273
  emits: ["confirm", "cancel", "action"],
1190
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
+ });
1191
1289
  return () => (0, import_runtime_core15.h)("VAlertDialog", {
1192
- visible: props.visible,
1290
+ visible: debouncedVisible.value,
1193
1291
  title: props.title,
1194
1292
  message: props.message,
1195
1293
  buttons: props.buttons,
@@ -1229,8 +1327,18 @@ var VWebView = (0, import_runtime_core17.defineComponent)({
1229
1327
  },
1230
1328
  emits: ["load", "error", "message"],
1231
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
+ });
1232
1340
  return () => (0, import_runtime_core17.h)("VWebView", {
1233
- source: props.source,
1341
+ source: sanitizedSource.value,
1234
1342
  style: props.style,
1235
1343
  javaScriptEnabled: props.javaScriptEnabled,
1236
1344
  onLoad: (e) => emit("load", e),
@@ -1876,15 +1984,33 @@ function useHaptics() {
1876
1984
  }
1877
1985
 
1878
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
+ }
1879
1999
  function useAsyncStorage() {
1880
2000
  function getItem(key) {
1881
2001
  return NativeBridge.invokeNativeModule("AsyncStorage", "getItem", [key]);
1882
2002
  }
1883
2003
  function setItem(key, value) {
1884
- 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
+ );
1885
2008
  }
1886
2009
  function removeItem(key) {
1887
- return NativeBridge.invokeNativeModule("AsyncStorage", "removeItem", [key]).then(() => void 0);
2010
+ return queueWrite(
2011
+ key,
2012
+ () => NativeBridge.invokeNativeModule("AsyncStorage", "removeItem", [key]).then(() => void 0)
2013
+ );
1888
2014
  }
1889
2015
  function getAllKeys() {
1890
2016
  return NativeBridge.invokeNativeModule("AsyncStorage", "getAllKeys", []);
@@ -2021,15 +2147,20 @@ var import_runtime_core33 = require("@vue/runtime-core");
2021
2147
  function useNetwork() {
2022
2148
  const isConnected = (0, import_runtime_core33.ref)(true);
2023
2149
  const connectionType = (0, import_runtime_core33.ref)("unknown");
2024
- NativeBridge.invokeNativeModule("Network", "getStatus").then((status) => {
2025
- isConnected.value = status.isConnected;
2026
- connectionType.value = status.connectionType;
2027
- }).catch(() => {
2028
- });
2150
+ let lastEventTime = 0;
2029
2151
  const unsubscribe = NativeBridge.onGlobalEvent("network:change", (payload) => {
2152
+ lastEventTime = Date.now();
2030
2153
  isConnected.value = payload.isConnected;
2031
2154
  connectionType.value = payload.connectionType;
2032
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
+ });
2033
2164
  (0, import_runtime_core33.onUnmounted)(unsubscribe);
2034
2165
  return { isConnected, connectionType };
2035
2166
  }
@@ -2086,21 +2217,39 @@ function useGeolocation() {
2086
2217
  const error = (0, import_runtime_core35.ref)(null);
2087
2218
  let watchId = null;
2088
2219
  async function getCurrentPosition() {
2089
- const result = await NativeBridge.invokeNativeModule("Geolocation", "getCurrentPosition");
2090
- coords.value = result;
2091
- 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
+ }
2092
2230
  }
2093
2231
  async function watchPosition() {
2094
- const id = await NativeBridge.invokeNativeModule("Geolocation", "watchPosition");
2095
- watchId = id;
2096
- const unsubscribe = NativeBridge.onGlobalEvent("location:update", (payload) => {
2097
- coords.value = payload;
2098
- });
2099
- (0, import_runtime_core35.onUnmounted)(() => {
2100
- unsubscribe();
2101
- if (watchId !== null) clearWatch(watchId);
2102
- });
2103
- 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
+ }
2104
2253
  }
2105
2254
  async function clearWatch(id) {
2106
2255
  await NativeBridge.invokeNativeModule("Geolocation", "clearWatch", [id]);
@@ -2233,6 +2382,10 @@ function useHttp(config = {}) {
2233
2382
  }
2234
2383
  const loading = (0, import_runtime_core38.ref)(false);
2235
2384
  const error = (0, import_runtime_core38.ref)(null);
2385
+ let isMounted = true;
2386
+ (0, import_runtime_core38.onUnmounted)(() => {
2387
+ isMounted = false;
2388
+ });
2236
2389
  async function request(method, url, options = {}) {
2237
2390
  const fullUrl = config.baseURL ? `${config.baseURL}${url}` : url;
2238
2391
  loading.value = true;
@@ -2252,6 +2405,9 @@ function useHttp(config = {}) {
2252
2405
  }
2253
2406
  const response = await fetch(fullUrl, fetchOptions);
2254
2407
  const data = await response.json();
2408
+ if (!isMounted) {
2409
+ return { data, status: response.status, ok: response.ok, headers: {} };
2410
+ }
2255
2411
  return {
2256
2412
  data,
2257
2413
  status: response.status,
@@ -2260,10 +2416,14 @@ function useHttp(config = {}) {
2260
2416
  };
2261
2417
  } catch (e) {
2262
2418
  const msg = e instanceof Error ? e.message : String(e);
2263
- error.value = msg;
2419
+ if (isMounted) {
2420
+ error.value = msg;
2421
+ }
2264
2422
  throw e;
2265
2423
  } finally {
2266
- loading.value = false;
2424
+ if (isMounted) {
2425
+ loading.value = false;
2426
+ }
2267
2427
  }
2268
2428
  }
2269
2429
  return {
@@ -2299,7 +2459,11 @@ function useBackHandler(handler) {
2299
2459
  let unsubscribe = null;
2300
2460
  (0, import_runtime_core40.onMounted)(() => {
2301
2461
  unsubscribe = NativeBridge.onGlobalEvent("hardware:backPress", () => {
2302
- handler();
2462
+ const handled = handler();
2463
+ if (!handled) {
2464
+ NativeBridge.invokeNativeModule("BackHandler", "exitApp", []).catch(() => {
2465
+ });
2466
+ }
2303
2467
  });
2304
2468
  });
2305
2469
  (0, import_runtime_core40.onUnmounted)(() => {
@@ -2389,6 +2553,8 @@ function useWebSocket(url, options = {}) {
2389
2553
  const error = (0, import_runtime_core43.ref)(null);
2390
2554
  let reconnectAttempts = 0;
2391
2555
  let reconnectTimer = null;
2556
+ const MAX_PENDING_MESSAGES = 100;
2557
+ const pendingMessages = [];
2392
2558
  const unsubscribers = [];
2393
2559
  unsubscribers.push(
2394
2560
  NativeBridge.onGlobalEvent("websocket:open", (payload) => {
@@ -2396,6 +2562,12 @@ function useWebSocket(url, options = {}) {
2396
2562
  status.value = "OPEN";
2397
2563
  error.value = null;
2398
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
+ }
2399
2571
  })
2400
2572
  );
2401
2573
  unsubscribers.push(
@@ -2410,9 +2582,10 @@ function useWebSocket(url, options = {}) {
2410
2582
  status.value = "CLOSED";
2411
2583
  if (autoReconnect && reconnectAttempts < maxReconnectAttempts && payload.code !== 1e3) {
2412
2584
  reconnectAttempts++;
2585
+ const backoffMs = reconnectInterval * Math.pow(2, reconnectAttempts - 1);
2413
2586
  reconnectTimer = setTimeout(() => {
2414
2587
  open();
2415
- }, reconnectInterval);
2588
+ }, backoffMs);
2416
2589
  }
2417
2590
  })
2418
2591
  );
@@ -2432,8 +2605,17 @@ function useWebSocket(url, options = {}) {
2432
2605
  });
2433
2606
  }
2434
2607
  function send(data) {
2435
- if (status.value !== "OPEN") return;
2436
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
+ }
2437
2619
  NativeBridge.invokeNativeModule("WebSocket", "send", [connectionId, message]).catch((err) => {
2438
2620
  error.value = err.message;
2439
2621
  });
@@ -2717,22 +2899,22 @@ function useDatabase(name = "default") {
2717
2899
  }
2718
2900
  async function transaction(callback) {
2719
2901
  await ensureOpen();
2720
- const statements = [];
2721
- const ctx = {
2722
- execute: async (sql, params) => {
2723
- statements.push({ sql, params: params ?? [] });
2724
- return { rowsAffected: 0 };
2725
- },
2726
- query: async (sql, params) => {
2727
- if (statements.length > 0) {
2728
- 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 ?? []]);
2729
2910
  }
2730
- return NativeBridge.invokeNativeModule("Database", "query", [name, sql, params ?? []]);
2731
- }
2732
- };
2733
- await callback(ctx);
2734
- if (statements.length > 0) {
2735
- 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;
2736
2918
  }
2737
2919
  }
2738
2920
  async function close() {
@@ -3121,6 +3303,8 @@ function useOTAUpdate(serverUrl) {
3121
3303
  await NativeBridge.invokeNativeModule("OTA", "downloadUpdate", [downloadUrl, expectedHash || ""]);
3122
3304
  status.value = "ready";
3123
3305
  } catch (err) {
3306
+ await NativeBridge.invokeNativeModule("OTA", "cleanupPartialDownload", []).catch(() => {
3307
+ });
3124
3308
  error.value = err?.message || String(err);
3125
3309
  status.value = "error";
3126
3310
  throw err;
@@ -3129,7 +3313,17 @@ function useOTAUpdate(serverUrl) {
3129
3313
  }
3130
3314
  }
3131
3315
  async function applyUpdate() {
3316
+ if (status.value !== "ready") {
3317
+ throw new Error("No update ready to apply. Call downloadUpdate() first.");
3318
+ }
3132
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
+ }
3133
3327
  try {
3134
3328
  await NativeBridge.invokeNativeModule("OTA", "applyUpdate", []);
3135
3329
  const info = await NativeBridge.invokeNativeModule("OTA", "getCurrentVersion", []);
package/dist/index.d.cts CHANGED
@@ -602,6 +602,10 @@ declare const VButton: _vue_runtime_core.DefineComponent<_vue_runtime_core.Extra
602
602
  * The component maps `modelValue` to the native `text` prop and listens
603
603
  * for `changetext` events from the native side to update the model.
604
604
  *
605
+ * Handles CJK IME composition correctly: during composition, model updates
606
+ * are deferred until the user commits the composed character to avoid
607
+ * v-model desync.
608
+ *
605
609
  * @example
606
610
  * ```vue
607
611
  * <VInput
@@ -845,6 +849,11 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
845
849
  default: boolean;
846
850
  };
847
851
  contentContainerStyle: PropType<ViewStyle>;
852
+ /** Minimum interval in ms between scroll event emissions. Default: 16 (~60fps) */
853
+ scrollEventThrottle: {
854
+ type: NumberConstructor;
855
+ default: number;
856
+ };
848
857
  /** Whether the pull-to-refresh indicator is active */
849
858
  refreshing: {
850
859
  type: BooleanConstructor;
@@ -883,6 +892,11 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
883
892
  default: boolean;
884
893
  };
885
894
  contentContainerStyle: PropType<ViewStyle>;
895
+ /** Minimum interval in ms between scroll event emissions. Default: 16 (~60fps) */
896
+ scrollEventThrottle: {
897
+ type: NumberConstructor;
898
+ default: number;
899
+ };
886
900
  /** Whether the pull-to-refresh indicator is active */
887
901
  refreshing: {
888
902
  type: BooleanConstructor;
@@ -903,6 +917,7 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
903
917
  scrollEnabled: boolean;
904
918
  bounces: boolean;
905
919
  pagingEnabled: boolean;
920
+ scrollEventThrottle: number;
906
921
  refreshing: boolean;
907
922
  }, {}, {}, {}, string, _vue_runtime_core.ComponentProvideOptions, true, {}, any>;
908
923
 
@@ -912,6 +927,9 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
912
927
  * Maps to UIImageView on iOS. Loads images from URIs asynchronously
913
928
  * with built-in caching. Supports various resize modes.
914
929
  *
930
+ * Exposes a reactive `loading` ref (via template ref) that is `true`
931
+ * while the image is being fetched and `false` once it loads or errors.
932
+ *
915
933
  * @example
916
934
  * ```vue
917
935
  * <VImage
@@ -1306,6 +1324,9 @@ interface WebViewSource {
1306
1324
  /**
1307
1325
  * Embedded web view component backed by WKWebView.
1308
1326
  *
1327
+ * URI sources are validated to block dangerous schemes such as `javascript:`
1328
+ * and `data:text/html` which could lead to XSS.
1329
+ *
1309
1330
  * @example
1310
1331
  * <VWebView :source="{ uri: 'https://example.com' }" style="flex: 1" @load="onLoad" />
1311
1332
  */
@@ -2179,6 +2200,8 @@ declare function useHaptics(): {
2179
2200
  * Async key-value storage composable backed by UserDefaults.
2180
2201
  *
2181
2202
  * All operations are Promise-based and run on a background thread.
2203
+ * Write operations (setItem, removeItem) are serialized per key to
2204
+ * prevent race conditions from concurrent access.
2182
2205
  *
2183
2206
  * @example
2184
2207
  * ```ts
@@ -3479,6 +3502,8 @@ declare class NativeBridgeImpl {
3479
3502
  private nextCallbackId;
3480
3503
  /** Maximum callback ID before wraparound (safe for 32-bit signed int) */
3481
3504
  private static readonly MAX_CALLBACK_ID;
3505
+ /** Maximum number of pending callbacks before evicting the oldest */
3506
+ private static readonly MAX_PENDING_CALLBACKS;
3482
3507
  /** Global event listeners: eventName -> Set of callbacks */
3483
3508
  private globalEventHandlers;
3484
3509
  /**
package/dist/index.d.ts CHANGED
@@ -602,6 +602,10 @@ declare const VButton: _vue_runtime_core.DefineComponent<_vue_runtime_core.Extra
602
602
  * The component maps `modelValue` to the native `text` prop and listens
603
603
  * for `changetext` events from the native side to update the model.
604
604
  *
605
+ * Handles CJK IME composition correctly: during composition, model updates
606
+ * are deferred until the user commits the composed character to avoid
607
+ * v-model desync.
608
+ *
605
609
  * @example
606
610
  * ```vue
607
611
  * <VInput
@@ -845,6 +849,11 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
845
849
  default: boolean;
846
850
  };
847
851
  contentContainerStyle: PropType<ViewStyle>;
852
+ /** Minimum interval in ms between scroll event emissions. Default: 16 (~60fps) */
853
+ scrollEventThrottle: {
854
+ type: NumberConstructor;
855
+ default: number;
856
+ };
848
857
  /** Whether the pull-to-refresh indicator is active */
849
858
  refreshing: {
850
859
  type: BooleanConstructor;
@@ -883,6 +892,11 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
883
892
  default: boolean;
884
893
  };
885
894
  contentContainerStyle: PropType<ViewStyle>;
895
+ /** Minimum interval in ms between scroll event emissions. Default: 16 (~60fps) */
896
+ scrollEventThrottle: {
897
+ type: NumberConstructor;
898
+ default: number;
899
+ };
886
900
  /** Whether the pull-to-refresh indicator is active */
887
901
  refreshing: {
888
902
  type: BooleanConstructor;
@@ -903,6 +917,7 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
903
917
  scrollEnabled: boolean;
904
918
  bounces: boolean;
905
919
  pagingEnabled: boolean;
920
+ scrollEventThrottle: number;
906
921
  refreshing: boolean;
907
922
  }, {}, {}, {}, string, _vue_runtime_core.ComponentProvideOptions, true, {}, any>;
908
923
 
@@ -912,6 +927,9 @@ declare const VScrollView: _vue_runtime_core.DefineComponent<_vue_runtime_core.E
912
927
  * Maps to UIImageView on iOS. Loads images from URIs asynchronously
913
928
  * with built-in caching. Supports various resize modes.
914
929
  *
930
+ * Exposes a reactive `loading` ref (via template ref) that is `true`
931
+ * while the image is being fetched and `false` once it loads or errors.
932
+ *
915
933
  * @example
916
934
  * ```vue
917
935
  * <VImage
@@ -1306,6 +1324,9 @@ interface WebViewSource {
1306
1324
  /**
1307
1325
  * Embedded web view component backed by WKWebView.
1308
1326
  *
1327
+ * URI sources are validated to block dangerous schemes such as `javascript:`
1328
+ * and `data:text/html` which could lead to XSS.
1329
+ *
1309
1330
  * @example
1310
1331
  * <VWebView :source="{ uri: 'https://example.com' }" style="flex: 1" @load="onLoad" />
1311
1332
  */
@@ -2179,6 +2200,8 @@ declare function useHaptics(): {
2179
2200
  * Async key-value storage composable backed by UserDefaults.
2180
2201
  *
2181
2202
  * All operations are Promise-based and run on a background thread.
2203
+ * Write operations (setItem, removeItem) are serialized per key to
2204
+ * prevent race conditions from concurrent access.
2182
2205
  *
2183
2206
  * @example
2184
2207
  * ```ts
@@ -3479,6 +3502,8 @@ declare class NativeBridgeImpl {
3479
3502
  private nextCallbackId;
3480
3503
  /** Maximum callback ID before wraparound (safe for 32-bit signed int) */
3481
3504
  private static readonly MAX_CALLBACK_ID;
3505
+ /** Maximum number of pending callbacks before evicting the oldest */
3506
+ private static readonly MAX_PENDING_CALLBACKS;
3482
3507
  /** Global event listeners: eventName -> Set of callbacks */
3483
3508
  private globalEventHandlers;
3484
3509
  /**