@sovereignbase/station-client 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,346 @@
1
+ /*
2
+ * Copyright 2026 Sovereignbase
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+
18
+ // src/StationClient/class.ts
19
+ import { encode, decode } from "@msgpack/msgpack";
20
+ var StationClient = class {
21
+ eventTarget = new EventTarget();
22
+ lockName;
23
+ channelName;
24
+ webSocketUrl;
25
+ instanceId = self.crypto.randomUUID();
26
+ onlineHandler = () => {
27
+ void this.opportunisticConnect();
28
+ };
29
+ broadcastChannel = null;
30
+ webSocket = null;
31
+ isLeader = false;
32
+ isClosed = false;
33
+ isConnecting = false;
34
+ outboundQueue = [];
35
+ pendingTransacts = /* @__PURE__ */ new Map();
36
+ pendingTransactTargets = /* @__PURE__ */ new Map();
37
+ /**
38
+ * Initializes a new {@link StationClient} instance.
39
+ *
40
+ * @param webSocketUrl The base station WebSocket URL. When omitted, the instance operates in local-only mode.
41
+ */
42
+ constructor(webSocketUrl = "") {
43
+ this.webSocketUrl = webSocketUrl;
44
+ this.channelName = `origin-channel-lock::${this.webSocketUrl}`;
45
+ this.lockName = `origin-channel-lock::${this.webSocketUrl}`;
46
+ this.broadcastChannel = new BroadcastChannel(this.channelName);
47
+ this.broadcastChannel.onmessage = (event) => {
48
+ const envelope = event.data;
49
+ if (!envelope) return;
50
+ if (envelope.kind === "relay") {
51
+ this.eventTarget.dispatchEvent(
52
+ new CustomEvent("message", { detail: envelope.message })
53
+ );
54
+ if (!this.isLeader) return;
55
+ this.sendToStation(envelope.message);
56
+ return;
57
+ }
58
+ if (envelope.kind === "transact-response") {
59
+ if (envelope.target !== this.instanceId) return;
60
+ const pending = this.pendingTransacts.get(envelope.id);
61
+ if (!pending) return;
62
+ this.pendingTransacts.delete(envelope.id);
63
+ pending.cleanup();
64
+ pending.resolve(envelope.message);
65
+ return;
66
+ }
67
+ if (envelope.kind === "transact-abort") {
68
+ if (!this.isLeader) return;
69
+ const pendingTarget2 = this.pendingTransactTargets.get(envelope.id);
70
+ if (pendingTarget2) clearTimeout(pendingTarget2.timeoutId);
71
+ this.pendingTransactTargets.delete(envelope.id);
72
+ return;
73
+ }
74
+ if (!this.isLeader) return;
75
+ if (!this.webSocketUrl || self.navigator.onLine !== true || !this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {
76
+ this.broadcastChannel?.postMessage({
77
+ kind: "transact-response",
78
+ id: envelope.id,
79
+ target: envelope.source,
80
+ message: false
81
+ });
82
+ return;
83
+ }
84
+ const pendingTarget = this.pendingTransactTargets.get(envelope.id);
85
+ if (pendingTarget) clearTimeout(pendingTarget.timeoutId);
86
+ this.pendingTransactTargets.set(envelope.id, {
87
+ target: envelope.source,
88
+ timeoutId: setTimeout(() => {
89
+ this.pendingTransactTargets.delete(envelope.id);
90
+ }, envelope.ttlMs ?? 3e4)
91
+ });
92
+ this.sendToStation([
93
+ "station-client-request",
94
+ envelope.id,
95
+ envelope.message
96
+ ]);
97
+ };
98
+ if (this.webSocketUrl && navigator.onLine) void this.opportunisticConnect();
99
+ if (this.webSocketUrl) {
100
+ self.addEventListener("online", this.onlineHandler);
101
+ }
102
+ }
103
+ /**main methods*/
104
+ /**
105
+ * Broadcasts a message to other same-origin contexts and opportunistically forwards it to the base station.
106
+ *
107
+ * @param message The message to broadcast.
108
+ */
109
+ relay(message) {
110
+ if (this.isClosed) return;
111
+ this.broadcastChannel?.postMessage({ kind: "relay", message });
112
+ this.sendToStation(message);
113
+ }
114
+ /**
115
+ * Sends a request to the base station and resolves with the corresponding response message.
116
+ *
117
+ * @param message The message to send.
118
+ * @param options Options that control cancellation and stale follower cleanup.
119
+ * @returns A promise that resolves with the response message, or `false` when the request cannot be issued.
120
+ */
121
+ transact(message, options = {}) {
122
+ if (this.isClosed) return Promise.resolve(false);
123
+ const id = self.crypto.randomUUID();
124
+ const { signal, ttlMs } = options;
125
+ return new Promise((resolve, reject) => {
126
+ const abortReason = () => signal?.reason ?? new DOMException("The operation was aborted.", "AbortError");
127
+ if (signal?.aborted) {
128
+ reject(abortReason());
129
+ return;
130
+ }
131
+ if (!this.webSocketUrl || self.navigator.onLine !== true) {
132
+ resolve(false);
133
+ return;
134
+ }
135
+ if (this.isLeader && (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN)) {
136
+ resolve(false);
137
+ return;
138
+ }
139
+ const handleAbort = () => {
140
+ this.pendingTransacts.delete(id);
141
+ const pendingTarget = this.pendingTransactTargets.get(id);
142
+ if (pendingTarget) clearTimeout(pendingTarget.timeoutId);
143
+ this.pendingTransactTargets.delete(id);
144
+ signal?.removeEventListener("abort", handleAbort);
145
+ if (!this.isLeader) {
146
+ this.broadcastChannel?.postMessage({ kind: "transact-abort", id });
147
+ }
148
+ reject(abortReason());
149
+ };
150
+ this.pendingTransacts.set(id, {
151
+ resolve,
152
+ reject,
153
+ cleanup: () => {
154
+ signal?.removeEventListener("abort", handleAbort);
155
+ }
156
+ });
157
+ signal?.addEventListener("abort", handleAbort, { once: true });
158
+ if (this.isLeader) {
159
+ this.sendToStation(["station-client-request", id, message]);
160
+ return;
161
+ }
162
+ this.broadcastChannel?.postMessage({
163
+ kind: "transact",
164
+ id,
165
+ source: this.instanceId,
166
+ ttlMs,
167
+ message
168
+ });
169
+ });
170
+ }
171
+ /**
172
+ * Closes the client and releases its local and remote resources.
173
+ */
174
+ close() {
175
+ const wasLeader = this.isLeader;
176
+ const broadcastChannel = this.broadcastChannel;
177
+ this.isClosed = true;
178
+ self.removeEventListener("online", this.onlineHandler);
179
+ if (!wasLeader) {
180
+ for (const id of this.pendingTransacts.keys()) {
181
+ try {
182
+ broadcastChannel?.postMessage({ kind: "transact-abort", id });
183
+ } catch {
184
+ }
185
+ }
186
+ }
187
+ try {
188
+ broadcastChannel?.close();
189
+ } catch {
190
+ }
191
+ try {
192
+ this.webSocket?.close(1e3, "closed");
193
+ } catch {
194
+ }
195
+ this.broadcastChannel = null;
196
+ this.webSocket = null;
197
+ this.isLeader = false;
198
+ this.outboundQueue.length = 0;
199
+ for (const pending of this.pendingTransacts.values()) {
200
+ pending.cleanup();
201
+ pending.reject(new Error("Station client closed"));
202
+ }
203
+ this.pendingTransacts.clear();
204
+ for (const pendingTarget of this.pendingTransactTargets.values()) {
205
+ clearTimeout(pendingTarget.timeoutId);
206
+ }
207
+ this.pendingTransactTargets.clear();
208
+ }
209
+ /**listeners*/
210
+ /**
211
+ * Appends an event listener for events whose type attribute value is `type`.
212
+ *
213
+ * @param type The event type to listen for.
214
+ * @param listener The callback that receives the event.
215
+ * @param options An options object that specifies characteristics about the event listener.
216
+ */
217
+ addEventListener(type, listener, options) {
218
+ this.eventTarget.addEventListener(
219
+ type,
220
+ listener,
221
+ options
222
+ );
223
+ }
224
+ /**
225
+ * Removes an event listener previously registered with {@link addEventListener}.
226
+ *
227
+ * @param type The event type to remove.
228
+ * @param listener The callback to remove.
229
+ * @param options An options object that specifies characteristics about the event listener.
230
+ */
231
+ removeEventListener(type, listener, options) {
232
+ this.eventTarget.removeEventListener(
233
+ type,
234
+ listener,
235
+ options
236
+ );
237
+ }
238
+ /**helpers*/
239
+ sendToStation(message) {
240
+ if (!this.isLeader || !this.webSocketUrl) return;
241
+ if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {
242
+ if (self.navigator.onLine) {
243
+ if (this.outboundQueue.length >= 64) this.outboundQueue.shift();
244
+ this.outboundQueue.push(message);
245
+ }
246
+ return;
247
+ }
248
+ try {
249
+ this.webSocket.send(encode(message));
250
+ } catch {
251
+ }
252
+ }
253
+ flushOutboundQueue() {
254
+ if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) return;
255
+ while (this.outboundQueue.length > 0) {
256
+ const message = this.outboundQueue.shift();
257
+ if (!message) continue;
258
+ try {
259
+ this.webSocket.send(encode(message));
260
+ } catch {
261
+ this.outboundQueue.unshift(message);
262
+ return;
263
+ }
264
+ }
265
+ }
266
+ async opportunisticConnect() {
267
+ if (this.isClosed || this.isConnecting || !this.webSocketUrl) return;
268
+ if (!self.navigator.locks) return;
269
+ this.isConnecting = true;
270
+ try {
271
+ while (!this.isClosed) {
272
+ if (self.navigator.onLine !== true) return;
273
+ await self.navigator.locks.request(
274
+ this.lockName,
275
+ { ifAvailable: true },
276
+ async (lockHandle) => {
277
+ if (!lockHandle || this.isClosed) return;
278
+ this.isLeader = true;
279
+ let socket;
280
+ try {
281
+ socket = new WebSocket(this.webSocketUrl);
282
+ } catch {
283
+ this.isLeader = false;
284
+ this.webSocket = null;
285
+ return;
286
+ }
287
+ socket.binaryType = "arraybuffer";
288
+ this.webSocket = socket;
289
+ socket.onopen = () => {
290
+ this.flushOutboundQueue();
291
+ };
292
+ socket.onmessage = (event) => {
293
+ const message = decode(event.data);
294
+ if (!message) return;
295
+ if (Array.isArray(message) && message[0] === "station-client-response" && typeof message[1] === "string") {
296
+ const id = message[1];
297
+ const pendingTarget = this.pendingTransactTargets.get(id);
298
+ if (pendingTarget) {
299
+ clearTimeout(pendingTarget.timeoutId);
300
+ this.pendingTransactTargets.delete(id);
301
+ this.broadcastChannel?.postMessage({
302
+ kind: "transact-response",
303
+ id,
304
+ target: pendingTarget.target,
305
+ message: message[2]
306
+ });
307
+ return;
308
+ }
309
+ const pending = this.pendingTransacts.get(id);
310
+ if (!pending) return;
311
+ this.pendingTransacts.delete(id);
312
+ pending.cleanup();
313
+ pending.resolve(message[2]);
314
+ return;
315
+ }
316
+ this.eventTarget.dispatchEvent(
317
+ new CustomEvent("message", { detail: message })
318
+ );
319
+ this.broadcastChannel?.postMessage({
320
+ kind: "relay",
321
+ message
322
+ });
323
+ };
324
+ socket.onclose = () => {
325
+ if (this.webSocket === socket) this.webSocket = null;
326
+ this.isLeader = false;
327
+ };
328
+ await new Promise((resolve) => {
329
+ socket.addEventListener("close", () => resolve(), { once: true });
330
+ });
331
+ this.isLeader = false;
332
+ if (this.webSocket === socket) this.webSocket = null;
333
+ }
334
+ );
335
+ if (this.isClosed || self.navigator.onLine !== true) return;
336
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
337
+ }
338
+ } finally {
339
+ this.isConnecting = false;
340
+ }
341
+ }
342
+ };
343
+ export {
344
+ StationClient
345
+ };
346
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/StationClient/class.ts"],"sourcesContent":["import { encode, decode } from '@msgpack/msgpack'\nimport type {\n StationClientEventMap,\n StationClientEventListenerFor,\n StationClientLocalMessageShape,\n StationClientPendingTransact,\n StationClientPendingTransactTarget,\n StationClientRemoteMessageShape,\n StationClientTransactOptions,\n} from '../.types/index.js'\n\n/**\n * Represents a base station client that coordinates local tab messaging and an opportunistic base station transport.\n *\n * @template T The application message shape.\n */\nexport class StationClient<T extends Record<string, unknown>> {\n private readonly eventTarget = new EventTarget()\n private readonly lockName: string\n private readonly channelName: string\n private readonly webSocketUrl: string\n private readonly instanceId = self.crypto.randomUUID()\n private readonly onlineHandler = () => {\n void this.opportunisticConnect()\n }\n private broadcastChannel: BroadcastChannel | null = null\n private webSocket: WebSocket | null = null\n private isLeader: boolean = false\n private isClosed: boolean = false\n private isConnecting: boolean = false\n private readonly outboundQueue: StationClientRemoteMessageShape<T>[] = []\n private readonly pendingTransacts = new Map<\n string,\n StationClientPendingTransact<T>\n >()\n private readonly pendingTransactTargets = new Map<\n string,\n StationClientPendingTransactTarget\n >()\n\n /**\n * Initializes a new {@link StationClient} instance.\n *\n * @param webSocketUrl The base station WebSocket URL. When omitted, the instance operates in local-only mode.\n */\n constructor(webSocketUrl: string = '') {\n this.webSocketUrl = webSocketUrl\n this.channelName = `origin-channel-lock::${this.webSocketUrl}`\n this.lockName = `origin-channel-lock::${this.webSocketUrl}`\n\n this.broadcastChannel = new BroadcastChannel(this.channelName)\n this.broadcastChannel.onmessage = (\n event: MessageEvent<StationClientLocalMessageShape<T>>\n ) => {\n const envelope = event.data\n if (!envelope) return\n\n if (envelope.kind === 'relay') {\n this.eventTarget.dispatchEvent(\n new CustomEvent('message', { detail: envelope.message })\n )\n if (!this.isLeader) return\n\n this.sendToStation(envelope.message)\n return\n }\n\n if (envelope.kind === 'transact-response') {\n if (envelope.target !== this.instanceId) return\n\n const pending = this.pendingTransacts.get(envelope.id)\n if (!pending) return\n\n this.pendingTransacts.delete(envelope.id)\n pending.cleanup()\n pending.resolve(envelope.message)\n return\n }\n\n if (envelope.kind === 'transact-abort') {\n if (!this.isLeader) return\n\n const pendingTarget = this.pendingTransactTargets.get(envelope.id)\n if (pendingTarget) clearTimeout(pendingTarget.timeoutId)\n this.pendingTransactTargets.delete(envelope.id)\n return\n }\n\n if (!this.isLeader) return\n\n if (\n !this.webSocketUrl ||\n self.navigator.onLine !== true ||\n !this.webSocket ||\n this.webSocket.readyState !== WebSocket.OPEN\n ) {\n this.broadcastChannel?.postMessage({\n kind: 'transact-response',\n id: envelope.id,\n target: envelope.source,\n message: false,\n })\n return\n }\n\n const pendingTarget = this.pendingTransactTargets.get(envelope.id)\n if (pendingTarget) clearTimeout(pendingTarget.timeoutId)\n\n this.pendingTransactTargets.set(envelope.id, {\n target: envelope.source,\n timeoutId: setTimeout(() => {\n this.pendingTransactTargets.delete(envelope.id)\n }, envelope.ttlMs ?? 30_000),\n })\n this.sendToStation([\n 'station-client-request',\n envelope.id,\n envelope.message,\n ])\n }\n\n if (this.webSocketUrl && navigator.onLine) void this.opportunisticConnect()\n if (this.webSocketUrl) {\n self.addEventListener('online', this.onlineHandler)\n }\n }\n /**main methods*/\n\n /**\n * Broadcasts a message to other same-origin contexts and opportunistically forwards it to the base station.\n *\n * @param message The message to broadcast.\n */\n relay(message: T) {\n if (this.isClosed) return\n\n this.broadcastChannel?.postMessage({ kind: 'relay', message })\n this.sendToStation(message)\n }\n\n /**\n * Sends a request to the base station and resolves with the corresponding response message.\n *\n * @param message The message to send.\n * @param options Options that control cancellation and stale follower cleanup.\n * @returns A promise that resolves with the response message, or `false` when the request cannot be issued.\n */\n transact(\n message: T,\n options: StationClientTransactOptions = {}\n ): Promise<T | false> {\n if (this.isClosed) return Promise.resolve(false)\n\n const id = self.crypto.randomUUID()\n const { signal, ttlMs } = options\n\n return new Promise<T | false>((resolve, reject) => {\n const abortReason = () =>\n signal?.reason ??\n new DOMException('The operation was aborted.', 'AbortError')\n\n if (signal?.aborted) {\n reject(abortReason())\n return\n }\n\n if (!this.webSocketUrl || self.navigator.onLine !== true) {\n resolve(false)\n return\n }\n\n if (\n this.isLeader &&\n (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN)\n ) {\n resolve(false)\n return\n }\n\n const handleAbort = () => {\n this.pendingTransacts.delete(id)\n const pendingTarget = this.pendingTransactTargets.get(id)\n if (pendingTarget) clearTimeout(pendingTarget.timeoutId)\n this.pendingTransactTargets.delete(id)\n signal?.removeEventListener('abort', handleAbort)\n\n if (!this.isLeader) {\n this.broadcastChannel?.postMessage({ kind: 'transact-abort', id })\n }\n\n reject(abortReason())\n }\n\n this.pendingTransacts.set(id, {\n resolve,\n reject,\n cleanup: () => {\n signal?.removeEventListener('abort', handleAbort)\n },\n })\n signal?.addEventListener('abort', handleAbort, { once: true })\n\n if (this.isLeader) {\n this.sendToStation(['station-client-request', id, message])\n return\n }\n\n this.broadcastChannel?.postMessage({\n kind: 'transact',\n id,\n source: this.instanceId,\n ttlMs,\n message,\n })\n })\n }\n\n /**\n * Closes the client and releases its local and remote resources.\n */\n close(): void {\n const wasLeader = this.isLeader\n const broadcastChannel = this.broadcastChannel\n this.isClosed = true\n self.removeEventListener('online', this.onlineHandler)\n\n if (!wasLeader) {\n for (const id of this.pendingTransacts.keys()) {\n try {\n broadcastChannel?.postMessage({ kind: 'transact-abort', id })\n } catch {}\n }\n }\n\n try {\n broadcastChannel?.close()\n } catch {}\n try {\n this.webSocket?.close(1000, 'closed')\n } catch {}\n\n this.broadcastChannel = null\n this.webSocket = null\n this.isLeader = false\n this.outboundQueue.length = 0\n for (const pending of this.pendingTransacts.values()) {\n pending.cleanup()\n pending.reject(new Error('Station client closed'))\n }\n this.pendingTransacts.clear()\n for (const pendingTarget of this.pendingTransactTargets.values()) {\n clearTimeout(pendingTarget.timeoutId)\n }\n this.pendingTransactTargets.clear()\n }\n\n /**listeners*/\n\n /**\n * Appends an event listener for events whose type attribute value is `type`.\n *\n * @param type The event type to listen for.\n * @param listener The callback that receives the event.\n * @param options An options object that specifies characteristics about the event listener.\n */\n addEventListener<K extends keyof StationClientEventMap<T>>(\n type: K,\n listener: StationClientEventListenerFor<T, K> | null,\n options?: boolean | AddEventListenerOptions\n ): void {\n this.eventTarget.addEventListener(\n type,\n listener as EventListenerOrEventListenerObject | null,\n options\n )\n }\n\n /**\n * Removes an event listener previously registered with {@link addEventListener}.\n *\n * @param type The event type to remove.\n * @param listener The callback to remove.\n * @param options An options object that specifies characteristics about the event listener.\n */\n removeEventListener<K extends keyof StationClientEventMap<T>>(\n type: K,\n listener: StationClientEventListenerFor<T, K> | null,\n options?: boolean | EventListenerOptions\n ): void {\n this.eventTarget.removeEventListener(\n type,\n listener as EventListenerOrEventListenerObject | null,\n options\n )\n }\n\n /**helpers*/\n\n private sendToStation(message: StationClientRemoteMessageShape<T>) {\n if (!this.isLeader || !this.webSocketUrl) return\n\n if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) {\n if (self.navigator.onLine) {\n if (this.outboundQueue.length >= 64) this.outboundQueue.shift()\n this.outboundQueue.push(message)\n }\n return\n }\n\n try {\n this.webSocket.send(encode(message))\n } catch {}\n }\n\n private flushOutboundQueue() {\n if (!this.webSocket || this.webSocket.readyState !== WebSocket.OPEN) return\n\n while (this.outboundQueue.length > 0) {\n const message = this.outboundQueue.shift()\n if (!message) continue\n\n try {\n this.webSocket.send(encode(message))\n } catch {\n this.outboundQueue.unshift(message)\n return\n }\n }\n }\n\n private async opportunisticConnect() {\n if (this.isClosed || this.isConnecting || !this.webSocketUrl) return\n if (!self.navigator.locks) return\n\n this.isConnecting = true\n\n try {\n while (!this.isClosed) {\n if (self.navigator.onLine !== true) return\n\n await self.navigator.locks.request(\n this.lockName,\n { ifAvailable: true },\n async (lockHandle) => {\n if (!lockHandle || this.isClosed) return\n this.isLeader = true\n\n let socket: WebSocket\n\n try {\n socket = new WebSocket(this.webSocketUrl)\n } catch {\n this.isLeader = false\n this.webSocket = null\n return\n }\n\n socket.binaryType = 'arraybuffer'\n this.webSocket = socket\n\n socket.onopen = () => {\n this.flushOutboundQueue()\n }\n\n socket.onmessage = (event: MessageEvent<ArrayBuffer>) => {\n const message = decode(event.data)\n if (!message) return\n\n if (\n Array.isArray(message) &&\n message[0] === 'station-client-response' &&\n typeof message[1] === 'string'\n ) {\n const id = message[1]\n const pendingTarget = this.pendingTransactTargets.get(id)\n if (pendingTarget) {\n clearTimeout(pendingTarget.timeoutId)\n this.pendingTransactTargets.delete(id)\n\n this.broadcastChannel?.postMessage({\n kind: 'transact-response',\n id,\n target: pendingTarget.target,\n message: message[2] as T,\n })\n return\n }\n\n const pending = this.pendingTransacts.get(id)\n if (!pending) return\n\n this.pendingTransacts.delete(id)\n pending.cleanup()\n pending.resolve(message[2] as T)\n return\n }\n\n this.eventTarget.dispatchEvent(\n new CustomEvent('message', { detail: message })\n )\n\n this.broadcastChannel?.postMessage({\n kind: 'relay',\n message: message as T,\n })\n }\n\n socket.onclose = () => {\n if (this.webSocket === socket) this.webSocket = null\n this.isLeader = false\n }\n\n await new Promise<void>((resolve) => {\n socket.addEventListener('close', () => resolve(), { once: true })\n })\n\n this.isLeader = false\n if (this.webSocket === socket) this.webSocket = null\n }\n )\n\n if (this.isClosed || self.navigator.onLine !== true) return\n await new Promise<void>((resolve) => setTimeout(resolve, 10_000))\n }\n } finally {\n this.isConnecting = false\n }\n }\n}\n"],"mappings":"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","names":["pendingTarget"]}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "type": "module",
3
+ "license": "Apache-2.0",
4
+ "name": "@sovereignbase/station-client",
5
+ "version": "0.0.0",
6
+ "description": "Local-first station client for tabs and workers sharing an opportunistic Sovereignbase base station transport.",
7
+ "keywords": [
8
+ "typescript",
9
+ "sovereignbase",
10
+ "station-client",
11
+ "base-station",
12
+ "local-first",
13
+ "broadcastchannel",
14
+ "websocket",
15
+ "tab-sync",
16
+ "worker"
17
+ ],
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs",
26
+ "default": "./dist/index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "LICENSE",
36
+ "README.md"
37
+ ],
38
+ "sideEffects": false,
39
+ "scripts": {
40
+ "format": "prettier . --write",
41
+ "build": "tsup && node in-browser-testing-build.js",
42
+ "test": "npm run build && node test/run-coverage.mjs && node test/e2e/run.mjs",
43
+ "prepare": "husky",
44
+ "prepublishOnly": "npm run test"
45
+ },
46
+ "c8": {
47
+ "all": true,
48
+ "include": [
49
+ "dist/**/*.js"
50
+ ],
51
+ "exclude": [
52
+ "dist/**/*.d.ts",
53
+ "test/**",
54
+ "benchmark/**",
55
+ "playwright.config.ts"
56
+ ]
57
+ },
58
+ "devDependencies": {
59
+ "@commitlint/cli": "^20.5.0",
60
+ "@commitlint/config-conventional": "^20.5.0",
61
+ "@playwright/test": "^1.59.1",
62
+ "@sovereignbase/convergent-replicated-struct": "^1.0.1",
63
+ "@types/node": "^25.5.2",
64
+ "c8": "^11.0.0",
65
+ "edge-runtime": "^4.0.1",
66
+ "fast-glob": "^3.3.3",
67
+ "husky": "^9.1.7",
68
+ "playwright": "^1.59.1",
69
+ "prettier": "^3.8.1",
70
+ "tsup": "^8.5.1",
71
+ "typescript": "^5.9.3",
72
+ "wrangler": "^4.80.0"
73
+ },
74
+ "dependencies": {
75
+ "@msgpack/msgpack": "^3.1.3"
76
+ },
77
+ "repository": {
78
+ "type": "git",
79
+ "url": "git+https://github.com/sovereignbase/station-client.git"
80
+ },
81
+ "bugs": {
82
+ "url": "https://github.com/sovereignbase/station-client/issues"
83
+ },
84
+ "homepage": "https://sovereignbase.dev/station-client"
85
+ }