@modelnex/sdk 0.5.16 → 0.5.18

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 +215 -88
  2. package/dist/index.mjs +215 -88
  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,101 @@ 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
+
2721
+ // src/utils/tour-playback-guards.ts
2722
+ function shouldExecuteTourCommandBatch(isPlaybackActive) {
2723
+ return isPlaybackActive;
2724
+ }
2725
+ function shouldAcceptTourStart(state) {
2726
+ return !state.isPlaybackActive && !state.startRequested;
2727
+ }
2728
+ function shouldRunTourAutoDiscovery(state) {
2729
+ return state.enableAutoDiscovery && !state.disabled && !state.isPlaybackActive && !state.startRequested && !state.hasPendingTour;
2730
+ }
2731
+ function shouldLogTourDebugEntry(state) {
2732
+ if (!state.isPlaybackActive) {
2733
+ if (state.entryType !== "tour_start") {
2734
+ return false;
2735
+ }
2736
+ if (state.entryTourType && state.entryTourType !== state.experienceType) {
2737
+ return false;
2738
+ }
2739
+ }
2740
+ if (state.entryTourType && state.entryTourType !== state.experienceType) {
2741
+ return false;
2742
+ }
2743
+ return true;
2744
+ }
2745
+
2652
2746
  // src/hooks/useTourPlayback.ts
2653
2747
  function resolveElement(step) {
2654
2748
  const el = step.element;
@@ -3182,14 +3276,17 @@ function useTourPlayback({
3182
3276
  const showCaptionsRef = (0, import_react12.useRef)(showCaptions);
3183
3277
  const runIdRef = (0, import_react12.useRef)(null);
3184
3278
  const turnIdRef = (0, import_react12.useRef)(null);
3279
+ const startRequestedRef = (0, import_react12.useRef)(false);
3185
3280
  const socketRef = (0, import_react12.useRef)(null);
3186
3281
  const socketIdRef = (0, import_react12.useRef)(socketId);
3187
3282
  const commandUrlRef = (0, import_react12.useRef)(commandUrl);
3283
+ const websiteIdRef = (0, import_react12.useRef)(websiteId);
3188
3284
  const onStepChangeRef = (0, import_react12.useRef)(onStepChange);
3189
3285
  const isActiveRef = (0, import_react12.useRef)(false);
3190
3286
  const activeCommandBatchIdRef = (0, import_react12.useRef)(null);
3191
3287
  socketIdRef.current = socketId;
3192
3288
  commandUrlRef.current = commandUrl;
3289
+ websiteIdRef.current = websiteId;
3193
3290
  onStepChangeRef.current = onStepChange;
3194
3291
  isActiveRef.current = isActive;
3195
3292
  reviewModeRef.current = isReviewMode;
@@ -3200,25 +3297,17 @@ function useTourPlayback({
3200
3297
  (0, import_react12.useEffect)(() => {
3201
3298
  if (disabled) return;
3202
3299
  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;
3300
+ const socket = tourSocketPool.acquire(serverUrl);
3213
3301
  socketRef.current = socket;
3214
- socket.on("connect", () => {
3302
+ const handleConnect = () => {
3215
3303
  console.log("[TourClient] Connected to tour agent server:", socket.id);
3216
3304
  const profile = userProfileRef.current;
3217
- if (websiteId && profile?.userId && socket.connected) {
3218
- socket.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3305
+ const currentWebsiteId = websiteIdRef.current;
3306
+ if (currentWebsiteId && profile?.userId) {
3307
+ emitSocketEvent(socket, "tour:init", { websiteId: currentWebsiteId, userId: profile.userId, userType: profile.type });
3219
3308
  }
3220
- });
3221
- socket.on("tour:server_state", (payload) => {
3309
+ };
3310
+ const handleServerState = (payload) => {
3222
3311
  if (typeof payload?.runId === "number") {
3223
3312
  runIdRef.current = payload.runId;
3224
3313
  }
@@ -3226,8 +3315,8 @@ function useTourPlayback({
3226
3315
  turnIdRef.current = payload.turnId ?? null;
3227
3316
  }
3228
3317
  setServerState(payload);
3229
- });
3230
- socket.on("tour:command_cancel", (payload) => {
3318
+ };
3319
+ const handleCommandCancel = (payload) => {
3231
3320
  console.log("[TourClient] Received command_cancel:", payload);
3232
3321
  if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
3233
3322
  activeCommandBatchIdRef.current = null;
@@ -3238,12 +3327,16 @@ function useTourPlayback({
3238
3327
  window.speechSynthesis.cancel();
3239
3328
  }
3240
3329
  }
3241
- });
3242
- socket.on("tour:command", async (payload) => {
3330
+ };
3331
+ const handleCommand = async (payload) => {
3332
+ if (!shouldExecuteTourCommandBatch(isActiveRef.current)) {
3333
+ const activeType = experienceTypeRef.current;
3334
+ console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
3335
+ return;
3336
+ }
3243
3337
  const emitIfOpen = (ev, data) => {
3244
3338
  if (socketRef.current !== socket) return;
3245
- if (!socket.connected) return;
3246
- socket.emit(ev, data);
3339
+ emitSocketEvent(socket, ev, data);
3247
3340
  };
3248
3341
  console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
3249
3342
  runCleanup(pendingManualWaitCleanupRef.current);
@@ -3736,8 +3829,8 @@ function useTourPlayback({
3736
3829
  turnId: turnIdRef.current
3737
3830
  });
3738
3831
  clearCommandBatchId();
3739
- });
3740
- socket.on("tour:start", async (tourData) => {
3832
+ };
3833
+ const handleTourStart = async (tourData) => {
3741
3834
  if (isActiveRef.current) return;
3742
3835
  runIdRef.current = typeof tourData.runId === "number" ? tourData.runId : runIdRef.current;
3743
3836
  const tour = tourData.tourContext ?? tourRef.current;
@@ -3747,6 +3840,7 @@ function useTourPlayback({
3747
3840
  return;
3748
3841
  }
3749
3842
  skipRequestedRef.current = false;
3843
+ startRequestedRef.current = false;
3750
3844
  const total = tourData.totalSteps ?? tour?.steps?.length ?? 0;
3751
3845
  isActiveRef.current = true;
3752
3846
  setIsActive(true);
@@ -3770,8 +3864,8 @@ function useTourPlayback({
3770
3864
  try {
3771
3865
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
3772
3866
  const aom = generateMinifiedAOM2();
3773
- if (socketRef.current === socket && socket.connected) {
3774
- socket.emit("tour:sync_dom", {
3867
+ if (socketRef.current === socket) {
3868
+ emitSocketEvent(socket, "tour:sync_dom", {
3775
3869
  url: window.location.pathname + window.location.search + window.location.hash,
3776
3870
  aom: aom.nodes,
3777
3871
  domSummary: captureDomSummary()
@@ -3780,8 +3874,8 @@ function useTourPlayback({
3780
3874
  } catch (e) {
3781
3875
  console.warn("[TourClient] Initial DOM sync failed:", e);
3782
3876
  }
3783
- });
3784
- socket.on("tour:update", (payload) => {
3877
+ };
3878
+ const handleTourUpdate = (payload) => {
3785
3879
  const updatedTour = payload?.tourContext;
3786
3880
  if (!updatedTour?.id || updatedTour.id !== tourRef.current?.id) {
3787
3881
  return;
@@ -3798,13 +3892,22 @@ function useTourPlayback({
3798
3892
  onStepChangeRef.current?.(clampedStepIndex, nextTotal, updatedTour);
3799
3893
  }
3800
3894
  }
3801
- });
3802
- socket.on("tour:end", () => {
3895
+ };
3896
+ const handleTourEndEvent = () => {
3803
3897
  setServerState((prev) => prev ? { ...prev, isActive: false, phase: "completed" } : prev);
3804
3898
  handleTourEnd();
3805
- });
3806
- socket.on("tour:debug_log", (entry) => {
3899
+ };
3900
+ const handleDebugLog = (entry) => {
3807
3901
  const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
3902
+ const entryTourType = entry?.data?.tourContext?.type ?? tourRef.current?.type ?? null;
3903
+ if (!shouldLogTourDebugEntry({
3904
+ isPlaybackActive: isActiveRef.current,
3905
+ entryType: entry?.type,
3906
+ entryTourType,
3907
+ experienceType: experienceTypeRef.current
3908
+ })) {
3909
+ return;
3910
+ }
3808
3911
  if (isDev) {
3809
3912
  console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
3810
3913
  if (typeof window !== "undefined") {
@@ -3813,29 +3916,40 @@ function useTourPlayback({
3813
3916
  }));
3814
3917
  }
3815
3918
  }
3816
- });
3919
+ };
3920
+ socket.on("connect", handleConnect);
3921
+ socket.on("tour:server_state", handleServerState);
3922
+ socket.on("tour:command_cancel", handleCommandCancel);
3923
+ socket.on("tour:command", handleCommand);
3924
+ socket.on("tour:start", handleTourStart);
3925
+ socket.on("tour:update", handleTourUpdate);
3926
+ socket.on("tour:end", handleTourEndEvent);
3927
+ socket.on("tour:debug_log", handleDebugLog);
3817
3928
  console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
3818
3929
  return () => {
3819
- const toClose = createdSocket ?? socketRef.current;
3820
- if (toClose) {
3821
- toClose.disconnect();
3822
- }
3823
- createdSocket = null;
3930
+ socket.off("connect", handleConnect);
3931
+ socket.off("tour:server_state", handleServerState);
3932
+ socket.off("tour:command_cancel", handleCommandCancel);
3933
+ socket.off("tour:command", handleCommand);
3934
+ socket.off("tour:start", handleTourStart);
3935
+ socket.off("tour:update", handleTourUpdate);
3936
+ socket.off("tour:end", handleTourEndEvent);
3937
+ socket.off("tour:debug_log", handleDebugLog);
3938
+ const toClose = socket;
3824
3939
  socketRef.current = null;
3825
3940
  setServerState(null);
3826
3941
  runIdRef.current = null;
3827
3942
  turnIdRef.current = null;
3943
+ tourSocketPool.release(serverUrl, toClose);
3828
3944
  };
3829
- }, [serverUrl, websiteId, disabled]);
3945
+ }, [serverUrl, disabled]);
3830
3946
  (0, import_react12.useEffect)(() => {
3831
3947
  if (disabled) return;
3832
3948
  const s = socketRef.current;
3833
3949
  const profile = userProfile;
3834
- if (!s?.connected || !websiteId || !profile?.userId) return;
3950
+ if (!websiteId || !profile?.userId) return;
3835
3951
  const timer = setTimeout(() => {
3836
- if (s.connected) {
3837
- s.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3838
- }
3952
+ emitSocketEvent(s, "tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3839
3953
  }, 150);
3840
3954
  return () => clearTimeout(timer);
3841
3955
  }, [disabled, websiteId, userProfile?.userId, userProfile?.type]);
@@ -3845,8 +3959,8 @@ function useTourPlayback({
3845
3959
  }
3846
3960
  }, [showCaptions, isReviewMode]);
3847
3961
  (0, import_react12.useEffect)(() => {
3848
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3849
- socketRef.current.emit("tour:client_state", {
3962
+ if (!isActiveRef.current) return;
3963
+ emitSocketEvent(socketRef.current, "tour:client_state", {
3850
3964
  runId: runIdRef.current,
3851
3965
  turnId: turnIdRef.current,
3852
3966
  commandBatchId: activeCommandBatchIdRef.current,
@@ -3859,19 +3973,17 @@ function useTourPlayback({
3859
3973
  });
3860
3974
  }, [isActive, playbackState, voice.isListening, voice.isSpeaking]);
3861
3975
  const syncAOM = (0, import_react12.useCallback)(async () => {
3862
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3976
+ if (!isActiveRef.current) return;
3863
3977
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await Promise.resolve().then(() => (init_aom(), aom_exports));
3864
3978
  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
- }
3979
+ emitSocketEvent(socketRef.current, "tour:sync_dom", {
3980
+ url: window.location.pathname + window.location.search + window.location.hash,
3981
+ aom: aom.nodes,
3982
+ domSummary: captureDomSummary()
3983
+ });
3872
3984
  }, []);
3873
3985
  const interruptExecution = (0, import_react12.useCallback)((transcript) => {
3874
- if (!socketRef.current?.connected || !isActiveRef.current) return false;
3986
+ if (!isSocketWritable(socketRef.current) || !isActiveRef.current) return false;
3875
3987
  if (!commandInFlightRef.current && !voice.isSpeaking) return false;
3876
3988
  interruptedForQuestionRef.current = true;
3877
3989
  activeExecutionTokenRef.current += 1;
@@ -3879,17 +3991,16 @@ function useTourPlayback({
3879
3991
  removeHighlight();
3880
3992
  removeCaption();
3881
3993
  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
- });
3994
+ if (emitSocketEvent(socketRef.current, "tour:action_result", {
3995
+ success: true,
3996
+ interrupted: true,
3997
+ transcript,
3998
+ commandBatchId: activeCommandBatchIdRef.current,
3999
+ runId: runIdRef.current,
4000
+ turnId: turnIdRef.current
4001
+ })) {
3891
4002
  activeCommandBatchIdRef.current = null;
3892
- socketRef.current.emit("tour:user_input", {
4003
+ emitSocketEvent(socketRef.current, "tour:user_input", {
3893
4004
  transcript,
3894
4005
  interrupted: true,
3895
4006
  runId: runIdRef.current,
@@ -3902,6 +4013,7 @@ function useTourPlayback({
3902
4013
  const stopTour = (0, import_react12.useCallback)(() => {
3903
4014
  skipRequestedRef.current = true;
3904
4015
  isActiveRef.current = false;
4016
+ startRequestedRef.current = false;
3905
4017
  activeExecutionTokenRef.current += 1;
3906
4018
  commandInFlightRef.current = false;
3907
4019
  activeCommandBatchIdRef.current = null;
@@ -3911,9 +4023,7 @@ function useTourPlayback({
3911
4023
  removeCaption();
3912
4024
  voice.stopSpeaking();
3913
4025
  voice.stopListening();
3914
- if (socketRef.current?.connected) {
3915
- socketRef.current.emit("tour:abort");
3916
- }
4026
+ emitSocketEvent(socketRef.current, "tour:abort");
3917
4027
  if (reviewModeRef.current && tourRef.current?.id && previewRunIdRef.current) {
3918
4028
  void logPreviewEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, previewRunIdRef.current, websiteId, {
3919
4029
  stepOrder: stepIndexRef.current,
@@ -3945,6 +4055,7 @@ function useTourPlayback({
3945
4055
  const endingPreviewRunId = previewRunIdRef.current;
3946
4056
  const endingStepOrder = stepIndexRef.current;
3947
4057
  isActiveRef.current = false;
4058
+ startRequestedRef.current = false;
3948
4059
  setPlaybackState("complete");
3949
4060
  removeHighlight();
3950
4061
  removeCaption();
@@ -3978,6 +4089,13 @@ function useTourPlayback({
3978
4089
  onTourEnd?.();
3979
4090
  }, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId]);
3980
4091
  const runTour = (0, import_react12.useCallback)(async (tour, options) => {
4092
+ if (!shouldAcceptTourStart({
4093
+ isPlaybackActive: isActiveRef.current,
4094
+ startRequested: startRequestedRef.current
4095
+ })) {
4096
+ console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
4097
+ return;
4098
+ }
3981
4099
  setPendingTour(null);
3982
4100
  pendingTourRef.current = null;
3983
4101
  let retries = 0;
@@ -3985,10 +4103,11 @@ function useTourPlayback({
3985
4103
  await new Promise((r) => setTimeout(r, 200));
3986
4104
  retries++;
3987
4105
  }
3988
- if (!socketRef.current?.connected) {
4106
+ if (!isSocketWritable(socketRef.current)) {
3989
4107
  console.warn("[TourClient] Cannot run tour, socket not connected.");
3990
4108
  return;
3991
4109
  }
4110
+ startRequestedRef.current = true;
3992
4111
  const shouldReview = Boolean(options?.reviewMode);
3993
4112
  resetCaptionSuppression();
3994
4113
  setReviewStatusMessage(null);
@@ -4013,17 +4132,20 @@ function useTourPlayback({
4013
4132
  previewRunIdRef.current = null;
4014
4133
  }
4015
4134
  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
- }
4135
+ emitSocketEvent(socketRef.current, "tour:request_start", {
4136
+ tourId: tour.id,
4137
+ previewRunId: previewRunIdRef.current,
4138
+ tourContext: tour
4139
+ });
4023
4140
  }, [serverUrl, websiteId]);
4024
4141
  (0, import_react12.useEffect)(() => {
4025
- if (!enableAutoDiscovery) return;
4026
- if (disabled) return;
4142
+ if (!shouldRunTourAutoDiscovery({
4143
+ enableAutoDiscovery,
4144
+ disabled,
4145
+ isPlaybackActive: isActiveRef.current,
4146
+ startRequested: startRequestedRef.current,
4147
+ hasPendingTour: Boolean(pendingTourRef.current)
4148
+ })) return;
4027
4149
  if (typeof window === "undefined") return;
4028
4150
  const params = new URLSearchParams(window.location.search);
4029
4151
  const queryParam = experienceType === "onboarding" ? "modelnex_test_workflow" : "modelnex_test_tour";
@@ -4071,8 +4193,13 @@ function useTourPlayback({
4071
4193
  };
4072
4194
  }, [serverUrl, toursApiBase, disabled, websiteId, experienceType, enableAutoDiscovery]);
4073
4195
  (0, import_react12.useEffect)(() => {
4074
- if (!enableAutoDiscovery) return;
4075
- if (disabled) return;
4196
+ if (!shouldRunTourAutoDiscovery({
4197
+ enableAutoDiscovery,
4198
+ disabled,
4199
+ isPlaybackActive: isActiveRef.current,
4200
+ startRequested: startRequestedRef.current,
4201
+ hasPendingTour: Boolean(pendingTourRef.current)
4202
+ })) return;
4076
4203
  if (!websiteId || !userProfile) return;
4077
4204
  if (typeof window !== "undefined") {
4078
4205
  const params = new URLSearchParams(window.location.search);
@@ -4111,7 +4238,7 @@ function useTourPlayback({
4111
4238
  cancelled = true;
4112
4239
  clearTimeout(timer);
4113
4240
  };
4114
- }, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile, enableAutoDiscovery]);
4241
+ }, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, enableAutoDiscovery]);
4115
4242
  (0, import_react12.useEffect)(() => {
4116
4243
  if (!disabled || !isActiveRef.current) return;
4117
4244
  stopTour();
@@ -4163,8 +4290,8 @@ function useTourPlayback({
4163
4290
  const revisionVersion = Number(response?.revision?.versionNumber);
4164
4291
  const appliedMessage = Number.isFinite(revisionVersion) && revisionVersion > 0 ? `Applied to the draft as version ${revisionVersion}.` : "Correction applied to the draft.";
4165
4292
  setReviewStatusMessage(apply ? appliedMessage : "Correction saved for review.");
4166
- if (apply && playbackState === "paused" && socketRef.current?.connected && isActiveRef.current) {
4167
- socketRef.current.emit("tour:resume");
4293
+ if (apply && playbackState === "paused" && isActiveRef.current) {
4294
+ emitSocketEvent(socketRef.current, "tour:resume");
4168
4295
  setPlaybackState("executing");
4169
4296
  }
4170
4297
  } catch (err) {
@@ -4188,14 +4315,14 @@ function useTourPlayback({
4188
4315
  stopTour();
4189
4316
  }, [stopTour]);
4190
4317
  const pauseTour = (0, import_react12.useCallback)(() => {
4191
- if (socketRef.current?.connected && isActiveRef.current) {
4192
- socketRef.current.emit("tour:pause");
4318
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4319
+ emitSocketEvent(socketRef.current, "tour:pause");
4193
4320
  setPlaybackState("paused");
4194
4321
  }
4195
4322
  }, []);
4196
4323
  const resumeTour = (0, import_react12.useCallback)(() => {
4197
- if (socketRef.current?.connected && isActiveRef.current) {
4198
- socketRef.current.emit("tour:resume");
4324
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4325
+ emitSocketEvent(socketRef.current, "tour:resume");
4199
4326
  setPlaybackState("executing");
4200
4327
  }
4201
4328
  }, []);
@@ -4217,9 +4344,9 @@ function useTourPlayback({
4217
4344
  if (voiceInputResolveRef.current) {
4218
4345
  console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
4219
4346
  voiceInputResolveRef.current(text);
4220
- } else if (socketRef.current?.connected) {
4347
+ } else if (isSocketWritable(socketRef.current)) {
4221
4348
  console.log("[TourAgent] Forwarding ambient voice to server:", text);
4222
- socketRef.current.emit("tour:user_input", {
4349
+ emitSocketEvent(socketRef.current, "tour:user_input", {
4223
4350
  transcript: text,
4224
4351
  runId: runIdRef.current,
4225
4352
  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,101 @@ 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
+
2511
+ // src/utils/tour-playback-guards.ts
2512
+ function shouldExecuteTourCommandBatch(isPlaybackActive) {
2513
+ return isPlaybackActive;
2514
+ }
2515
+ function shouldAcceptTourStart(state) {
2516
+ return !state.isPlaybackActive && !state.startRequested;
2517
+ }
2518
+ function shouldRunTourAutoDiscovery(state) {
2519
+ return state.enableAutoDiscovery && !state.disabled && !state.isPlaybackActive && !state.startRequested && !state.hasPendingTour;
2520
+ }
2521
+ function shouldLogTourDebugEntry(state) {
2522
+ if (!state.isPlaybackActive) {
2523
+ if (state.entryType !== "tour_start") {
2524
+ return false;
2525
+ }
2526
+ if (state.entryTourType && state.entryTourType !== state.experienceType) {
2527
+ return false;
2528
+ }
2529
+ }
2530
+ if (state.entryTourType && state.entryTourType !== state.experienceType) {
2531
+ return false;
2532
+ }
2533
+ return true;
2534
+ }
2535
+
2442
2536
  // src/hooks/useTourPlayback.ts
2443
2537
  function resolveElement(step) {
2444
2538
  const el = step.element;
@@ -2972,14 +3066,17 @@ function useTourPlayback({
2972
3066
  const showCaptionsRef = useRef8(showCaptions);
2973
3067
  const runIdRef = useRef8(null);
2974
3068
  const turnIdRef = useRef8(null);
3069
+ const startRequestedRef = useRef8(false);
2975
3070
  const socketRef = useRef8(null);
2976
3071
  const socketIdRef = useRef8(socketId);
2977
3072
  const commandUrlRef = useRef8(commandUrl);
3073
+ const websiteIdRef = useRef8(websiteId);
2978
3074
  const onStepChangeRef = useRef8(onStepChange);
2979
3075
  const isActiveRef = useRef8(false);
2980
3076
  const activeCommandBatchIdRef = useRef8(null);
2981
3077
  socketIdRef.current = socketId;
2982
3078
  commandUrlRef.current = commandUrl;
3079
+ websiteIdRef.current = websiteId;
2983
3080
  onStepChangeRef.current = onStepChange;
2984
3081
  isActiveRef.current = isActive;
2985
3082
  reviewModeRef.current = isReviewMode;
@@ -2990,25 +3087,17 @@ function useTourPlayback({
2990
3087
  useEffect11(() => {
2991
3088
  if (disabled) return;
2992
3089
  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;
3090
+ const socket = tourSocketPool.acquire(serverUrl);
3003
3091
  socketRef.current = socket;
3004
- socket.on("connect", () => {
3092
+ const handleConnect = () => {
3005
3093
  console.log("[TourClient] Connected to tour agent server:", socket.id);
3006
3094
  const profile = userProfileRef.current;
3007
- if (websiteId && profile?.userId && socket.connected) {
3008
- socket.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3095
+ const currentWebsiteId = websiteIdRef.current;
3096
+ if (currentWebsiteId && profile?.userId) {
3097
+ emitSocketEvent(socket, "tour:init", { websiteId: currentWebsiteId, userId: profile.userId, userType: profile.type });
3009
3098
  }
3010
- });
3011
- socket.on("tour:server_state", (payload) => {
3099
+ };
3100
+ const handleServerState = (payload) => {
3012
3101
  if (typeof payload?.runId === "number") {
3013
3102
  runIdRef.current = payload.runId;
3014
3103
  }
@@ -3016,8 +3105,8 @@ function useTourPlayback({
3016
3105
  turnIdRef.current = payload.turnId ?? null;
3017
3106
  }
3018
3107
  setServerState(payload);
3019
- });
3020
- socket.on("tour:command_cancel", (payload) => {
3108
+ };
3109
+ const handleCommandCancel = (payload) => {
3021
3110
  console.log("[TourClient] Received command_cancel:", payload);
3022
3111
  if (payload.commandBatchId && activeCommandBatchIdRef.current === payload.commandBatchId) {
3023
3112
  activeCommandBatchIdRef.current = null;
@@ -3028,12 +3117,16 @@ function useTourPlayback({
3028
3117
  window.speechSynthesis.cancel();
3029
3118
  }
3030
3119
  }
3031
- });
3032
- socket.on("tour:command", async (payload) => {
3120
+ };
3121
+ const handleCommand = async (payload) => {
3122
+ if (!shouldExecuteTourCommandBatch(isActiveRef.current)) {
3123
+ const activeType = experienceTypeRef.current;
3124
+ console.log("[TourClient] Ignoring command batch for inactive playback hook:", activeType, payload.stepIndex);
3125
+ return;
3126
+ }
3033
3127
  const emitIfOpen = (ev, data) => {
3034
3128
  if (socketRef.current !== socket) return;
3035
- if (!socket.connected) return;
3036
- socket.emit(ev, data);
3129
+ emitSocketEvent(socket, ev, data);
3037
3130
  };
3038
3131
  console.log("[TourClient] Received command batch:", payload.stepIndex, payload.commands);
3039
3132
  runCleanup(pendingManualWaitCleanupRef.current);
@@ -3526,8 +3619,8 @@ function useTourPlayback({
3526
3619
  turnId: turnIdRef.current
3527
3620
  });
3528
3621
  clearCommandBatchId();
3529
- });
3530
- socket.on("tour:start", async (tourData) => {
3622
+ };
3623
+ const handleTourStart = async (tourData) => {
3531
3624
  if (isActiveRef.current) return;
3532
3625
  runIdRef.current = typeof tourData.runId === "number" ? tourData.runId : runIdRef.current;
3533
3626
  const tour = tourData.tourContext ?? tourRef.current;
@@ -3537,6 +3630,7 @@ function useTourPlayback({
3537
3630
  return;
3538
3631
  }
3539
3632
  skipRequestedRef.current = false;
3633
+ startRequestedRef.current = false;
3540
3634
  const total = tourData.totalSteps ?? tour?.steps?.length ?? 0;
3541
3635
  isActiveRef.current = true;
3542
3636
  setIsActive(true);
@@ -3560,8 +3654,8 @@ function useTourPlayback({
3560
3654
  try {
3561
3655
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await import("./aom-HDYNCIOY.mjs");
3562
3656
  const aom = generateMinifiedAOM2();
3563
- if (socketRef.current === socket && socket.connected) {
3564
- socket.emit("tour:sync_dom", {
3657
+ if (socketRef.current === socket) {
3658
+ emitSocketEvent(socket, "tour:sync_dom", {
3565
3659
  url: window.location.pathname + window.location.search + window.location.hash,
3566
3660
  aom: aom.nodes,
3567
3661
  domSummary: captureDomSummary()
@@ -3570,8 +3664,8 @@ function useTourPlayback({
3570
3664
  } catch (e) {
3571
3665
  console.warn("[TourClient] Initial DOM sync failed:", e);
3572
3666
  }
3573
- });
3574
- socket.on("tour:update", (payload) => {
3667
+ };
3668
+ const handleTourUpdate = (payload) => {
3575
3669
  const updatedTour = payload?.tourContext;
3576
3670
  if (!updatedTour?.id || updatedTour.id !== tourRef.current?.id) {
3577
3671
  return;
@@ -3588,13 +3682,22 @@ function useTourPlayback({
3588
3682
  onStepChangeRef.current?.(clampedStepIndex, nextTotal, updatedTour);
3589
3683
  }
3590
3684
  }
3591
- });
3592
- socket.on("tour:end", () => {
3685
+ };
3686
+ const handleTourEndEvent = () => {
3593
3687
  setServerState((prev) => prev ? { ...prev, isActive: false, phase: "completed" } : prev);
3594
3688
  handleTourEnd();
3595
- });
3596
- socket.on("tour:debug_log", (entry) => {
3689
+ };
3690
+ const handleDebugLog = (entry) => {
3597
3691
  const isDev = devModeRef.current || process.env.NODE_ENV === "development" || typeof window !== "undefined" && window.MODELNEX_DEBUG;
3692
+ const entryTourType = entry?.data?.tourContext?.type ?? tourRef.current?.type ?? null;
3693
+ if (!shouldLogTourDebugEntry({
3694
+ isPlaybackActive: isActiveRef.current,
3695
+ entryType: entry?.type,
3696
+ entryTourType,
3697
+ experienceType: experienceTypeRef.current
3698
+ })) {
3699
+ return;
3700
+ }
3598
3701
  if (isDev) {
3599
3702
  console.log(`%c[ModelNex Tour] ${entry.type}`, "color: #3b82f6; font-weight: bold", entry);
3600
3703
  if (typeof window !== "undefined") {
@@ -3603,29 +3706,40 @@ function useTourPlayback({
3603
3706
  }));
3604
3707
  }
3605
3708
  }
3606
- });
3709
+ };
3710
+ socket.on("connect", handleConnect);
3711
+ socket.on("tour:server_state", handleServerState);
3712
+ socket.on("tour:command_cancel", handleCommandCancel);
3713
+ socket.on("tour:command", handleCommand);
3714
+ socket.on("tour:start", handleTourStart);
3715
+ socket.on("tour:update", handleTourUpdate);
3716
+ socket.on("tour:end", handleTourEndEvent);
3717
+ socket.on("tour:debug_log", handleDebugLog);
3607
3718
  console.log("[ModelNex SDK] Tour playback initialized. Debug logs enabled:", devModeRef.current || process.env.NODE_ENV === "development");
3608
3719
  return () => {
3609
- const toClose = createdSocket ?? socketRef.current;
3610
- if (toClose) {
3611
- toClose.disconnect();
3612
- }
3613
- createdSocket = null;
3720
+ socket.off("connect", handleConnect);
3721
+ socket.off("tour:server_state", handleServerState);
3722
+ socket.off("tour:command_cancel", handleCommandCancel);
3723
+ socket.off("tour:command", handleCommand);
3724
+ socket.off("tour:start", handleTourStart);
3725
+ socket.off("tour:update", handleTourUpdate);
3726
+ socket.off("tour:end", handleTourEndEvent);
3727
+ socket.off("tour:debug_log", handleDebugLog);
3728
+ const toClose = socket;
3614
3729
  socketRef.current = null;
3615
3730
  setServerState(null);
3616
3731
  runIdRef.current = null;
3617
3732
  turnIdRef.current = null;
3733
+ tourSocketPool.release(serverUrl, toClose);
3618
3734
  };
3619
- }, [serverUrl, websiteId, disabled]);
3735
+ }, [serverUrl, disabled]);
3620
3736
  useEffect11(() => {
3621
3737
  if (disabled) return;
3622
3738
  const s = socketRef.current;
3623
3739
  const profile = userProfile;
3624
- if (!s?.connected || !websiteId || !profile?.userId) return;
3740
+ if (!websiteId || !profile?.userId) return;
3625
3741
  const timer = setTimeout(() => {
3626
- if (s.connected) {
3627
- s.emit("tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3628
- }
3742
+ emitSocketEvent(s, "tour:init", { websiteId, userId: profile.userId, userType: profile.type });
3629
3743
  }, 150);
3630
3744
  return () => clearTimeout(timer);
3631
3745
  }, [disabled, websiteId, userProfile?.userId, userProfile?.type]);
@@ -3635,8 +3749,8 @@ function useTourPlayback({
3635
3749
  }
3636
3750
  }, [showCaptions, isReviewMode]);
3637
3751
  useEffect11(() => {
3638
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3639
- socketRef.current.emit("tour:client_state", {
3752
+ if (!isActiveRef.current) return;
3753
+ emitSocketEvent(socketRef.current, "tour:client_state", {
3640
3754
  runId: runIdRef.current,
3641
3755
  turnId: turnIdRef.current,
3642
3756
  commandBatchId: activeCommandBatchIdRef.current,
@@ -3649,19 +3763,17 @@ function useTourPlayback({
3649
3763
  });
3650
3764
  }, [isActive, playbackState, voice.isListening, voice.isSpeaking]);
3651
3765
  const syncAOM = useCallback7(async () => {
3652
- if (!socketRef.current?.connected || !isActiveRef.current) return;
3766
+ if (!isActiveRef.current) return;
3653
3767
  const { generateMinifiedAOM: generateMinifiedAOM2 } = await import("./aom-HDYNCIOY.mjs");
3654
3768
  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
- }
3769
+ emitSocketEvent(socketRef.current, "tour:sync_dom", {
3770
+ url: window.location.pathname + window.location.search + window.location.hash,
3771
+ aom: aom.nodes,
3772
+ domSummary: captureDomSummary()
3773
+ });
3662
3774
  }, []);
3663
3775
  const interruptExecution = useCallback7((transcript) => {
3664
- if (!socketRef.current?.connected || !isActiveRef.current) return false;
3776
+ if (!isSocketWritable(socketRef.current) || !isActiveRef.current) return false;
3665
3777
  if (!commandInFlightRef.current && !voice.isSpeaking) return false;
3666
3778
  interruptedForQuestionRef.current = true;
3667
3779
  activeExecutionTokenRef.current += 1;
@@ -3669,17 +3781,16 @@ function useTourPlayback({
3669
3781
  removeHighlight();
3670
3782
  removeCaption();
3671
3783
  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
- });
3784
+ if (emitSocketEvent(socketRef.current, "tour:action_result", {
3785
+ success: true,
3786
+ interrupted: true,
3787
+ transcript,
3788
+ commandBatchId: activeCommandBatchIdRef.current,
3789
+ runId: runIdRef.current,
3790
+ turnId: turnIdRef.current
3791
+ })) {
3681
3792
  activeCommandBatchIdRef.current = null;
3682
- socketRef.current.emit("tour:user_input", {
3793
+ emitSocketEvent(socketRef.current, "tour:user_input", {
3683
3794
  transcript,
3684
3795
  interrupted: true,
3685
3796
  runId: runIdRef.current,
@@ -3692,6 +3803,7 @@ function useTourPlayback({
3692
3803
  const stopTour = useCallback7(() => {
3693
3804
  skipRequestedRef.current = true;
3694
3805
  isActiveRef.current = false;
3806
+ startRequestedRef.current = false;
3695
3807
  activeExecutionTokenRef.current += 1;
3696
3808
  commandInFlightRef.current = false;
3697
3809
  activeCommandBatchIdRef.current = null;
@@ -3701,9 +3813,7 @@ function useTourPlayback({
3701
3813
  removeCaption();
3702
3814
  voice.stopSpeaking();
3703
3815
  voice.stopListening();
3704
- if (socketRef.current?.connected) {
3705
- socketRef.current.emit("tour:abort");
3706
- }
3816
+ emitSocketEvent(socketRef.current, "tour:abort");
3707
3817
  if (reviewModeRef.current && tourRef.current?.id && previewRunIdRef.current) {
3708
3818
  void logPreviewEvent(serverUrl, toursApiBaseRef.current, tourRef.current.id, previewRunIdRef.current, websiteId, {
3709
3819
  stepOrder: stepIndexRef.current,
@@ -3735,6 +3845,7 @@ function useTourPlayback({
3735
3845
  const endingPreviewRunId = previewRunIdRef.current;
3736
3846
  const endingStepOrder = stepIndexRef.current;
3737
3847
  isActiveRef.current = false;
3848
+ startRequestedRef.current = false;
3738
3849
  setPlaybackState("complete");
3739
3850
  removeHighlight();
3740
3851
  removeCaption();
@@ -3768,6 +3879,13 @@ function useTourPlayback({
3768
3879
  onTourEnd?.();
3769
3880
  }, [experienceType, userProfile, serverUrl, voice, onTourEnd, websiteId]);
3770
3881
  const runTour = useCallback7(async (tour, options) => {
3882
+ if (!shouldAcceptTourStart({
3883
+ isPlaybackActive: isActiveRef.current,
3884
+ startRequested: startRequestedRef.current
3885
+ })) {
3886
+ console.log("[TourClient] Ignoring duplicate start request while playback is already active or starting:", tour.id);
3887
+ return;
3888
+ }
3771
3889
  setPendingTour(null);
3772
3890
  pendingTourRef.current = null;
3773
3891
  let retries = 0;
@@ -3775,10 +3893,11 @@ function useTourPlayback({
3775
3893
  await new Promise((r) => setTimeout(r, 200));
3776
3894
  retries++;
3777
3895
  }
3778
- if (!socketRef.current?.connected) {
3896
+ if (!isSocketWritable(socketRef.current)) {
3779
3897
  console.warn("[TourClient] Cannot run tour, socket not connected.");
3780
3898
  return;
3781
3899
  }
3900
+ startRequestedRef.current = true;
3782
3901
  const shouldReview = Boolean(options?.reviewMode);
3783
3902
  resetCaptionSuppression();
3784
3903
  setReviewStatusMessage(null);
@@ -3803,17 +3922,20 @@ function useTourPlayback({
3803
3922
  previewRunIdRef.current = null;
3804
3923
  }
3805
3924
  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
- }
3925
+ emitSocketEvent(socketRef.current, "tour:request_start", {
3926
+ tourId: tour.id,
3927
+ previewRunId: previewRunIdRef.current,
3928
+ tourContext: tour
3929
+ });
3813
3930
  }, [serverUrl, websiteId]);
3814
3931
  useEffect11(() => {
3815
- if (!enableAutoDiscovery) return;
3816
- if (disabled) return;
3932
+ if (!shouldRunTourAutoDiscovery({
3933
+ enableAutoDiscovery,
3934
+ disabled,
3935
+ isPlaybackActive: isActiveRef.current,
3936
+ startRequested: startRequestedRef.current,
3937
+ hasPendingTour: Boolean(pendingTourRef.current)
3938
+ })) return;
3817
3939
  if (typeof window === "undefined") return;
3818
3940
  const params = new URLSearchParams(window.location.search);
3819
3941
  const queryParam = experienceType === "onboarding" ? "modelnex_test_workflow" : "modelnex_test_tour";
@@ -3861,8 +3983,13 @@ function useTourPlayback({
3861
3983
  };
3862
3984
  }, [serverUrl, toursApiBase, disabled, websiteId, experienceType, enableAutoDiscovery]);
3863
3985
  useEffect11(() => {
3864
- if (!enableAutoDiscovery) return;
3865
- if (disabled) return;
3986
+ if (!shouldRunTourAutoDiscovery({
3987
+ enableAutoDiscovery,
3988
+ disabled,
3989
+ isPlaybackActive: isActiveRef.current,
3990
+ startRequested: startRequestedRef.current,
3991
+ hasPendingTour: Boolean(pendingTourRef.current)
3992
+ })) return;
3866
3993
  if (!websiteId || !userProfile) return;
3867
3994
  if (typeof window !== "undefined") {
3868
3995
  const params = new URLSearchParams(window.location.search);
@@ -3901,7 +4028,7 @@ function useTourPlayback({
3901
4028
  cancelled = true;
3902
4029
  clearTimeout(timer);
3903
4030
  };
3904
- }, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile, enableAutoDiscovery]);
4031
+ }, [websiteId, serverUrl, toursApiBase, disabled, experienceType, userProfile?.userId, userProfile?.type, userProfile?.isNewUser, enableAutoDiscovery]);
3905
4032
  useEffect11(() => {
3906
4033
  if (!disabled || !isActiveRef.current) return;
3907
4034
  stopTour();
@@ -3953,8 +4080,8 @@ function useTourPlayback({
3953
4080
  const revisionVersion = Number(response?.revision?.versionNumber);
3954
4081
  const appliedMessage = Number.isFinite(revisionVersion) && revisionVersion > 0 ? `Applied to the draft as version ${revisionVersion}.` : "Correction applied to the draft.";
3955
4082
  setReviewStatusMessage(apply ? appliedMessage : "Correction saved for review.");
3956
- if (apply && playbackState === "paused" && socketRef.current?.connected && isActiveRef.current) {
3957
- socketRef.current.emit("tour:resume");
4083
+ if (apply && playbackState === "paused" && isActiveRef.current) {
4084
+ emitSocketEvent(socketRef.current, "tour:resume");
3958
4085
  setPlaybackState("executing");
3959
4086
  }
3960
4087
  } catch (err) {
@@ -3978,14 +4105,14 @@ function useTourPlayback({
3978
4105
  stopTour();
3979
4106
  }, [stopTour]);
3980
4107
  const pauseTour = useCallback7(() => {
3981
- if (socketRef.current?.connected && isActiveRef.current) {
3982
- socketRef.current.emit("tour:pause");
4108
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4109
+ emitSocketEvent(socketRef.current, "tour:pause");
3983
4110
  setPlaybackState("paused");
3984
4111
  }
3985
4112
  }, []);
3986
4113
  const resumeTour = useCallback7(() => {
3987
- if (socketRef.current?.connected && isActiveRef.current) {
3988
- socketRef.current.emit("tour:resume");
4114
+ if (isSocketWritable(socketRef.current) && isActiveRef.current) {
4115
+ emitSocketEvent(socketRef.current, "tour:resume");
3989
4116
  setPlaybackState("executing");
3990
4117
  }
3991
4118
  }, []);
@@ -4007,9 +4134,9 @@ function useTourPlayback({
4007
4134
  if (voiceInputResolveRef.current) {
4008
4135
  console.log("[TourAgent] Resolving loop waiting_voice with text:", text);
4009
4136
  voiceInputResolveRef.current(text);
4010
- } else if (socketRef.current?.connected) {
4137
+ } else if (isSocketWritable(socketRef.current)) {
4011
4138
  console.log("[TourAgent] Forwarding ambient voice to server:", text);
4012
- socketRef.current.emit("tour:user_input", {
4139
+ emitSocketEvent(socketRef.current, "tour:user_input", {
4013
4140
  transcript: text,
4014
4141
  runId: runIdRef.current,
4015
4142
  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.18",
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",