@typexim/web-sdk 0.0.6 → 0.0.8-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -3
- package/dist/types/enums/sdk.d.ts +0 -1
- package/dist/types/interfaces/business.d.ts +45 -1
- package/dist/types/interfaces/sdk/api.d.ts +41 -1
- package/dist/types/modules/ChatSDK.d.ts +24 -3
- package/dist/types/modules/TabSessionCoordinator.d.ts +5 -5
- package/dist/types/utils/misc.d.ts +10 -0
- package/dist/web-sdk.esm.js +1 -1
- package/dist/web-sdk.js +1 -1
- package/dist-test/enums/business.js +139 -0
- package/dist-test/enums/index.js +2 -0
- package/dist-test/enums/sdk.js +24 -0
- package/dist-test/events.js +50 -0
- package/dist-test/index.js +26 -0
- package/dist-test/interfaces/business.js +1 -0
- package/dist-test/interfaces/index.js +3 -0
- package/dist-test/interfaces/pushMessage.js +1 -0
- package/dist-test/interfaces/sdk/api.js +1 -0
- package/dist-test/interfaces/sdk/common.js +1 -0
- package/dist-test/interfaces/sdk/index.js +2 -0
- package/dist-test/modules/Base.js +70 -0
- package/dist-test/modules/BusinessWebsocket.js +68 -0
- package/dist-test/modules/ChatSDK.js +698 -0
- package/dist-test/modules/FeedCache.js +211 -0
- package/dist-test/modules/HttpClient.js +104 -0
- package/dist-test/modules/Logger.js +73 -0
- package/dist-test/modules/PushMessageProcessor.js +437 -0
- package/dist-test/modules/ReConnectWebsocket.js +223 -0
- package/dist-test/modules/Storage.js +27 -0
- package/dist-test/modules/TabSessionCoordinator.js +289 -0
- package/dist-test/modules/Version.js +20 -0
- package/dist-test/utils/date.js +4 -0
- package/dist-test/utils/http.js +13 -0
- package/dist-test/utils/makeSign.js +79 -0
- package/dist-test/utils/message.js +146 -0
- package/dist-test/utils/misc.js +13 -0
- package/dist-test/utils/promise.js +10 -0
- package/dist-test/utils/sleep.js +3 -0
- package/dist-test/utils/websocket.js +24 -0
- package/package.json +3 -2
- package/test/chat-sdk-independent-login.test.mjs +218 -0
- package/test/{chat-sdk-restore-login.test.mjs → chat-sdk-session-restore.test.mjs} +69 -29
- package/test/tab-session-coordinator.test.mjs +30 -64
- package/dist/types/modules/SingletonLock.d.ts +0 -12
- package/test/singleton-lock.test.mjs +0 -75
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { access, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const channels = new Set();
|
|
8
|
+
const storageListeners = new Set();
|
|
9
|
+
const storageData = new Map();
|
|
10
|
+
|
|
11
|
+
class MockBroadcastChannel {
|
|
12
|
+
constructor(name) {
|
|
13
|
+
this.name = name;
|
|
14
|
+
this.onmessage = null;
|
|
15
|
+
channels.add(this);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
postMessage(message) {
|
|
19
|
+
for (const channel of channels) {
|
|
20
|
+
if (channel !== this && channel.name === this.name && channel.onmessage) {
|
|
21
|
+
channel.onmessage({ data: message });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
close() {
|
|
27
|
+
channels.delete(this);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class MockWebSocket {
|
|
32
|
+
static OPEN = 1;
|
|
33
|
+
|
|
34
|
+
constructor() {
|
|
35
|
+
this.OPEN = 1;
|
|
36
|
+
this.readyState = 1;
|
|
37
|
+
setTimeout(() => this.onopen?.(), 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
send() {}
|
|
41
|
+
|
|
42
|
+
close() {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function installBrowserMocks() {
|
|
46
|
+
channels.clear();
|
|
47
|
+
storageListeners.clear();
|
|
48
|
+
storageData.clear();
|
|
49
|
+
|
|
50
|
+
Object.defineProperty(globalThis, 'navigator', {
|
|
51
|
+
configurable: true,
|
|
52
|
+
value: {},
|
|
53
|
+
});
|
|
54
|
+
globalThis.BroadcastChannel = MockBroadcastChannel;
|
|
55
|
+
globalThis.WebSocket = MockWebSocket;
|
|
56
|
+
globalThis.window = {
|
|
57
|
+
addEventListener: () => {},
|
|
58
|
+
removeEventListener: () => {},
|
|
59
|
+
};
|
|
60
|
+
globalThis.localStorage = {
|
|
61
|
+
getItem: (key) => storageData.get(key) ?? null,
|
|
62
|
+
setItem: (key, value) => {
|
|
63
|
+
storageData.set(key, value);
|
|
64
|
+
},
|
|
65
|
+
removeItem: (key) => storageData.delete(key),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createSdk(ChatSDK) {
|
|
70
|
+
const sdk = new ChatSDK({
|
|
71
|
+
httpHost: 'https://example.test',
|
|
72
|
+
websocketHost: 'wss://example.test',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
sdk.httpClient.get = async () => ({
|
|
76
|
+
ok: true,
|
|
77
|
+
data: null,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return sdk;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function patchDistTestImports(dirUrl) {
|
|
84
|
+
const entries = await readdir(dirUrl, { withFileTypes: true });
|
|
85
|
+
|
|
86
|
+
await Promise.all(
|
|
87
|
+
entries.map(async (entry) => {
|
|
88
|
+
const entryUrl = new URL(
|
|
89
|
+
`${entry.name}${entry.isDirectory() ? '/' : ''}`,
|
|
90
|
+
dirUrl,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
await patchDistTestImports(entryUrl);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!entry.name.endsWith('.js')) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const source = await readFile(entryUrl, 'utf8');
|
|
103
|
+
const patched = await patchRelativeImports(source, entryUrl);
|
|
104
|
+
|
|
105
|
+
if (patched !== source) {
|
|
106
|
+
await writeFile(entryUrl, patched);
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function pathExists(path) {
|
|
113
|
+
try {
|
|
114
|
+
await access(path);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function patchRelativeImports(source, sourceUrl) {
|
|
122
|
+
const sourcePath = fileURLToPath(sourceUrl);
|
|
123
|
+
const sourceDir = dirname(sourcePath);
|
|
124
|
+
const importRegex =
|
|
125
|
+
/(?<prefix>from\s+['"])(?<specifier>\.\.?\/[^'"]+)(?<suffix>['"])/g;
|
|
126
|
+
const matches = [...source.matchAll(importRegex)];
|
|
127
|
+
let patched = source;
|
|
128
|
+
|
|
129
|
+
for (const match of matches) {
|
|
130
|
+
const { prefix, specifier, suffix } = match.groups;
|
|
131
|
+
|
|
132
|
+
if (specifier.endsWith('.js')) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const targetPath = resolve(sourceDir, specifier);
|
|
137
|
+
let nextSpecifier = specifier;
|
|
138
|
+
|
|
139
|
+
if (await pathExists(`${targetPath}.js`)) {
|
|
140
|
+
nextSpecifier = `${specifier}.js`;
|
|
141
|
+
} else if (await pathExists(resolve(targetPath, 'index.js'))) {
|
|
142
|
+
nextSpecifier = `${specifier}/index.js`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (nextSpecifier !== specifier) {
|
|
146
|
+
patched = patched.replaceAll(
|
|
147
|
+
`${prefix}${specifier}${suffix}`,
|
|
148
|
+
`${prefix}${nextSpecifier}${suffix}`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return patched;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
test('two SDK instances can login independently without a cross-tab lock', async () => {
|
|
157
|
+
installBrowserMocks();
|
|
158
|
+
|
|
159
|
+
await patchDistTestImports(new URL('../dist-test/', import.meta.url));
|
|
160
|
+
const { default: ChatSDK } = await import('../dist-test/modules/ChatSDK.js');
|
|
161
|
+
|
|
162
|
+
const firstSdk = createSdk(ChatSDK);
|
|
163
|
+
const secondSdk = createSdk(ChatSDK);
|
|
164
|
+
|
|
165
|
+
let sessionLoginCalls = 0;
|
|
166
|
+
|
|
167
|
+
const mockPost = async (url, body) => {
|
|
168
|
+
if (url === '/device_api/device_id') {
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
data: 'device-shared',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (url === '/user/session/login') {
|
|
176
|
+
sessionLoginCalls += 1;
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
data: {
|
|
180
|
+
id: `user-${body.userCode}`,
|
|
181
|
+
key: `secret-${body.userCode}`,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
code: -1,
|
|
189
|
+
msg: `unexpected post ${url}`,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
firstSdk.httpClient.post = mockPost;
|
|
194
|
+
secondSdk.httpClient.post = mockPost;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
// Wait for auto-restore timeouts so login does not share an in-flight restore.
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
199
|
+
|
|
200
|
+
const firstRes = await firstSdk.login({ user_code: 'code-a' });
|
|
201
|
+
const secondRes = await secondSdk.login({ user_code: 'code-a' });
|
|
202
|
+
|
|
203
|
+
assert.equal(firstRes.ok, true);
|
|
204
|
+
assert.equal(secondRes.ok, true);
|
|
205
|
+
assert.equal(sessionLoginCalls, 2);
|
|
206
|
+
assert.equal(firstSdk.context.isLogin, true);
|
|
207
|
+
assert.equal(firstSdk.context.isReady, true);
|
|
208
|
+
assert.equal(secondSdk.context.isLogin, true);
|
|
209
|
+
assert.equal(secondSdk.context.isReady, true);
|
|
210
|
+
assert.equal(firstSdk.context.loginUser?.sessionId, 'code-a');
|
|
211
|
+
assert.equal(secondSdk.context.loginUser?.sessionId, 'code-a');
|
|
212
|
+
} finally {
|
|
213
|
+
firstSdk.afterLogout();
|
|
214
|
+
secondSdk.afterLogout();
|
|
215
|
+
firstSdk.tabSessionCoordinator.destroy();
|
|
216
|
+
secondSdk.tabSessionCoordinator.destroy();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
@@ -47,25 +47,9 @@ function installBrowserMocks() {
|
|
|
47
47
|
storageListeners.clear();
|
|
48
48
|
storageData.clear();
|
|
49
49
|
|
|
50
|
-
let lockHeld = false;
|
|
51
50
|
Object.defineProperty(globalThis, 'navigator', {
|
|
52
51
|
configurable: true,
|
|
53
|
-
value: {
|
|
54
|
-
locks: {
|
|
55
|
-
request: async (_name, options, callback) => {
|
|
56
|
-
if (lockHeld && options?.ifAvailable) {
|
|
57
|
-
return callback(null);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
lockHeld = true;
|
|
61
|
-
try {
|
|
62
|
-
return await callback({});
|
|
63
|
-
} finally {
|
|
64
|
-
lockHeld = false;
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
52
|
+
value: {},
|
|
69
53
|
});
|
|
70
54
|
globalThis.BroadcastChannel = MockBroadcastChannel;
|
|
71
55
|
globalThis.WebSocket = MockWebSocket;
|
|
@@ -177,7 +161,7 @@ async function patchRelativeImports(source, sourceUrl) {
|
|
|
177
161
|
return patched;
|
|
178
162
|
}
|
|
179
163
|
|
|
180
|
-
test('
|
|
164
|
+
test('new tab restores neighbor session without login API and without kicking neighbor', async () => {
|
|
181
165
|
installBrowserMocks();
|
|
182
166
|
|
|
183
167
|
await patchDistTestImports(new URL('../dist-test/', import.meta.url));
|
|
@@ -192,10 +176,9 @@ test('login restores an active tab session before calling session login API', as
|
|
|
192
176
|
secretKey: 'secret-1',
|
|
193
177
|
sessionId: 'session-1',
|
|
194
178
|
};
|
|
195
|
-
await oldSdk.singletonLock.requestLock();
|
|
196
179
|
|
|
197
|
-
const newSdk = createSdk(ChatSDK);
|
|
198
180
|
let sessionLoginApiCalled = false;
|
|
181
|
+
const newSdk = createSdk(ChatSDK);
|
|
199
182
|
newSdk.httpClient.post = async (url) => {
|
|
200
183
|
if (url === '/user/session/login') {
|
|
201
184
|
sessionLoginApiCalled = true;
|
|
@@ -208,25 +191,82 @@ test('login restores an active tab session before calling session login API', as
|
|
|
208
191
|
};
|
|
209
192
|
|
|
210
193
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
194
|
+
// Wait for auto-restore scheduled in init.
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
214
196
|
|
|
215
|
-
assert.equal(res.ok, true);
|
|
216
|
-
assert.equal(sessionLoginApiCalled, false);
|
|
217
197
|
assert.equal(newSdk.context.isLogin, true);
|
|
218
198
|
assert.equal(newSdk.context.isReady, true);
|
|
199
|
+
assert.equal(sessionLoginApiCalled, false);
|
|
219
200
|
assert.deepEqual(newSdk.context.loginUser, {
|
|
220
201
|
userId: 'user-1',
|
|
221
202
|
secretKey: 'secret-1',
|
|
222
203
|
sessionId: 'session-1',
|
|
223
204
|
});
|
|
224
|
-
assert.equal(oldSdk.context.isLogin,
|
|
225
|
-
assert.equal(oldSdk.context.isReady,
|
|
205
|
+
assert.equal(oldSdk.context.isLogin, true);
|
|
206
|
+
assert.equal(oldSdk.context.isReady, true);
|
|
207
|
+
|
|
208
|
+
const loginRes = await newSdk.login({ user_code: 'session-1' });
|
|
209
|
+
assert.equal(loginRes.ok, true);
|
|
210
|
+
assert.equal(sessionLoginApiCalled, false);
|
|
226
211
|
} finally {
|
|
227
|
-
|
|
228
|
-
|
|
212
|
+
oldSdk.afterLogout();
|
|
213
|
+
newSdk.afterLogout();
|
|
229
214
|
oldSdk.tabSessionCoordinator.destroy();
|
|
230
215
|
newSdk.tabSessionCoordinator.destroy();
|
|
231
216
|
}
|
|
232
217
|
});
|
|
218
|
+
|
|
219
|
+
test('logout on one tab clears login state on sibling tabs without calling logout API again', async () => {
|
|
220
|
+
installBrowserMocks();
|
|
221
|
+
|
|
222
|
+
await patchDistTestImports(new URL('../dist-test/', import.meta.url));
|
|
223
|
+
const { default: ChatSDK } = await import('../dist-test/modules/ChatSDK.js');
|
|
224
|
+
|
|
225
|
+
const firstSdk = createSdk(ChatSDK);
|
|
226
|
+
firstSdk.context.deviceId = 'device-1';
|
|
227
|
+
firstSdk.context.isLogin = true;
|
|
228
|
+
firstSdk.context.isReady = true;
|
|
229
|
+
firstSdk.context.loginUser = {
|
|
230
|
+
userId: 'user-1',
|
|
231
|
+
secretKey: 'secret-1',
|
|
232
|
+
sessionId: 'session-1',
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const secondSdk = createSdk(ChatSDK);
|
|
236
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
237
|
+
|
|
238
|
+
assert.equal(secondSdk.context.isLogin, true);
|
|
239
|
+
|
|
240
|
+
let logoutApiCalls = 0;
|
|
241
|
+
firstSdk.httpClient.get = async (url) => {
|
|
242
|
+
if (url === '/user/logout') {
|
|
243
|
+
logoutApiCalls += 1;
|
|
244
|
+
return { ok: true, data: null };
|
|
245
|
+
}
|
|
246
|
+
return { ok: true, data: null };
|
|
247
|
+
};
|
|
248
|
+
secondSdk.httpClient.get = async (url) => {
|
|
249
|
+
if (url === '/user/logout') {
|
|
250
|
+
logoutApiCalls += 1;
|
|
251
|
+
return { ok: true, data: null };
|
|
252
|
+
}
|
|
253
|
+
return { ok: true, data: null };
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const res = await firstSdk.logout();
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
259
|
+
|
|
260
|
+
assert.equal(res.ok, true);
|
|
261
|
+
assert.equal(logoutApiCalls, 1);
|
|
262
|
+
assert.equal(firstSdk.context.isLogin, false);
|
|
263
|
+
assert.equal(firstSdk.context.isReady, false);
|
|
264
|
+
assert.equal(secondSdk.context.isLogin, false);
|
|
265
|
+
assert.equal(secondSdk.context.isReady, false);
|
|
266
|
+
} finally {
|
|
267
|
+
firstSdk.afterLogout();
|
|
268
|
+
secondSdk.afterLogout();
|
|
269
|
+
firstSdk.tabSessionCoordinator.destroy();
|
|
270
|
+
secondSdk.tabSessionCoordinator.destroy();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
@@ -53,54 +53,6 @@ function installBrowserMocks() {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
test('a new tab takeover request only notifies the old tab', async () => {
|
|
57
|
-
installBrowserMocks();
|
|
58
|
-
|
|
59
|
-
let oldTabKicked = 0;
|
|
60
|
-
let newTabKicked = 0;
|
|
61
|
-
const oldTab = new TabSessionCoordinator({
|
|
62
|
-
onTakeoverRequest: () => {
|
|
63
|
-
oldTabKicked += 1;
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
const newTab = new TabSessionCoordinator({
|
|
67
|
-
onTakeoverRequest: () => {
|
|
68
|
-
newTabKicked += 1;
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
newTab.requestTakeover();
|
|
73
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
74
|
-
|
|
75
|
-
assert.equal(oldTabKicked, 1);
|
|
76
|
-
assert.equal(newTabKicked, 0);
|
|
77
|
-
|
|
78
|
-
oldTab.destroy();
|
|
79
|
-
newTab.destroy();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('destroyed coordinators ignore later takeover requests', async () => {
|
|
83
|
-
installBrowserMocks();
|
|
84
|
-
|
|
85
|
-
let oldTabKicked = 0;
|
|
86
|
-
const oldTab = new TabSessionCoordinator({
|
|
87
|
-
onTakeoverRequest: () => {
|
|
88
|
-
oldTabKicked += 1;
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
const newTab = new TabSessionCoordinator({
|
|
92
|
-
onTakeoverRequest: () => {},
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
oldTab.destroy();
|
|
96
|
-
newTab.requestTakeover();
|
|
97
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
98
|
-
|
|
99
|
-
assert.equal(oldTabKicked, 0);
|
|
100
|
-
|
|
101
|
-
newTab.destroy();
|
|
102
|
-
});
|
|
103
|
-
|
|
104
56
|
test('a new tab can request the current login session from an old tab', async () => {
|
|
105
57
|
installBrowserMocks();
|
|
106
58
|
|
|
@@ -114,11 +66,8 @@ test('a new tab can request the current login session from an old tab', async ()
|
|
|
114
66
|
};
|
|
115
67
|
const oldTab = new TabSessionCoordinator({
|
|
116
68
|
getCurrentSession: () => session,
|
|
117
|
-
onTakeoverRequest: () => {},
|
|
118
|
-
});
|
|
119
|
-
const newTab = new TabSessionCoordinator({
|
|
120
|
-
onTakeoverRequest: () => {},
|
|
121
69
|
});
|
|
70
|
+
const newTab = new TabSessionCoordinator({});
|
|
122
71
|
|
|
123
72
|
const restoredSession = await newTab.requestCurrentSession(50);
|
|
124
73
|
|
|
@@ -141,11 +90,8 @@ test('login session responses are not persisted through localStorage', async ()
|
|
|
141
90
|
};
|
|
142
91
|
const oldTab = new TabSessionCoordinator({
|
|
143
92
|
getCurrentSession: () => session,
|
|
144
|
-
onTakeoverRequest: () => {},
|
|
145
|
-
});
|
|
146
|
-
const newTab = new TabSessionCoordinator({
|
|
147
|
-
onTakeoverRequest: () => {},
|
|
148
93
|
});
|
|
94
|
+
const newTab = new TabSessionCoordinator({});
|
|
149
95
|
|
|
150
96
|
const restoredSession = await newTab.requestCurrentSession(50);
|
|
151
97
|
const storedMessages = [...storageData.values()].map((value) =>
|
|
@@ -185,11 +131,8 @@ test('duplicate session requests from fallback channels are answered once', asyn
|
|
|
185
131
|
sessionRequestCount += 1;
|
|
186
132
|
return session;
|
|
187
133
|
},
|
|
188
|
-
onTakeoverRequest: () => {},
|
|
189
|
-
});
|
|
190
|
-
const newTab = new TabSessionCoordinator({
|
|
191
|
-
onTakeoverRequest: () => {},
|
|
192
134
|
});
|
|
135
|
+
const newTab = new TabSessionCoordinator({});
|
|
193
136
|
|
|
194
137
|
await newTab.requestCurrentSession(50);
|
|
195
138
|
|
|
@@ -204,11 +147,8 @@ test('requesting a login session returns null when no tab is logged in', async (
|
|
|
204
147
|
|
|
205
148
|
const oldTab = new TabSessionCoordinator({
|
|
206
149
|
getCurrentSession: () => null,
|
|
207
|
-
onTakeoverRequest: () => {},
|
|
208
|
-
});
|
|
209
|
-
const newTab = new TabSessionCoordinator({
|
|
210
|
-
onTakeoverRequest: () => {},
|
|
211
150
|
});
|
|
151
|
+
const newTab = new TabSessionCoordinator({});
|
|
212
152
|
|
|
213
153
|
const restoredSession = await newTab.requestCurrentSession(1);
|
|
214
154
|
|
|
@@ -217,3 +157,29 @@ test('requesting a login session returns null when no tab is logged in', async (
|
|
|
217
157
|
oldTab.destroy();
|
|
218
158
|
newTab.destroy();
|
|
219
159
|
});
|
|
160
|
+
|
|
161
|
+
test('logout broadcast notifies other tabs once', async () => {
|
|
162
|
+
installBrowserMocks();
|
|
163
|
+
|
|
164
|
+
let oldTabLogout = 0;
|
|
165
|
+
let newTabLogout = 0;
|
|
166
|
+
const oldTab = new TabSessionCoordinator({
|
|
167
|
+
onLogoutBroadcast: () => {
|
|
168
|
+
oldTabLogout += 1;
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const newTab = new TabSessionCoordinator({
|
|
172
|
+
onLogoutBroadcast: () => {
|
|
173
|
+
newTabLogout += 1;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
newTab.publishLogout();
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
179
|
+
|
|
180
|
+
assert.equal(oldTabLogout, 1);
|
|
181
|
+
assert.equal(newTabLogout, 0);
|
|
182
|
+
|
|
183
|
+
oldTab.destroy();
|
|
184
|
+
newTab.destroy();
|
|
185
|
+
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export type SingletonLockRequestOptions = {
|
|
2
|
-
timeoutMs?: number;
|
|
3
|
-
retryIntervalMs?: number;
|
|
4
|
-
};
|
|
5
|
-
export default class SingletonLock {
|
|
6
|
-
private obtained;
|
|
7
|
-
private abortLockObtain;
|
|
8
|
-
constructor();
|
|
9
|
-
requestLock(options?: SingletonLockRequestOptions): Promise<boolean>;
|
|
10
|
-
private requestLockOnce;
|
|
11
|
-
releaseLock(): void;
|
|
12
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { test } from 'node:test';
|
|
3
|
-
|
|
4
|
-
import SingletonLock from '../dist-test/modules/SingletonLock.js';
|
|
5
|
-
|
|
6
|
-
function installMockLocks(sequence) {
|
|
7
|
-
let callIndex = 0;
|
|
8
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
9
|
-
configurable: true,
|
|
10
|
-
value: {
|
|
11
|
-
locks: {
|
|
12
|
-
request: (_name, _options, callback) => {
|
|
13
|
-
const available = sequence[Math.min(callIndex, sequence.length - 1)];
|
|
14
|
-
callIndex += 1;
|
|
15
|
-
Promise.resolve().then(() => callback(available ? {} : null));
|
|
16
|
-
},
|
|
17
|
-
},
|
|
18
|
-
},
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function installNavigator(value) {
|
|
23
|
-
Object.defineProperty(globalThis, 'navigator', {
|
|
24
|
-
configurable: true,
|
|
25
|
-
value,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test('requestLock retries until an old tab releases the singleton lock', async () => {
|
|
30
|
-
installMockLocks([false, false, true]);
|
|
31
|
-
|
|
32
|
-
const lock = new SingletonLock();
|
|
33
|
-
const obtained = await lock.requestLock({
|
|
34
|
-
retryIntervalMs: 1,
|
|
35
|
-
timeoutMs: 50,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
assert.equal(obtained, true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('requestLock gives up when the old tab does not release in time', async () => {
|
|
42
|
-
installMockLocks([false]);
|
|
43
|
-
|
|
44
|
-
const lock = new SingletonLock();
|
|
45
|
-
const obtained = await lock.requestLock({
|
|
46
|
-
retryIntervalMs: 1,
|
|
47
|
-
timeoutMs: 5,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
assert.equal(obtained, false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('requestLock fails closed when Web Locks is unavailable', async () => {
|
|
54
|
-
installNavigator({});
|
|
55
|
-
|
|
56
|
-
const lock = new SingletonLock();
|
|
57
|
-
const obtained = await lock.requestLock();
|
|
58
|
-
|
|
59
|
-
assert.equal(obtained, false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('requestLock fails closed when Web Locks throws', async () => {
|
|
63
|
-
installNavigator({
|
|
64
|
-
locks: {
|
|
65
|
-
request: () => {
|
|
66
|
-
throw new Error('locks unavailable');
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const lock = new SingletonLock();
|
|
72
|
-
const obtained = await lock.requestLock();
|
|
73
|
-
|
|
74
|
-
assert.equal(obtained, false);
|
|
75
|
-
});
|