@base44-preview/platform 0.0.0-stage → 0.1.0-pr.312.2ef55e3

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.
@@ -0,0 +1,251 @@
1
+ import { io } from "socket.io-client";
2
+ import { PlatformSocketError } from "../errors.js";
3
+ import { appFromRoom, appPattern, decode, decodeSnapshot, eventNames, object, roomFor, roomNotices, sessionEndings } from "./builder-protocol.js";
4
+ import { notify, Subscription } from "./builder-subscription.js";
5
+ const MAX_DENIAL_RETRIES = 5;
6
+ /** @internal */
7
+ export class BuilderSocket {
8
+ constructor(config, options) {
9
+ this.subscriptions = new Map();
10
+ this.closed = false;
11
+ this.tokenFresh = false;
12
+ this.denialRetries = 0;
13
+ this.authAttempt = 0;
14
+ this.options = { ...config, onError: options.onError };
15
+ this.socket = io(config.socketUrl, {
16
+ path: "/ws/socket.io/", transports: ["websocket"], autoConnect: false,
17
+ forceNew: true, reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 10000,
18
+ timeout: 20000,
19
+ auth: (callback) => { void this.authenticate(callback); },
20
+ });
21
+ this.socket.on("connect", () => {
22
+ var _a;
23
+ this.tokenFresh = false;
24
+ this.denialRetries = 0;
25
+ for (const subscription of this.subscriptions.values())
26
+ this.join(subscription);
27
+ (_a = this.resolveConnect) === null || _a === void 0 ? void 0 : _a.call(this);
28
+ this.clearConnecting();
29
+ });
30
+ this.socket.on("disconnect", (reason) => {
31
+ ++this.authAttempt;
32
+ if (this.closed || reason !== "io server disconnect")
33
+ return; // Transport loss reconnects by itself.
34
+ const ending = this.ending;
35
+ this.ending = undefined;
36
+ if (ending === "session_expired")
37
+ this.socket.connect(); // Renew through getSessionToken.
38
+ else
39
+ this.connectionError(new PlatformSocketError(ending !== null && ending !== void 0 ? ending : "connection_failed"));
40
+ });
41
+ this.socket.on("connect_error", (error) => this.refused(error));
42
+ this.socket.io.on("reconnect_failed", () => this.connectionError(new PlatformSocketError("connection_failed")));
43
+ this.socket.on("app.snapshot", (raw) => {
44
+ var _a;
45
+ try {
46
+ const snapshot = decodeSnapshot(raw);
47
+ (_a = this.subscriptions.get(appFromRoom(snapshot.room))) === null || _a === void 0 ? void 0 : _a.snapshot(snapshot);
48
+ }
49
+ catch (_b) {
50
+ this.protocolError(raw);
51
+ }
52
+ });
53
+ this.socket.on("session.ended", (raw) => this.sessionEnded(raw));
54
+ for (const [name, code] of Object.entries(roomNotices))
55
+ this.socket.on(name, (raw) => this.roomNotice(code, raw));
56
+ for (const type of eventNames)
57
+ this.socket.on(type, (raw) => {
58
+ try {
59
+ const appId = appFromRoom(object(raw).room);
60
+ if (!appId)
61
+ throw new Error("Invalid app");
62
+ const event = decode(type, appId, raw);
63
+ const subscription = this.subscriptions.get(appId);
64
+ subscription === null || subscription === void 0 ? void 0 : subscription.event(event);
65
+ // The server does not resend a snapshot after a rewrite of main: rejoining asks for one.
66
+ if (subscription && event.type === "conversation.changed" && event.data.branch_id == null)
67
+ this.join(subscription);
68
+ }
69
+ catch (_a) {
70
+ this.protocolError(raw);
71
+ }
72
+ });
73
+ }
74
+ /** Connect with the session token. Resolves on CONNECT, before snapshots arrive.
75
+ * Transport loss retries up to five times and rejoins active subscriptions.
76
+ * Call again after a failure or `session_replaced`; concurrent calls share one attempt.
77
+ */
78
+ connect() {
79
+ if (this.closed)
80
+ return Promise.reject(new PlatformSocketError("client_closed"));
81
+ if (this.socket.connected)
82
+ return Promise.resolve();
83
+ if (this.connecting)
84
+ return this.connecting;
85
+ const promise = new Promise((resolve, reject) => {
86
+ this.resolveConnect = resolve;
87
+ this.rejectConnect = reject;
88
+ });
89
+ this.connecting = promise;
90
+ this.socket.connect();
91
+ return promise;
92
+ }
93
+ /** Subscribe before or after connecting. One subscription per app, maximum eight, each on the
94
+ * session's allowlist. Snapshots and events are awaited in order per app; a failed callback,
95
+ * an invalid frame or a server refusal ends the subscription.
96
+ */
97
+ subscribe(appId, options) {
98
+ if (this.closed)
99
+ throw new PlatformSocketError("client_closed");
100
+ if (!appPattern.test(appId))
101
+ throw new TypeError("appId must be 24 lowercase hexadecimal characters");
102
+ if (this.subscriptions.has(appId))
103
+ throw new TypeError("An app may only have one subscription per builder session");
104
+ if (this.subscriptions.size >= 8)
105
+ throw new PlatformSocketError("subscription_limit", appId);
106
+ const subscription = new Subscription(appId, { ...options }, () => {
107
+ this.subscriptions.delete(appId);
108
+ if (this.socket.connected)
109
+ this.socket.emit("leave", roomFor(appId));
110
+ });
111
+ this.subscriptions.set(appId, subscription);
112
+ if (this.socket.connected)
113
+ this.join(subscription);
114
+ return subscription;
115
+ }
116
+ /** Stop delivery, cancel reconnection and release all listeners. Idempotent and terminal. */
117
+ close() {
118
+ var _a, _b;
119
+ if (this.closed)
120
+ return;
121
+ this.closed = true;
122
+ ++this.authAttempt;
123
+ (_a = this.cancelAuth) === null || _a === void 0 ? void 0 : _a.call(this);
124
+ clearTimeout(this.retryTimer);
125
+ for (const subscription of this.subscriptions.values())
126
+ subscription.unsubscribe();
127
+ (_b = this.rejectConnect) === null || _b === void 0 ? void 0 : _b.call(this, new PlatformSocketError("client_closed"));
128
+ this.clearConnecting();
129
+ this.socket.removeAllListeners();
130
+ this.socket.io.removeAllListeners();
131
+ this.socket.disconnect();
132
+ }
133
+ join(subscription) {
134
+ this.socket.emit("join", roomFor(subscription.appId));
135
+ }
136
+ async authenticate(callback) {
137
+ var _a;
138
+ const attempt = ++this.authAttempt;
139
+ (_a = this.cancelAuth) === null || _a === void 0 ? void 0 : _a.call(this);
140
+ let timer;
141
+ const timeout = new Promise((_, reject) => {
142
+ timer = setTimeout(() => reject(new Error("Session token timeout")), 20000);
143
+ this.cancelAuth = () => { clearTimeout(timer); reject(new Error("Cancelled")); };
144
+ });
145
+ try {
146
+ if (!this.token) {
147
+ const token = await Promise.race([Promise.resolve().then(() => this.options.getSessionToken()), timeout]);
148
+ if (this.closed || attempt !== this.authAttempt)
149
+ return;
150
+ if (typeof token !== "string" || !token.trim())
151
+ throw new Error("Missing session token");
152
+ this.token = token;
153
+ this.tokenFresh = true;
154
+ }
155
+ callback({ session_token: this.token });
156
+ }
157
+ catch (_b) {
158
+ if (this.closed || attempt !== this.authAttempt)
159
+ return;
160
+ this.socket.disconnect();
161
+ this.connectionError(new PlatformSocketError("session_unavailable"));
162
+ }
163
+ finally {
164
+ clearTimeout(timer);
165
+ if (attempt === this.authAttempt)
166
+ this.cancelAuth = undefined;
167
+ }
168
+ }
169
+ /** A connect the server refused. Transport errors are Socket.IO's to retry. */
170
+ refused(error) {
171
+ var _a;
172
+ if (this.closed || error.message !== "connection_denied")
173
+ return;
174
+ if (((_a = error.data) === null || _a === void 0 ? void 0 : _a.retryable) === true) {
175
+ this.retryDenied();
176
+ return;
177
+ }
178
+ // A token this attempt did not just fetch may have expired: renew it once.
179
+ const renew = !this.tokenFresh;
180
+ this.token = undefined;
181
+ if (renew)
182
+ this.socket.connect();
183
+ else
184
+ this.connectionError(new PlatformSocketError("connection_denied"));
185
+ }
186
+ retryDenied() {
187
+ if (++this.denialRetries > MAX_DENIAL_RETRIES) {
188
+ this.denialRetries = 0;
189
+ this.connectionError(new PlatformSocketError("connection_failed"));
190
+ return;
191
+ }
192
+ const delay = Math.min(1000 * 2 ** (this.denialRetries - 1), 10000) * (0.5 + Math.random() / 2);
193
+ this.retryTimer = setTimeout(() => { this.retryTimer = undefined; if (!this.closed)
194
+ this.socket.connect(); }, delay);
195
+ }
196
+ clearConnecting() {
197
+ this.connecting = undefined;
198
+ this.resolveConnect = undefined;
199
+ this.rejectConnect = undefined;
200
+ }
201
+ connectionError(error) {
202
+ var _a;
203
+ (_a = this.rejectConnect) === null || _a === void 0 ? void 0 : _a.call(this, error);
204
+ this.clearConnecting();
205
+ if (!this.closed)
206
+ notify(this.options.onError, error);
207
+ }
208
+ sessionEnded(raw) {
209
+ try {
210
+ const reason = object(object(raw).data).reason;
211
+ const ending = typeof reason === "string" && Object.prototype.hasOwnProperty.call(sessionEndings, reason)
212
+ ? sessionEndings[reason] : undefined;
213
+ if (!ending)
214
+ throw new Error("Unknown reason");
215
+ // The server disconnects next; the disconnect handler renews or stops.
216
+ this.token = undefined;
217
+ this.ending = ending;
218
+ }
219
+ catch (_a) {
220
+ this.protocolError(raw);
221
+ }
222
+ }
223
+ roomNotice(code, raw) {
224
+ try {
225
+ const appId = appFromRoom(object(raw).room);
226
+ if (!appId)
227
+ throw new Error("Invalid app");
228
+ const subscription = this.subscriptions.get(appId);
229
+ if (code === "snapshot_unavailable")
230
+ subscription === null || subscription === void 0 ? void 0 : subscription.warn(code);
231
+ else
232
+ subscription === null || subscription === void 0 ? void 0 : subscription.fail(code);
233
+ }
234
+ catch (_a) {
235
+ this.protocolError(raw);
236
+ }
237
+ }
238
+ protocolError(raw) {
239
+ var _a;
240
+ const frame = raw && typeof raw === "object" ? raw : {};
241
+ const appId = appFromRoom(frame.room);
242
+ if (appId)
243
+ (_a = this.subscriptions.get(appId)) === null || _a === void 0 ? void 0 : _a.fail("protocol_error");
244
+ else {
245
+ // Unknown routing: no app can be told which update it missed.
246
+ for (const subscription of [...this.subscriptions.values()])
247
+ subscription.fail("protocol_error");
248
+ this.connectionError(new PlatformSocketError("protocol_error"));
249
+ }
250
+ }
251
+ }
@@ -0,0 +1,23 @@
1
+ import type { PlatformEvent, PlatformSnapshot } from "./builder.events.types.js";
2
+ import { PlatformSocketError } from "../errors.js";
3
+ import type { PlatformSocketErrorCode } from "../errors.types.js";
4
+ import type { PlatformSubscription, SubscriptionOptions } from "./builder.types.js";
5
+ /** @internal */
6
+ export declare function notify(callback: (error: PlatformSocketError) => void, error: PlatformSocketError): void;
7
+ /** @internal */
8
+ export declare class Subscription implements PlatformSubscription {
9
+ readonly appId: string;
10
+ private options;
11
+ private remove;
12
+ active: boolean;
13
+ private pending;
14
+ private tail;
15
+ constructor(appId: string, options: SubscriptionOptions, remove: () => void);
16
+ private enqueue;
17
+ event(event: PlatformEvent): void;
18
+ snapshot(snapshot: PlatformSnapshot): void;
19
+ /** A non-fatal problem; the subscription keeps receiving events. */
20
+ warn(code: PlatformSocketErrorCode): void;
21
+ fail(code: PlatformSocketErrorCode): void;
22
+ unsubscribe(): void;
23
+ }
@@ -0,0 +1,55 @@
1
+ import { PlatformSocketError } from "../errors.js";
2
+ /** @internal */
3
+ export function notify(callback, error) {
4
+ try {
5
+ callback(error);
6
+ }
7
+ catch ( /* An error observer cannot interrupt other app subscriptions. */_a) { /* An error observer cannot interrupt other app subscriptions. */ }
8
+ }
9
+ /** @internal */
10
+ export class Subscription {
11
+ constructor(appId, options, remove) {
12
+ this.appId = appId;
13
+ this.options = options;
14
+ this.remove = remove;
15
+ this.active = true;
16
+ this.pending = 0;
17
+ this.tail = Promise.resolve();
18
+ }
19
+ enqueue(work) {
20
+ if (!this.active)
21
+ return;
22
+ if (this.pending >= 1000) {
23
+ this.fail("delivery_overflow");
24
+ return;
25
+ }
26
+ this.pending++;
27
+ this.tail = this.tail.then(async () => {
28
+ if (this.active)
29
+ await work();
30
+ }).catch(() => this.fail("handler_failed")).finally(() => { this.pending--; });
31
+ }
32
+ event(event) {
33
+ this.enqueue(() => this.options.onEvent(event));
34
+ }
35
+ snapshot(snapshot) {
36
+ this.enqueue(() => this.options.onSnapshot(snapshot));
37
+ }
38
+ /** A non-fatal problem; the subscription keeps receiving events. */
39
+ warn(code) {
40
+ if (this.active)
41
+ notify(this.options.onError, new PlatformSocketError(code, this.appId));
42
+ }
43
+ fail(code) {
44
+ if (!this.active)
45
+ return;
46
+ this.unsubscribe();
47
+ notify(this.options.onError, new PlatformSocketError(code, this.appId));
48
+ }
49
+ unsubscribe() {
50
+ if (!this.active)
51
+ return;
52
+ this.active = false;
53
+ this.remove();
54
+ }
55
+ }
@@ -0,0 +1,4 @@
1
+ import type { PlatformClientOptions } from "../client.types.js";
2
+ import type { BuilderModule } from "./builder.types.js";
3
+ /** @internal */
4
+ export declare function createBuilder(config: PlatformClientOptions): BuilderModule;