@modelnex/sdk 0.5.16 → 0.5.17

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.
Files changed (3) hide show
  1. package/dist/index.js +149 -83
  2. package/dist/index.mjs +149 -83
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2626,7 +2626,6 @@ function isTourEligible(tour, userProfile) {
2626
2626
 
2627
2627
  // src/hooks/useTourPlayback.ts
2628
2628
  var import_react12 = require("react");
2629
- var import_socket2 = require("socket.io-client");
2630
2629
 
2631
2630
  // src/utils/retryLookup.ts
2632
2631
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -2649,6 +2648,76 @@ async function retryLookup({
2649
2648
  }
2650
2649
  }
2651
2650
 
2651
+ // src/utils/tour-socket-pool.ts
2652
+ var import_socket2 = require("socket.io-client");
2653
+ function isSocketWritable(socket) {
2654
+ if (!socket?.connected) return false;
2655
+ const engine = socket.io?.engine;
2656
+ if (!engine) return true;
2657
+ if (typeof engine.readyState === "string" && engine.readyState !== "open") return false;
2658
+ if (engine.transport && "writable" in engine.transport && engine.transport.writable === false) return false;
2659
+ return true;
2660
+ }
2661
+ function emitSocketEvent(socket, event, payload) {
2662
+ if (!isSocketWritable(socket)) return false;
2663
+ socket.emit(event, payload);
2664
+ return true;
2665
+ }
2666
+ function createTourSocketPool({
2667
+ createSocket = (serverUrl) => (0, import_socket2.io)(serverUrl, {
2668
+ path: "/socket.io",
2669
+ transports: resolveSocketIoTransports(serverUrl, "polling-first"),
2670
+ autoConnect: true,
2671
+ reconnection: true,
2672
+ reconnectionAttempts: 10,
2673
+ reconnectionDelay: 1e3
2674
+ }),
2675
+ releaseDelayMs = 2500,
2676
+ scheduleRelease = (callback, delayMs) => setTimeout(callback, delayMs),
2677
+ cancelRelease = (timer) => clearTimeout(timer)
2678
+ } = {}) {
2679
+ const pooledSockets = /* @__PURE__ */ new Map();
2680
+ return {
2681
+ acquire(serverUrl) {
2682
+ const pooled = pooledSockets.get(serverUrl);
2683
+ if (pooled) {
2684
+ if (pooled.releaseTimer) {
2685
+ cancelRelease(pooled.releaseTimer);
2686
+ pooled.releaseTimer = null;
2687
+ }
2688
+ pooled.leases += 1;
2689
+ return pooled.socket;
2690
+ }
2691
+ const socket = createSocket(serverUrl);
2692
+ pooledSockets.set(serverUrl, {
2693
+ socket,
2694
+ leases: 1,
2695
+ releaseTimer: null
2696
+ });
2697
+ return socket;
2698
+ },
2699
+ release(serverUrl, socket) {
2700
+ const pooled = pooledSockets.get(serverUrl);
2701
+ if (!pooled || pooled.socket !== socket) return;
2702
+ pooled.leases = Math.max(0, pooled.leases - 1);
2703
+ if (pooled.leases > 0) return;
2704
+ pooled.releaseTimer = scheduleRelease(() => {
2705
+ const latest = pooledSockets.get(serverUrl);
2706
+ if (!latest || latest !== pooled || latest.leases > 0) return;
2707
+ latest.socket.disconnect();
2708
+ pooledSockets.delete(serverUrl);
2709
+ }, releaseDelayMs);
2710
+ },
2711
+ // Test-only introspection
2712
+ getSnapshot(serverUrl) {
2713
+ const pooled = pooledSockets.get(serverUrl);
2714
+ if (!pooled) return null;
2715
+ return { leases: pooled.leases, socket: pooled.socket };
2716
+ }
2717
+ };
2718
+ }
2719
+ var tourSocketPool = createTourSocketPool();
2720
+
2652
2721
  // src/hooks/useTourPlayback.ts
2653
2722
  function resolveElement(step) {
2654
2723
  const el = step.element;
@@ -3185,11 +3254,13 @@ function useTourPlayback({
3185
3254
  const socketRef = (0, import_react12.useRef)(null);
3186
3255
  const socketIdRef = (0, import_react12.useRef)(socketId);
3187
3256
  const commandUrlRef = (0, import_react12.useRef)(commandUrl);
3257
+ const websiteIdRef = (0, import_react12.useRef)(websiteId);
3188
3258
  const onStepChangeRef = (0, import_react12.useRef)(onStepChange);
3189
3259
  const isActiveRef = (0, import_react12.useRef)(false);
3190
3260
  const activeCommandBatchIdRef = (0, import_react12.useRef)(null);
3191
3261
  socketIdRef.current = socketId;
3192
3262
  commandUrlRef.current = commandUrl;
3263
+ websiteIdRef.current = websiteId;
3193
3264
  onStepChangeRef.current = onStepChange;
3194
3265
  isActiveRef.current = isActive;
3195
3266
  reviewModeRef.current = isReviewMode;
@@ -3200,25 +3271,17 @@ function useTourPlayback({
3200
3271
  (0, import_react12.useEffect)(() => {
3201
3272
  if (disabled) return;
3202
3273
  if (typeof window === "undefined") return;
3203
- let createdSocket = null;
3204
- const socket = (0, import_socket2.io)(serverUrl, {
3205
- path: "/socket.io",
3206
- transports: resolveSocketIoTransports(serverUrl, "polling-first"),
3207
- autoConnect: true,
3208
- reconnection: true,
3209
- reconnectionAttempts: 10,
3210
- reconnectionDelay: 1e3
3211
- });
3212
- createdSocket = socket;
3274
+ const socket = tourSocketPool.acquire(serverUrl);
3213
3275
  socketRef.current = socket;
3214
- socket.on("connect", () => {
3276
+ const handleConnect = () => {
3215
3277
  console.log("[TourClient] Connected to tour agent server:", socket.id);
3216
3278
  const profile = userProfileRef.current;
3217
- if (websiteId && profile?.userId && socket.connected) {
3218
- socket.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3279
+ const currentWebsiteId = websiteIdRef.current;
3280
+ if (currentWebsiteId && profile?.userId) {
3281
+ emitSocketEvent(socket, "tour:init", { websiteId: currentWebsiteId, userId: profile.userId, userType: profile.type });
3219
3282
  }
3220
- });
3221
- socket.on("tour:server_state", (payload) => {
3283
+ };
3284
+ const handleServerState = (payload) => {
3222
3285
  if (typeof payload?.runId === "number") {
3223
3286
  runIdRef.current = payload.runId;
3224
3287
  }
@@ -3226,8 +3289,8 @@ function useTourPlayback({
3226
3289
  turnIdRef.current = payload.turnId ?? null;
3227
3290
  }
3228
3291
  setServerState(payload);
3229
- });
3230
- socket.on("tour:command_cancel", (payload) => {
3292
+ };
3293
+ const handleCommandCancel = (payload) => {
3231
3294
  console.log("[TourClient] Received command_cancel:", payload);
3232
3295
  if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
3233
3296
  activeCommandBatchIdRef.current = null;
@@ -3238,12 +3301,11 @@ function useTourPlayback({
3238
3301
  window.speechSynthesis.cancel();
3239
3302
  }
3240
3303
  }
3241
- });
3242
- socket.on("tour:command", async (payload) => {
3304
+ };
3305
+ const handleCommand = async (payload) => {
3243
3306
  const emitIfOpen = (ev, data) => {
3244
3307
  if (socketRef.current !== socket) return;
3245
- if (!socket.connected) return;
3246
- socket.emit(ev, data);
3308
+ emitSocketEvent(socket, ev, data);
3247
3309
  };
3248
3310
  console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
3249
3311
  runCleanup(pendingManualWaitCleanupRef.current);
@@ -3736,8 +3798,8 @@ function useTourPlayback({
3736
3798
  turnId: turnIdRef.current
3737
3799
  });
3738
3800
  clearCommandBatchId();
3739
- });
3740
- socket.on("tour:start", async (tourData) => {
3801
+ };
3802
+ const handleTourStart = async (tourData) => {
3741
3803
  if (isActiveRef.current) return;
3742
3804
  runIdRef.current = typeof tourData.runId === "number" ? tourData.runId : runIdRef.current;
3743
3805
  const tour = tourData.tourContext ?? tourRef.current;
@@ -3770,8 +3832,8 @@ function useTourPlayback({
3770
3832
  try {
3771
3833
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
3772
3834
  const aom = generateMinifiedAOM2();
3773
- if (socketRef.current === socket && socket.connected) {
3774
- socket.emit("tour:sync_dom", {
3835
+ if (socketRef.current === socket) {
3836
+ emitSocketEvent(socket, "tour:sync_dom", {
3775
3837
  url: window.location.pathname + window.location.search + window.location.hash,
3776
3838
  aom: aom.nodes,
3777
3839
  domSummary: captureDomSummary()
@@ -3780,8 +3842,8 @@ function useTourPlayback({
3780
3842
  } catch (e) {
3781
3843
  console.warn("[TourClient] Initial DOM sync failed:", e);
3782
3844
  }
3783
- });
3784
- socket.on("tour:update", (payload) => {
3845
+ };
3846
+ const handleTourUpdate = (payload) => {
3785
3847
  const updatedTour = payload?.tourContext;
3786
3848
  if (!updatedTour?.id || updatedTour.id !== tourRef.current?.id) {
3787
3849
  return;
@@ -3798,12 +3860,12 @@ function useTourPlayback({
3798
3860
  onStepChangeRef.current?.(clampedStepIndex, nextTotal, updatedTour);
3799
3861
  }
3800
3862
  }
3801
- });
3802
- socket.on("tour:end", () => {
3863
+ };
3864
+ const handleTourEndEvent = () => {
3803
3865
  setServerState((prev) => prev ? { ...prev, isActive: false, phase: "completed" } : prev);
3804
3866
  handleTourEnd();
3805
- });
3806
- socket.on("tour:debug_log", (entry) => {
3867
+ };
3868
+ const handleDebugLog = (entry) => {
3807
3869
  const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
3808
3870
  if (isDev) {
3809
3871
  console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
@@ -3813,29 +3875,40 @@ function useTourPlayback({
3813
3875
  }));
3814
3876
  }
3815
3877
  }
3816
- });
3878
+ };
3879
+ socket.on("connect", handleConnect);
3880
+ socket.on("tour:server_state", handleServerState);
3881
+ socket.on("tour:command_cancel", handleCommandCancel);
3882
+ socket.on("tour:command", handleCommand);
3883
+ socket.on("tour:start", handleTourStart);
3884
+ socket.on("tour:update", handleTourUpdate);
3885
+ socket.on("tour:end", handleTourEndEvent);
3886
+ socket.on("tour:debug_log", handleDebugLog);
3817
3887
  console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
3818
3888
  return () => {
3819
- const toClose = createdSocket ?? socketRef.current;
3820
- if (toClose) {
3821
- toClose.disconnect();
3822
- }
3823
- createdSocket = null;
3889
+ socket.off("connect", handleConnect);
3890
+ socket.off("tour:server_state", handleServerState);
3891
+ socket.off("tour:command_cancel", handleCommandCancel);
3892
+ socket.off("tour:command", handleCommand);
3893
+ socket.off("tour:start", handleTourStart);
3894
+ socket.off("tour:update", handleTourUpdate);
3895
+ socket.off("tour:end", handleTourEndEvent);
3896
+ socket.off("tour:debug_log", handleDebugLog);
3897
+ const toClose = socket;
3824
3898
  socketRef.current = null;
3825
3899
  setServerState(null);
3826
3900
  runIdRef.current = null;
3827
3901
  turnIdRef.current = null;
3902
+ tourSocketPool.release(serverUrl, toClose);
3828
3903
  };
3829
- }, [serverUrl, websiteId, disabled]);
3904
+ }, [serverUrl, disabled]);
3830
3905
  (0, import_react12.useEffect)(() => {
3831
3906
  if (disabled) return;
3832
3907
  const s = socketRef.current;
3833
3908
  const profile = userProfile;
3834
- if (!s?.connected || !websiteId || !profile?.userId) return;
3909
+ if (!websiteId || !profile?.userId) return;
3835
3910
  const timer = setTimeout(() => {
3836
- if (s.connected) {
3837
- s.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3838
- }
3911
+ emitSocketEvent(s, "tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3839
3912
  }, 150);
3840
3913
  return () => clearTimeout(timer);
3841
3914
  }, [disabled, websiteId, userProfile?.userId, userProfile?.type]);
@@ -3845,8 +3918,8 @@ function useTourPlayback({
3845
3918
  }
3846
3919
  }, [showCaptions, isReviewMode]);
3847
3920
  (0, import_react12.useEffect)(() => {
3848
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3849
- socketRef.current.emit("tour:client_state", {
3921
+ if (!isActiveRef.current) return;
3922
+ emitSocketEvent(socketRef.current, "tour:client_state", {
3850
3923
  runId: runIdRef.current,
3851
3924
  turnId: turnIdRef.current,
3852
3925
  commandBatchId: activeCommandBatchIdRef.current,
@@ -3859,19 +3932,17 @@ function useTourPlayback({
3859
3932
  });
3860
3933
  }, [isActive, playbackState, voice.isListening, voice.isSpeaking]);
3861
3934
  const syncAOM = (0, import_react12.useCallback)(async () => {
3862
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3935
+ if (!isActiveRef.current) return;
3863
3936
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
3864
3937
  const aom = generateMinifiedAOM2();
3865
- if (socketRef.current?.connected) {
3866
- socketRef.current.emit("tour:sync_dom", {
3867
- url: window.location.pathname + window.location.search + window.location.hash,
3868
- aom: aom.nodes,
3869
- domSummary: captureDomSummary()
3870
- });
3871
- }
3938
+ emitSocketEvent(socketRef.current, "tour:sync_dom", {
3939
+ url: window.location.pathname + window.location.search + window.location.hash,
3940
+ aom: aom.nodes,
3941
+ domSummary: captureDomSummary()
3942
+ });
3872
3943
  }, []);
3873
3944
  const interruptExecution = (0, import_react12.useCallback)((transcript) => {
3874
- if (!socketRef.current?.connected || !isActiveRef.current) return false;
3945
+ if (!isSocketWritable(socketRef.current) || !isActiveRef.current) return false;
3875
3946
  if (!commandInFlightRef.current && !voice.isSpeaking) return false;
3876
3947
  interruptedForQuestionRef.current = true;
3877
3948
  activeExecutionTokenRef.current += 1;
@@ -3879,17 +3950,16 @@ function useTourPlayback({
3879
3950
  removeHighlight();
3880
3951
  removeCaption();
3881
3952
  voice.stopSpeaking();
3882
- if (socketRef.current?.connected) {
3883
- socketRef.current.emit("tour:action_result", {
3884
- success: true,
3885
- interrupted: true,
3886
- transcript,
3887
- commandBatchId: activeCommandBatchIdRef.current,
3888
- runId: runIdRef.current,
3889
- turnId: turnIdRef.current
3890
- });
3953
+ if (emitSocketEvent(socketRef.current, "tour:action_result", {
3954
+ success: true,
3955
+ interrupted: true,
3956
+ transcript,
3957
+ commandBatchId: activeCommandBatchIdRef.current,
3958
+ runId: runIdRef.current,
3959
+ turnId: turnIdRef.current
3960
+ })) {
3891
3961
  activeCommandBatchIdRef.current = null;
3892
- socketRef.current.emit("tour:user_input", {
3962
+ emitSocketEvent(socketRef.current, "tour:user_input", {
3893
3963
  transcript,
3894
3964
  interrupted: true,
3895
3965
  runId: runIdRef.current,
@@ -3911,9 +3981,7 @@ function useTourPlayback({
3911
3981
  removeCaption();
3912
3982
  voice.stopSpeaking();
3913
3983
  voice.stopListening();
3914
- if (socketRef.current?.connected) {
3915
- socketRef.current.emit("tour:abort");
3916
- }
3984
+ emitSocketEvent(socketRef.current, "tour:abort");
3917
3985
  if (reviewModeRef.current && tourRef.current?.id && previewRunIdRef.current) {
3918
3986
  void logPreviewEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, previewRunIdRef.current, websiteId, {
3919
3987
  stepOrder: stepIndexRef.current,
@@ -3985,7 +4053,7 @@ function useTourPlayback({
3985
4053
  await new Promise((r) => setTimeout(r, 200));
3986
4054
  retries++;
3987
4055
  }
3988
- if (!socketRef.current?.connected) {
4056
+ if (!isSocketWritable(socketRef.current)) {
3989
4057
  console.warn("[TourClient] Cannot run tour, socket not connected.");
3990
4058
  return;
3991
4059
  }
@@ -4013,13 +4081,11 @@ function useTourPlayback({
4013
4081
  previewRunIdRef.current = null;
4014
4082
  }
4015
4083
  tourRef.current = tour;
4016
- if (socketRef.current?.connected) {
4017
- socketRef.current.emit("tour:request_start", {
4018
- tourId: tour.id,
4019
- previewRunId: previewRunIdRef.current,
4020
- tourContext: tour
4021
- });
4022
- }
4084
+ emitSocketEvent(socketRef.current, "tour:request_start", {
4085
+ tourId: tour.id,
4086
+ previewRunId: previewRunIdRef.current,
4087
+ tourContext: tour
4088
+ });
4023
4089
  }, [serverUrl, websiteId]);
4024
4090
  (0, import_react12.useEffect)(() => {
4025
4091
  if (!enableAutoDiscovery) return;
@@ -4163,8 +4229,8 @@ function useTourPlayback({
4163
4229
  const revisionVersion = Number(response?.revision?.versionNumber);
4164
4230
  const appliedMessage = Number.isFinite(revisionVersion) && revisionVersion > 0 ? `Applied to the draft as version ${revisionVersion}.` : "Correction applied to the draft.";
4165
4231
  setReviewStatusMessage(apply ? appliedMessage : "Correction saved for review.");
4166
- if (apply && playbackState === "paused" && socketRef.current?.connected && isActiveRef.current) {
4167
- socketRef.current.emit("tour:resume");
4232
+ if (apply && playbackState === "paused" && isActiveRef.current) {
4233
+ emitSocketEvent(socketRef.current, "tour:resume");
4168
4234
  setPlaybackState("executing");
4169
4235
  }
4170
4236
  } catch (err) {
@@ -4188,14 +4254,14 @@ function useTourPlayback({
4188
4254
  stopTour();
4189
4255
  }, [stopTour]);
4190
4256
  const pauseTour = (0, import_react12.useCallback)(() => {
4191
- if (socketRef.current?.connected && isActiveRef.current) {
4192
- socketRef.current.emit("tour:pause");
4257
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4258
+ emitSocketEvent(socketRef.current, "tour:pause");
4193
4259
  setPlaybackState("paused");
4194
4260
  }
4195
4261
  }, []);
4196
4262
  const resumeTour = (0, import_react12.useCallback)(() => {
4197
- if (socketRef.current?.connected && isActiveRef.current) {
4198
- socketRef.current.emit("tour:resume");
4263
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4264
+ emitSocketEvent(socketRef.current, "tour:resume");
4199
4265
  setPlaybackState("executing");
4200
4266
  }
4201
4267
  }, []);
@@ -4217,9 +4283,9 @@ function useTourPlayback({
4217
4283
  if (voiceInputResolveRef.current) {
4218
4284
  console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
4219
4285
  voiceInputResolveRef.current(text);
4220
- } else if (socketRef.current?.connected) {
4286
+ } else if (isSocketWritable(socketRef.current)) {
4221
4287
  console.log("[TourAgent] Forwarding ambient voice to server:", text);
4222
- socketRef.current.emit("tour:user_input", {
4288
+ emitSocketEvent(socketRef.current, "tour:user_input", {
4223
4289
  transcript: text,
4224
4290
  runId: runIdRef.current,
4225
4291
  turnId: turnIdRef.current
package/dist/index.mjs CHANGED
@@ -2416,7 +2416,6 @@ function isTourEligible(tour, userProfile) {
2416
2416
 
2417
2417
  // src/hooks/useTourPlayback.ts
2418
2418
  import { useState as useState7, useRef as useRef8, useCallback as useCallback7, useEffect as useEffect11, useContext as useContext4 } from "react";
2419
- import { io as io2 } from "socket.io-client";
2420
2419
 
2421
2420
  // src/utils/retryLookup.ts
2422
2421
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -2439,6 +2438,76 @@ async function retryLookup({
2439
2438
  }
2440
2439
  }
2441
2440
 
2441
+ // src/utils/tour-socket-pool.ts
2442
+ import { io as io2 } from "socket.io-client";
2443
+ function isSocketWritable(socket) {
2444
+ if (!socket?.connected) return false;
2445
+ const engine = socket.io?.engine;
2446
+ if (!engine) return true;
2447
+ if (typeof engine.readyState === "string" && engine.readyState !== "open") return false;
2448
+ if (engine.transport && "writable" in engine.transport && engine.transport.writable === false) return false;
2449
+ return true;
2450
+ }
2451
+ function emitSocketEvent(socket, event, payload) {
2452
+ if (!isSocketWritable(socket)) return false;
2453
+ socket.emit(event, payload);
2454
+ return true;
2455
+ }
2456
+ function createTourSocketPool({
2457
+ createSocket = (serverUrl) => io2(serverUrl, {
2458
+ path: "/socket.io",
2459
+ transports: resolveSocketIoTransports(serverUrl, "polling-first"),
2460
+ autoConnect: true,
2461
+ reconnection: true,
2462
+ reconnectionAttempts: 10,
2463
+ reconnectionDelay: 1e3
2464
+ }),
2465
+ releaseDelayMs = 2500,
2466
+ scheduleRelease = (callback, delayMs) => setTimeout(callback, delayMs),
2467
+ cancelRelease = (timer) => clearTimeout(timer)
2468
+ } = {}) {
2469
+ const pooledSockets = /* @__PURE__ */ new Map();
2470
+ return {
2471
+ acquire(serverUrl) {
2472
+ const pooled = pooledSockets.get(serverUrl);
2473
+ if (pooled) {
2474
+ if (pooled.releaseTimer) {
2475
+ cancelRelease(pooled.releaseTimer);
2476
+ pooled.releaseTimer = null;
2477
+ }
2478
+ pooled.leases += 1;
2479
+ return pooled.socket;
2480
+ }
2481
+ const socket = createSocket(serverUrl);
2482
+ pooledSockets.set(serverUrl, {
2483
+ socket,
2484
+ leases: 1,
2485
+ releaseTimer: null
2486
+ });
2487
+ return socket;
2488
+ },
2489
+ release(serverUrl, socket) {
2490
+ const pooled = pooledSockets.get(serverUrl);
2491
+ if (!pooled || pooled.socket !== socket) return;
2492
+ pooled.leases = Math.max(0, pooled.leases - 1);
2493
+ if (pooled.leases > 0) return;
2494
+ pooled.releaseTimer = scheduleRelease(() => {
2495
+ const latest = pooledSockets.get(serverUrl);
2496
+ if (!latest || latest !== pooled || latest.leases > 0) return;
2497
+ latest.socket.disconnect();
2498
+ pooledSockets.delete(serverUrl);
2499
+ }, releaseDelayMs);
2500
+ },
2501
+ // Test-only introspection
2502
+ getSnapshot(serverUrl) {
2503
+ const pooled = pooledSockets.get(serverUrl);
2504
+ if (!pooled) return null;
2505
+ return { leases: pooled.leases, socket: pooled.socket };
2506
+ }
2507
+ };
2508
+ }
2509
+ var tourSocketPool = createTourSocketPool();
2510
+
2442
2511
  // src/hooks/useTourPlayback.ts
2443
2512
  function resolveElement(step) {
2444
2513
  const el = step.element;
@@ -2975,11 +3044,13 @@ function useTourPlayback({
2975
3044
  const socketRef = useRef8(null);
2976
3045
  const socketIdRef = useRef8(socketId);
2977
3046
  const commandUrlRef = useRef8(commandUrl);
3047
+ const websiteIdRef = useRef8(websiteId);
2978
3048
  const onStepChangeRef = useRef8(onStepChange);
2979
3049
  const isActiveRef = useRef8(false);
2980
3050
  const activeCommandBatchIdRef = useRef8(null);
2981
3051
  socketIdRef.current = socketId;
2982
3052
  commandUrlRef.current = commandUrl;
3053
+ websiteIdRef.current = websiteId;
2983
3054
  onStepChangeRef.current = onStepChange;
2984
3055
  isActiveRef.current = isActive;
2985
3056
  reviewModeRef.current = isReviewMode;
@@ -2990,25 +3061,17 @@ function useTourPlayback({
2990
3061
  useEffect11(() => {
2991
3062
  if (disabled) return;
2992
3063
  if (typeof window === "undefined") return;
2993
- let createdSocket = null;
2994
- const socket = io2(serverUrl, {
2995
- path: "/socket.io",
2996
- transports: resolveSocketIoTransports(serverUrl, "polling-first"),
2997
- autoConnect: true,
2998
- reconnection: true,
2999
- reconnectionAttempts: 10,
3000
- reconnectionDelay: 1e3
3001
- });
3002
- createdSocket = socket;
3064
+ const socket = tourSocketPool.acquire(serverUrl);
3003
3065
  socketRef.current = socket;
3004
- socket.on("connect", () => {
3066
+ const handleConnect = () => {
3005
3067
  console.log("[TourClient] Connected to tour agent server:", socket.id);
3006
3068
  const profile = userProfileRef.current;
3007
- if (websiteId && profile?.userId && socket.connected) {
3008
- socket.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3069
+ const currentWebsiteId = websiteIdRef.current;
3070
+ if (currentWebsiteId && profile?.userId) {
3071
+ emitSocketEvent(socket, "tour:init", { websiteId: currentWebsiteId, userId: profile.userId, userType: profile.type });
3009
3072
  }
3010
- });
3011
- socket.on("tour:server_state", (payload) => {
3073
+ };
3074
+ const handleServerState = (payload) => {
3012
3075
  if (typeof payload?.runId === "number") {
3013
3076
  runIdRef.current = payload.runId;
3014
3077
  }
@@ -3016,8 +3079,8 @@ function useTourPlayback({
3016
3079
  turnIdRef.current = payload.turnId ?? null;
3017
3080
  }
3018
3081
  setServerState(payload);
3019
- });
3020
- socket.on("tour:command_cancel", (payload) => {
3082
+ };
3083
+ const handleCommandCancel = (payload) => {
3021
3084
  console.log("[TourClient] Received command_cancel:", payload);
3022
3085
  if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
3023
3086
  activeCommandBatchIdRef.current = null;
@@ -3028,12 +3091,11 @@ function useTourPlayback({
3028
3091
  window.speechSynthesis.cancel();
3029
3092
  }
3030
3093
  }
3031
- });
3032
- socket.on("tour:command", async (payload) => {
3094
+ };
3095
+ const handleCommand = async (payload) => {
3033
3096
  const emitIfOpen = (ev, data) => {
3034
3097
  if (socketRef.current !== socket) return;
3035
- if (!socket.connected) return;
3036
- socket.emit(ev, data);
3098
+ emitSocketEvent(socket, ev, data);
3037
3099
  };
3038
3100
  console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
3039
3101
  runCleanup(pendingManualWaitCleanupRef.current);
@@ -3526,8 +3588,8 @@ function useTourPlayback({
3526
3588
  turnId: turnIdRef.current
3527
3589
  });
3528
3590
  clearCommandBatchId();
3529
- });
3530
- socket.on("tour:start", async (tourData) => {
3591
+ };
3592
+ const handleTourStart = async (tourData) => {
3531
3593
  if (isActiveRef.current) return;
3532
3594
  runIdRef.current = typeof tourData.runId === "number" ? tourData.runId : runIdRef.current;
3533
3595
  const tour = tourData.tourContext ?? tourRef.current;
@@ -3560,8 +3622,8 @@ function useTourPlayback({
3560
3622
  try {
3561
3623
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await import("./aom-HDYNCIOY.mjs");
3562
3624
  const aom = generateMinifiedAOM2();
3563
- if (socketRef.current === socket && socket.connected) {
3564
- socket.emit("tour:sync_dom", {
3625
+ if (socketRef.current === socket) {
3626
+ emitSocketEvent(socket, "tour:sync_dom", {
3565
3627
  url: window.location.pathname + window.location.search + window.location.hash,
3566
3628
  aom: aom.nodes,
3567
3629
  domSummary: captureDomSummary()
@@ -3570,8 +3632,8 @@ function useTourPlayback({
3570
3632
  } catch (e) {
3571
3633
  console.warn("[TourClient] Initial DOM sync failed:", e);
3572
3634
  }
3573
- });
3574
- socket.on("tour:update", (payload) => {
3635
+ };
3636
+ const handleTourUpdate = (payload) => {
3575
3637
  const updatedTour = payload?.tourContext;
3576
3638
  if (!updatedTour?.id || updatedTour.id !== tourRef.current?.id) {
3577
3639
  return;
@@ -3588,12 +3650,12 @@ function useTourPlayback({
3588
3650
  onStepChangeRef.current?.(clampedStepIndex, nextTotal, updatedTour);
3589
3651
  }
3590
3652
  }
3591
- });
3592
- socket.on("tour:end", () => {
3653
+ };
3654
+ const handleTourEndEvent = () => {
3593
3655
  setServerState((prev) => prev ? { ...prev, isActive: false, phase: "completed" } : prev);
3594
3656
  handleTourEnd();
3595
- });
3596
- socket.on("tour:debug_log", (entry) => {
3657
+ };
3658
+ const handleDebugLog = (entry) => {
3597
3659
  const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
3598
3660
  if (isDev) {
3599
3661
  console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
@@ -3603,29 +3665,40 @@ function useTourPlayback({
3603
3665
  }));
3604
3666
  }
3605
3667
  }
3606
- });
3668
+ };
3669
+ socket.on("connect", handleConnect);
3670
+ socket.on("tour:server_state", handleServerState);
3671
+ socket.on("tour:command_cancel", handleCommandCancel);
3672
+ socket.on("tour:command", handleCommand);
3673
+ socket.on("tour:start", handleTourStart);
3674
+ socket.on("tour:update", handleTourUpdate);
3675
+ socket.on("tour:end", handleTourEndEvent);
3676
+ socket.on("tour:debug_log", handleDebugLog);
3607
3677
  console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
3608
3678
  return () => {
3609
- const toClose = createdSocket ?? socketRef.current;
3610
- if (toClose) {
3611
- toClose.disconnect();
3612
- }
3613
- createdSocket = null;
3679
+ socket.off("connect", handleConnect);
3680
+ socket.off("tour:server_state", handleServerState);
3681
+ socket.off("tour:command_cancel", handleCommandCancel);
3682
+ socket.off("tour:command", handleCommand);
3683
+ socket.off("tour:start", handleTourStart);
3684
+ socket.off("tour:update", handleTourUpdate);
3685
+ socket.off("tour:end", handleTourEndEvent);
3686
+ socket.off("tour:debug_log", handleDebugLog);
3687
+ const toClose = socket;
3614
3688
  socketRef.current = null;
3615
3689
  setServerState(null);
3616
3690
  runIdRef.current = null;
3617
3691
  turnIdRef.current = null;
3692
+ tourSocketPool.release(serverUrl, toClose);
3618
3693
  };
3619
- }, [serverUrl, websiteId, disabled]);
3694
+ }, [serverUrl, disabled]);
3620
3695
  useEffect11(() => {
3621
3696
  if (disabled) return;
3622
3697
  const s = socketRef.current;
3623
3698
  const profile = userProfile;
3624
- if (!s?.connected || !websiteId || !profile?.userId) return;
3699
+ if (!websiteId || !profile?.userId) return;
3625
3700
  const timer = setTimeout(() => {
3626
- if (s.connected) {
3627
- s.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3628
- }
3701
+ emitSocketEvent(s, "tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3629
3702
  }, 150);
3630
3703
  return () => clearTimeout(timer);
3631
3704
  }, [disabled, websiteId, userProfile?.userId, userProfile?.type]);
@@ -3635,8 +3708,8 @@ function useTourPlayback({
3635
3708
  }
3636
3709
  }, [showCaptions, isReviewMode]);
3637
3710
  useEffect11(() => {
3638
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3639
- socketRef.current.emit("tour:client_state", {
3711
+ if (!isActiveRef.current) return;
3712
+ emitSocketEvent(socketRef.current, "tour:client_state", {
3640
3713
  runId: runIdRef.current,
3641
3714
  turnId: turnIdRef.current,
3642
3715
  commandBatchId: activeCommandBatchIdRef.current,
@@ -3649,19 +3722,17 @@ function useTourPlayback({
3649
3722
  });
3650
3723
  }, [isActive, playbackState, voice.isListening, voice.isSpeaking]);
3651
3724
  const syncAOM = useCallback7(async () => {
3652
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3725
+ if (!isActiveRef.current) return;
3653
3726
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await import("./aom-HDYNCIOY.mjs");
3654
3727
  const aom = generateMinifiedAOM2();
3655
- if (socketRef.current?.connected) {
3656
- socketRef.current.emit("tour:sync_dom", {
3657
- url: window.location.pathname + window.location.search + window.location.hash,
3658
- aom: aom.nodes,
3659
- domSummary: captureDomSummary()
3660
- });
3661
- }
3728
+ emitSocketEvent(socketRef.current, "tour:sync_dom", {
3729
+ url: window.location.pathname + window.location.search + window.location.hash,
3730
+ aom: aom.nodes,
3731
+ domSummary: captureDomSummary()
3732
+ });
3662
3733
  }, []);
3663
3734
  const interruptExecution = useCallback7((transcript) => {
3664
- if (!socketRef.current?.connected || !isActiveRef.current) return false;
3735
+ if (!isSocketWritable(socketRef.current) || !isActiveRef.current) return false;
3665
3736
  if (!commandInFlightRef.current && !voice.isSpeaking) return false;
3666
3737
  interruptedForQuestionRef.current = true;
3667
3738
  activeExecutionTokenRef.current += 1;
@@ -3669,17 +3740,16 @@ function useTourPlayback({
3669
3740
  removeHighlight();
3670
3741
  removeCaption();
3671
3742
  voice.stopSpeaking();
3672
- if (socketRef.current?.connected) {
3673
- socketRef.current.emit("tour:action_result", {
3674
- success: true,
3675
- interrupted: true,
3676
- transcript,
3677
- commandBatchId: activeCommandBatchIdRef.current,
3678
- runId: runIdRef.current,
3679
- turnId: turnIdRef.current
3680
- });
3743
+ if (emitSocketEvent(socketRef.current, "tour:action_result", {
3744
+ success: true,
3745
+ interrupted: true,
3746
+ transcript,
3747
+ commandBatchId: activeCommandBatchIdRef.current,
3748
+ runId: runIdRef.current,
3749
+ turnId: turnIdRef.current
3750
+ })) {
3681
3751
  activeCommandBatchIdRef.current = null;
3682
- socketRef.current.emit("tour:user_input", {
3752
+ emitSocketEvent(socketRef.current, "tour:user_input", {
3683
3753
  transcript,
3684
3754
  interrupted: true,
3685
3755
  runId: runIdRef.current,
@@ -3701,9 +3771,7 @@ function useTourPlayback({
3701
3771
  removeCaption();
3702
3772
  voice.stopSpeaking();
3703
3773
  voice.stopListening();
3704
- if (socketRef.current?.connected) {
3705
- socketRef.current.emit("tour:abort");
3706
- }
3774
+ emitSocketEvent(socketRef.current, "tour:abort");
3707
3775
  if (reviewModeRef.current && tourRef.current?.id && previewRunIdRef.current) {
3708
3776
  void logPreviewEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, previewRunIdRef.current, websiteId, {
3709
3777
  stepOrder: stepIndexRef.current,
@@ -3775,7 +3843,7 @@ function useTourPlayback({
3775
3843
  await new Promise((r) => setTimeout(r, 200));
3776
3844
  retries++;
3777
3845
  }
3778
- if (!socketRef.current?.connected) {
3846
+ if (!isSocketWritable(socketRef.current)) {
3779
3847
  console.warn("[TourClient] Cannot run tour, socket not connected.");
3780
3848
  return;
3781
3849
  }
@@ -3803,13 +3871,11 @@ function useTourPlayback({
3803
3871
  previewRunIdRef.current = null;
3804
3872
  }
3805
3873
  tourRef.current = tour;
3806
- if (socketRef.current?.connected) {
3807
- socketRef.current.emit("tour:request_start", {
3808
- tourId: tour.id,
3809
- previewRunId: previewRunIdRef.current,
3810
- tourContext: tour
3811
- });
3812
- }
3874
+ emitSocketEvent(socketRef.current, "tour:request_start", {
3875
+ tourId: tour.id,
3876
+ previewRunId: previewRunIdRef.current,
3877
+ tourContext: tour
3878
+ });
3813
3879
  }, [serverUrl, websiteId]);
3814
3880
  useEffect11(() => {
3815
3881
  if (!enableAutoDiscovery) return;
@@ -3953,8 +4019,8 @@ function useTourPlayback({
3953
4019
  const revisionVersion = Number(response?.revision?.versionNumber);
3954
4020
  const appliedMessage = Number.isFinite(revisionVersion) && revisionVersion > 0 ? `Applied to the draft as version ${revisionVersion}.` : "Correction applied to the draft.";
3955
4021
  setReviewStatusMessage(apply ? appliedMessage : "Correction saved for review.");
3956
- if (apply && playbackState === "paused" && socketRef.current?.connected && isActiveRef.current) {
3957
- socketRef.current.emit("tour:resume");
4022
+ if (apply && playbackState === "paused" && isActiveRef.current) {
4023
+ emitSocketEvent(socketRef.current, "tour:resume");
3958
4024
  setPlaybackState("executing");
3959
4025
  }
3960
4026
  } catch (err) {
@@ -3978,14 +4044,14 @@ function useTourPlayback({
3978
4044
  stopTour();
3979
4045
  }, [stopTour]);
3980
4046
  const pauseTour = useCallback7(() => {
3981
- if (socketRef.current?.connected && isActiveRef.current) {
3982
- socketRef.current.emit("tour:pause");
4047
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4048
+ emitSocketEvent(socketRef.current, "tour:pause");
3983
4049
  setPlaybackState("paused");
3984
4050
  }
3985
4051
  }, []);
3986
4052
  const resumeTour = useCallback7(() => {
3987
- if (socketRef.current?.connected && isActiveRef.current) {
3988
- socketRef.current.emit("tour:resume");
4053
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4054
+ emitSocketEvent(socketRef.current, "tour:resume");
3989
4055
  setPlaybackState("executing");
3990
4056
  }
3991
4057
  }, []);
@@ -4007,9 +4073,9 @@ function useTourPlayback({
4007
4073
  if (voiceInputResolveRef.current) {
4008
4074
  console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
4009
4075
  voiceInputResolveRef.current(text);
4010
- } else if (socketRef.current?.connected) {
4076
+ } else if (isSocketWritable(socketRef.current)) {
4011
4077
  console.log("[TourAgent] Forwarding ambient voice to server:", text);
4012
- socketRef.current.emit("tour:user_input", {
4078
+ emitSocketEvent(socketRef.current, "tour:user_input", {
4013
4079
  transcript: text,
4014
4080
  runId: runIdRef.current,
4015
4081
  turnId: turnIdRef.current
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelnex/sdk",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "React SDK for natural language control of web apps via AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",