@frockbot/applet-sdk 0.3.24 → 0.3.26
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/package.json +1 -1
- package/src/client/transport.ts +74 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/applet-sdk",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.26",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the `applet` CLI.",
|
package/src/client/transport.ts
CHANGED
|
@@ -96,6 +96,23 @@ export class AppletTransport {
|
|
|
96
96
|
#closed = false;
|
|
97
97
|
#resyncQueued = false;
|
|
98
98
|
#txnSeq = 0;
|
|
99
|
+
/**
|
|
100
|
+
* Every socket this transport has opened gets a number, and only the newest
|
|
101
|
+
* one owns the shared connection state. `error` and `close` both fire for a
|
|
102
|
+
* single failure, and a socket the transport has already given up on can
|
|
103
|
+
* still call back later, so a callback carrying a superseded identity is
|
|
104
|
+
* dropped instead of reconnecting or clearing state a live socket owns.
|
|
105
|
+
*/
|
|
106
|
+
#socketSeq = 0;
|
|
107
|
+
#currentSocketId = 0;
|
|
108
|
+
/**
|
|
109
|
+
* At most one reconnect is outstanding. The scheduler seam cannot cancel a
|
|
110
|
+
* timer, so a retry carries an identity too: `#pendingRetryId` is cleared or
|
|
111
|
+
* replaced whenever the retry is superseded, and a closure that fires with a
|
|
112
|
+
* stale identity does nothing.
|
|
113
|
+
*/
|
|
114
|
+
#retrySeq = 0;
|
|
115
|
+
#pendingRetryId = 0;
|
|
99
116
|
|
|
100
117
|
constructor(options: AppletTransportOptions = {}) {
|
|
101
118
|
this.#options = {
|
|
@@ -116,23 +133,54 @@ export class AppletTransport {
|
|
|
116
133
|
return () => this.#listeners.delete(listener);
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Open the socket. Called by the dev runner, the tests, and the `init`
|
|
138
|
+
* bridge. Calling it again — a fresh viewer token after the old one expired,
|
|
139
|
+
* say — supersedes whatever socket or pending reconnect is outstanding, so a
|
|
140
|
+
* reconnect never races the connection the caller just asked for.
|
|
141
|
+
*/
|
|
120
142
|
connect(init: AppletInitV1): void {
|
|
121
143
|
this.#init = init;
|
|
122
144
|
this.#closed = false;
|
|
145
|
+
this.#reset();
|
|
146
|
+
this.#attempt = 0;
|
|
123
147
|
this.#open();
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
close(): void {
|
|
127
151
|
this.#closed = true;
|
|
128
|
-
this.#
|
|
129
|
-
this.#socket = undefined;
|
|
152
|
+
this.#reset(1000, "closed");
|
|
130
153
|
this.#failPending(new Error("The Applet connection was closed"));
|
|
131
154
|
this.#setState({ status: "closed" });
|
|
132
155
|
}
|
|
133
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Give up the current socket and any pending reconnect. The socket is closed
|
|
159
|
+
* rather than merely forgotten, so a superseded connection cannot keep
|
|
160
|
+
* receiving frames, and its late `close` arrives with a stale identity.
|
|
161
|
+
*/
|
|
162
|
+
#reset(code = 1000, reason = "superseded"): void {
|
|
163
|
+
this.#pendingRetryId = 0;
|
|
164
|
+
const socket = this.#socket;
|
|
165
|
+
this.#socket = undefined;
|
|
166
|
+
this.#currentSocketId = 0;
|
|
167
|
+
this.#synced = false;
|
|
168
|
+
if (socket) {
|
|
169
|
+
try {
|
|
170
|
+
socket.close(code, reason);
|
|
171
|
+
} catch {
|
|
172
|
+
// A socket that is already gone needs no closing.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
134
177
|
#open(): void {
|
|
135
178
|
if (!this.#init || this.#closed) return;
|
|
179
|
+
// Never open while another attempt is current.
|
|
180
|
+
if (this.#socket) return;
|
|
181
|
+
this.#pendingRetryId = 0;
|
|
182
|
+
const id = ++this.#socketSeq;
|
|
183
|
+
this.#currentSocketId = id;
|
|
136
184
|
this.#setState({
|
|
137
185
|
status: this.#attempt === 0 ? "connecting" : "reconnecting",
|
|
138
186
|
});
|
|
@@ -140,10 +188,14 @@ export class AppletTransport {
|
|
|
140
188
|
url.searchParams.set("token", this.#init.token);
|
|
141
189
|
const socket = this.#options.socketFactory(url.toString());
|
|
142
190
|
this.#socket = socket;
|
|
143
|
-
socket.onopen = () =>
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
socket.
|
|
191
|
+
socket.onopen = () => {
|
|
192
|
+
if (this.#currentSocketId === id) this.#handshake();
|
|
193
|
+
};
|
|
194
|
+
socket.onmessage = (event) => {
|
|
195
|
+
if (this.#currentSocketId === id) this.#receive(event.data);
|
|
196
|
+
};
|
|
197
|
+
socket.onclose = () => this.#dropped(id);
|
|
198
|
+
socket.onerror = () => this.#dropped(id);
|
|
147
199
|
}
|
|
148
200
|
|
|
149
201
|
#handshake(): void {
|
|
@@ -156,11 +208,18 @@ export class AppletTransport {
|
|
|
156
208
|
});
|
|
157
209
|
}
|
|
158
210
|
|
|
159
|
-
|
|
211
|
+
/**
|
|
212
|
+
* One failure reaches here twice — `error` then `close` — and a socket the
|
|
213
|
+
* transport already replaced can reach here at any time. Only the socket that
|
|
214
|
+
* still owns the connection schedules a replacement.
|
|
215
|
+
*/
|
|
216
|
+
#dropped(id: number): void {
|
|
217
|
+
if (id !== this.#currentSocketId) return;
|
|
160
218
|
if (this.#closed) return;
|
|
161
|
-
this.#
|
|
162
|
-
this.#synced = false;
|
|
219
|
+
this.#reset(1000, "dropped");
|
|
163
220
|
this.#failPending(new Error("The Applet connection dropped"));
|
|
221
|
+
const retryId = ++this.#retrySeq;
|
|
222
|
+
this.#pendingRetryId = retryId;
|
|
164
223
|
const delay = Math.min(
|
|
165
224
|
this.#options.maximumBackoffMs,
|
|
166
225
|
this.#options.minimumBackoffMs * 2 ** this.#attempt,
|
|
@@ -168,7 +227,11 @@ export class AppletTransport {
|
|
|
168
227
|
this.#attempt += 1;
|
|
169
228
|
this.#setState({ status: "reconnecting" });
|
|
170
229
|
this.#options.schedule(
|
|
171
|
-
() =>
|
|
230
|
+
() => {
|
|
231
|
+
if (this.#pendingRetryId !== retryId) return;
|
|
232
|
+
this.#pendingRetryId = 0;
|
|
233
|
+
this.#open();
|
|
234
|
+
},
|
|
172
235
|
delay * (0.5 + Math.random() / 2),
|
|
173
236
|
);
|
|
174
237
|
}
|