@palbase/web 7.3.8 → 7.4.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/{analytics-facade-BER0EyYT.d.ts → analytics-facade-But6gu2r.d.ts} +135 -16
- package/dist/{analytics-facade-CQXCQSoF.d.cts → analytics-facade-f8IVujJW.d.cts} +135 -16
- package/dist/{chunk-6DGCUHSO.js → chunk-4WMJTUDN.js} +28 -19
- package/dist/chunk-4WMJTUDN.js.map +1 -0
- package/dist/{chunk-CFDU23TB.js → chunk-G56VGUYZ.js} +3 -3
- package/dist/chunk-G56VGUYZ.js.map +1 -0
- package/dist/{chunk-HT5SNA5H.js → chunk-VHUCJBBQ.js} +2 -2
- package/dist/chunk-VHUCJBBQ.js.map +1 -0
- package/dist/{chunk-A42CPPQ7.js → chunk-WXBOPPHU.js} +502 -38
- package/dist/chunk-WXBOPPHU.js.map +1 -0
- package/dist/gen/cli.cjs +2 -2
- package/dist/gen/cli.cjs.map +1 -1
- package/dist/gen/cli.js +2 -2
- package/dist/gen/cli.js.map +1 -1
- package/dist/index.cjs +514 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/internal.cjs +527 -54
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +24 -4
- package/dist/internal.d.ts +24 -4
- package/dist/internal.js +3 -3
- package/dist/next/client.cjs +521 -51
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +3 -3
- package/dist/next/index.cjs +528 -55
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +2 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +4 -4
- package/dist/next/index.js.map +1 -1
- package/dist/next/proxy.cjs +1 -1
- package/dist/next/proxy.cjs.map +1 -1
- package/dist/next/proxy.js +3 -3
- package/dist/{pb-WAQvwCfM.d.cts → pb-9MMDNQ9p.d.cts} +6 -1
- package/dist/{pb-BRlmBIAm.d.ts → pb-DRmdWQzG.d.ts} +6 -1
- package/dist/react/index.cjs +22 -19
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-6DGCUHSO.js.map +0 -1
- package/dist/chunk-A42CPPQ7.js.map +0 -1
- package/dist/chunk-CFDU23TB.js.map +0 -1
- package/dist/chunk-HT5SNA5H.js.map +0 -1
package/dist/internal.cjs
CHANGED
|
@@ -179,13 +179,22 @@ function wirePlatform() {
|
|
|
179
179
|
return host === "browser" ? "web" : host;
|
|
180
180
|
}
|
|
181
181
|
var PALBASE_DEFAULT_HOST = "api.palbase.studio";
|
|
182
|
-
var API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20}$/;
|
|
182
|
+
var API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20,}$/;
|
|
183
183
|
function parseEnvironmentRef(apiKey) {
|
|
184
184
|
return API_KEY_RE.exec(apiKey)?.[1] ?? null;
|
|
185
185
|
}
|
|
186
186
|
var MAX_RETRIES = 3;
|
|
187
187
|
var INITIAL_BACKOFF_MS = 200;
|
|
188
188
|
var MAX_RETRY_DELAY_MS = 1e4;
|
|
189
|
+
function withRetryHint(body, response) {
|
|
190
|
+
if (response.status !== 429) return body;
|
|
191
|
+
const data = body?.data;
|
|
192
|
+
const alreadyStated = typeof body?.retry_after === "number" || typeof data === "object" && data !== null && "retryAfter" in data;
|
|
193
|
+
if (alreadyStated) return body;
|
|
194
|
+
const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
|
|
195
|
+
if (Number.isNaN(seconds) || seconds <= 0) return body;
|
|
196
|
+
return { ...body, retry_after: seconds };
|
|
197
|
+
}
|
|
189
198
|
var HttpClient = class _HttpClient {
|
|
190
199
|
apiKey;
|
|
191
200
|
options;
|
|
@@ -255,7 +264,7 @@ var HttpClient = class _HttpClient {
|
|
|
255
264
|
if (this.apiKey && parseEnvironmentRef(this.apiKey) === null) {
|
|
256
265
|
throw new PalbaseError(
|
|
257
266
|
"invalid_api_key",
|
|
258
|
-
'Invalid API key format. Expected pb_{environment_ref}_c{
|
|
267
|
+
'Invalid API key format. Expected pb_{environment_ref}_c{at least 20 base62 chars}. For dev/staging pass `url: "https://api.dev.palbase.studio"` via options.',
|
|
259
268
|
0
|
|
260
269
|
);
|
|
261
270
|
}
|
|
@@ -361,7 +370,7 @@ var HttpClient = class _HttpClient {
|
|
|
361
370
|
errorBody?.error ?? "unknown_error",
|
|
362
371
|
errorBody?.error_description ?? response.statusText,
|
|
363
372
|
response.status,
|
|
364
|
-
errorBody
|
|
373
|
+
withRetryHint(errorBody, response)
|
|
365
374
|
),
|
|
366
375
|
status: response.status
|
|
367
376
|
};
|
|
@@ -484,6 +493,15 @@ function isFieldErrorArray(value) {
|
|
|
484
493
|
(v) => typeof v === "object" && v !== null && typeof v.field === "string" && typeof v.message === "string"
|
|
485
494
|
);
|
|
486
495
|
}
|
|
496
|
+
function fieldErrorsOf(envelope) {
|
|
497
|
+
const scoped = pickField(pickField(envelope, "data"), "fields");
|
|
498
|
+
if (isFieldErrorArray(scoped)) return scoped;
|
|
499
|
+
const flat = pickField(envelope, "fields");
|
|
500
|
+
return isFieldErrorArray(flat) ? flat : void 0;
|
|
501
|
+
}
|
|
502
|
+
function retryAfterOf(envelope) {
|
|
503
|
+
return pickNumber(envelope, "retry_after") ?? pickNumber(pickField(envelope, "data"), "retryAfter");
|
|
504
|
+
}
|
|
487
505
|
function pickField(value, key) {
|
|
488
506
|
if (typeof value === "object" && value !== null) {
|
|
489
507
|
return value[key];
|
|
@@ -509,20 +527,15 @@ function fromPalbaseError(err) {
|
|
|
509
527
|
if (err.code === "network_error") return new BackendError("network", base);
|
|
510
528
|
if (err.status === 401) return new BackendError("unauthorized", base);
|
|
511
529
|
if (err.status === 429)
|
|
512
|
-
return new BackendError("rateLimited", {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
});
|
|
516
|
-
const nested = pickField(err.details, "details");
|
|
517
|
-
if (err.status === 400 && isFieldErrorArray(nested))
|
|
518
|
-
return new BackendError("validation", { ...base, fields: nested });
|
|
530
|
+
return new BackendError("rateLimited", { ...base, retryAfter: retryAfterOf(err.details) });
|
|
531
|
+
const fields = fieldErrorsOf(err.details);
|
|
532
|
+
if (err.status === 400 && fields) return new BackendError("validation", { ...base, fields });
|
|
519
533
|
return new BackendError("server", base);
|
|
520
534
|
}
|
|
521
535
|
function fromEnvelope(status, body) {
|
|
522
536
|
const code = pickString(body, "error") ?? "http_error";
|
|
523
537
|
const message = pickString(body, "error_description") ?? `HTTP ${status}`;
|
|
524
538
|
const requestId = pickString(body, "request_id");
|
|
525
|
-
const details = pickField(body, "details");
|
|
526
539
|
const params = {
|
|
527
540
|
code,
|
|
528
541
|
message,
|
|
@@ -532,13 +545,9 @@ function fromEnvelope(status, body) {
|
|
|
532
545
|
};
|
|
533
546
|
if (status === 401) return new BackendError("unauthorized", params);
|
|
534
547
|
if (status === 429)
|
|
535
|
-
return new BackendError("rateLimited", {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
retryAfter: pickNumber(body, "retry_after") ?? pickNumber(details, "retry_after")
|
|
539
|
-
});
|
|
540
|
-
if (status === 400 && isFieldErrorArray(details))
|
|
541
|
-
return new BackendError("validation", { ...params, fields: details });
|
|
548
|
+
return new BackendError("rateLimited", { ...params, retryAfter: retryAfterOf(body) });
|
|
549
|
+
const fields = fieldErrorsOf(body);
|
|
550
|
+
if (status === 400 && fields) return new BackendError("validation", { ...params, fields });
|
|
542
551
|
return new BackendError("server", params);
|
|
543
552
|
}
|
|
544
553
|
function isBackendError(e) {
|
|
@@ -1594,7 +1603,7 @@ var PalbeAnalytics = class {
|
|
|
1594
1603
|
};
|
|
1595
1604
|
|
|
1596
1605
|
// src/api-key.ts
|
|
1597
|
-
var PUBLISHABLE_API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20}$/;
|
|
1606
|
+
var PUBLISHABLE_API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20,}$/;
|
|
1598
1607
|
function environmentRefFromPublishableApiKey(apiKey) {
|
|
1599
1608
|
const match = PUBLISHABLE_API_KEY_RE.exec(apiKey);
|
|
1600
1609
|
return match ? match[1] ?? "" : "";
|
|
@@ -1866,24 +1875,29 @@ var PalbeAuth = class {
|
|
|
1866
1875
|
* Self-service account erasure ("right to be forgotten"). Calls palauth
|
|
1867
1876
|
* `DELETE /auth/user` (session-authed + fresh re-auth): password users pass
|
|
1868
1877
|
* `{ password }`; passwordless/OAuth users rely on a freshly stepped-up
|
|
1869
|
-
* session.
|
|
1870
|
-
* already revoked server-side — so we tear down
|
|
1871
|
-
* `signOut()`'s clear, minus the pointless `/logout`)
|
|
1872
|
-
* `signedOut{reason:'accountDeleted'}`.
|
|
1878
|
+
* session. The handler answers **204 with no body** — the erasure has already
|
|
1879
|
+
* finished and every session is already revoked server-side — so we tear down
|
|
1880
|
+
* local state (mirroring `signOut()`'s clear, minus the pointless `/logout`)
|
|
1881
|
+
* and emit `signedOut{reason:'accountDeleted'}`.
|
|
1882
|
+
*
|
|
1883
|
+
* Returns nothing, because there is nothing to return: the handler used to
|
|
1884
|
+
* answer 202 with an `{erasure_id}` naming an asynchronous run, and that id
|
|
1885
|
+
* was removed along with the run (`gdpr_handlers.go:221-229`). Reading it
|
|
1886
|
+
* here threw a `TypeError` AFTER the account was already gone — a success
|
|
1887
|
+
* reported as a failure.
|
|
1873
1888
|
*
|
|
1874
1889
|
* On 401 `reauth_required` / 503 `not_configured` (or `erasure_unavailable`)
|
|
1875
1890
|
* the request throws BEFORE the teardown line, so the local session is left
|
|
1876
1891
|
* fully intact — the deletion did not happen and the user is still signed in.
|
|
1877
1892
|
*/
|
|
1878
1893
|
async deleteAccount(params = {}) {
|
|
1879
|
-
|
|
1894
|
+
await palbeRequest(this.rt, "DELETE", "/auth/user", {
|
|
1880
1895
|
body: params.password ? { password: params.password } : {}
|
|
1881
1896
|
});
|
|
1882
1897
|
this.nextSignOutReason = "accountDeleted";
|
|
1883
1898
|
this.rt.tokenManager.clearSession();
|
|
1884
1899
|
this.rt.storage.clear();
|
|
1885
1900
|
this.cachedUser = null;
|
|
1886
|
-
return { erasureId: res.erasure_id };
|
|
1887
1901
|
}
|
|
1888
1902
|
/**
|
|
1889
1903
|
* @internal — invoked by the transport choke point (`request.ts`), not app code.
|
|
@@ -2370,9 +2384,15 @@ var PalbeCalls = class {
|
|
|
2370
2384
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
2371
2385
|
* @returns A `Call` in `connecting` state; transitions to `active` once the media room connects.
|
|
2372
2386
|
*
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2387
|
+
* Every failure below arrives as kind `'server'` — this SDK's error kinds are
|
|
2388
|
+
* `notConfigured | validation | unauthorized | rateLimited | server | network
|
|
2389
|
+
* | decode`, and only a 401, a 429 or a 400 carrying a field-error array gets
|
|
2390
|
+
* its own kind. So branch on `err.code`, never on the kind.
|
|
2391
|
+
*
|
|
2392
|
+
* @throws BackendError('server', { code: 'calling_unavailable' }) no SFU configured on this stack (503)
|
|
2393
|
+
* @throws BackendError('server', { code: 'not_group_member' }) the caller is not a member of the group (403)
|
|
2394
|
+
* @throws BackendError('server', { code: 'call_room_full' }) the call is at its participant limit (409)
|
|
2395
|
+
* @throws BackendError('server', { code: 'call_provider_error' }) the media provider failed upstream (502)
|
|
2376
2396
|
*/
|
|
2377
2397
|
async start(groupId, { media = ["audio", "video"] } = {}) {
|
|
2378
2398
|
if (!groupId) {
|
|
@@ -2393,7 +2413,11 @@ var PalbeCalls = class {
|
|
|
2393
2413
|
* Accept an incoming call in a messaging group.
|
|
2394
2414
|
*
|
|
2395
2415
|
* @param groupId - Messaging group display-id
|
|
2396
|
-
* @param callId - Call identifier
|
|
2416
|
+
* @param callId - Call identifier, carried by the `call_invite` the server
|
|
2417
|
+
* fans onto the group's conversation topic
|
|
2418
|
+
* (`pb.realtime.channel('messaging:conv:<rfcGroupId>').on('call_invite', …)`).
|
|
2419
|
+
* This SDK ships no web push: on the web the invite arrives over realtime or
|
|
2420
|
+
* over whatever signalling the app already runs.
|
|
2397
2421
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
2398
2422
|
*/
|
|
2399
2423
|
async accept(groupId, callId, { media = ["audio", "video"] } = {}) {
|
|
@@ -4496,14 +4520,11 @@ var Chat = class {
|
|
|
4496
4520
|
get title() {
|
|
4497
4521
|
if (this.titleOverride) return this.titleOverride;
|
|
4498
4522
|
if (this._group?.name) return this._group.name;
|
|
4499
|
-
if (this.
|
|
4500
|
-
const
|
|
4501
|
-
|
|
4502
|
-
const peer = this.memberCache.find((m) => m.userId === mode.peerUserId);
|
|
4503
|
-
return peer?.displayName ?? "Chat";
|
|
4504
|
-
}
|
|
4505
|
-
return "Group";
|
|
4523
|
+
if (this.kind === "direct") {
|
|
4524
|
+
const peer = this.memberCache.find((m) => !m.isSelf);
|
|
4525
|
+
return peer?.displayName ?? "Chat";
|
|
4506
4526
|
}
|
|
4527
|
+
if (this._draft) return "Group";
|
|
4507
4528
|
const tail = this.id.startsWith("grp_") ? this.id.slice(4) : this.id;
|
|
4508
4529
|
const short = tail.slice(-4);
|
|
4509
4530
|
return short ? `Group ${short}` : "Group";
|
|
@@ -5477,6 +5498,11 @@ var MessageHub = class {
|
|
|
5477
5498
|
emitConv(displayId, e) {
|
|
5478
5499
|
for (const fn of this.convListeners.get(displayId) ?? []) fn(e);
|
|
5479
5500
|
}
|
|
5501
|
+
/** How many live conv listeners a group still has — the census the LAST
|
|
5502
|
+
* subscriber reads before tearing the conv topic (and its presence) down. */
|
|
5503
|
+
convListenerCount(displayId) {
|
|
5504
|
+
return this.convListeners.get(displayId)?.size ?? 0;
|
|
5505
|
+
}
|
|
5480
5506
|
add(map, key, fn) {
|
|
5481
5507
|
let set = map.get(key);
|
|
5482
5508
|
if (!set) {
|
|
@@ -5514,6 +5540,9 @@ var MessageDeliverySource = class {
|
|
|
5514
5540
|
started = false;
|
|
5515
5541
|
wakeUnsub = null;
|
|
5516
5542
|
// Per-observed-group conv subscription + heartbeat (presence/typing/read).
|
|
5543
|
+
// `teardown` is the ONE departure path: it stops the heartbeat, broadcasts the
|
|
5544
|
+
// offline frame and drops the subscriptions, so no caller can tear an
|
|
5545
|
+
// observation down without saying goodbye (see observeConversation).
|
|
5517
5546
|
observed = /* @__PURE__ */ new Map();
|
|
5518
5547
|
/** Subscribe the device wake topic + run an initial drain. Idempotent. */
|
|
5519
5548
|
async start() {
|
|
@@ -5538,10 +5567,7 @@ var MessageDeliverySource = class {
|
|
|
5538
5567
|
stop() {
|
|
5539
5568
|
this.wakeUnsub?.();
|
|
5540
5569
|
this.wakeUnsub = null;
|
|
5541
|
-
for (const [, o] of this.observed)
|
|
5542
|
-
o.unsub();
|
|
5543
|
-
if (o.heartbeat) clearInterval(o.heartbeat);
|
|
5544
|
-
}
|
|
5570
|
+
for (const [, o] of this.observed) o.teardown();
|
|
5545
5571
|
this.observed.clear();
|
|
5546
5572
|
this.started = false;
|
|
5547
5573
|
}
|
|
@@ -5772,14 +5798,26 @@ var MessageDeliverySource = class {
|
|
|
5772
5798
|
emitPresence(true);
|
|
5773
5799
|
const heartbeat = setInterval(() => emitPresence(true), 25e3);
|
|
5774
5800
|
this.observed.set(group.displayId, {
|
|
5775
|
-
|
|
5801
|
+
teardown: () => {
|
|
5802
|
+
clearInterval(heartbeat);
|
|
5803
|
+
try {
|
|
5804
|
+
emitPresence(false);
|
|
5805
|
+
} catch {
|
|
5806
|
+
}
|
|
5776
5807
|
for (const s of subs) s.cancel();
|
|
5777
|
-
}
|
|
5778
|
-
heartbeat
|
|
5808
|
+
}
|
|
5779
5809
|
});
|
|
5780
5810
|
} catch {
|
|
5781
5811
|
}
|
|
5782
5812
|
}
|
|
5813
|
+
/** Stop observing a group's conv topic: announce OFFLINE, stop the heartbeat
|
|
5814
|
+
* and drop the subscriptions. Idempotent — an unobserved group is a no-op. */
|
|
5815
|
+
unobserveConversation(group) {
|
|
5816
|
+
const o = this.observed.get(group.displayId);
|
|
5817
|
+
if (!o) return;
|
|
5818
|
+
this.observed.delete(group.displayId);
|
|
5819
|
+
o.teardown();
|
|
5820
|
+
}
|
|
5783
5821
|
/** Announce typing on a group's conv topic (the app calls per keystroke). */
|
|
5784
5822
|
setTyping(group, isTyping) {
|
|
5785
5823
|
this.observeConversation(group);
|
|
@@ -8007,7 +8045,11 @@ var MessagingCoordinator = class {
|
|
|
8007
8045
|
subscribeLive(group, chat) {
|
|
8008
8046
|
let offMsg = null;
|
|
8009
8047
|
let offConv = null;
|
|
8048
|
+
let resolved = null;
|
|
8049
|
+
let cancelled = false;
|
|
8010
8050
|
void this.resolve().then((r) => {
|
|
8051
|
+
if (cancelled) return;
|
|
8052
|
+
resolved = r;
|
|
8011
8053
|
offMsg = r.hub.onMessage(group.displayId, (m) => {
|
|
8012
8054
|
void chat.ingestLive(m);
|
|
8013
8055
|
});
|
|
@@ -8015,8 +8057,12 @@ var MessagingCoordinator = class {
|
|
|
8015
8057
|
offConv = r.hub.onConv(group.displayId, (e) => chat.applyConv(e.event, e.payload));
|
|
8016
8058
|
});
|
|
8017
8059
|
return () => {
|
|
8060
|
+
cancelled = true;
|
|
8018
8061
|
offMsg?.();
|
|
8019
8062
|
offConv?.();
|
|
8063
|
+
if (resolved && resolved.hub.convListenerCount(group.displayId) === 0) {
|
|
8064
|
+
resolved.source.unobserveConversation(group);
|
|
8065
|
+
}
|
|
8020
8066
|
};
|
|
8021
8067
|
}
|
|
8022
8068
|
async userIdForDevice(group, deviceId) {
|
|
@@ -8874,8 +8920,33 @@ function unwrapBroadcast(frame) {
|
|
|
8874
8920
|
function stripRealtimePrefix(topic) {
|
|
8875
8921
|
return topic.startsWith(REALTIME_PREFIX) ? topic.slice(REALTIME_PREFIX.length) : topic;
|
|
8876
8922
|
}
|
|
8923
|
+
function encodeStateSet(joinRef, ref, topic, key, value, life) {
|
|
8924
|
+
return JSON.stringify([
|
|
8925
|
+
joinRef,
|
|
8926
|
+
String(ref),
|
|
8927
|
+
REALTIME_PREFIX + topic,
|
|
8928
|
+
"state_set",
|
|
8929
|
+
{ key, value, life }
|
|
8930
|
+
]);
|
|
8931
|
+
}
|
|
8932
|
+
function encodeStateDel(joinRef, ref, topic, key) {
|
|
8933
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_del", { key }]);
|
|
8934
|
+
}
|
|
8935
|
+
function encodeStateResync(joinRef, ref, topic) {
|
|
8936
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_resync", {}]);
|
|
8937
|
+
}
|
|
8938
|
+
function encodeAccessToken(joinRef, ref, topic, token) {
|
|
8939
|
+
return JSON.stringify([
|
|
8940
|
+
joinRef,
|
|
8941
|
+
String(ref),
|
|
8942
|
+
REALTIME_PREFIX + topic,
|
|
8943
|
+
"access_token",
|
|
8944
|
+
{ access_token: token }
|
|
8945
|
+
]);
|
|
8946
|
+
}
|
|
8877
8947
|
|
|
8878
8948
|
// src/realtime/connection.ts
|
|
8949
|
+
var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
8879
8950
|
var WS_OPEN = 1;
|
|
8880
8951
|
var DEFAULT_HEARTBEAT_MS = 25e3;
|
|
8881
8952
|
var DEFAULT_MAX_BACKOFF_SECONDS = 30;
|
|
@@ -9108,6 +9179,31 @@ var RealtimeSocket = class {
|
|
|
9108
9179
|
this.handleChannelClose(frame);
|
|
9109
9180
|
return;
|
|
9110
9181
|
}
|
|
9182
|
+
if (frame.event === "state_snapshot") {
|
|
9183
|
+
this.handlers?.onStateSnapshot(stripRealtimePrefix(frame.topic), frame.payload);
|
|
9184
|
+
return;
|
|
9185
|
+
}
|
|
9186
|
+
if (frame.event === "state_diff") {
|
|
9187
|
+
this.handlers?.onStateDiff(stripRealtimePrefix(frame.topic), frame.payload);
|
|
9188
|
+
return;
|
|
9189
|
+
}
|
|
9190
|
+
if (frame.event === "phx_reply") {
|
|
9191
|
+
const body = frame.payload;
|
|
9192
|
+
const topic = stripRealtimePrefix(frame.topic);
|
|
9193
|
+
if (body && body.status === "ok" && body.response && typeof body.response === "object") {
|
|
9194
|
+
this.handlers?.onJoinInfo(topic, body.response);
|
|
9195
|
+
return;
|
|
9196
|
+
}
|
|
9197
|
+
if (body && body.status === "error" && String(frame.ref) === this.joinRefs.get(topic)) {
|
|
9198
|
+
const reason = typeof body.response?.["reason"] === "string" ? body.response["reason"] : "unauthorized";
|
|
9199
|
+
if (!RETRYABLE_REFUSALS.has(reason)) {
|
|
9200
|
+
this.joinedTopics.delete(topic);
|
|
9201
|
+
this.joinRefs.delete(topic);
|
|
9202
|
+
}
|
|
9203
|
+
this.handlers?.onChannelRefused(topic, reason);
|
|
9204
|
+
}
|
|
9205
|
+
return;
|
|
9206
|
+
}
|
|
9111
9207
|
const inbound = unwrapBroadcast(frame);
|
|
9112
9208
|
if (inbound) {
|
|
9113
9209
|
this.channelRejoinAttempts.delete(inbound.topic);
|
|
@@ -9148,6 +9244,40 @@ var RealtimeSocket = class {
|
|
|
9148
9244
|
}, delayMs);
|
|
9149
9245
|
this.channelRejoinTimers.set(topic, timer);
|
|
9150
9246
|
}
|
|
9247
|
+
// MARK: state
|
|
9248
|
+
/** Write one entry on a joined channel's state. Dropped when the topic has no
|
|
9249
|
+
* live join: the server refuses state on a channel this socket is not on, and
|
|
9250
|
+
* the reconnect's join brings a fresh snapshot anyway. */
|
|
9251
|
+
sendStateSet(topic, key, value, life) {
|
|
9252
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9253
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9254
|
+
this.send(encodeStateSet(joinRef, this.nextRef(), topic, key, value, life));
|
|
9255
|
+
}
|
|
9256
|
+
/** Remove one entry. The server refuses a key this connection does not own. */
|
|
9257
|
+
sendStateDel(topic, key) {
|
|
9258
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9259
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9260
|
+
this.send(encodeStateDel(joinRef, this.nextRef(), topic, key));
|
|
9261
|
+
}
|
|
9262
|
+
/** Ask for a fresh snapshot — the answer to a detected gap. */
|
|
9263
|
+
sendStateResync(topic) {
|
|
9264
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9265
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9266
|
+
this.send(encodeStateResync(joinRef, this.nextRef(), topic));
|
|
9267
|
+
}
|
|
9268
|
+
/**
|
|
9269
|
+
* Present a fresher credential on every joined channel WITHOUT rejoining.
|
|
9270
|
+
*
|
|
9271
|
+
* The alternative — dropping and rejoining — reaps this connection's
|
|
9272
|
+
* ephemeral entries (its presence) and forces a full resync on every channel.
|
|
9273
|
+
* A token rotation is not a reconnection and must not look like one.
|
|
9274
|
+
*/
|
|
9275
|
+
sendAccessToken(token) {
|
|
9276
|
+
if (!this.isOpen()) return;
|
|
9277
|
+
for (const [topic, joinRef] of this.joinRefs) {
|
|
9278
|
+
this.send(encodeAccessToken(joinRef, this.nextRef(), topic, token));
|
|
9279
|
+
}
|
|
9280
|
+
}
|
|
9151
9281
|
// MARK: helpers
|
|
9152
9282
|
/** Drop a single topic's token-expiry rejoin state (cancel its pending timer). */
|
|
9153
9283
|
clearChannelRejoin(topic) {
|
|
@@ -9173,6 +9303,60 @@ var RealtimeSocket = class {
|
|
|
9173
9303
|
}
|
|
9174
9304
|
};
|
|
9175
9305
|
|
|
9306
|
+
// src/realtime/state.ts
|
|
9307
|
+
var PRESENCE_PREFIX = "$p:";
|
|
9308
|
+
var ChannelState = class {
|
|
9309
|
+
/** The sequence this local copy is current as of. */
|
|
9310
|
+
seq = 0;
|
|
9311
|
+
/** key → value. Lifetimes are the server's business; a reader only needs the
|
|
9312
|
+
* values, and presence is derived from the key shape. */
|
|
9313
|
+
entries = /* @__PURE__ */ new Map();
|
|
9314
|
+
/** Replace everything. A snapshot is the WHOLE picture, never a merge —
|
|
9315
|
+
* merging one would keep entries the server has since removed. */
|
|
9316
|
+
applySnapshot(body) {
|
|
9317
|
+
this.entries.clear();
|
|
9318
|
+
for (const [k, e] of Object.entries(body.entries ?? {})) {
|
|
9319
|
+
this.entries.set(k, e.v);
|
|
9320
|
+
}
|
|
9321
|
+
this.seq = body.seq;
|
|
9322
|
+
}
|
|
9323
|
+
/**
|
|
9324
|
+
* Apply a diff, or report that it cannot be applied.
|
|
9325
|
+
*
|
|
9326
|
+
* `gap` when this copy is behind the diff's starting point: frames were
|
|
9327
|
+
* missed, and applying anyway would produce a state that never existed on the
|
|
9328
|
+
* server. The state is left untouched so the caller can resync from something
|
|
9329
|
+
* consistent.
|
|
9330
|
+
*
|
|
9331
|
+
* A diff at or below the current sequence is already applied — a resend after
|
|
9332
|
+
* a retry — and is silently accepted so duplicates are harmless.
|
|
9333
|
+
*/
|
|
9334
|
+
applyDiff(body) {
|
|
9335
|
+
if (this.seq < body.from_seq) return "gap";
|
|
9336
|
+
if (body.seq <= this.seq) return "applied";
|
|
9337
|
+
for (const [k, e] of Object.entries(body.set ?? {})) this.entries.set(k, e.v);
|
|
9338
|
+
for (const k of body.del ?? []) this.entries.delete(k);
|
|
9339
|
+
this.seq = body.seq;
|
|
9340
|
+
return "applied";
|
|
9341
|
+
}
|
|
9342
|
+
/**
|
|
9343
|
+
* Everyone else's presence.
|
|
9344
|
+
*
|
|
9345
|
+
* `selfConn` is this connection's id, as the join reply reported it. Its own
|
|
9346
|
+
* entry is excluded because an app rendering "who else is here" would
|
|
9347
|
+
* otherwise always show one extra face — itself.
|
|
9348
|
+
*/
|
|
9349
|
+
presence(selfConn) {
|
|
9350
|
+
const own = selfConn === void 0 ? void 0 : PRESENCE_PREFIX + selfConn;
|
|
9351
|
+
const out = [];
|
|
9352
|
+
for (const [k, v] of this.entries) {
|
|
9353
|
+
if (!k.startsWith(PRESENCE_PREFIX) || k === own) continue;
|
|
9354
|
+
out.push(v);
|
|
9355
|
+
}
|
|
9356
|
+
return out;
|
|
9357
|
+
}
|
|
9358
|
+
};
|
|
9359
|
+
|
|
9176
9360
|
// src/realtime/facade.ts
|
|
9177
9361
|
var StatusStore = class {
|
|
9178
9362
|
currentState = "idle";
|
|
@@ -9208,6 +9392,7 @@ var StatusStore = class {
|
|
|
9208
9392
|
for (const listener of this.listeners) listener(snapshot);
|
|
9209
9393
|
}
|
|
9210
9394
|
};
|
|
9395
|
+
var RETRYABLE_REASONS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
9211
9396
|
var RealtimeChannel = class {
|
|
9212
9397
|
/** The app-defined channel name (bare sub-topic, no "realtime:" prefix). */
|
|
9213
9398
|
name;
|
|
@@ -9225,6 +9410,23 @@ var RealtimeChannel = class {
|
|
|
9225
9410
|
on(event, handler) {
|
|
9226
9411
|
return this.owner.subscribe(this.name, event, handler);
|
|
9227
9412
|
}
|
|
9413
|
+
/**
|
|
9414
|
+
* Called when the server REFUSES this channel.
|
|
9415
|
+
*
|
|
9416
|
+
* A join is an authorization decision, and a decision the app never hears is
|
|
9417
|
+
* the same as no decision at all: silence looks exactly like a slow network.
|
|
9418
|
+
* This is where a refusal arrives, carrying the server's own distinction —
|
|
9419
|
+
* `retryable: false` means "you may not" and asking again will not help;
|
|
9420
|
+
* `retryable: true` means the project could not be asked, and the socket's
|
|
9421
|
+
* next reconnect retries on its own.
|
|
9422
|
+
*
|
|
9423
|
+
* pb.realtime.channel('room:42').onError((err) => {
|
|
9424
|
+
* if (!err.retryable) showJoinDenied()
|
|
9425
|
+
* })
|
|
9426
|
+
*/
|
|
9427
|
+
onError(handler) {
|
|
9428
|
+
return this.owner.subscribeError(this.name, handler);
|
|
9429
|
+
}
|
|
9228
9430
|
/**
|
|
9229
9431
|
* Broadcast `payload` to the channel's other subscribers (web addition —
|
|
9230
9432
|
* iOS is receive-only). Joins the channel if it isn't already; queued
|
|
@@ -9233,6 +9435,57 @@ var RealtimeChannel = class {
|
|
|
9233
9435
|
send(event, payload = {}) {
|
|
9234
9436
|
this.owner.send(this.name, event, payload);
|
|
9235
9437
|
}
|
|
9438
|
+
/**
|
|
9439
|
+
* Watch one key of the channel's shared state.
|
|
9440
|
+
*
|
|
9441
|
+
* The handler is called with the CURRENT value straight away when there is
|
|
9442
|
+
* one, and again on every change. That is the difference from `on()`: a
|
|
9443
|
+
* subscriber to an event learns nothing until the next one, while state is
|
|
9444
|
+
* already there when you ask.
|
|
9445
|
+
*/
|
|
9446
|
+
onState(key, handler) {
|
|
9447
|
+
return this.owner.subscribeState(this.name, key, handler);
|
|
9448
|
+
}
|
|
9449
|
+
/** Read one key without subscribing. */
|
|
9450
|
+
stateValue(key) {
|
|
9451
|
+
return this.owner.stateOf(this.name).entries.get(key);
|
|
9452
|
+
}
|
|
9453
|
+
/** Write one key. `durable` outlives this connection; `ephemeral` is reaped
|
|
9454
|
+
* when it closes, which is what presence is built on. */
|
|
9455
|
+
setState(key, value, opts = {}) {
|
|
9456
|
+
this.owner.writeState(this.name, key, value, opts.life ?? "durable");
|
|
9457
|
+
}
|
|
9458
|
+
/** Remove a key this connection wrote. */
|
|
9459
|
+
clearState(key) {
|
|
9460
|
+
this.owner.clearState(this.name, key);
|
|
9461
|
+
}
|
|
9462
|
+
/**
|
|
9463
|
+
* Announce this client on the channel. `meta` is whatever other participants
|
|
9464
|
+
* should see — a name, an avatar, a cursor.
|
|
9465
|
+
*
|
|
9466
|
+
* It is ephemeral state keyed by this connection, so it disappears when the
|
|
9467
|
+
* socket does. Nothing has to be un-announced on a crash.
|
|
9468
|
+
*/
|
|
9469
|
+
presenceEnter(meta) {
|
|
9470
|
+
this.owner.presenceWrite(this.name, meta);
|
|
9471
|
+
}
|
|
9472
|
+
/** Replace the announced value — a cursor moving is this, at whatever rate
|
|
9473
|
+
* the app likes: the server conflates writes it cannot deliver fast enough. */
|
|
9474
|
+
presenceUpdate(meta) {
|
|
9475
|
+
this.owner.presenceWrite(this.name, meta);
|
|
9476
|
+
}
|
|
9477
|
+
/** Leave early. Closing the socket does this on its own. */
|
|
9478
|
+
presenceLeave() {
|
|
9479
|
+
this.owner.presenceClear(this.name);
|
|
9480
|
+
}
|
|
9481
|
+
/** Everyone else currently announced, this client excluded. */
|
|
9482
|
+
presenceOthers() {
|
|
9483
|
+
return this.owner.presenceOthers(this.name);
|
|
9484
|
+
}
|
|
9485
|
+
/** Watch the announced set. Called on every change. */
|
|
9486
|
+
onPresence(handler) {
|
|
9487
|
+
return this.owner.subscribePresence(this.name, handler);
|
|
9488
|
+
}
|
|
9236
9489
|
};
|
|
9237
9490
|
var PalbeRealtime = class {
|
|
9238
9491
|
rt;
|
|
@@ -9242,9 +9495,21 @@ var PalbeRealtime = class {
|
|
|
9242
9495
|
// Per-(topic, event) handlers — an event may have multiple handlers
|
|
9243
9496
|
// (multiple .on calls); entry identity is the unsubscribe token.
|
|
9244
9497
|
handlers = /* @__PURE__ */ new Set();
|
|
9498
|
+
// Per-topic refusal handlers. Kept apart from `handlers` because they are not
|
|
9499
|
+
// refcounted: registering one must not join a channel, and cancelling the
|
|
9500
|
+
// last one must not leave a channel that still has event handlers on it.
|
|
9501
|
+
errorHandlers = /* @__PURE__ */ new Set();
|
|
9245
9502
|
// Joins are refcounted per topic so the socket only leaves a topic when
|
|
9246
9503
|
// its last handler cancels (iOS RealtimeClient parity).
|
|
9247
9504
|
topicRefcount = /* @__PURE__ */ new Map();
|
|
9505
|
+
// Per-topic shared state, and this connection's identity on each topic (from
|
|
9506
|
+
// the join ack). Presence is keyed by that identity, so a write before the
|
|
9507
|
+
// ack has nowhere to go and is queued until it arrives.
|
|
9508
|
+
states = /* @__PURE__ */ new Map();
|
|
9509
|
+
connIds = /* @__PURE__ */ new Map();
|
|
9510
|
+
pendingPresence = /* @__PURE__ */ new Map();
|
|
9511
|
+
stateWatchers = /* @__PURE__ */ new Set();
|
|
9512
|
+
presenceWatchers = /* @__PURE__ */ new Set();
|
|
9248
9513
|
constructor(rt) {
|
|
9249
9514
|
this.rt = rt;
|
|
9250
9515
|
}
|
|
@@ -9287,6 +9552,20 @@ var PalbeRealtime = class {
|
|
|
9287
9552
|
get status() {
|
|
9288
9553
|
return this.statusStore;
|
|
9289
9554
|
}
|
|
9555
|
+
/**
|
|
9556
|
+
* Hand the socket a fresher credential for the SAME user, without rejoining.
|
|
9557
|
+
*
|
|
9558
|
+
* Called when the app's token rotates. The alternative — letting the channels
|
|
9559
|
+
* die at expiry and rejoining — reaps this connection's ephemeral entries
|
|
9560
|
+
* (its presence, on every channel) and forces a full state resync. A token
|
|
9561
|
+
* getting older is not a disconnection and must not cost one.
|
|
9562
|
+
*
|
|
9563
|
+
* A no-op before a socket exists: the first connect resolves a fresh token
|
|
9564
|
+
* on its own.
|
|
9565
|
+
*/
|
|
9566
|
+
refreshToken(token) {
|
|
9567
|
+
this.socket?.sendAccessToken(token);
|
|
9568
|
+
}
|
|
9290
9569
|
/** @internal */
|
|
9291
9570
|
subscribe(topic, event, handler) {
|
|
9292
9571
|
const socket = this.ensureSocket();
|
|
@@ -9301,7 +9580,9 @@ var PalbeRealtime = class {
|
|
|
9301
9580
|
if (cancelled) return;
|
|
9302
9581
|
cancelled = true;
|
|
9303
9582
|
this.handlers.delete(entry);
|
|
9304
|
-
const
|
|
9583
|
+
const held = this.topicRefcount.get(topic);
|
|
9584
|
+
if (held === void 0) return;
|
|
9585
|
+
const next = held - 1;
|
|
9305
9586
|
if (next <= 0) {
|
|
9306
9587
|
this.topicRefcount.delete(topic);
|
|
9307
9588
|
socket.leaveTopic(topic);
|
|
@@ -9311,6 +9592,25 @@ var PalbeRealtime = class {
|
|
|
9311
9592
|
}
|
|
9312
9593
|
};
|
|
9313
9594
|
}
|
|
9595
|
+
/**
|
|
9596
|
+
* Register a refusal handler for one topic. Does NOT join: watching for a
|
|
9597
|
+
* refusal is not a subscription, and an app that registers one before its
|
|
9598
|
+
* first `.on()` should not be joining anything yet.
|
|
9599
|
+
*
|
|
9600
|
+
* @internal
|
|
9601
|
+
*/
|
|
9602
|
+
subscribeError(topic, handler) {
|
|
9603
|
+
const entry = { topic, handler };
|
|
9604
|
+
this.errorHandlers.add(entry);
|
|
9605
|
+
let cancelled = false;
|
|
9606
|
+
return {
|
|
9607
|
+
cancel: () => {
|
|
9608
|
+
if (cancelled) return;
|
|
9609
|
+
cancelled = true;
|
|
9610
|
+
this.errorHandlers.delete(entry);
|
|
9611
|
+
}
|
|
9612
|
+
};
|
|
9613
|
+
}
|
|
9314
9614
|
/** @internal */
|
|
9315
9615
|
send(topic, event, payload) {
|
|
9316
9616
|
const socket = this.ensureSocket();
|
|
@@ -9332,7 +9632,11 @@ var PalbeRealtime = class {
|
|
|
9332
9632
|
onReconnect: () => {
|
|
9333
9633
|
},
|
|
9334
9634
|
onStateChange: (state) => this.statusStore.setState(state),
|
|
9335
|
-
onChannelError: (topic) => this.handleChannelError(topic)
|
|
9635
|
+
onChannelError: (topic) => this.handleChannelError(topic),
|
|
9636
|
+
onChannelRefused: (topic, reason) => this.handleChannelRefused(topic, reason),
|
|
9637
|
+
onStateSnapshot: (topic, body) => this.applySnapshot(topic, body),
|
|
9638
|
+
onStateDiff: (topic, body) => this.applyDiff(topic, body),
|
|
9639
|
+
onJoinInfo: (topic, info) => this.applyJoinInfo(topic, info)
|
|
9336
9640
|
});
|
|
9337
9641
|
}
|
|
9338
9642
|
return this.socket;
|
|
@@ -9346,19 +9650,185 @@ var PalbeRealtime = class {
|
|
|
9346
9650
|
}
|
|
9347
9651
|
}
|
|
9348
9652
|
/**
|
|
9349
|
-
|
|
9350
|
-
*
|
|
9351
|
-
*
|
|
9352
|
-
*
|
|
9653
|
+
/**
|
|
9654
|
+
* The server refused a channel. Tell whoever asked, and — when the answer is
|
|
9655
|
+
* final — stop pretending the app is subscribed to it.
|
|
9656
|
+
*
|
|
9657
|
+
* `unavailable` leaves everything in place: the project could not be reached
|
|
9658
|
+
* to decide, the socket rejoins on its next reconnect, and dropping the app's
|
|
9659
|
+
* handlers would make a momentary outage look like a permanent denial.
|
|
9660
|
+
*/
|
|
9661
|
+
handleChannelRefused(topic, reason) {
|
|
9662
|
+
const retryable = RETRYABLE_REASONS.has(reason);
|
|
9663
|
+
for (const entry of this.errorHandlers) {
|
|
9664
|
+
if (entry.topic === topic) entry.handler({ topic, reason, retryable });
|
|
9665
|
+
}
|
|
9666
|
+
if (retryable) return;
|
|
9667
|
+
this.forgetTopic(topic);
|
|
9668
|
+
}
|
|
9669
|
+
/**
|
|
9670
|
+
* A channel died for good — a token-expiry rejoin that exhausted its attempts.
|
|
9671
|
+
* The app must stop expecting anything on it, and the status observable flips
|
|
9672
|
+
* to `'error'` (the React `useChannel` hook reports that).
|
|
9673
|
+
*
|
|
9674
|
+
* It clears the SAME registries a final refusal clears, and used to clear only
|
|
9675
|
+
* the event handlers. A state or presence watcher left behind here waits
|
|
9676
|
+
* forever for a snapshot that cannot arrive, and fires twice if the app ever
|
|
9677
|
+
* re-subscribes — the identical failure its sibling three functions up
|
|
9678
|
+
* describes, on a channel that was given up on rather than refused.
|
|
9353
9679
|
*/
|
|
9354
9680
|
handleChannelError(topic) {
|
|
9681
|
+
this.forgetTopic(topic);
|
|
9682
|
+
this.statusStore.setState("error");
|
|
9683
|
+
}
|
|
9684
|
+
/** Drop every registration this topic holds. One place, because "which
|
|
9685
|
+
* registries" is a question two callers must never answer differently. */
|
|
9686
|
+
forgetTopic(topic) {
|
|
9355
9687
|
for (const entry of this.handlers) {
|
|
9356
9688
|
if (entry.topic === topic) this.handlers.delete(entry);
|
|
9357
9689
|
}
|
|
9690
|
+
for (const entry of this.stateWatchers) {
|
|
9691
|
+
if (entry.topic === topic) this.stateWatchers.delete(entry);
|
|
9692
|
+
}
|
|
9693
|
+
for (const entry of this.presenceWatchers) {
|
|
9694
|
+
if (entry.topic === topic) this.presenceWatchers.delete(entry);
|
|
9695
|
+
}
|
|
9696
|
+
this.pendingPresence.delete(topic);
|
|
9697
|
+
this.states.delete(topic);
|
|
9698
|
+
this.connIds.delete(topic);
|
|
9358
9699
|
this.topicRefcount.delete(topic);
|
|
9359
|
-
|
|
9700
|
+
}
|
|
9701
|
+
// ── state ──────────────────────────────────────────────────────────────────
|
|
9702
|
+
/** @internal */
|
|
9703
|
+
stateOf(topic) {
|
|
9704
|
+
let st = this.states.get(topic);
|
|
9705
|
+
if (!st) {
|
|
9706
|
+
st = new ChannelState();
|
|
9707
|
+
this.states.set(topic, st);
|
|
9708
|
+
}
|
|
9709
|
+
return st;
|
|
9710
|
+
}
|
|
9711
|
+
/** @internal */
|
|
9712
|
+
subscribeState(topic, key, handler) {
|
|
9713
|
+
const socket = this.ensureSocket();
|
|
9714
|
+
const entry = { topic, key, handler };
|
|
9715
|
+
this.stateWatchers.add(entry);
|
|
9716
|
+
this.retainTopic(topic, socket);
|
|
9717
|
+
const known = this.stateOf(topic).entries.get(key);
|
|
9718
|
+
if (known !== void 0) handler(known);
|
|
9719
|
+
let cancelled = false;
|
|
9720
|
+
return {
|
|
9721
|
+
cancel: () => {
|
|
9722
|
+
if (cancelled) return;
|
|
9723
|
+
cancelled = true;
|
|
9724
|
+
this.stateWatchers.delete(entry);
|
|
9725
|
+
this.releaseTopic(topic, socket);
|
|
9726
|
+
}
|
|
9727
|
+
};
|
|
9728
|
+
}
|
|
9729
|
+
/** @internal */
|
|
9730
|
+
subscribePresence(topic, handler) {
|
|
9731
|
+
const socket = this.ensureSocket();
|
|
9732
|
+
const entry = { topic, handler };
|
|
9733
|
+
this.presenceWatchers.add(entry);
|
|
9734
|
+
this.retainTopic(topic, socket);
|
|
9735
|
+
handler(this.presenceOthers(topic));
|
|
9736
|
+
let cancelled = false;
|
|
9737
|
+
return {
|
|
9738
|
+
cancel: () => {
|
|
9739
|
+
if (cancelled) return;
|
|
9740
|
+
cancelled = true;
|
|
9741
|
+
this.presenceWatchers.delete(entry);
|
|
9742
|
+
this.releaseTopic(topic, socket);
|
|
9743
|
+
}
|
|
9744
|
+
};
|
|
9745
|
+
}
|
|
9746
|
+
/** @internal */
|
|
9747
|
+
writeState(topic, key, value, life) {
|
|
9748
|
+
const socket = this.ensureSocket();
|
|
9749
|
+
socket.joinTopic(topic);
|
|
9750
|
+
socket.sendStateSet(topic, key, value, life);
|
|
9751
|
+
}
|
|
9752
|
+
/** @internal */
|
|
9753
|
+
clearState(topic, key) {
|
|
9754
|
+
this.ensureSocket().sendStateDel(topic, key);
|
|
9755
|
+
}
|
|
9756
|
+
/** @internal */
|
|
9757
|
+
presenceWrite(topic, meta) {
|
|
9758
|
+
const socket = this.ensureSocket();
|
|
9759
|
+
socket.joinTopic(topic);
|
|
9760
|
+
const conn = this.connIds.get(topic);
|
|
9761
|
+
if (conn === void 0) {
|
|
9762
|
+
this.pendingPresence.set(topic, meta);
|
|
9763
|
+
return;
|
|
9764
|
+
}
|
|
9765
|
+
socket.sendStateSet(topic, PRESENCE_KEY(conn), meta, "ephemeral");
|
|
9766
|
+
}
|
|
9767
|
+
/** @internal */
|
|
9768
|
+
presenceClear(topic) {
|
|
9769
|
+
this.pendingPresence.delete(topic);
|
|
9770
|
+
const conn = this.connIds.get(topic);
|
|
9771
|
+
if (conn === void 0) return;
|
|
9772
|
+
this.ensureSocket().sendStateDel(topic, PRESENCE_KEY(conn));
|
|
9773
|
+
}
|
|
9774
|
+
/** @internal */
|
|
9775
|
+
presenceOthers(topic) {
|
|
9776
|
+
return this.stateOf(topic).presence(this.connIds.get(topic));
|
|
9777
|
+
}
|
|
9778
|
+
retainTopic(topic, socket) {
|
|
9779
|
+
const count = (this.topicRefcount.get(topic) ?? 0) + 1;
|
|
9780
|
+
this.topicRefcount.set(topic, count);
|
|
9781
|
+
if (count === 1) socket.joinTopic(topic);
|
|
9782
|
+
}
|
|
9783
|
+
releaseTopic(topic, socket) {
|
|
9784
|
+
const next = (this.topicRefcount.get(topic) ?? 1) - 1;
|
|
9785
|
+
if (next <= 0) {
|
|
9786
|
+
this.topicRefcount.delete(topic);
|
|
9787
|
+
socket.leaveTopic(topic);
|
|
9788
|
+
return;
|
|
9789
|
+
}
|
|
9790
|
+
this.topicRefcount.set(topic, next);
|
|
9791
|
+
}
|
|
9792
|
+
applySnapshot(topic, body) {
|
|
9793
|
+
this.stateOf(topic).applySnapshot(body);
|
|
9794
|
+
this.notifyState(topic);
|
|
9795
|
+
}
|
|
9796
|
+
applyDiff(topic, body) {
|
|
9797
|
+
if (this.stateOf(topic).applyDiff(body) === "gap") {
|
|
9798
|
+
this.socket?.sendStateResync(topic);
|
|
9799
|
+
return;
|
|
9800
|
+
}
|
|
9801
|
+
this.notifyState(topic);
|
|
9802
|
+
}
|
|
9803
|
+
applyJoinInfo(topic, info) {
|
|
9804
|
+
if (typeof info.conn !== "string" || info.conn === "") return;
|
|
9805
|
+
this.connIds.set(topic, info.conn);
|
|
9806
|
+
const seq = info.state_seq;
|
|
9807
|
+
if (typeof seq === "number" && seq < this.stateOf(topic).seq) {
|
|
9808
|
+
this.stateOf(topic).applySnapshot({ seq, entries: {} });
|
|
9809
|
+
}
|
|
9810
|
+
const held = this.pendingPresence.get(topic);
|
|
9811
|
+
if (held !== void 0) {
|
|
9812
|
+
this.pendingPresence.delete(topic);
|
|
9813
|
+
this.socket?.sendStateSet(topic, PRESENCE_KEY(info.conn), held, "ephemeral");
|
|
9814
|
+
}
|
|
9815
|
+
this.notifyState(topic);
|
|
9816
|
+
}
|
|
9817
|
+
notifyState(topic) {
|
|
9818
|
+
this.statusStore.recordEvent(/* @__PURE__ */ new Date());
|
|
9819
|
+
const st = this.stateOf(topic);
|
|
9820
|
+
for (const w of this.stateWatchers) {
|
|
9821
|
+
if (w.topic === topic) w.handler(st.entries.get(w.key));
|
|
9822
|
+
}
|
|
9823
|
+
if (this.presenceWatchers.size > 0) {
|
|
9824
|
+
const others = this.presenceOthers(topic);
|
|
9825
|
+
for (const w of this.presenceWatchers) {
|
|
9826
|
+
if (w.topic === topic) w.handler(others);
|
|
9827
|
+
}
|
|
9828
|
+
}
|
|
9360
9829
|
}
|
|
9361
9830
|
};
|
|
9831
|
+
var PRESENCE_KEY = (conn) => `$p:${conn}`;
|
|
9362
9832
|
|
|
9363
9833
|
// src/storage.ts
|
|
9364
9834
|
function memorySessionStorage() {
|
|
@@ -9419,7 +9889,7 @@ function defaultSessionStorage(key) {
|
|
|
9419
9889
|
}
|
|
9420
9890
|
|
|
9421
9891
|
// src/version.ts
|
|
9422
|
-
var VERSION = "7.
|
|
9892
|
+
var VERSION = "7.4.0";
|
|
9423
9893
|
|
|
9424
9894
|
// src/runtime.ts
|
|
9425
9895
|
function buildRuntime(config) {
|
|
@@ -9597,6 +10067,7 @@ async function callEndpoint(resolveRt, name, input, options) {
|
|
|
9597
10067
|
}
|
|
9598
10068
|
|
|
9599
10069
|
// src/upload.ts
|
|
10070
|
+
var UPLOAD_PATH = "/v1/storage/upload";
|
|
9600
10071
|
function abortError() {
|
|
9601
10072
|
return new BackendError("network", { code: "aborted", message: "Upload aborted" });
|
|
9602
10073
|
}
|
|
@@ -9650,7 +10121,9 @@ async function buildHeaders(rt, extra) {
|
|
|
9650
10121
|
}
|
|
9651
10122
|
function buildForm(options) {
|
|
9652
10123
|
const form = new FormData();
|
|
9653
|
-
|
|
10124
|
+
const { body, ...loose } = options.fields ?? {};
|
|
10125
|
+
if (body !== void 0) form.append("body", body);
|
|
10126
|
+
else if (Object.keys(loose).length > 0) form.append("body", JSON.stringify(loose));
|
|
9654
10127
|
const file = options.contentType && options.file.type !== options.contentType ? new Blob([options.file], { type: options.contentType }) : options.file;
|
|
9655
10128
|
const filename = options.filename ?? (typeof File !== "undefined" && options.file instanceof File ? options.file.name : "file");
|
|
9656
10129
|
form.append("file", file, filename);
|
|
@@ -9724,8 +10197,8 @@ async function uploadViaFetch(url, form, headers, options) {
|
|
|
9724
10197
|
async function uploadEndpoint(resolveRt, name, options) {
|
|
9725
10198
|
const rt = resolveRt();
|
|
9726
10199
|
checkConstraints(options);
|
|
9727
|
-
const
|
|
9728
|
-
const url = `${rt.config.url}${
|
|
10200
|
+
const route = name.startsWith("/") ? name : `/${name}`;
|
|
10201
|
+
const url = `${rt.config.url}${UPLOAD_PATH}${route}`;
|
|
9729
10202
|
const headers = await buildHeaders(rt, options.headers);
|
|
9730
10203
|
const form = buildForm(options);
|
|
9731
10204
|
const useXHR = options.onProgress !== void 0 && typeof XMLHttpRequest !== "undefined";
|