@interfere/next 0.0.14 → 0.0.15-alpha.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.
Files changed (232) hide show
  1. package/README.md +1 -6
  2. package/dist/build/env-config.d.mts +7 -0
  3. package/dist/build/env-config.d.mts.map +1 -0
  4. package/dist/build/env-config.mjs +17 -0
  5. package/dist/build/env-config.mjs.map +1 -0
  6. package/dist/build/logger.d.mts +11 -0
  7. package/dist/build/logger.d.mts.map +1 -0
  8. package/dist/build/logger.mjs +155 -0
  9. package/dist/build/logger.mjs.map +1 -0
  10. package/dist/build/release-program.d.mts +19 -0
  11. package/dist/build/release-program.d.mts.map +1 -0
  12. package/dist/build/release-program.mjs +92 -0
  13. package/dist/build/release-program.mjs.map +1 -0
  14. package/dist/build/secret-key.d.mts +10 -0
  15. package/dist/build/secret-key.d.mts.map +1 -0
  16. package/dist/build/secret-key.mjs +16 -0
  17. package/dist/build/secret-key.mjs.map +1 -0
  18. package/dist/build/services/config.service.d.mts +9 -0
  19. package/dist/build/services/config.service.d.mts.map +1 -0
  20. package/dist/build/services/config.service.mjs +8 -0
  21. package/dist/build/services/config.service.mjs.map +1 -0
  22. package/dist/build/services/preflight.service.d.mts +19 -0
  23. package/dist/build/services/preflight.service.d.mts.map +1 -0
  24. package/dist/build/services/preflight.service.mjs +76 -0
  25. package/dist/build/services/preflight.service.mjs.map +1 -0
  26. package/dist/build/services/release-identity.service.d.mts +22 -0
  27. package/dist/build/services/release-identity.service.d.mts.map +1 -0
  28. package/dist/build/services/release-identity.service.mjs +48 -0
  29. package/dist/build/services/release-identity.service.mjs.map +1 -0
  30. package/dist/build/services/source-map.service.d.mts +24 -0
  31. package/dist/build/services/source-map.service.d.mts.map +1 -0
  32. package/dist/build/services/source-map.service.mjs +58 -0
  33. package/dist/build/services/source-map.service.mjs.map +1 -0
  34. package/dist/build/source-maps/api.d.mts +35 -0
  35. package/dist/build/source-maps/api.d.mts.map +1 -0
  36. package/dist/build/source-maps/api.mjs +61 -0
  37. package/dist/build/source-maps/api.mjs.map +1 -0
  38. package/dist/build/source-maps/client.d.mts +73 -0
  39. package/dist/build/source-maps/client.d.mts.map +1 -0
  40. package/dist/build/source-maps/client.mjs +228 -0
  41. package/dist/build/source-maps/client.mjs.map +1 -0
  42. package/dist/build/source-maps/errors.d.mts +109 -0
  43. package/dist/build/source-maps/errors.d.mts.map +1 -0
  44. package/dist/build/source-maps/errors.mjs +22 -0
  45. package/dist/build/source-maps/errors.mjs.map +1 -0
  46. package/dist/build/source-maps/files.d.mts +35 -0
  47. package/dist/build/source-maps/files.d.mts.map +1 -0
  48. package/dist/build/source-maps/files.mjs +222 -0
  49. package/dist/build/source-maps/files.mjs.map +1 -0
  50. package/dist/build/source-maps/providers/deployment/detector.d.mts +26 -0
  51. package/dist/build/source-maps/providers/deployment/detector.d.mts.map +1 -0
  52. package/dist/build/source-maps/providers/deployment/detector.mjs +22 -0
  53. package/dist/build/source-maps/providers/deployment/detector.mjs.map +1 -0
  54. package/dist/build/source-maps/providers/deployment/types.d.mts +12 -0
  55. package/dist/build/source-maps/providers/deployment/types.d.mts.map +1 -0
  56. package/dist/build/source-maps/providers/deployment/types.mjs +3 -0
  57. package/dist/build/source-maps/providers/deployment/vercel.d.mts +6 -0
  58. package/dist/build/source-maps/providers/deployment/vercel.d.mts.map +1 -0
  59. package/dist/build/source-maps/providers/deployment/vercel.mjs +44 -0
  60. package/dist/build/source-maps/providers/deployment/vercel.mjs.map +1 -0
  61. package/dist/build/source-maps/providers/source-control/detector.d.mts +15 -0
  62. package/dist/build/source-maps/providers/source-control/detector.d.mts.map +1 -0
  63. package/dist/build/source-maps/providers/source-control/detector.mjs +22 -0
  64. package/dist/build/source-maps/providers/source-control/detector.mjs.map +1 -0
  65. package/dist/build/source-maps/providers/source-control/git.d.mts +6 -0
  66. package/dist/build/source-maps/providers/source-control/git.d.mts.map +1 -0
  67. package/dist/build/source-maps/providers/source-control/git.mjs +50 -0
  68. package/dist/build/source-maps/providers/source-control/git.mjs.map +1 -0
  69. package/dist/build/source-maps/providers/source-control/types.d.mts +12 -0
  70. package/dist/build/source-maps/providers/source-control/types.d.mts.map +1 -0
  71. package/dist/build/source-maps/providers/source-control/types.mjs +3 -0
  72. package/dist/build/with-interfere.d.mts +49 -0
  73. package/dist/build/with-interfere.d.mts.map +1 -0
  74. package/dist/build/with-interfere.mjs +75 -0
  75. package/dist/build/with-interfere.mjs.map +1 -0
  76. package/dist/client/client.d.mts +3 -0
  77. package/dist/client/client.mjs +5 -0
  78. package/dist/client/provider.d.mts +22 -0
  79. package/dist/client/provider.d.mts.map +1 -0
  80. package/dist/client/provider.mjs +33 -0
  81. package/dist/client/provider.mjs.map +1 -0
  82. package/dist/lib/env.d.mts +12 -0
  83. package/dist/lib/env.d.mts.map +1 -0
  84. package/dist/lib/env.mjs +17 -0
  85. package/dist/lib/env.mjs.map +1 -0
  86. package/dist/lib/test-utils/make-next-request.d.mts +6 -0
  87. package/dist/lib/test-utils/make-next-request.d.mts.map +1 -0
  88. package/dist/lib/test-utils/make-next-request.mjs +12 -0
  89. package/dist/lib/test-utils/make-next-request.mjs.map +1 -0
  90. package/dist/lib/types.d.mts +22 -0
  91. package/dist/lib/types.d.mts.map +1 -0
  92. package/dist/lib/types.mjs +7 -0
  93. package/dist/lib/types.mjs.map +1 -0
  94. package/dist/server/middleware.d.mts +11 -0
  95. package/dist/server/middleware.d.mts.map +1 -0
  96. package/dist/server/middleware.mjs +84 -0
  97. package/dist/server/middleware.mjs.map +1 -0
  98. package/dist/server/proxy.d.mts +6 -0
  99. package/dist/server/proxy.d.mts.map +1 -0
  100. package/dist/server/proxy.mjs +29 -0
  101. package/dist/server/proxy.mjs.map +1 -0
  102. package/dist/server/route-handler.d.mts +9 -0
  103. package/dist/server/route-handler.d.mts.map +1 -0
  104. package/dist/server/route-handler.mjs +172 -0
  105. package/dist/server/route-handler.mjs.map +1 -0
  106. package/dist/server/services/config.service.d.mts +21 -0
  107. package/dist/server/services/config.service.d.mts.map +1 -0
  108. package/dist/server/services/config.service.mjs +43 -0
  109. package/dist/server/services/config.service.mjs.map +1 -0
  110. package/dist/server/services/error-tracking.service.d.mts +19 -0
  111. package/dist/server/services/error-tracking.service.d.mts.map +1 -0
  112. package/dist/server/services/error-tracking.service.mjs +31 -0
  113. package/dist/server/services/error-tracking.service.mjs.map +1 -0
  114. package/package.json +67 -36
  115. package/dist/__tests__/build/with-interfere-coverage.test.d.ts +0 -2
  116. package/dist/__tests__/build/with-interfere-coverage.test.d.ts.map +0 -1
  117. package/dist/__tests__/build/with-interfere-coverage.test.js +0 -295
  118. package/dist/__tests__/build/with-interfere-coverage.test.js.map +0 -1
  119. package/dist/__tests__/build/with-interfere.test.d.ts +0 -2
  120. package/dist/__tests__/build/with-interfere.test.d.ts.map +0 -1
  121. package/dist/__tests__/build/with-interfere.test.js +0 -363
  122. package/dist/__tests__/build/with-interfere.test.js.map +0 -1
  123. package/dist/__tests__/core/client.test.d.ts +0 -2
  124. package/dist/__tests__/core/client.test.d.ts.map +0 -1
  125. package/dist/__tests__/core/client.test.js +0 -373
  126. package/dist/__tests__/core/client.test.js.map +0 -1
  127. package/dist/__tests__/core/encoders.test.d.ts +0 -2
  128. package/dist/__tests__/core/encoders.test.d.ts.map +0 -1
  129. package/dist/__tests__/core/encoders.test.js +0 -56
  130. package/dist/__tests__/core/encoders.test.js.map +0 -1
  131. package/dist/__tests__/core/rage-click.test.d.ts +0 -2
  132. package/dist/__tests__/core/rage-click.test.d.ts.map +0 -1
  133. package/dist/__tests__/core/rage-click.test.js +0 -121
  134. package/dist/__tests__/core/rage-click.test.js.map +0 -1
  135. package/dist/__tests__/core/session-manager.test.d.ts +0 -2
  136. package/dist/__tests__/core/session-manager.test.d.ts.map +0 -1
  137. package/dist/__tests__/core/session-manager.test.js +0 -1168
  138. package/dist/__tests__/core/session-manager.test.js.map +0 -1
  139. package/dist/__tests__/integration/release-upload.test.d.ts +0 -2
  140. package/dist/__tests__/integration/release-upload.test.d.ts.map +0 -1
  141. package/dist/__tests__/integration/release-upload.test.js +0 -153
  142. package/dist/__tests__/integration/release-upload.test.js.map +0 -1
  143. package/dist/__tests__/provider.test.d.ts +0 -2
  144. package/dist/__tests__/provider.test.d.ts.map +0 -1
  145. package/dist/__tests__/provider.test.js +0 -84
  146. package/dist/__tests__/provider.test.js.map +0 -1
  147. package/dist/__tests__/session/persistence.test.d.ts +0 -2
  148. package/dist/__tests__/session/persistence.test.d.ts.map +0 -1
  149. package/dist/__tests__/session/persistence.test.js +0 -129
  150. package/dist/__tests__/session/persistence.test.js.map +0 -1
  151. package/dist/__tests__/session/session-summary.test.d.ts +0 -2
  152. package/dist/__tests__/session/session-summary.test.d.ts.map +0 -1
  153. package/dist/__tests__/session/session-summary.test.js +0 -763
  154. package/dist/__tests__/session/session-summary.test.js.map +0 -1
  155. package/dist/client.d.ts +0 -75
  156. package/dist/client.d.ts.map +0 -1
  157. package/dist/client.js +0 -123
  158. package/dist/client.js.map +0 -1
  159. package/dist/config.d.ts +0 -40
  160. package/dist/config.d.ts.map +0 -1
  161. package/dist/config.js +0 -340
  162. package/dist/config.js.map +0 -1
  163. package/dist/index.d.ts +0 -37
  164. package/dist/index.d.ts.map +0 -1
  165. package/dist/index.js +0 -49
  166. package/dist/index.js.map +0 -1
  167. package/dist/index.jsx +0 -87
  168. package/dist/index.jsx.map +0 -1
  169. package/dist/lib/core/client-core.d.ts +0 -27
  170. package/dist/lib/core/client-core.d.ts.map +0 -1
  171. package/dist/lib/core/client-core.js +0 -152
  172. package/dist/lib/core/client-core.js.map +0 -1
  173. package/dist/lib/core/constants.d.ts +0 -12
  174. package/dist/lib/core/constants.d.ts.map +0 -1
  175. package/dist/lib/core/constants.js +0 -17
  176. package/dist/lib/core/constants.js.map +0 -1
  177. package/dist/lib/core/debug.d.ts +0 -47
  178. package/dist/lib/core/debug.d.ts.map +0 -1
  179. package/dist/lib/core/debug.js +0 -79
  180. package/dist/lib/core/debug.js.map +0 -1
  181. package/dist/lib/core/encoders.d.ts +0 -3
  182. package/dist/lib/core/encoders.d.ts.map +0 -1
  183. package/dist/lib/core/encoders.js +0 -5
  184. package/dist/lib/core/encoders.js.map +0 -1
  185. package/dist/lib/core/error-handlers.d.ts +0 -14
  186. package/dist/lib/core/error-handlers.d.ts.map +0 -1
  187. package/dist/lib/core/error-handlers.js +0 -191
  188. package/dist/lib/core/error-handlers.js.map +0 -1
  189. package/dist/lib/core/runtime.d.ts +0 -7
  190. package/dist/lib/core/runtime.d.ts.map +0 -1
  191. package/dist/lib/core/runtime.js +0 -16
  192. package/dist/lib/core/runtime.js.map +0 -1
  193. package/dist/lib/persistence/storage.d.ts +0 -5
  194. package/dist/lib/persistence/storage.d.ts.map +0 -1
  195. package/dist/lib/persistence/storage.js +0 -67
  196. package/dist/lib/persistence/storage.js.map +0 -1
  197. package/dist/lib/session/constants.d.ts +0 -19
  198. package/dist/lib/session/constants.d.ts.map +0 -1
  199. package/dist/lib/session/constants.js +0 -34
  200. package/dist/lib/session/constants.js.map +0 -1
  201. package/dist/lib/session/persistence.d.ts +0 -58
  202. package/dist/lib/session/persistence.d.ts.map +0 -1
  203. package/dist/lib/session/persistence.js +0 -179
  204. package/dist/lib/session/persistence.js.map +0 -1
  205. package/dist/lib/session/rage-click.d.ts +0 -17
  206. package/dist/lib/session/rage-click.d.ts.map +0 -1
  207. package/dist/lib/session/rage-click.js +0 -104
  208. package/dist/lib/session/rage-click.js.map +0 -1
  209. package/dist/lib/session/replay.d.ts +0 -3
  210. package/dist/lib/session/replay.d.ts.map +0 -1
  211. package/dist/lib/session/replay.js +0 -109
  212. package/dist/lib/session/replay.js.map +0 -1
  213. package/dist/lib/session/session-manager.d.ts +0 -126
  214. package/dist/lib/session/session-manager.d.ts.map +0 -1
  215. package/dist/lib/session/session-manager.js +0 -635
  216. package/dist/lib/session/session-manager.js.map +0 -1
  217. package/dist/lib/session/session-summary.d.ts +0 -3
  218. package/dist/lib/session/session-summary.d.ts.map +0 -1
  219. package/dist/lib/session/session-summary.js +0 -214
  220. package/dist/lib/session/session-summary.js.map +0 -1
  221. package/dist/middleware.d.ts +0 -8
  222. package/dist/middleware.d.ts.map +0 -1
  223. package/dist/middleware.js +0 -139
  224. package/dist/middleware.js.map +0 -1
  225. package/dist/types/storage.d.ts +0 -7
  226. package/dist/types/storage.d.ts.map +0 -1
  227. package/dist/types/storage.js +0 -2
  228. package/dist/types/storage.js.map +0 -1
  229. package/dist/types.d.ts +0 -6
  230. package/dist/types.d.ts.map +0 -1
  231. package/dist/types.js +0 -4
  232. package/dist/types.js.map +0 -1
@@ -1,1168 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { sessionStore } from "../../lib/persistence/storage.js";
3
- import { DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, SESSION_ID, toMs, } from "../../lib/session/constants.js";
4
- import { SessionChangeReason, SessionIdManager, } from "../../lib/session/session-manager.js";
5
- // Mock dependencies
6
- vi.mock("../storage/index.js");
7
- vi.mock("./persistence.js");
8
- describe("SessionIdManager", () => {
9
- let mockClock;
10
- let sessionIdGenerator;
11
- let windowIdGenerator;
12
- let currentTime;
13
- // Test constants
14
- const TEST_SURFACE = "test_surface";
15
- const TEST_SESSION_ID = "test-session-id";
16
- const TEST_WINDOW_ID = "test-window-id";
17
- const TEST_TIMESTAMP = 1_603_107_479_471;
18
- const MINUTES_IN_HOUR = 60;
19
- const SECONDS_IN_MINUTE = 60;
20
- const MS_IN_SECOND = 1000;
21
- const HOURS_IN_DAY = 24;
22
- const THIRTY_MINUTES = 30;
23
- const THIRTY_MINUTES_MS = THIRTY_MINUTES * SECONDS_IN_MINUTE * MS_IN_SECOND;
24
- const TWENTY_FOUR_HOURS_MS = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SECOND;
25
- const ONE_HOUR_MS = MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SECOND;
26
- const ONE_SECOND_MS = MS_IN_SECOND;
27
- const FIVE_MINUTES = 5;
28
- const FIVE_MINUTES_SECONDS = FIVE_MINUTES * SECONDS_IN_MINUTE;
29
- const THIRTY_SECONDS = 30;
30
- const ONE_HUNDRED_EXTRA_SECONDS = 100;
31
- const FIVE_THOUSAND_MULTIPLIER = 5;
32
- const FIVE_THOUSAND_MS = FIVE_THOUSAND_MULTIPLIER * MS_IN_SECOND;
33
- const ONE_HUNDRED_MS = 100;
34
- const TWO_HUNDRED_MS = 200;
35
- const THROTTLE_PERCENTAGE = 0.1;
36
- const IDLE_TIMEOUT_BUFFER = 1.1;
37
- const HEARTBEAT_INTERVAL_MS = 5000;
38
- const STALE_WINDOW_TIMEOUT_MS = 61_000; // Updated to match new 60s timeout + 1s
39
- const RECENT_HEARTBEAT_MS = 10_000;
40
- const NEWER_TIMESTAMP_OFFSET = 1000;
41
- const OLDER_TIMESTAMP_OFFSET = 500;
42
- const FUTURE_TIMESTAMP_OFFSET = 10_000;
43
- const PAST_TIMESTAMP_OFFSET = 1000;
44
- const ZERO_TIMESTAMP = 0;
45
- const createTestConfig = (overrides = {}) => ({
46
- persistence: "localStorage",
47
- ...overrides,
48
- });
49
- const createSessionManager = (config = createTestConfig(), persistenceOverrides = {}) => {
50
- const persistence = {
51
- register: vi.fn(),
52
- getProperty: vi
53
- .fn()
54
- .mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
55
- isDisabled: vi.fn().mockReturnValue(false),
56
- ...persistenceOverrides,
57
- };
58
- return new SessionIdManager(TEST_SURFACE, config, persistence, {
59
- sessionId: sessionIdGenerator,
60
- windowId: windowIdGenerator,
61
- clock: mockClock,
62
- });
63
- };
64
- beforeEach(() => {
65
- currentTime = TEST_TIMESTAMP;
66
- // Mock clock
67
- mockClock = {
68
- now: vi.fn(() => currentTime),
69
- setTimeout: vi.fn((callback, delay) => setTimeout(callback, delay)),
70
- clearTimeout: vi.fn((id) => clearTimeout(id)),
71
- };
72
- // Mock ID generators
73
- sessionIdGenerator = vi.fn(() => TEST_SESSION_ID);
74
- windowIdGenerator = vi.fn(() => TEST_WINDOW_ID);
75
- // Mock sessionStore
76
- const mockSessionStore = sessionStore;
77
- mockSessionStore._is_supported = vi.fn().mockReturnValue(true);
78
- mockSessionStore._parse = vi.fn().mockReturnValue(null);
79
- mockSessionStore._set = vi.fn();
80
- mockSessionStore._remove = vi.fn();
81
- // Clear all mocks
82
- vi.clearAllMocks();
83
- });
84
- afterEach(() => {
85
- vi.useRealTimers();
86
- });
87
- describe("initialization", () => {
88
- it("generates initial session and window IDs on first use", () => {
89
- const sessionManager = createSessionManager();
90
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
91
- expect(result).toMatchObject({
92
- sessionId: TEST_SESSION_ID,
93
- windowId: TEST_WINDOW_ID,
94
- sessionStartTimestamp: currentTime,
95
- lastActivityTimestamp: currentTime,
96
- changeReason: {
97
- [SessionChangeReason.NO_SESSION_ID]: true,
98
- [SessionChangeReason.ACTIVITY_TIMEOUT]: true,
99
- [SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: false,
100
- },
101
- });
102
- });
103
- it("generates initial session even in read-only mode", () => {
104
- const sessionManager = createSessionManager();
105
- const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
106
- expect(result.sessionId).toBe(TEST_SESSION_ID);
107
- expect(result.windowId).toBe(TEST_WINDOW_ID);
108
- });
109
- it("uses bootstrap session ID from config", () => {
110
- const bootstrapSessionId = "bootstrap-session-id";
111
- const config = createTestConfig({ sessionId: bootstrapSessionId });
112
- const sessionManager = createSessionManager(config);
113
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
114
- expect(result.sessionId).toBe(bootstrapSessionId);
115
- });
116
- it("handles invalid bootstrap session ID gracefully", () => {
117
- const config = createTestConfig({ sessionId: "invalid-id" });
118
- const mockPersistence = {
119
- register: vi.fn(() => {
120
- throw new Error("Invalid session ID");
121
- }),
122
- getProperty: vi
123
- .fn()
124
- .mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
125
- isDisabled: vi.fn().mockReturnValue(false),
126
- };
127
- // Should not throw and should generate new session ID
128
- expect(() => createSessionManager(config, mockPersistence)).not.toThrow();
129
- });
130
- it("sets up window ID persistence correctly", () => {
131
- const mockSessionStore = sessionStore;
132
- mockSessionStore._parse = vi
133
- .fn()
134
- .mockReturnValueOnce("existing-window-id") // window ID
135
- .mockReturnValueOnce(undefined); // primary window exists
136
- createSessionManager();
137
- expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists", true);
138
- });
139
- it("cycles window ID when primary window already exists", () => {
140
- const mockSessionStore = sessionStore;
141
- mockSessionStore._parse = vi
142
- .fn()
143
- .mockReturnValueOnce("existing-window-id") // window ID
144
- .mockReturnValueOnce(true); // primary window exists
145
- createSessionManager();
146
- expect(mockSessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_window_id");
147
- });
148
- it("handles disabled persistence correctly", () => {
149
- const mockPersistence = {
150
- register: vi.fn(),
151
- getProperty: vi
152
- .fn()
153
- .mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
154
- isDisabled: vi.fn().mockReturnValue(true),
155
- };
156
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
157
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
158
- expect(result.sessionId).toBe(TEST_SESSION_ID);
159
- expect(sessionStore._set).not.toHaveBeenCalled();
160
- });
161
- });
162
- describe("session timeout configuration", () => {
163
- const createSessionManagerWithTimeout = (timeout) => {
164
- const config = createTestConfig({
165
- sessionIdleTimeoutSeconds: timeout,
166
- });
167
- return createSessionManager(config);
168
- };
169
- it("uses static session timeout regardless of config", () => {
170
- const sessionManager = createSessionManagerWithTimeout(FIVE_MINUTES_SECONDS);
171
- // Session timeout is now static - always uses default
172
- expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
173
- });
174
- it("clamps timeout to default when below minimum", () => {
175
- const sessionManager = createSessionManagerWithTimeout(THIRTY_SECONDS);
176
- expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
177
- });
178
- it("clamps timeout to default when above maximum", () => {
179
- const sessionManager = createSessionManagerWithTimeout(MAX_SESSION_IDLE_TIMEOUT_SECONDS + ONE_HUNDRED_EXTRA_SECONDS);
180
- expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
181
- });
182
- it("uses default timeout when undefined", () => {
183
- const sessionManager = createSessionManagerWithTimeout(undefined);
184
- expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
185
- });
186
- });
187
- describe("session persistence and retrieval", () => {
188
- it("reuses existing session when not expired", () => {
189
- const existingTimestamp = currentTime - ONE_SECOND_MS;
190
- const startTimestamp = existingTimestamp - ONE_HOUR_MS;
191
- const mockPersistence = {
192
- register: vi.fn(),
193
- getProperty: vi
194
- .fn()
195
- .mockReturnValue([
196
- existingTimestamp,
197
- "existing-session-id",
198
- startTimestamp,
199
- ]),
200
- isDisabled: vi.fn().mockReturnValue(false),
201
- };
202
- const mockSessionStore = sessionStore;
203
- mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
204
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
205
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
206
- expect(result).toEqual({
207
- sessionId: "existing-session-id",
208
- windowId: "existing-window-id",
209
- sessionStartTimestamp: startTimestamp,
210
- lastActivityTimestamp: currentTime, // Updated to current time
211
- });
212
- });
213
- it("generates new session when activity timeout exceeded", () => {
214
- const oldTimestamp = currentTime - (THIRTY_MINUTES_MS + ONE_SECOND_MS);
215
- const startTimestamp = oldTimestamp - ONE_HOUR_MS;
216
- const mockPersistence = {
217
- register: vi.fn(),
218
- getProperty: vi
219
- .fn()
220
- .mockReturnValue([oldTimestamp, "old-session-id", startTimestamp]),
221
- isDisabled: vi.fn().mockReturnValue(false),
222
- };
223
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
224
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
225
- expect(result).toMatchObject({
226
- sessionId: TEST_SESSION_ID,
227
- windowId: TEST_WINDOW_ID,
228
- sessionStartTimestamp: currentTime,
229
- lastActivityTimestamp: currentTime,
230
- changeReason: {
231
- [SessionChangeReason.ACTIVITY_TIMEOUT]: true,
232
- [SessionChangeReason.NO_SESSION_ID]: false,
233
- [SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: false,
234
- },
235
- });
236
- });
237
- it("generates new session when maximum session length exceeded", () => {
238
- const oldTimestamp = currentTime - ONE_SECOND_MS;
239
- const startTimestamp = currentTime - (TWENTY_FOUR_HOURS_MS + ONE_SECOND_MS);
240
- const mockPersistence = {
241
- register: vi.fn(),
242
- getProperty: vi
243
- .fn()
244
- .mockReturnValue([oldTimestamp, "old-session-id", startTimestamp]),
245
- isDisabled: vi.fn().mockReturnValue(false),
246
- };
247
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
248
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
249
- expect(result).toMatchObject({
250
- sessionId: TEST_SESSION_ID,
251
- windowId: TEST_WINDOW_ID,
252
- sessionStartTimestamp: currentTime,
253
- changeReason: {
254
- [SessionChangeReason.ACTIVITY_TIMEOUT]: false,
255
- [SessionChangeReason.NO_SESSION_ID]: false,
256
- [SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: true,
257
- },
258
- });
259
- });
260
- it("generates new session when maximum length exceeded even in read-only mode", () => {
261
- const oldTimestamp = currentTime - ONE_SECOND_MS;
262
- const startTimestamp = currentTime - (TWENTY_FOUR_HOURS_MS + ONE_SECOND_MS);
263
- const mockPersistence = {
264
- register: vi.fn(),
265
- getProperty: vi
266
- .fn()
267
- .mockReturnValue([oldTimestamp, "old-session-id", startTimestamp]),
268
- isDisabled: vi.fn().mockReturnValue(false),
269
- };
270
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
271
- const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
272
- expect(result.sessionId).toBe(TEST_SESSION_ID);
273
- expect(result.changeReason?.[SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]).toBe(true);
274
- });
275
- it("does not update activity timestamp in read-only mode for active sessions", () => {
276
- const oldTimestamp = currentTime - ONE_SECOND_MS;
277
- const startTimestamp = oldTimestamp - ONE_HOUR_MS;
278
- const mockPersistence = {
279
- register: vi.fn(),
280
- getProperty: vi
281
- .fn()
282
- .mockReturnValue([
283
- oldTimestamp,
284
- "existing-session-id",
285
- startTimestamp,
286
- ]),
287
- isDisabled: vi.fn().mockReturnValue(false),
288
- };
289
- const mockSessionStore = sessionStore;
290
- mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
291
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
292
- const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
293
- expect(result.lastActivityTimestamp).toBe(oldTimestamp); // Not updated
294
- expect(mockPersistence.register).toHaveBeenCalledWith({
295
- [SESSION_ID]: [oldTimestamp, "existing-session-id", startTimestamp],
296
- });
297
- });
298
- it("populates session start timestamp when missing", () => {
299
- const mockPersistence = {
300
- register: vi.fn(),
301
- getProperty: vi.fn().mockReturnValue([
302
- currentTime,
303
- "existing-session-id",
304
- // Missing start timestamp (legacy format)
305
- ]),
306
- isDisabled: vi.fn().mockReturnValue(false),
307
- };
308
- const mockSessionStore = sessionStore;
309
- mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
310
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
311
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
312
- expect(result.sessionStartTimestamp).toBe(currentTime);
313
- expect(mockPersistence.register).toHaveBeenCalledWith({
314
- [SESSION_ID]: [currentTime, "existing-session-id", currentTime],
315
- });
316
- });
317
- it("uses current time when no timestamp provided", () => {
318
- const sessionManager = createSessionManager();
319
- // Mock Date.now to return a specific time
320
- const mockNow = TEST_TIMESTAMP + FIVE_THOUSAND_MS;
321
- mockClock.now = vi.fn(() => mockNow);
322
- const result = sessionManager.checkAndGetSessionAndWindowId(false);
323
- expect(result.sessionStartTimestamp).toBe(mockNow);
324
- expect(result.lastActivityTimestamp).toBe(mockNow);
325
- });
326
- });
327
- describe("window ID management", () => {
328
- it("generates new window ID when none exists", () => {
329
- const mockPersistence = {
330
- register: vi.fn(),
331
- getProperty: vi
332
- .fn()
333
- .mockReturnValue([currentTime, "existing-session-id", currentTime]),
334
- isDisabled: vi.fn().mockReturnValue(false),
335
- };
336
- const mockSessionStore = sessionStore;
337
- mockSessionStore._parse = vi.fn().mockReturnValue(null); // No existing window ID
338
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
339
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
340
- expect(result.windowId).toBe(TEST_WINDOW_ID);
341
- expect(result.sessionId).toBe("existing-session-id"); // Session ID unchanged
342
- expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_window_id", TEST_WINDOW_ID);
343
- });
344
- it("handles session storage not supported", () => {
345
- const mockSessionStore = sessionStore;
346
- mockSessionStore._is_supported = vi.fn().mockReturnValue(false);
347
- const sessionManager = createSessionManager();
348
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
349
- expect(result.windowId).toBe(TEST_WINDOW_ID);
350
- expect(mockSessionStore._set).not.toHaveBeenCalled();
351
- });
352
- it("stores and retrieves window ID correctly", () => {
353
- const mockSessionStore = sessionStore;
354
- mockSessionStore._parse = vi.fn().mockReturnValue("stored-window-id");
355
- const sessionManager = createSessionManager();
356
- // Access private method for testing
357
- const windowId = sessionManager._getWindowId();
358
- expect(windowId).toBe("stored-window-id");
359
- });
360
- });
361
- describe("session callbacks", () => {
362
- it("calls registered callbacks when session changes", () => {
363
- const callback = vi.fn();
364
- const sessionManager = createSessionManager();
365
- sessionManager.onSessionId(callback);
366
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
367
- expect(callback).toHaveBeenCalledWith(TEST_SESSION_ID, TEST_WINDOW_ID, expect.objectContaining({
368
- [SessionChangeReason.NO_SESSION_ID]: true,
369
- }));
370
- });
371
- it("calls callback immediately if session already exists", () => {
372
- const mockPersistence = {
373
- register: vi.fn(),
374
- getProperty: vi
375
- .fn()
376
- .mockReturnValue([currentTime, "existing-session-id", currentTime]),
377
- isDisabled: vi.fn().mockReturnValue(false),
378
- };
379
- const mockSessionStore = sessionStore;
380
- mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
381
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
382
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime); // Initialize session
383
- const callback = vi.fn();
384
- sessionManager.onSessionId(callback);
385
- expect(callback).toHaveBeenCalledWith("existing-session-id", "existing-window-id", undefined // No change reason for existing session
386
- );
387
- });
388
- it("allows unsubscribing from callbacks", () => {
389
- const callback = vi.fn();
390
- const sessionManager = createSessionManager();
391
- const unsubscribe = sessionManager.onSessionId(callback);
392
- // Callback is called immediately with initial state (null, null) and change reason
393
- expect(callback).toHaveBeenCalledWith(null, null, {
394
- [SessionChangeReason.NO_SESSION_ID]: true,
395
- });
396
- callback.mockClear();
397
- unsubscribe();
398
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
399
- // Should not be called again after unsubscribing
400
- expect(callback).not.toHaveBeenCalled();
401
- });
402
- it("does not call callbacks when values do not change", () => {
403
- const mockPersistence = {
404
- register: vi.fn(),
405
- getProperty: vi
406
- .fn()
407
- .mockReturnValue([
408
- currentTime - ONE_SECOND_MS,
409
- "existing-session-id",
410
- currentTime - ONE_HOUR_MS,
411
- ]),
412
- isDisabled: vi.fn().mockReturnValue(false),
413
- };
414
- const mockSessionStore = sessionStore;
415
- mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
416
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
417
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime); // Initialize
418
- const callback = vi.fn();
419
- sessionManager.onSessionId(callback);
420
- callback.mockClear(); // Clear the immediate call
421
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime + ONE_SECOND_MS); // Second call
422
- expect(callback).not.toHaveBeenCalled();
423
- });
424
- });
425
- describe("idle timeout enforcement", () => {
426
- beforeEach(() => {
427
- vi.useFakeTimers();
428
- });
429
- it("starts idle timeout timer on initialization", () => {
430
- const sessionManager = createSessionManager();
431
- expect(sessionManager
432
- ._enforceIdleTimeout).toBeDefined();
433
- expect(mockClock.setTimeout).toHaveBeenCalled();
434
- });
435
- it("resets idle timer when checking session (non-read-only)", () => {
436
- const sessionManager = createSessionManager();
437
- // Advance time to ensure throttling doesn't prevent timer reset
438
- currentTime +=
439
- sessionManager.sessionTimeoutMs * THROTTLE_PERCENTAGE + ONE_SECOND_MS;
440
- mockClock.now = vi.fn(() => currentTime);
441
- const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
442
- const setTimeoutSpy = vi.spyOn(mockClock, "setTimeout");
443
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
444
- expect(clearTimeoutSpy).toHaveBeenCalled();
445
- expect(setTimeoutSpy).toHaveBeenCalled();
446
- });
447
- it("does not reset idle timer in read-only mode", () => {
448
- const sessionManager = createSessionManager();
449
- const originalTimer = sessionManager._enforceIdleTimeout;
450
- sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
451
- expect(sessionManager
452
- ._enforceIdleTimeout).toEqual(originalTimer);
453
- });
454
- it("resets session when idle timeout is exceeded", () => {
455
- const mockPersistence = {
456
- register: vi.fn(),
457
- getProperty: vi.fn(),
458
- isDisabled: vi.fn().mockReturnValue(false),
459
- };
460
- // Set up initial active session
461
- mockPersistence.getProperty.mockReturnValue([
462
- currentTime,
463
- "active-session-id",
464
- currentTime,
465
- ]);
466
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
467
- const resetSpy = vi.spyOn(sessionManager, "resetSessionId");
468
- const callback = vi.fn();
469
- sessionManager.onSessionId(callback);
470
- callback.mockClear(); // Clear initial call
471
- // Simulate session becoming idle
472
- const idleTimestamp = currentTime - (sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
473
- mockPersistence.getProperty.mockReturnValue([
474
- idleTimestamp,
475
- "active-session-id",
476
- currentTime,
477
- ]);
478
- // Fast-forward time to trigger idle timeout
479
- vi.advanceTimersByTime(sessionManager.sessionTimeoutMs * IDLE_TIMEOUT_BUFFER + ONE_SECOND_MS);
480
- // Verify reset was called with correct reason and callbacks were fired
481
- expect(resetSpy).toHaveBeenCalledWith({
482
- [SessionChangeReason.ACTIVITY_TIMEOUT]: true,
483
- });
484
- expect(callback).toHaveBeenCalledWith(null, null, {
485
- [SessionChangeReason.ACTIVITY_TIMEOUT]: true,
486
- });
487
- });
488
- it("does not reset session if activity is recent when timer fires", () => {
489
- const mockPersistence = {
490
- register: vi.fn(),
491
- getProperty: vi.fn(),
492
- isDisabled: vi.fn().mockReturnValue(false),
493
- };
494
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
495
- const resetSpy = vi.spyOn(sessionManager, "resetSessionId");
496
- // Initially set up idle session
497
- const idleTimestamp = currentTime - (sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
498
- mockPersistence.getProperty.mockReturnValue([
499
- idleTimestamp,
500
- "session-id",
501
- currentTime,
502
- ]);
503
- // Fast-forward almost to timeout (buffer now divides, so timer fires earlier)
504
- const timeAdvance1 = sessionManager.sessionTimeoutMs / IDLE_TIMEOUT_BUFFER - ONE_HUNDRED_MS;
505
- vi.advanceTimersByTime(timeAdvance1);
506
- currentTime += timeAdvance1;
507
- mockClock.now = vi.fn(() => currentTime);
508
- // Before timer fires, simulate recent activity
509
- const recentTimestamp = currentTime - ONE_SECOND_MS; // Recent activity
510
- mockPersistence.getProperty.mockReturnValue([
511
- recentTimestamp,
512
- "session-id",
513
- currentTime,
514
- ]);
515
- // Let timer fire
516
- const timeAdvance2 = TWO_HUNDRED_MS;
517
- vi.advanceTimersByTime(timeAdvance2);
518
- currentTime += timeAdvance2;
519
- mockClock.now = vi.fn(() => currentTime);
520
- expect(resetSpy).not.toHaveBeenCalled();
521
- });
522
- it("always resets idle timer for non-read-only calls", () => {
523
- const sessionManager = createSessionManager();
524
- const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
525
- const setTimeoutSpy = vi.spyOn(mockClock, "setTimeout");
526
- clearTimeoutSpy.mockClear();
527
- setTimeoutSpy.mockClear();
528
- // First call should reset timer
529
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
530
- expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
531
- expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
532
- clearTimeoutSpy.mockClear();
533
- setTimeoutSpy.mockClear();
534
- // Second call should also reset timer (no more throttling)
535
- currentTime += ONE_HUNDRED_MS;
536
- mockClock.now = vi.fn(() => currentTime);
537
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
538
- expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
539
- expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
540
- });
541
- });
542
- describe("session reset", () => {
543
- it("clears existing session data", () => {
544
- const mockPersistence = {
545
- register: vi.fn(),
546
- getProperty: vi
547
- .fn()
548
- .mockReturnValue([currentTime, "existing-session-id", currentTime]),
549
- isDisabled: vi.fn().mockReturnValue(false),
550
- };
551
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
552
- sessionManager.resetSessionId();
553
- expect(mockPersistence.register).toHaveBeenCalledWith({
554
- [SESSION_ID]: [null, null, null],
555
- });
556
- });
557
- it("generates new session after reset", () => {
558
- const mockPersistence = {
559
- register: vi.fn(),
560
- getProperty: vi.fn(),
561
- isDisabled: vi.fn().mockReturnValue(false),
562
- };
563
- // Start with existing session
564
- mockPersistence.getProperty.mockReturnValue([
565
- currentTime,
566
- "existing-session-id",
567
- currentTime,
568
- ]);
569
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
570
- // Reset the session
571
- sessionManager.resetSessionId();
572
- // After reset, getProperty should return null values
573
- mockPersistence.getProperty.mockReturnValue([null, null, null]);
574
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
575
- expect(result.sessionId).toBe(TEST_SESSION_ID);
576
- expect(result.changeReason?.[SessionChangeReason.NO_SESSION_ID]).toBe(true);
577
- });
578
- });
579
- describe("cleanup and disposal", () => {
580
- it("clears timeout on disposal", () => {
581
- const sessionManager = createSessionManager();
582
- const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
583
- sessionManager.dispose();
584
- expect(clearTimeoutSpy).toHaveBeenCalled();
585
- expect(sessionManager
586
- ._enforceIdleTimeout).toBeUndefined();
587
- });
588
- it("removes event listeners on disposal", () => {
589
- // Mock window and addEventListener
590
- const mockRemoveEventListener = vi.fn();
591
- const originalWindow = globalThis.window;
592
- globalThis.window = {
593
- addEventListener: vi.fn(),
594
- removeEventListener: mockRemoveEventListener,
595
- };
596
- const sessionManager = createSessionManager();
597
- sessionManager.dispose();
598
- expect(mockRemoveEventListener).toHaveBeenCalledWith("beforeunload", expect.any(Function));
599
- // Restore original window
600
- globalThis.window = originalWindow;
601
- });
602
- it("clears session change handlers on disposal", () => {
603
- const sessionManager = createSessionManager();
604
- const callback = vi.fn();
605
- sessionManager.onSessionId(callback);
606
- sessionManager.dispose();
607
- expect(sessionManager
608
- ._sessionIdChangedHandlers).toEqual([]);
609
- });
610
- it("handles disposal when no window is available", () => {
611
- const originalWindow = globalThis.window;
612
- // Set window to undefined to simulate no window environment
613
- globalThis.window = undefined;
614
- const sessionManager = createSessionManager();
615
- expect(() => sessionManager.dispose()).not.toThrow();
616
- // Restore window
617
- globalThis.window = originalWindow;
618
- });
619
- });
620
- describe("beforeunload handling", () => {
621
- it("removes window ID and conditionally removes primary window flag on beforeunload", () => {
622
- const mockRemoveEventListener = vi.fn();
623
- const mockAddEventListener = vi.fn();
624
- const originalWindow = globalThis.window;
625
- globalThis.window = {
626
- addEventListener: mockAddEventListener,
627
- removeEventListener: mockRemoveEventListener,
628
- };
629
- // Mock sessionStore to simulate this window being the primary window
630
- const mockSessionStore = sessionStore;
631
- mockSessionStore._parse.mockImplementation((key) => {
632
- if (key.includes("window_id")) {
633
- return TEST_WINDOW_ID; // Return the same window ID that the manager will have
634
- }
635
- return null;
636
- });
637
- const sessionManager = createSessionManager();
638
- // Initialize session to set up window ID
639
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
640
- // Get the registered beforeunload handler
641
- const beforeUnloadHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "beforeunload")?.[1];
642
- expect(beforeUnloadHandler).toBeDefined();
643
- // Call the handler
644
- beforeUnloadHandler?.();
645
- // Should remove window ID, heartbeat, and primary window flag (since this is the primary window)
646
- expect(sessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_window_id");
647
- expect(sessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat");
648
- expect(sessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
649
- const EXPECTED_REMOVE_CALLS = 3; // window ID + heartbeat + primary window flag
650
- expect(sessionStore._remove).toHaveBeenCalledTimes(EXPECTED_REMOVE_CALLS);
651
- // Restore window
652
- globalThis.window = originalWindow;
653
- });
654
- it("does not set up beforeunload handler when window is undefined", () => {
655
- const originalWindow = globalThis.window;
656
- globalThis.window = undefined;
657
- expect(() => createSessionManager()).not.toThrow();
658
- // Restore window
659
- globalThis.window = originalWindow;
660
- });
661
- });
662
- describe("heartbeat logic", () => {
663
- let originalWindow;
664
- let originalDocument;
665
- beforeEach(() => {
666
- vi.useFakeTimers();
667
- // Store originals
668
- originalWindow = globalThis.window;
669
- originalDocument = globalThis.document;
670
- // Mock window and document
671
- globalThis.window = {
672
- addEventListener: vi.fn(),
673
- removeEventListener: vi.fn(),
674
- };
675
- globalThis.document = {
676
- visibilityState: "visible",
677
- addEventListener: vi.fn(),
678
- removeEventListener: vi.fn(),
679
- };
680
- });
681
- afterEach(() => {
682
- vi.useRealTimers();
683
- // Restore originals
684
- globalThis.window = originalWindow;
685
- globalThis.document = originalDocument;
686
- });
687
- it("writes initial heartbeat on initialization", () => {
688
- const mockSessionStore = sessionStore;
689
- mockSessionStore._is_supported.mockReturnValue(true);
690
- mockSessionStore._parse.mockReturnValue(null); // No existing data
691
- createSessionManager();
692
- expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat", currentTime);
693
- });
694
- it("starts periodic heartbeat interval", () => {
695
- const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
696
- createSessionManager();
697
- expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), HEARTBEAT_INTERVAL_MS);
698
- });
699
- it("skips heartbeat when document visibility is not visible", () => {
700
- const mockSessionStore = sessionStore;
701
- // Set document to hidden
702
- globalThis.document = {
703
- visibilityState: "hidden",
704
- addEventListener: vi.fn(),
705
- removeEventListener: vi.fn(),
706
- };
707
- createSessionManager();
708
- // Clear initial heartbeat call
709
- mockSessionStore._set.mockClear();
710
- // Advance time to trigger heartbeat
711
- vi.advanceTimersByTime(HEARTBEAT_INTERVAL_MS);
712
- // Should not have written heartbeat due to hidden visibility
713
- expect(mockSessionStore._set).not.toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat", expect.any(Number));
714
- });
715
- it("writes heartbeat when document becomes visible", () => {
716
- const mockSessionStore = sessionStore;
717
- // Start hidden
718
- globalThis.document = {
719
- visibilityState: "hidden",
720
- addEventListener: vi.fn(),
721
- removeEventListener: vi.fn(),
722
- };
723
- createSessionManager();
724
- mockSessionStore._set.mockClear();
725
- // Make visible and advance time
726
- globalThis.document = {
727
- visibilityState: "visible",
728
- addEventListener: vi.fn(),
729
- removeEventListener: vi.fn(),
730
- };
731
- currentTime += HEARTBEAT_INTERVAL_MS;
732
- mockClock.now = vi.fn(() => currentTime);
733
- vi.advanceTimersByTime(HEARTBEAT_INTERVAL_MS);
734
- expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat", currentTime);
735
- });
736
- it("recovers correctly when storage throws during heartbeat", () => {
737
- const mockSessionStore = sessionStore;
738
- createSessionManager();
739
- // Make storage throw on heartbeat after initial setup
740
- let heartbeatCalls = 0;
741
- mockSessionStore._set.mockImplementation((key, _value) => {
742
- if (key.includes("heartbeat")) {
743
- heartbeatCalls++;
744
- if (heartbeatCalls > 1) {
745
- // Allow first heartbeat, fail subsequent ones
746
- throw new Error("Storage quota exceeded");
747
- }
748
- }
749
- });
750
- // Advance time to trigger heartbeat - should not throw
751
- expect(() => {
752
- vi.advanceTimersByTime(HEARTBEAT_INTERVAL_MS);
753
- }).not.toThrow();
754
- // Verify that heartbeat errors were handled gracefully (no exception thrown)
755
- // The storage cache invalidation happens when actual storage errors occur during heartbeat writes
756
- });
757
- it("does not start heartbeat when window is undefined", () => {
758
- const savedWindow = globalThis.window;
759
- globalThis.window = undefined;
760
- const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
761
- createSessionManager();
762
- expect(setIntervalSpy).not.toHaveBeenCalled();
763
- // Restore window
764
- globalThis.window = savedWindow;
765
- });
766
- });
767
- describe("stale window cleanup", () => {
768
- beforeEach(() => {
769
- vi.useFakeTimers();
770
- });
771
- afterEach(() => {
772
- vi.useRealTimers();
773
- });
774
- it("removes flags when heartbeat is older than 60 seconds", () => {
775
- const mockSessionStore = sessionStore;
776
- mockSessionStore._is_supported.mockReturnValue(true);
777
- // Mock stale heartbeat (older than 60 seconds)
778
- const staleHeartbeat = currentTime - STALE_WINDOW_TIMEOUT_MS;
779
- mockSessionStore._parse.mockImplementation((key) => {
780
- if (key.includes("heartbeat")) {
781
- return staleHeartbeat;
782
- }
783
- return null;
784
- });
785
- createSessionManager();
786
- // Should have removed the primary window flag due to stale heartbeat
787
- expect(mockSessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
788
- });
789
- it("removes phantom primary flag when no heartbeat exists", () => {
790
- const mockSessionStore = sessionStore;
791
- mockSessionStore._is_supported.mockReturnValue(true);
792
- // Mock no heartbeat but primary window exists
793
- mockSessionStore._parse.mockImplementation((key) => {
794
- if (key.includes("heartbeat")) {
795
- return null; // No heartbeat
796
- }
797
- if (key.includes("primary_window_exists")) {
798
- return true; // But primary flag exists (phantom)
799
- }
800
- return null;
801
- });
802
- createSessionManager();
803
- // Should have removed the phantom primary window flag
804
- expect(mockSessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
805
- });
806
- it("does not remove flags when heartbeat is recent", () => {
807
- const mockSessionStore = sessionStore;
808
- mockSessionStore._is_supported.mockReturnValue(true);
809
- // Mock recent heartbeat (within 60 seconds)
810
- const recentHeartbeat = currentTime - RECENT_HEARTBEAT_MS;
811
- mockSessionStore._parse.mockImplementation((key) => {
812
- if (key.includes("heartbeat")) {
813
- return recentHeartbeat;
814
- }
815
- return null;
816
- });
817
- createSessionManager();
818
- // Should not have removed any flags due to recent heartbeat
819
- expect(mockSessionStore._remove).not.toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
820
- });
821
- it("handles cleanup gracefully when storage throws", () => {
822
- const mockSessionStore = sessionStore;
823
- mockSessionStore._is_supported.mockReturnValue(true);
824
- mockSessionStore._parse.mockImplementation((key) => {
825
- if (key.includes("heartbeat")) {
826
- throw new Error("Storage error");
827
- }
828
- return null;
829
- });
830
- // Should not throw even when storage fails during cleanup
831
- expect(() => createSessionManager()).not.toThrow();
832
- });
833
- });
834
- describe("optimistic locking in persistence", () => {
835
- it("syncs local state when another tabs activity timestamp is newer", () => {
836
- const newerTimestamp = currentTime + NEWER_TIMESTAMP_OFFSET;
837
- const olderLocalTimestamp = currentTime;
838
- const mockPersistence = {
839
- register: vi.fn(),
840
- getProperty: vi
841
- .fn()
842
- .mockReturnValueOnce([
843
- olderLocalTimestamp,
844
- "local-session-id",
845
- currentTime,
846
- ])
847
- .mockReturnValue([
848
- newerTimestamp,
849
- "remote-session-id",
850
- currentTime - PAST_TIMESTAMP_OFFSET,
851
- ]),
852
- isDisabled: vi.fn().mockReturnValue(false),
853
- };
854
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
855
- // Initialize with local session
856
- sessionManager.checkAndGetSessionAndWindowId(false, olderLocalTimestamp);
857
- // Try to update with older timestamp - should sync from persistence instead
858
- sessionManager.checkAndGetSessionAndWindowId(false, olderLocalTimestamp + OLDER_TIMESTAMP_OFFSET);
859
- // Should have synced to the newer remote session
860
- const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
861
- expect(result.sessionId).toBe("remote-session-id");
862
- expect(result.lastActivityTimestamp).toBe(newerTimestamp);
863
- });
864
- it("adds jitter in production but not in test environment", () => {
865
- // Test environment should not add jitter
866
- const mockPersistence = {
867
- register: vi.fn(),
868
- getProperty: vi
869
- .fn()
870
- .mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
871
- isDisabled: vi.fn().mockReturnValue(false),
872
- };
873
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
874
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
875
- // In test environment, timestamp should be exact (no jitter)
876
- expect(mockPersistence.register).toHaveBeenCalledWith({
877
- [SESSION_ID]: [currentTime, TEST_SESSION_ID, currentTime],
878
- });
879
- });
880
- it("handles equal timestamps correctly", () => {
881
- const sameTimestamp = currentTime;
882
- const mockPersistence = {
883
- register: vi.fn(),
884
- getProperty: vi
885
- .fn()
886
- .mockReturnValue([sameTimestamp, "existing-session-id", currentTime]),
887
- isDisabled: vi.fn().mockReturnValue(false),
888
- };
889
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
890
- // With same timestamp, should proceed with local update
891
- sessionManager.checkAndGetSessionAndWindowId(false, sameTimestamp);
892
- expect(mockPersistence.register).toHaveBeenCalledWith({
893
- [SESSION_ID]: [sameTimestamp, "existing-session-id", currentTime],
894
- });
895
- });
896
- it("gracefully handles persistence failures during optimistic locking", () => {
897
- const mockPersistence = {
898
- register: vi.fn().mockImplementation(() => {
899
- throw new Error("Storage quota exceeded");
900
- }),
901
- getProperty: vi
902
- .fn()
903
- .mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
904
- isDisabled: vi.fn().mockReturnValue(false),
905
- };
906
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
907
- // Should not throw even when persistence fails
908
- expect(() => {
909
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
910
- }).not.toThrow();
911
- });
912
- });
913
- describe("activity timestamp edge cases", () => {
914
- it("handles clock skew backwards gracefully", () => {
915
- const futureTimestamp = currentTime + FUTURE_TIMESTAMP_OFFSET;
916
- const pastTimestamp = currentTime - PAST_TIMESTAMP_OFFSET;
917
- const mockPersistence = {
918
- register: vi.fn(),
919
- getProperty: vi
920
- .fn()
921
- .mockReturnValue([futureTimestamp, "session-id", currentTime]),
922
- isDisabled: vi.fn().mockReturnValue(false),
923
- };
924
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
925
- // Access with past timestamp (clock moved backwards)
926
- const result = sessionManager.checkAndGetSessionAndWindowId(false, pastTimestamp);
927
- // Session should still be considered active despite backwards clock
928
- expect(result.sessionId).toBe("session-id");
929
- expect(result.lastActivityTimestamp).toBe(pastTimestamp); // Updated to current
930
- });
931
- it("updates activity timestamp when regenerating due to timeout in non-readonly mode", () => {
932
- const oldTimestamp = currentTime - (THIRTY_MINUTES_MS + ONE_SECOND_MS);
933
- const mockPersistence = {
934
- register: vi.fn(),
935
- getProperty: vi
936
- .fn()
937
- .mockReturnValue([
938
- oldTimestamp,
939
- "old-session-id",
940
- currentTime - ONE_HOUR_MS,
941
- ]),
942
- isDisabled: vi.fn().mockReturnValue(false),
943
- };
944
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
945
- const result = sessionManager.checkAndGetSessionAndWindowId(false, // readOnly = false
946
- currentTime);
947
- // New session should have current time as last activity
948
- expect(result.sessionId).toBe(TEST_SESSION_ID);
949
- expect(result.lastActivityTimestamp).toBe(currentTime);
950
- expect(result.changeReason?.[SessionChangeReason.ACTIVITY_TIMEOUT]).toBe(true);
951
- });
952
- it("prevents infinite regeneration loop when accessing expired session in readonly mode", () => {
953
- const oldTimestamp = currentTime - (THIRTY_MINUTES_MS + ONE_SECOND_MS);
954
- const mockPersistence = {
955
- register: vi.fn(),
956
- getProperty: vi
957
- .fn()
958
- .mockReturnValue([
959
- oldTimestamp,
960
- "old-session-id",
961
- currentTime - ONE_HOUR_MS,
962
- ]),
963
- isDisabled: vi.fn().mockReturnValue(false),
964
- };
965
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
966
- // First call in readonly mode - should regenerate session
967
- const result1 = sessionManager.checkAndGetSessionAndWindowId(true, // readOnly = true (like debug calls)
968
- currentTime);
969
- expect(result1.sessionId).toBe(TEST_SESSION_ID);
970
- expect(result1.changeReason?.[SessionChangeReason.ACTIVITY_TIMEOUT]).toBe(true);
971
- // CRITICAL: The new session should have fresh activity timestamp to prevent infinite loop
972
- expect(result1.lastActivityTimestamp).toBe(currentTime); // This should be currentTime, not oldTimestamp
973
- // Update mock to return the new session data
974
- mockPersistence.getProperty.mockReturnValue([
975
- result1.lastActivityTimestamp,
976
- result1.sessionId,
977
- result1.sessionStartTimestamp,
978
- ]);
979
- // Second call in readonly mode - should NOT regenerate again
980
- const result2 = sessionManager.checkAndGetSessionAndWindowId(true, // readOnly = true
981
- currentTime + ONE_SECOND_MS // 1 second later
982
- );
983
- // Should be the same session (no regeneration)
984
- expect(result2.sessionId).toBe(result1.sessionId);
985
- expect(result2.changeReason).toBeUndefined(); // No change reason
986
- });
987
- it("handles timestamp validation errors gracefully", () => {
988
- const mockPersistence = {
989
- register: vi.fn(),
990
- getProperty: vi.fn().mockReturnValue([
991
- "invalid-timestamp", // Not a number
992
- "session-id",
993
- "invalid-start-timestamp",
994
- ]),
995
- isDisabled: vi.fn().mockReturnValue(false),
996
- };
997
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
998
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
999
- // Should generate new session due to invalid data
1000
- expect(result.sessionId).toBe(TEST_SESSION_ID);
1001
- // Invalid timestamp data triggers activity timeout (not no session)
1002
- expect(result.changeReason).toMatchObject({
1003
- [SessionChangeReason.ACTIVITY_TIMEOUT]: true,
1004
- });
1005
- });
1006
- });
1007
- describe("reset session with explicit reason", () => {
1008
- it("propagates explicit reset reason to handlers", () => {
1009
- const callback = vi.fn();
1010
- const sessionManager = createSessionManager();
1011
- sessionManager.onSessionId(callback);
1012
- callback.mockClear(); // Clear initial call
1013
- const explicitReason = {
1014
- [SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: true,
1015
- };
1016
- sessionManager.resetSessionId(explicitReason);
1017
- expect(callback).toHaveBeenCalledWith(null, null, explicitReason);
1018
- });
1019
- it("uses default reason when no explicit reason provided", () => {
1020
- const callback = vi.fn();
1021
- const sessionManager = createSessionManager();
1022
- sessionManager.onSessionId(callback);
1023
- callback.mockClear(); // Clear initial call
1024
- sessionManager.resetSessionId();
1025
- expect(callback).toHaveBeenCalledWith(null, null, {
1026
- [SessionChangeReason.NO_SESSION_ID]: true,
1027
- });
1028
- });
1029
- it("calls resetSessionId with correct reason during idle timeout", () => {
1030
- vi.useFakeTimers();
1031
- const mockPersistence = {
1032
- register: vi.fn(),
1033
- getProperty: vi.fn(),
1034
- isDisabled: vi.fn().mockReturnValue(false),
1035
- };
1036
- // Set up initial active session
1037
- mockPersistence.getProperty.mockReturnValue([
1038
- currentTime,
1039
- "active-session-id",
1040
- currentTime,
1041
- ]);
1042
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
1043
- const callback = vi.fn();
1044
- sessionManager.onSessionId(callback);
1045
- callback.mockClear(); // Clear initial call
1046
- // Simulate session becoming idle
1047
- const idleTimestamp = currentTime - (sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
1048
- mockPersistence.getProperty.mockReturnValue([
1049
- idleTimestamp,
1050
- "active-session-id",
1051
- currentTime,
1052
- ]);
1053
- // Fast-forward time to trigger idle timeout
1054
- vi.advanceTimersByTime(sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
1055
- expect(callback).toHaveBeenCalledWith(null, null, {
1056
- [SessionChangeReason.ACTIVITY_TIMEOUT]: true,
1057
- });
1058
- vi.useRealTimers();
1059
- });
1060
- });
1061
- describe("dispose with active heartbeat", () => {
1062
- it("clears heartbeat interval on disposal", () => {
1063
- // Ensure window exists for heartbeat to start
1064
- const originalWindow = globalThis.window;
1065
- globalThis.window = {
1066
- addEventListener: vi.fn(),
1067
- removeEventListener: vi.fn(),
1068
- };
1069
- const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
1070
- const sessionManager = createSessionManager();
1071
- sessionManager.dispose();
1072
- expect(clearIntervalSpy).toHaveBeenCalled();
1073
- // Verify heartbeat interval is cleared
1074
- expect(sessionManager._heartbeatInterval).toBeNull();
1075
- // Restore window
1076
- globalThis.window = originalWindow;
1077
- });
1078
- it("handles disposal when no heartbeat interval exists", () => {
1079
- const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
1080
- // Create manager without heartbeat (window undefined)
1081
- const originalWindow = globalThis.window;
1082
- globalThis.window = undefined;
1083
- const sessionManager = createSessionManager();
1084
- sessionManager.dispose();
1085
- // Should not call clearInterval if no interval was created
1086
- expect(clearIntervalSpy).not.toHaveBeenCalled();
1087
- // Restore window
1088
- globalThis.window = originalWindow;
1089
- });
1090
- it("clears both timeout and interval on disposal", () => {
1091
- // Ensure window exists for heartbeat to start
1092
- const originalWindow = globalThis.window;
1093
- globalThis.window = {
1094
- addEventListener: vi.fn(),
1095
- removeEventListener: vi.fn(),
1096
- };
1097
- const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
1098
- const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
1099
- const sessionManager = createSessionManager();
1100
- sessionManager.dispose();
1101
- expect(clearTimeoutSpy).toHaveBeenCalled();
1102
- expect(clearIntervalSpy).toHaveBeenCalled();
1103
- // Verify both are cleared
1104
- expect(sessionManager
1105
- ._enforceIdleTimeout).toBeUndefined();
1106
- expect(sessionManager._heartbeatInterval).toBeNull();
1107
- // Restore window
1108
- globalThis.window = originalWindow;
1109
- });
1110
- });
1111
- describe("edge cases and error handling", () => {
1112
- it("handles malformed session data gracefully", () => {
1113
- const mockPersistence = {
1114
- register: vi.fn(),
1115
- getProperty: vi.fn().mockReturnValue("invalid-data"),
1116
- isDisabled: vi.fn().mockReturnValue(false),
1117
- };
1118
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
1119
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
1120
- expect(result.sessionId).toBe(TEST_SESSION_ID);
1121
- expect(result.changeReason?.[SessionChangeReason.NO_SESSION_ID]).toBe(true);
1122
- });
1123
- it("handles empty session array gracefully", () => {
1124
- const mockPersistence = {
1125
- register: vi.fn(),
1126
- getProperty: vi.fn().mockReturnValue([]),
1127
- isDisabled: vi.fn().mockReturnValue(false),
1128
- };
1129
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
1130
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
1131
- expect(result.sessionId).toBe(TEST_SESSION_ID);
1132
- });
1133
- it("handles session data with wrong types", () => {
1134
- const NUMBER_NOT_STRING = 123;
1135
- const mockPersistence = {
1136
- register: vi.fn(),
1137
- getProperty: vi.fn().mockReturnValue([
1138
- "not-a-number",
1139
- NUMBER_NOT_STRING, // Not a string
1140
- "also-not-a-number",
1141
- ]),
1142
- isDisabled: vi.fn().mockReturnValue(false),
1143
- };
1144
- const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
1145
- const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
1146
- expect(result.sessionId).toBe(TEST_SESSION_ID);
1147
- });
1148
- it("handles storage errors gracefully", () => {
1149
- // Create session manager first, then mock the storage to throw
1150
- const sessionManager = createSessionManager();
1151
- const mockSessionStore = sessionStore;
1152
- mockSessionStore._set = vi.fn((_key, _value) => {
1153
- throw new Error("Storage error");
1154
- });
1155
- // This should not throw even though storage fails
1156
- expect(() => {
1157
- sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
1158
- }).not.toThrow();
1159
- });
1160
- it("generates consistent session timeout", () => {
1161
- const config = createTestConfig();
1162
- const sessionManager = createSessionManager(config);
1163
- // Session timeout is now static - always uses default
1164
- expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
1165
- });
1166
- });
1167
- });
1168
- //# sourceMappingURL=session-manager.test.js.map