miakapi 3.0.31 → 4.0.0-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.
Files changed (124) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +372 -0
  3. package/contracts/miakapp-v3.json +6 -0
  4. package/dist/access-token-provider.d.ts +8 -0
  5. package/dist/access-token-provider.d.ts.map +1 -0
  6. package/dist/access-token-provider.js +167 -0
  7. package/dist/access-token-provider.js.map +1 -0
  8. package/dist/api.d.ts +224 -0
  9. package/dist/api.d.ts.map +1 -0
  10. package/dist/api.js +47 -0
  11. package/dist/api.js.map +1 -0
  12. package/dist/browser-api.d.ts +113 -0
  13. package/dist/browser-api.d.ts.map +1 -0
  14. package/dist/browser-api.js +2 -0
  15. package/dist/browser-api.js.map +1 -0
  16. package/dist/browser-client.d.ts +3 -0
  17. package/dist/browser-client.d.ts.map +1 -0
  18. package/dist/browser-client.js +678 -0
  19. package/dist/browser-client.js.map +1 -0
  20. package/dist/browser-relay-credential-provider.d.ts +3 -0
  21. package/dist/browser-relay-credential-provider.d.ts.map +1 -0
  22. package/dist/browser-relay-credential-provider.js +243 -0
  23. package/dist/browser-relay-credential-provider.js.map +1 -0
  24. package/dist/browser.d.ts +4 -0
  25. package/dist/browser.d.ts.map +1 -0
  26. package/dist/browser.js +4 -0
  27. package/dist/browser.js.map +1 -0
  28. package/dist/coordinator.d.ts +3 -0
  29. package/dist/coordinator.d.ts.map +1 -0
  30. package/dist/coordinator.js +547 -0
  31. package/dist/coordinator.js.map +1 -0
  32. package/dist/index.d.ts +4 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +4 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/internal/browser-errors.d.ts +31 -0
  37. package/dist/internal/browser-errors.d.ts.map +1 -0
  38. package/dist/internal/browser-errors.js +69 -0
  39. package/dist/internal/browser-errors.js.map +1 -0
  40. package/dist/internal/browser-socket.d.ts +8 -0
  41. package/dist/internal/browser-socket.d.ts.map +1 -0
  42. package/dist/internal/browser-socket.js +204 -0
  43. package/dist/internal/browser-socket.js.map +1 -0
  44. package/dist/internal/calls.d.ts +33 -0
  45. package/dist/internal/calls.d.ts.map +1 -0
  46. package/dist/internal/calls.js +525 -0
  47. package/dist/internal/calls.js.map +1 -0
  48. package/dist/internal/control-plane-response.d.ts +10 -0
  49. package/dist/internal/control-plane-response.d.ts.map +1 -0
  50. package/dist/internal/control-plane-response.js +283 -0
  51. package/dist/internal/control-plane-response.js.map +1 -0
  52. package/dist/internal/declarations.d.ts +88 -0
  53. package/dist/internal/declarations.d.ts.map +1 -0
  54. package/dist/internal/declarations.js +561 -0
  55. package/dist/internal/declarations.js.map +1 -0
  56. package/dist/internal/errors.d.ts +31 -0
  57. package/dist/internal/errors.d.ts.map +1 -0
  58. package/dist/internal/errors.js +72 -0
  59. package/dist/internal/errors.js.map +1 -0
  60. package/dist/internal/events.d.ts +27 -0
  61. package/dist/internal/events.d.ts.map +1 -0
  62. package/dist/internal/events.js +207 -0
  63. package/dist/internal/events.js.map +1 -0
  64. package/dist/internal/presence.d.ts +15 -0
  65. package/dist/internal/presence.d.ts.map +1 -0
  66. package/dist/internal/presence.js +99 -0
  67. package/dist/internal/presence.js.map +1 -0
  68. package/dist/internal/resources.d.ts +33 -0
  69. package/dist/internal/resources.d.ts.map +1 -0
  70. package/dist/internal/resources.js +136 -0
  71. package/dist/internal/resources.js.map +1 -0
  72. package/dist/internal/runtime.d.ts +34 -0
  73. package/dist/internal/runtime.d.ts.map +1 -0
  74. package/dist/internal/runtime.js +17 -0
  75. package/dist/internal/runtime.js.map +1 -0
  76. package/dist/internal/session.d.ts +33 -0
  77. package/dist/internal/session.d.ts.map +1 -0
  78. package/dist/internal/session.js +199 -0
  79. package/dist/internal/session.js.map +1 -0
  80. package/dist/internal/socket.d.ts +6 -0
  81. package/dist/internal/socket.d.ts.map +1 -0
  82. package/dist/internal/socket.js +176 -0
  83. package/dist/internal/socket.js.map +1 -0
  84. package/dist/internal/state.d.ts +30 -0
  85. package/dist/internal/state.d.ts.map +1 -0
  86. package/dist/internal/state.js +188 -0
  87. package/dist/internal/state.js.map +1 -0
  88. package/dist/internal/user-calls.d.ts +27 -0
  89. package/dist/internal/user-calls.d.ts.map +1 -0
  90. package/dist/internal/user-calls.js +325 -0
  91. package/dist/internal/user-calls.js.map +1 -0
  92. package/dist/internal/user-session.d.ts +36 -0
  93. package/dist/internal/user-session.d.ts.map +1 -0
  94. package/dist/internal/user-session.js +250 -0
  95. package/dist/internal/user-session.js.map +1 -0
  96. package/dist/internal/user-state.d.ts +21 -0
  97. package/dist/internal/user-state.d.ts.map +1 -0
  98. package/dist/internal/user-state.js +240 -0
  99. package/dist/internal/user-state.js.map +1 -0
  100. package/dist/internal/validation.d.ts +32 -0
  101. package/dist/internal/validation.d.ts.map +1 -0
  102. package/dist/internal/validation.js +553 -0
  103. package/dist/internal/validation.js.map +1 -0
  104. package/dist/protocol/codec.d.ts +75 -0
  105. package/dist/protocol/codec.d.ts.map +1 -0
  106. package/dist/protocol/codec.js +1091 -0
  107. package/dist/protocol/codec.js.map +1 -0
  108. package/dist/protocol/session.d.ts +14 -0
  109. package/dist/protocol/session.d.ts.map +1 -0
  110. package/dist/protocol/session.js +125 -0
  111. package/dist/protocol/session.js.map +1 -0
  112. package/dist/protocol/user-session.d.ts +14 -0
  113. package/dist/protocol/user-session.d.ts.map +1 -0
  114. package/dist/protocol/user-session.js +116 -0
  115. package/dist/protocol/user-session.js.map +1 -0
  116. package/package.json +58 -9
  117. package/.eslintrc.js +0 -16
  118. package/.gitattributes +0 -2
  119. package/main.js +0 -305
  120. package/miakode.js +0 -56
  121. package/screenshots/bigGrid.jpg +0 -0
  122. package/screenshots/formExample.jpg +0 -0
  123. package/screenshots/properties&table.jpg +0 -0
  124. package/test.js +0 -45
@@ -0,0 +1,678 @@
1
+ import { createBrowserRuntime } from './internal/browser-socket.js';
2
+ import { BrowserClientError, browserCancelled, browserInternalFailure, browserInvalidLifecycle, browserProtocolFailure, browserRelayFailure, browserUnavailable, safeBrowserLog, } from './internal/browser-errors.js';
3
+ import { UserCallManager } from './internal/user-calls.js';
4
+ import { childAbortController, createDeferred, IdSequence, ListenerSet, } from './internal/resources.js';
5
+ import { delay } from './internal/runtime.js';
6
+ import { parseUserHomeStatus, UserRelaySession } from './internal/user-session.js';
7
+ import { UserStateManager } from './internal/user-state.js';
8
+ import { validateBrowserClientOptions, validateBrowserRelayCredential, validateStartOptions, validateStopOptions, } from './internal/validation.js';
9
+ import { Opcode } from './protocol/codec.js';
10
+ const FIRST_RECONNECT_CEILING_MS = 1_000;
11
+ const MAX_RECONNECT_CEILING_MS = 30_000;
12
+ const SESSION_PHASE_TIMEOUT_MS = 10_000;
13
+ function relayInteger(frame, index, label, minimum = 0) {
14
+ const value = frame.payload[index];
15
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum) {
16
+ throw browserProtocolFailure(`${label} is not an integer`);
17
+ }
18
+ return value;
19
+ }
20
+ function relayBoolean(frame, index, label) {
21
+ const value = frame.payload[index];
22
+ if (typeof value !== 'boolean')
23
+ throw browserProtocolFailure(`${label} is not a boolean`);
24
+ return value;
25
+ }
26
+ function sameEpoch(left, right) {
27
+ return right instanceof Uint8Array
28
+ && right.length === left.length
29
+ && right.every((value, index) => value === left[index]);
30
+ }
31
+ class BrowserClientImpl {
32
+ #options;
33
+ #runtime;
34
+ #lifecycleListeners = new ListenerSet();
35
+ #errorListeners = new ListenerSet();
36
+ #homeListeners = new ListenerSet();
37
+ #loopController = new AbortController();
38
+ home;
39
+ state;
40
+ calls;
41
+ errors;
42
+ #status = 'idle';
43
+ #started = false;
44
+ #session;
45
+ #sessionEnd;
46
+ #sessionReady;
47
+ #startDeferred;
48
+ #stopDeferred;
49
+ #stopTimer;
50
+ #bootstrapTimer;
51
+ #loopTask;
52
+ #startSignal;
53
+ #startAbort;
54
+ #credentialRequest;
55
+ #reauthentication;
56
+ #reauthTimer;
57
+ #sessionRelayUrl;
58
+ #requestIds = new IdSequence();
59
+ #callIds = new IdSequence();
60
+ #localCallIds = new IdSequence();
61
+ #reconnectAttempt = 0;
62
+ #goawayRetryAfterMs;
63
+ #stateReady = false;
64
+ #functionReady = false;
65
+ #topicReady = false;
66
+ #homeStatus;
67
+ constructor(options, runtime) {
68
+ this.#options = options;
69
+ this.#runtime = runtime;
70
+ this.state = new UserStateManager(this);
71
+ this.calls = new UserCallManager(this);
72
+ this.home = Object.freeze({
73
+ snapshot: () => this.#homeStatus,
74
+ subscribe: (listener) => this.#subscribeHome(listener),
75
+ });
76
+ this.errors = Object.freeze({
77
+ subscribe: (listener) => {
78
+ if (typeof listener !== 'function')
79
+ throw new TypeError('error listener must be a function');
80
+ return this.#errorListeners.subscribe(listener);
81
+ },
82
+ });
83
+ }
84
+ get status() {
85
+ return this.#status;
86
+ }
87
+ start(options = {}) {
88
+ if (this.#started || this.#status !== 'idle') {
89
+ return Promise.reject(browserInvalidLifecycle('Browser client has already started or stopped'));
90
+ }
91
+ const signal = validateStartOptions(options);
92
+ if (signal?.aborted === true)
93
+ return Promise.reject(browserCancelled('not_dispatched'));
94
+ this.#started = true;
95
+ this.#startDeferred = createDeferred();
96
+ if (signal !== undefined) {
97
+ const abort = () => void this.stop();
98
+ this.#startSignal = signal;
99
+ this.#startAbort = abort;
100
+ signal.addEventListener('abort', abort, { once: true });
101
+ }
102
+ this.#loopTask = this.#runConnectionLoop();
103
+ void this.#loopTask.catch(() => {
104
+ const failure = browserInternalFailure();
105
+ this.#startDeferred?.reject(failure);
106
+ this.#emitFailure(failure);
107
+ void this.stop();
108
+ });
109
+ return this.#startDeferred.promise;
110
+ }
111
+ stop(options = {}) {
112
+ if (this.#stopDeferred !== undefined)
113
+ return this.#stopDeferred.promise;
114
+ let deadlineMs;
115
+ try {
116
+ deadlineMs = validateStopOptions(options);
117
+ }
118
+ catch (error) {
119
+ return Promise.reject(error);
120
+ }
121
+ this.#stopDeferred = createDeferred();
122
+ const stopping = browserCancelled('not_dispatched', 'Browser client is stopping');
123
+ this.#transition('stopping');
124
+ this.calls.stop();
125
+ this.state.stop();
126
+ this.#markHomeStale();
127
+ this.#clearReauthentication();
128
+ this.#abortCredentialRequest();
129
+ this.#loopController.abort(stopping);
130
+ this.#sessionReady?.reject(stopping);
131
+ this.#sessionEnd?.resolve({ failure: stopping });
132
+ this.#session?.terminate();
133
+ this.#startDeferred?.reject(stopping);
134
+ const finish = () => this.#finishStop();
135
+ if (this.#loopTask === undefined)
136
+ finish();
137
+ else {
138
+ this.#stopTimer = this.#runtime.setTimer(finish, deadlineMs);
139
+ void this.#loopTask.then(finish, finish);
140
+ }
141
+ return this.#stopDeferred.promise;
142
+ }
143
+ subscribe(listener) {
144
+ if (typeof listener !== 'function')
145
+ throw new TypeError('lifecycle listener must be a function');
146
+ return this.#lifecycleListeners.subscribe(listener);
147
+ }
148
+ nextRequestId() {
149
+ return this.#requestIds.take();
150
+ }
151
+ nextCallId() {
152
+ return this.#callIds.take();
153
+ }
154
+ nextLocalCallId() {
155
+ return this.#localCallIds.take();
156
+ }
157
+ runtime() {
158
+ return this.#runtime;
159
+ }
160
+ readySession() {
161
+ return this.#status === 'ready' ? this.#session : undefined;
162
+ }
163
+ send(frame) {
164
+ const session = this.#session;
165
+ if (session === undefined || (this.#status !== 'ready' && this.#status !== 'synchronizing')) {
166
+ return Promise.reject(browserUnavailable());
167
+ }
168
+ return session.send(frame);
169
+ }
170
+ stateSynchronized() {
171
+ this.#stateReady = true;
172
+ this.#maybeReady();
173
+ }
174
+ functionDictionarySynchronized() {
175
+ this.#functionReady = true;
176
+ this.#maybeReady();
177
+ }
178
+ transportFailure(error) {
179
+ const failure = error instanceof BrowserClientError
180
+ ? error
181
+ : browserUnavailable('Browser relay transport failed');
182
+ this.#sessionReady?.reject(failure);
183
+ this.#sessionEnd?.resolve({ failure });
184
+ this.#session?.terminate();
185
+ }
186
+ emitFailure(failure) {
187
+ this.#emitFailure(failure);
188
+ }
189
+ async #runConnectionLoop() {
190
+ let reason = 'initial';
191
+ let pendingCredential;
192
+ while (!this.#loopController.signal.aborted) {
193
+ let end = {};
194
+ let connectionEnd;
195
+ try {
196
+ this.#transition('connecting');
197
+ if (this.#loopController.signal.aborted)
198
+ break;
199
+ const credential = pendingCredential ?? await this.#getCredential(reason);
200
+ pendingCredential = undefined;
201
+ if (this.#loopController.signal.aborted)
202
+ break;
203
+ if (credential.expiresAtMs <= this.#runtime.now()) {
204
+ throw browserUnavailable('Browser relay credential expired before connection');
205
+ }
206
+ this.#transition('authenticating');
207
+ if (this.#loopController.signal.aborted)
208
+ break;
209
+ this.#requestIds = new IdSequence();
210
+ this.#callIds = new IdSequence();
211
+ connectionEnd = createDeferred();
212
+ const sessionReady = createDeferred();
213
+ void sessionReady.promise.catch(() => undefined);
214
+ this.#sessionEnd = connectionEnd;
215
+ this.#sessionReady = sessionReady;
216
+ this.#goawayRetryAfterMs = undefined;
217
+ const session = await UserRelaySession.connect(this.#runtime, this.#options.homeId, credential.relayUrl, credential.accessToken, this.#loopController.signal, {
218
+ frame: (frame) => this.#handleFrame(frame),
219
+ closed: () => {
220
+ const failure = browserUnavailable('Browser relay connection closed');
221
+ sessionReady.reject(failure);
222
+ connectionEnd?.resolve(this.#goawayRetryAfterMs === undefined
223
+ ? { failure }
224
+ : { failure, retryAfterMs: this.#goawayRetryAfterMs });
225
+ },
226
+ failed: (error) => {
227
+ const failure = error instanceof BrowserClientError
228
+ ? error
229
+ : browserUnavailable('Browser relay connection failed');
230
+ sessionReady.reject(failure);
231
+ connectionEnd?.resolve({ failure });
232
+ },
233
+ }, (createdSession) => {
234
+ this.#session = createdSession;
235
+ this.#sessionRelayUrl = credential.relayUrl;
236
+ });
237
+ if (this.#loopController.signal.aborted) {
238
+ session.terminate();
239
+ session.detach();
240
+ break;
241
+ }
242
+ if (credential.expiresAtMs <= this.#runtime.now()) {
243
+ session.terminate();
244
+ throw browserUnavailable('Browser relay credential expired during authentication');
245
+ }
246
+ this.#reconnectAttempt = 0;
247
+ this.#setHomeStatus(Object.freeze({
248
+ enrolled: session.welcome.readySession.enrolled,
249
+ coordinators: session.welcome.readySession.coordinators,
250
+ stale: false,
251
+ }));
252
+ if (this.#loopController.signal.aborted)
253
+ break;
254
+ this.#stateReady = false;
255
+ this.#functionReady = false;
256
+ this.#topicReady = false;
257
+ this.state.beginSession(session.welcome.epoch);
258
+ this.calls.beginSession(session.welcome.epoch);
259
+ this.#transition('synchronizing');
260
+ if (this.#loopController.signal.aborted)
261
+ break;
262
+ this.#scheduleReauthentication(Math.min(session.welcome.expiresAtMs, credential.expiresAtMs));
263
+ const bootstrapTimeout = this.#runtime.setTimer(() => {
264
+ const failure = browserUnavailable('Browser relay bootstrap timed out');
265
+ sessionReady.reject(failure);
266
+ connectionEnd?.resolve({ failure });
267
+ session.terminate();
268
+ }, SESSION_PHASE_TIMEOUT_MS);
269
+ this.#bootstrapTimer = bootstrapTimeout;
270
+ session.startDelivery();
271
+ try {
272
+ await Promise.race([
273
+ sessionReady.promise,
274
+ connectionEnd.promise.then((closed) => Promise.reject(closed.failure ?? browserUnavailable('Browser relay closed during synchronization'))),
275
+ ]);
276
+ }
277
+ finally {
278
+ bootstrapTimeout.cancel();
279
+ if (this.#bootstrapTimer === bootstrapTimeout)
280
+ this.#bootstrapTimer = undefined;
281
+ }
282
+ if (this.#loopController.signal.aborted)
283
+ break;
284
+ end = await connectionEnd.promise;
285
+ }
286
+ catch (error) {
287
+ if (this.#loopController.signal.aborted)
288
+ break;
289
+ end = connectionEnd?.settled === true
290
+ ? await connectionEnd.promise
291
+ : { failure: error instanceof BrowserClientError
292
+ ? error
293
+ : browserUnavailable('Browser relay connection attempt failed') };
294
+ }
295
+ const endedSession = this.#session;
296
+ this.#deactivateSession();
297
+ if (this.#loopController.signal.aborted)
298
+ break;
299
+ if (end.failure !== undefined)
300
+ this.#emitFailure(end.failure);
301
+ if (this.#loopController.signal.aborted)
302
+ break;
303
+ this.#transition('reconnecting', undefined, end.failure);
304
+ if (this.#loopController.signal.aborted)
305
+ break;
306
+ if (endedSession !== undefined
307
+ && !endedSession.transportClosed
308
+ && !await this.#waitForTransportClose(endedSession)) {
309
+ if (!this.#loopController.signal.aborted) {
310
+ const failure = browserUnavailable('Browser relay transport close timed out');
311
+ this.#emitFailure(failure);
312
+ this.#startDeferred?.reject(failure);
313
+ void this.stop();
314
+ }
315
+ break;
316
+ }
317
+ this.#disconnectSession();
318
+ if (this.#loopController.signal.aborted)
319
+ break;
320
+ if (end.handoffCredential !== undefined) {
321
+ pendingCredential = end.handoffCredential;
322
+ reason = 'reconnect';
323
+ continue;
324
+ }
325
+ const ceiling = Math.min(FIRST_RECONNECT_CEILING_MS * (2 ** this.#reconnectAttempt), MAX_RECONNECT_CEILING_MS);
326
+ this.#reconnectAttempt += 1;
327
+ const randomDelay = Math.floor(this.#runtime.random() * (ceiling + 1));
328
+ try {
329
+ await delay(this.#runtime, Math.max(randomDelay, end.retryAfterMs ?? 0), this.#loopController.signal);
330
+ }
331
+ catch {
332
+ break;
333
+ }
334
+ reason = 'reconnect';
335
+ }
336
+ }
337
+ async #getCredential(reason, timeoutMs = SESSION_PHASE_TIMEOUT_MS) {
338
+ if (this.#credentialRequest !== undefined)
339
+ return this.#credentialRequest.promise;
340
+ const child = childAbortController(this.#loopController.signal);
341
+ const request = Object.freeze({
342
+ homeId: this.#options.homeId,
343
+ reason,
344
+ signal: child.controller.signal,
345
+ });
346
+ let abort;
347
+ const interrupted = new Promise((_resolve, reject) => {
348
+ abort = () => reject(child.controller.signal.reason ?? browserCancelled('not_dispatched'));
349
+ if (child.controller.signal.aborted)
350
+ abort();
351
+ else
352
+ child.controller.signal.addEventListener('abort', abort, { once: true });
353
+ });
354
+ const provider = Promise.resolve()
355
+ .then(() => {
356
+ if (child.controller.signal.aborted) {
357
+ throw child.controller.signal.reason ?? browserCancelled('not_dispatched');
358
+ }
359
+ return this.#options.credentialProvider.getCredential(request);
360
+ })
361
+ .then((value) => validateBrowserRelayCredential(value, this.#runtime.now()))
362
+ .catch(() => { throw browserUnavailable('Browser relay credential provider failed'); });
363
+ const timeout = this.#runtime.setTimer(() => {
364
+ child.controller.abort(browserUnavailable('Browser relay credential request timed out'));
365
+ }, Math.max(1, Math.min(timeoutMs, SESSION_PHASE_TIMEOUT_MS)));
366
+ const promise = Promise.race([provider, interrupted]);
367
+ let disposed = false;
368
+ const dispose = () => {
369
+ if (disposed)
370
+ return;
371
+ disposed = true;
372
+ timeout.cancel();
373
+ if (abort !== undefined)
374
+ child.controller.signal.removeEventListener('abort', abort);
375
+ child.dispose();
376
+ };
377
+ const credentialRequest = { controller: child.controller, dispose, promise };
378
+ this.#credentialRequest = credentialRequest;
379
+ void promise.finally(() => {
380
+ dispose();
381
+ if (this.#credentialRequest === credentialRequest)
382
+ this.#credentialRequest = undefined;
383
+ }).catch(() => undefined);
384
+ return promise;
385
+ }
386
+ #abortCredentialRequest() {
387
+ const request = this.#credentialRequest;
388
+ if (request === undefined)
389
+ return;
390
+ request.controller.abort(browserCancelled('not_dispatched'));
391
+ request.dispose();
392
+ this.#credentialRequest = undefined;
393
+ }
394
+ #scheduleReauthentication(expiresAtMs) {
395
+ this.#clearReauthentication();
396
+ if (this.#status === 'draining' || this.#status === 'stopping' || this.#status === 'stopped')
397
+ return;
398
+ const remaining = Math.max(0, expiresAtMs - this.#runtime.now());
399
+ const lead = Math.min(30_000, Math.floor(remaining / 2));
400
+ this.#reauthTimer = this.#runtime.setTimer(() => void this.#reauthenticate(expiresAtMs), remaining - lead);
401
+ }
402
+ async #reauthenticate(currentExpiresAtMs) {
403
+ const session = this.#session;
404
+ if (session === undefined || this.#loopController.signal.aborted)
405
+ return;
406
+ try {
407
+ const credential = await this.#getCredential('reauth', Math.max(1, currentExpiresAtMs - this.#runtime.now()));
408
+ if (!this.#mayReauthenticate(session))
409
+ return;
410
+ if (this.#sessionRelayUrl === undefined) {
411
+ throw browserUnavailable('Browser relay routing state is unavailable');
412
+ }
413
+ if (credential.relayUrl !== this.#sessionRelayUrl) {
414
+ if (this.#sessionEnd === undefined || this.#sessionEnd.settled) {
415
+ throw browserUnavailable('Browser relay handoff state is unavailable');
416
+ }
417
+ session.terminate();
418
+ this.#sessionEnd.resolve({ handoffCredential: credential });
419
+ return;
420
+ }
421
+ const remaining = Math.min(currentExpiresAtMs, credential.expiresAtMs)
422
+ - this.#runtime.now();
423
+ if (remaining <= 0)
424
+ throw browserUnavailable('Browser authentication lease expired');
425
+ const requestId = this.nextRequestId();
426
+ const deferred = createDeferred();
427
+ void deferred.promise.catch(() => undefined);
428
+ const timer = this.#runtime.setTimer(() => {
429
+ deferred.reject(browserUnavailable('Browser reauthentication timed out'));
430
+ }, Math.max(1, Math.min(remaining, SESSION_PHASE_TIMEOUT_MS)));
431
+ this.#reauthentication = {
432
+ requestId,
433
+ maximumExpiresAtMs: credential.expiresAtMs,
434
+ deferred,
435
+ timer,
436
+ };
437
+ await session.send({ opcode: Opcode.Reauth, payload: [requestId, credential.accessToken] });
438
+ const expiresAtMs = await deferred.promise;
439
+ if (this.#mayReauthenticate(session))
440
+ this.#scheduleReauthentication(expiresAtMs);
441
+ }
442
+ catch {
443
+ if (this.#mayReauthenticate(session)) {
444
+ this.transportFailure(browserUnavailable('Browser reauthentication failed'));
445
+ }
446
+ }
447
+ }
448
+ #mayReauthenticate(session) {
449
+ return this.#session === session
450
+ && !this.#loopController.signal.aborted
451
+ && this.#status !== 'draining';
452
+ }
453
+ async #waitForTransportClose(session) {
454
+ const interrupted = createDeferred();
455
+ const abort = () => interrupted.resolve(false);
456
+ if (this.#loopController.signal.aborted)
457
+ return false;
458
+ this.#loopController.signal.addEventListener('abort', abort, { once: true });
459
+ if (this.#loopController.signal.aborted)
460
+ abort();
461
+ const timeout = this.#runtime.setTimer(() => interrupted.resolve(false), SESSION_PHASE_TIMEOUT_MS);
462
+ try {
463
+ return await Promise.race([
464
+ session.waitForTransportClose().then(() => true),
465
+ interrupted.promise,
466
+ ]);
467
+ }
468
+ finally {
469
+ timeout.cancel();
470
+ this.#loopController.signal.removeEventListener('abort', abort);
471
+ }
472
+ }
473
+ #clearReauthentication() {
474
+ this.#reauthTimer?.cancel();
475
+ this.#reauthTimer = undefined;
476
+ this.#reauthentication?.timer.cancel();
477
+ this.#reauthentication?.deferred.reject(browserCancelled('not_dispatched'));
478
+ this.#reauthentication = undefined;
479
+ }
480
+ #handleFrame(frame) {
481
+ try {
482
+ if (frame.opcode >= 0x80)
483
+ return;
484
+ if (frame.opcode === Opcode.Error) {
485
+ this.#handleRelayError(frame);
486
+ return;
487
+ }
488
+ if (frame.opcode === Opcode.Fatal) {
489
+ const code = relayInteger(frame, 1, 'FATAL.code', 1);
490
+ const retryable = relayBoolean(frame, 2, 'FATAL.retryable');
491
+ const failure = browserRelayFailure(code, retryable, 'not_dispatched');
492
+ if (retryable)
493
+ this.transportFailure(failure);
494
+ else {
495
+ this.#emitFailure(failure);
496
+ this.#startDeferred?.reject(failure);
497
+ void this.stop();
498
+ }
499
+ return;
500
+ }
501
+ if (frame.opcode === Opcode.ReauthOk) {
502
+ const requestId = relayInteger(frame, 0, 'REAUTH_OK.requestId', 1);
503
+ if (this.#reauthentication?.requestId !== requestId) {
504
+ throw browserProtocolFailure('REAUTH_OK is not correlated');
505
+ }
506
+ const expiresAtMs = relayInteger(frame, 1, 'REAUTH_OK.expiresAtMs', 1);
507
+ if (expiresAtMs <= this.#runtime.now()
508
+ || expiresAtMs > this.#reauthentication.maximumExpiresAtMs) {
509
+ throw browserProtocolFailure('REAUTH_OK expiry is outside the credential lease');
510
+ }
511
+ this.#reauthentication.timer.cancel();
512
+ this.#reauthentication.deferred.resolve(expiresAtMs);
513
+ this.#reauthentication = undefined;
514
+ return;
515
+ }
516
+ if (frame.opcode === Opcode.Goaway) {
517
+ this.#transition('draining');
518
+ this.#reauthTimer?.cancel();
519
+ this.#reauthTimer = undefined;
520
+ this.#abortCredentialRequest();
521
+ this.#goawayRetryAfterMs = relayInteger(frame, 0, 'GOAWAY.retryAfterMs');
522
+ return;
523
+ }
524
+ if (frame.opcode === Opcode.HomeStatus) {
525
+ this.#setHomeStatus(parseUserHomeStatus(frame.payload[0], frame.payload[1], false));
526
+ return;
527
+ }
528
+ if (frame.opcode === Opcode.TopicDict) {
529
+ if (this.#session === undefined || !sameEpoch(this.#session.welcome.epoch, frame.payload[0])) {
530
+ throw browserProtocolFailure('TOPIC_DICT uses a stale epoch');
531
+ }
532
+ if (frame.payload[1] === true) {
533
+ this.#topicReady = true;
534
+ this.#maybeReady();
535
+ }
536
+ return;
537
+ }
538
+ if (this.state.handleFrame(frame) || this.calls.handleFrame(frame))
539
+ return;
540
+ throw browserProtocolFailure('Relay sent an unsupported user frame');
541
+ }
542
+ catch (error) {
543
+ this.transportFailure(error instanceof Error ? error : browserProtocolFailure());
544
+ }
545
+ }
546
+ #handleRelayError(frame) {
547
+ const correlationId = relayInteger(frame, 0, 'ERROR.correlationId');
548
+ const sourceOpcode = relayInteger(frame, 1, 'ERROR.sourceOpcode');
549
+ const code = relayInteger(frame, 2, 'ERROR.code', 1);
550
+ const retryable = relayBoolean(frame, 3, 'ERROR.retryable');
551
+ if (sourceOpcode === Opcode.StateResync
552
+ && this.state.handleError(correlationId, code, retryable))
553
+ return;
554
+ if ((sourceOpcode === Opcode.Call || sourceOpcode === Opcode.CallCancel)
555
+ && this.calls.handleError(correlationId, code, retryable))
556
+ return;
557
+ if (sourceOpcode === Opcode.CallError
558
+ && this.calls.handleResponseError(correlationId, code, retryable))
559
+ return;
560
+ if (sourceOpcode === Opcode.Reauth && this.#reauthentication?.requestId === correlationId) {
561
+ this.#reauthentication.timer.cancel();
562
+ this.#reauthentication.deferred.reject(browserRelayFailure(code, retryable, 'not_dispatched'));
563
+ this.#reauthentication = undefined;
564
+ return;
565
+ }
566
+ if (correlationId !== 0 || sourceOpcode !== 0) {
567
+ throw browserProtocolFailure('ERROR is not correlated to an active operation');
568
+ }
569
+ this.#emitFailure(browserRelayFailure(code, retryable, 'not_dispatched'));
570
+ }
571
+ #maybeReady() {
572
+ const session = this.#session;
573
+ if (this.#status !== 'synchronizing'
574
+ || !this.#stateReady
575
+ || !this.#functionReady
576
+ || !this.#topicReady
577
+ || session === undefined)
578
+ return;
579
+ const ready = session.welcome.readySession;
580
+ this.#bootstrapTimer?.cancel();
581
+ this.#bootstrapTimer = undefined;
582
+ this.#transition('ready', ready);
583
+ this.#sessionReady?.resolve(undefined);
584
+ this.#startDeferred?.resolve(ready);
585
+ }
586
+ #disconnectSession() {
587
+ this.#deactivateSession();
588
+ this.#session?.detach();
589
+ this.#session = undefined;
590
+ this.#sessionRelayUrl = undefined;
591
+ this.#sessionEnd = undefined;
592
+ this.#sessionReady = undefined;
593
+ this.#goawayRetryAfterMs = undefined;
594
+ this.#stateReady = false;
595
+ this.#functionReady = false;
596
+ this.#topicReady = false;
597
+ }
598
+ #deactivateSession() {
599
+ this.#bootstrapTimer?.cancel();
600
+ this.#bootstrapTimer = undefined;
601
+ this.#clearReauthentication();
602
+ this.#abortCredentialRequest();
603
+ this.state.disconnected();
604
+ this.calls.disconnected();
605
+ this.#markHomeStale();
606
+ }
607
+ #emitFailure(failure) {
608
+ this.#errorListeners.emit(failure, () => {
609
+ safeBrowserLog(this.#options.logger, { level: 'error', event: 'error_listener_failed' });
610
+ });
611
+ }
612
+ #subscribeHome(listener) {
613
+ if (typeof listener !== 'function')
614
+ throw new TypeError('home listener must be a function');
615
+ const remove = this.#homeListeners.subscribe(listener);
616
+ if (this.#homeStatus !== undefined) {
617
+ try {
618
+ listener(this.#homeStatus);
619
+ }
620
+ catch {
621
+ this.#emitFailure(browserInternalFailure());
622
+ }
623
+ }
624
+ return remove;
625
+ }
626
+ #setHomeStatus(status) {
627
+ this.#homeStatus = status;
628
+ this.#homeListeners.emit(status, () => this.#emitFailure(browserInternalFailure()));
629
+ }
630
+ #markHomeStale() {
631
+ const status = this.#homeStatus;
632
+ if (status === undefined || status.stale)
633
+ return;
634
+ this.#setHomeStatus(Object.freeze({ ...status, stale: true }));
635
+ }
636
+ #transition(current, session, reason) {
637
+ if ((this.#status === 'stopping' || this.#status === 'stopped') && current !== 'stopped')
638
+ return;
639
+ if (this.#status === current)
640
+ return;
641
+ const previous = this.#status;
642
+ this.#status = current;
643
+ const event = session === undefined && reason === undefined
644
+ ? Object.freeze({ previous, current })
645
+ : session === undefined
646
+ ? Object.freeze({ previous, current, reason })
647
+ : reason === undefined
648
+ ? Object.freeze({ previous, current, session })
649
+ : Object.freeze({ previous, current, session, reason });
650
+ safeBrowserLog(this.#options.logger, { level: 'info', event: 'status_changed', status: current });
651
+ this.#lifecycleListeners.emit(event, () => {
652
+ safeBrowserLog(this.#options.logger, { level: 'error', event: 'lifecycle_listener_failed' });
653
+ });
654
+ }
655
+ #finishStop() {
656
+ if (this.#status === 'stopped')
657
+ return;
658
+ this.#stopTimer?.cancel();
659
+ this.#stopTimer = undefined;
660
+ this.#disconnectSession();
661
+ if (this.#startSignal !== undefined && this.#startAbort !== undefined) {
662
+ this.#startSignal.removeEventListener('abort', this.#startAbort);
663
+ }
664
+ this.#transition('stopped');
665
+ this.#stopDeferred?.resolve(undefined);
666
+ this.#lifecycleListeners.clear();
667
+ this.#errorListeners.clear();
668
+ this.#homeListeners.clear();
669
+ }
670
+ }
671
+ export function createBrowserClient(options) {
672
+ return new BrowserClientImpl(validateBrowserClientOptions(options), createBrowserRuntime());
673
+ }
674
+ /** @internal */
675
+ export function createBrowserClientWithRuntime(options, runtime) {
676
+ return new BrowserClientImpl(validateBrowserClientOptions(options), runtime);
677
+ }
678
+ //# sourceMappingURL=browser-client.js.map