@palbase/web 7.3.9 → 7.4.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.
- 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-EAOROUSW.js → chunk-4WMJTUDN.js} +26 -17
- package/dist/chunk-4WMJTUDN.js.map +1 -0
- package/dist/{chunk-Q5DUDL77.js → chunk-G56VGUYZ.js} +1 -1
- package/dist/chunk-G56VGUYZ.js.map +1 -0
- package/dist/{chunk-FCIMEU2E.js → chunk-VHUCJBBQ.js} +2 -2
- package/dist/chunk-VHUCJBBQ.js.map +1 -0
- package/dist/{chunk-H6YSCGQC.js → chunk-XJBMBUHU.js} +502 -38
- package/dist/chunk-XJBMBUHU.js.map +1 -0
- package/dist/gen/cli.cjs +2 -2
- package/dist/gen/cli.cjs.map +1 -1
- package/dist/gen/cli.js +3 -3
- 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 +524 -51
- 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 +518 -48
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +3 -3
- package/dist/next/index.cjs +524 -51
- 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.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-EAOROUSW.js.map +0 -1
- package/dist/chunk-FCIMEU2E.js.map +0 -1
- package/dist/chunk-H6YSCGQC.js.map +0 -1
- package/dist/chunk-Q5DUDL77.js.map +0 -1
package/dist/next/index.cjs
CHANGED
|
@@ -298,6 +298,15 @@ function parseEnvironmentRef(apiKey) {
|
|
|
298
298
|
var MAX_RETRIES = 3;
|
|
299
299
|
var INITIAL_BACKOFF_MS = 200;
|
|
300
300
|
var MAX_RETRY_DELAY_MS = 1e4;
|
|
301
|
+
function withRetryHint(body, response) {
|
|
302
|
+
if (response.status !== 429) return body;
|
|
303
|
+
const data = body?.data;
|
|
304
|
+
const alreadyStated = typeof body?.retry_after === "number" || typeof data === "object" && data !== null && "retryAfter" in data;
|
|
305
|
+
if (alreadyStated) return body;
|
|
306
|
+
const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
|
|
307
|
+
if (Number.isNaN(seconds) || seconds <= 0) return body;
|
|
308
|
+
return { ...body, retry_after: seconds };
|
|
309
|
+
}
|
|
301
310
|
var HttpClient = class _HttpClient {
|
|
302
311
|
apiKey;
|
|
303
312
|
options;
|
|
@@ -473,7 +482,7 @@ var HttpClient = class _HttpClient {
|
|
|
473
482
|
errorBody?.error ?? "unknown_error",
|
|
474
483
|
errorBody?.error_description ?? response.statusText,
|
|
475
484
|
response.status,
|
|
476
|
-
errorBody
|
|
485
|
+
withRetryHint(errorBody, response)
|
|
477
486
|
),
|
|
478
487
|
status: response.status
|
|
479
488
|
};
|
|
@@ -596,6 +605,15 @@ function isFieldErrorArray(value) {
|
|
|
596
605
|
(v) => typeof v === "object" && v !== null && typeof v.field === "string" && typeof v.message === "string"
|
|
597
606
|
);
|
|
598
607
|
}
|
|
608
|
+
function fieldErrorsOf(envelope) {
|
|
609
|
+
const scoped = pickField(pickField(envelope, "data"), "fields");
|
|
610
|
+
if (isFieldErrorArray(scoped)) return scoped;
|
|
611
|
+
const flat = pickField(envelope, "fields");
|
|
612
|
+
return isFieldErrorArray(flat) ? flat : void 0;
|
|
613
|
+
}
|
|
614
|
+
function retryAfterOf(envelope) {
|
|
615
|
+
return pickNumber(envelope, "retry_after") ?? pickNumber(pickField(envelope, "data"), "retryAfter");
|
|
616
|
+
}
|
|
599
617
|
function pickField(value, key) {
|
|
600
618
|
if (typeof value === "object" && value !== null) {
|
|
601
619
|
return value[key];
|
|
@@ -621,20 +639,15 @@ function fromPalbaseError(err) {
|
|
|
621
639
|
if (err.code === "network_error") return new BackendError("network", base);
|
|
622
640
|
if (err.status === 401) return new BackendError("unauthorized", base);
|
|
623
641
|
if (err.status === 429)
|
|
624
|
-
return new BackendError("rateLimited", {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
});
|
|
628
|
-
const nested = pickField(err.details, "details");
|
|
629
|
-
if (err.status === 400 && isFieldErrorArray(nested))
|
|
630
|
-
return new BackendError("validation", { ...base, fields: nested });
|
|
642
|
+
return new BackendError("rateLimited", { ...base, retryAfter: retryAfterOf(err.details) });
|
|
643
|
+
const fields = fieldErrorsOf(err.details);
|
|
644
|
+
if (err.status === 400 && fields) return new BackendError("validation", { ...base, fields });
|
|
631
645
|
return new BackendError("server", base);
|
|
632
646
|
}
|
|
633
647
|
function fromEnvelope(status, body) {
|
|
634
648
|
const code = pickString(body, "error") ?? "http_error";
|
|
635
649
|
const message = pickString(body, "error_description") ?? `HTTP ${status}`;
|
|
636
650
|
const requestId = pickString(body, "request_id");
|
|
637
|
-
const details = pickField(body, "details");
|
|
638
651
|
const params = {
|
|
639
652
|
code,
|
|
640
653
|
message,
|
|
@@ -644,13 +657,9 @@ function fromEnvelope(status, body) {
|
|
|
644
657
|
};
|
|
645
658
|
if (status === 401) return new BackendError("unauthorized", params);
|
|
646
659
|
if (status === 429)
|
|
647
|
-
return new BackendError("rateLimited", {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
retryAfter: pickNumber(body, "retry_after") ?? pickNumber(details, "retry_after")
|
|
651
|
-
});
|
|
652
|
-
if (status === 400 && isFieldErrorArray(details))
|
|
653
|
-
return new BackendError("validation", { ...params, fields: details });
|
|
660
|
+
return new BackendError("rateLimited", { ...params, retryAfter: retryAfterOf(body) });
|
|
661
|
+
const fields = fieldErrorsOf(body);
|
|
662
|
+
if (status === 400 && fields) return new BackendError("validation", { ...params, fields });
|
|
654
663
|
return new BackendError("server", params);
|
|
655
664
|
}
|
|
656
665
|
function isBackendError(e) {
|
|
@@ -1875,24 +1884,29 @@ var PalbeAuth = class {
|
|
|
1875
1884
|
* Self-service account erasure ("right to be forgotten"). Calls palauth
|
|
1876
1885
|
* `DELETE /auth/user` (session-authed + fresh re-auth): password users pass
|
|
1877
1886
|
* `{ password }`; passwordless/OAuth users rely on a freshly stepped-up
|
|
1878
|
-
* session.
|
|
1879
|
-
* already revoked server-side — so we tear down
|
|
1880
|
-
* `signOut()`'s clear, minus the pointless `/logout`)
|
|
1881
|
-
* `signedOut{reason:'accountDeleted'}`.
|
|
1887
|
+
* session. The handler answers **204 with no body** — the erasure has already
|
|
1888
|
+
* finished and every session is already revoked server-side — so we tear down
|
|
1889
|
+
* local state (mirroring `signOut()`'s clear, minus the pointless `/logout`)
|
|
1890
|
+
* and emit `signedOut{reason:'accountDeleted'}`.
|
|
1891
|
+
*
|
|
1892
|
+
* Returns nothing, because there is nothing to return: the handler used to
|
|
1893
|
+
* answer 202 with an `{erasure_id}` naming an asynchronous run, and that id
|
|
1894
|
+
* was removed along with the run (`gdpr_handlers.go:221-229`). Reading it
|
|
1895
|
+
* here threw a `TypeError` AFTER the account was already gone — a success
|
|
1896
|
+
* reported as a failure.
|
|
1882
1897
|
*
|
|
1883
1898
|
* On 401 `reauth_required` / 503 `not_configured` (or `erasure_unavailable`)
|
|
1884
1899
|
* the request throws BEFORE the teardown line, so the local session is left
|
|
1885
1900
|
* fully intact — the deletion did not happen and the user is still signed in.
|
|
1886
1901
|
*/
|
|
1887
1902
|
async deleteAccount(params = {}) {
|
|
1888
|
-
|
|
1903
|
+
await palbeRequest(this.rt, "DELETE", "/auth/user", {
|
|
1889
1904
|
body: params.password ? { password: params.password } : {}
|
|
1890
1905
|
});
|
|
1891
1906
|
this.nextSignOutReason = "accountDeleted";
|
|
1892
1907
|
this.rt.tokenManager.clearSession();
|
|
1893
1908
|
this.rt.storage.clear();
|
|
1894
1909
|
this.cachedUser = null;
|
|
1895
|
-
return { erasureId: res.erasure_id };
|
|
1896
1910
|
}
|
|
1897
1911
|
/**
|
|
1898
1912
|
* @internal — invoked by the transport choke point (`request.ts`), not app code.
|
|
@@ -2379,9 +2393,15 @@ var PalbeCalls = class {
|
|
|
2379
2393
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
2380
2394
|
* @returns A `Call` in `connecting` state; transitions to `active` once the media room connects.
|
|
2381
2395
|
*
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2396
|
+
* Every failure below arrives as kind `'server'` — this SDK's error kinds are
|
|
2397
|
+
* `notConfigured | validation | unauthorized | rateLimited | server | network
|
|
2398
|
+
* | decode`, and only a 401, a 429 or a 400 carrying a field-error array gets
|
|
2399
|
+
* its own kind. So branch on `err.code`, never on the kind.
|
|
2400
|
+
*
|
|
2401
|
+
* @throws BackendError('server', { code: 'calling_unavailable' }) no SFU configured on this stack (503)
|
|
2402
|
+
* @throws BackendError('server', { code: 'not_group_member' }) the caller is not a member of the group (403)
|
|
2403
|
+
* @throws BackendError('server', { code: 'call_room_full' }) the call is at its participant limit (409)
|
|
2404
|
+
* @throws BackendError('server', { code: 'call_provider_error' }) the media provider failed upstream (502)
|
|
2385
2405
|
*/
|
|
2386
2406
|
async start(groupId, { media = ["audio", "video"] } = {}) {
|
|
2387
2407
|
if (!groupId) {
|
|
@@ -2402,7 +2422,11 @@ var PalbeCalls = class {
|
|
|
2402
2422
|
* Accept an incoming call in a messaging group.
|
|
2403
2423
|
*
|
|
2404
2424
|
* @param groupId - Messaging group display-id
|
|
2405
|
-
* @param callId - Call identifier
|
|
2425
|
+
* @param callId - Call identifier, carried by the `call_invite` the server
|
|
2426
|
+
* fans onto the group's conversation topic
|
|
2427
|
+
* (`pb.realtime.channel('messaging:conv:<rfcGroupId>').on('call_invite', …)`).
|
|
2428
|
+
* This SDK ships no web push: on the web the invite arrives over realtime or
|
|
2429
|
+
* over whatever signalling the app already runs.
|
|
2406
2430
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
2407
2431
|
*/
|
|
2408
2432
|
async accept(groupId, callId, { media = ["audio", "video"] } = {}) {
|
|
@@ -4505,14 +4529,11 @@ var Chat = class {
|
|
|
4505
4529
|
get title() {
|
|
4506
4530
|
if (this.titleOverride) return this.titleOverride;
|
|
4507
4531
|
if (this._group?.name) return this._group.name;
|
|
4508
|
-
if (this.
|
|
4509
|
-
const
|
|
4510
|
-
|
|
4511
|
-
const peer = this.memberCache.find((m) => m.userId === mode.peerUserId);
|
|
4512
|
-
return peer?.displayName ?? "Chat";
|
|
4513
|
-
}
|
|
4514
|
-
return "Group";
|
|
4532
|
+
if (this.kind === "direct") {
|
|
4533
|
+
const peer = this.memberCache.find((m) => !m.isSelf);
|
|
4534
|
+
return peer?.displayName ?? "Chat";
|
|
4515
4535
|
}
|
|
4536
|
+
if (this._draft) return "Group";
|
|
4516
4537
|
const tail = this.id.startsWith("grp_") ? this.id.slice(4) : this.id;
|
|
4517
4538
|
const short = tail.slice(-4);
|
|
4518
4539
|
return short ? `Group ${short}` : "Group";
|
|
@@ -5486,6 +5507,11 @@ var MessageHub = class {
|
|
|
5486
5507
|
emitConv(displayId, e) {
|
|
5487
5508
|
for (const fn of this.convListeners.get(displayId) ?? []) fn(e);
|
|
5488
5509
|
}
|
|
5510
|
+
/** How many live conv listeners a group still has — the census the LAST
|
|
5511
|
+
* subscriber reads before tearing the conv topic (and its presence) down. */
|
|
5512
|
+
convListenerCount(displayId) {
|
|
5513
|
+
return this.convListeners.get(displayId)?.size ?? 0;
|
|
5514
|
+
}
|
|
5489
5515
|
add(map, key, fn) {
|
|
5490
5516
|
let set = map.get(key);
|
|
5491
5517
|
if (!set) {
|
|
@@ -5523,6 +5549,9 @@ var MessageDeliverySource = class {
|
|
|
5523
5549
|
started = false;
|
|
5524
5550
|
wakeUnsub = null;
|
|
5525
5551
|
// Per-observed-group conv subscription + heartbeat (presence/typing/read).
|
|
5552
|
+
// `teardown` is the ONE departure path: it stops the heartbeat, broadcasts the
|
|
5553
|
+
// offline frame and drops the subscriptions, so no caller can tear an
|
|
5554
|
+
// observation down without saying goodbye (see observeConversation).
|
|
5526
5555
|
observed = /* @__PURE__ */ new Map();
|
|
5527
5556
|
/** Subscribe the device wake topic + run an initial drain. Idempotent. */
|
|
5528
5557
|
async start() {
|
|
@@ -5547,10 +5576,7 @@ var MessageDeliverySource = class {
|
|
|
5547
5576
|
stop() {
|
|
5548
5577
|
this.wakeUnsub?.();
|
|
5549
5578
|
this.wakeUnsub = null;
|
|
5550
|
-
for (const [, o] of this.observed)
|
|
5551
|
-
o.unsub();
|
|
5552
|
-
if (o.heartbeat) clearInterval(o.heartbeat);
|
|
5553
|
-
}
|
|
5579
|
+
for (const [, o] of this.observed) o.teardown();
|
|
5554
5580
|
this.observed.clear();
|
|
5555
5581
|
this.started = false;
|
|
5556
5582
|
}
|
|
@@ -5781,14 +5807,26 @@ var MessageDeliverySource = class {
|
|
|
5781
5807
|
emitPresence(true);
|
|
5782
5808
|
const heartbeat = setInterval(() => emitPresence(true), 25e3);
|
|
5783
5809
|
this.observed.set(group.displayId, {
|
|
5784
|
-
|
|
5810
|
+
teardown: () => {
|
|
5811
|
+
clearInterval(heartbeat);
|
|
5812
|
+
try {
|
|
5813
|
+
emitPresence(false);
|
|
5814
|
+
} catch {
|
|
5815
|
+
}
|
|
5785
5816
|
for (const s of subs) s.cancel();
|
|
5786
|
-
}
|
|
5787
|
-
heartbeat
|
|
5817
|
+
}
|
|
5788
5818
|
});
|
|
5789
5819
|
} catch {
|
|
5790
5820
|
}
|
|
5791
5821
|
}
|
|
5822
|
+
/** Stop observing a group's conv topic: announce OFFLINE, stop the heartbeat
|
|
5823
|
+
* and drop the subscriptions. Idempotent — an unobserved group is a no-op. */
|
|
5824
|
+
unobserveConversation(group) {
|
|
5825
|
+
const o = this.observed.get(group.displayId);
|
|
5826
|
+
if (!o) return;
|
|
5827
|
+
this.observed.delete(group.displayId);
|
|
5828
|
+
o.teardown();
|
|
5829
|
+
}
|
|
5792
5830
|
/** Announce typing on a group's conv topic (the app calls per keystroke). */
|
|
5793
5831
|
setTyping(group, isTyping) {
|
|
5794
5832
|
this.observeConversation(group);
|
|
@@ -8016,7 +8054,11 @@ var MessagingCoordinator = class {
|
|
|
8016
8054
|
subscribeLive(group, chat) {
|
|
8017
8055
|
let offMsg = null;
|
|
8018
8056
|
let offConv = null;
|
|
8057
|
+
let resolved = null;
|
|
8058
|
+
let cancelled = false;
|
|
8019
8059
|
void this.resolve().then((r) => {
|
|
8060
|
+
if (cancelled) return;
|
|
8061
|
+
resolved = r;
|
|
8020
8062
|
offMsg = r.hub.onMessage(group.displayId, (m) => {
|
|
8021
8063
|
void chat.ingestLive(m);
|
|
8022
8064
|
});
|
|
@@ -8024,8 +8066,12 @@ var MessagingCoordinator = class {
|
|
|
8024
8066
|
offConv = r.hub.onConv(group.displayId, (e) => chat.applyConv(e.event, e.payload));
|
|
8025
8067
|
});
|
|
8026
8068
|
return () => {
|
|
8069
|
+
cancelled = true;
|
|
8027
8070
|
offMsg?.();
|
|
8028
8071
|
offConv?.();
|
|
8072
|
+
if (resolved && resolved.hub.convListenerCount(group.displayId) === 0) {
|
|
8073
|
+
resolved.source.unobserveConversation(group);
|
|
8074
|
+
}
|
|
8029
8075
|
};
|
|
8030
8076
|
}
|
|
8031
8077
|
async userIdForDevice(group, deviceId) {
|
|
@@ -8883,8 +8929,33 @@ function unwrapBroadcast(frame) {
|
|
|
8883
8929
|
function stripRealtimePrefix(topic) {
|
|
8884
8930
|
return topic.startsWith(REALTIME_PREFIX) ? topic.slice(REALTIME_PREFIX.length) : topic;
|
|
8885
8931
|
}
|
|
8932
|
+
function encodeStateSet(joinRef, ref, topic, key, value, life) {
|
|
8933
|
+
return JSON.stringify([
|
|
8934
|
+
joinRef,
|
|
8935
|
+
String(ref),
|
|
8936
|
+
REALTIME_PREFIX + topic,
|
|
8937
|
+
"state_set",
|
|
8938
|
+
{ key, value, life }
|
|
8939
|
+
]);
|
|
8940
|
+
}
|
|
8941
|
+
function encodeStateDel(joinRef, ref, topic, key) {
|
|
8942
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_del", { key }]);
|
|
8943
|
+
}
|
|
8944
|
+
function encodeStateResync(joinRef, ref, topic) {
|
|
8945
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_resync", {}]);
|
|
8946
|
+
}
|
|
8947
|
+
function encodeAccessToken(joinRef, ref, topic, token) {
|
|
8948
|
+
return JSON.stringify([
|
|
8949
|
+
joinRef,
|
|
8950
|
+
String(ref),
|
|
8951
|
+
REALTIME_PREFIX + topic,
|
|
8952
|
+
"access_token",
|
|
8953
|
+
{ access_token: token }
|
|
8954
|
+
]);
|
|
8955
|
+
}
|
|
8886
8956
|
|
|
8887
8957
|
// src/realtime/connection.ts
|
|
8958
|
+
var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
8888
8959
|
var WS_OPEN = 1;
|
|
8889
8960
|
var DEFAULT_HEARTBEAT_MS = 25e3;
|
|
8890
8961
|
var DEFAULT_MAX_BACKOFF_SECONDS = 30;
|
|
@@ -9117,6 +9188,31 @@ var RealtimeSocket = class {
|
|
|
9117
9188
|
this.handleChannelClose(frame);
|
|
9118
9189
|
return;
|
|
9119
9190
|
}
|
|
9191
|
+
if (frame.event === "state_snapshot") {
|
|
9192
|
+
this.handlers?.onStateSnapshot(stripRealtimePrefix(frame.topic), frame.payload);
|
|
9193
|
+
return;
|
|
9194
|
+
}
|
|
9195
|
+
if (frame.event === "state_diff") {
|
|
9196
|
+
this.handlers?.onStateDiff(stripRealtimePrefix(frame.topic), frame.payload);
|
|
9197
|
+
return;
|
|
9198
|
+
}
|
|
9199
|
+
if (frame.event === "phx_reply") {
|
|
9200
|
+
const body = frame.payload;
|
|
9201
|
+
const topic = stripRealtimePrefix(frame.topic);
|
|
9202
|
+
if (body && body.status === "ok" && body.response && typeof body.response === "object") {
|
|
9203
|
+
this.handlers?.onJoinInfo(topic, body.response);
|
|
9204
|
+
return;
|
|
9205
|
+
}
|
|
9206
|
+
if (body && body.status === "error" && String(frame.ref) === this.joinRefs.get(topic)) {
|
|
9207
|
+
const reason = typeof body.response?.["reason"] === "string" ? body.response["reason"] : "unauthorized";
|
|
9208
|
+
if (!RETRYABLE_REFUSALS.has(reason)) {
|
|
9209
|
+
this.joinedTopics.delete(topic);
|
|
9210
|
+
this.joinRefs.delete(topic);
|
|
9211
|
+
}
|
|
9212
|
+
this.handlers?.onChannelRefused(topic, reason);
|
|
9213
|
+
}
|
|
9214
|
+
return;
|
|
9215
|
+
}
|
|
9120
9216
|
const inbound = unwrapBroadcast(frame);
|
|
9121
9217
|
if (inbound) {
|
|
9122
9218
|
this.channelRejoinAttempts.delete(inbound.topic);
|
|
@@ -9157,6 +9253,40 @@ var RealtimeSocket = class {
|
|
|
9157
9253
|
}, delayMs);
|
|
9158
9254
|
this.channelRejoinTimers.set(topic, timer);
|
|
9159
9255
|
}
|
|
9256
|
+
// MARK: state
|
|
9257
|
+
/** Write one entry on a joined channel's state. Dropped when the topic has no
|
|
9258
|
+
* live join: the server refuses state on a channel this socket is not on, and
|
|
9259
|
+
* the reconnect's join brings a fresh snapshot anyway. */
|
|
9260
|
+
sendStateSet(topic, key, value, life) {
|
|
9261
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9262
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9263
|
+
this.send(encodeStateSet(joinRef, this.nextRef(), topic, key, value, life));
|
|
9264
|
+
}
|
|
9265
|
+
/** Remove one entry. The server refuses a key this connection does not own. */
|
|
9266
|
+
sendStateDel(topic, key) {
|
|
9267
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9268
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9269
|
+
this.send(encodeStateDel(joinRef, this.nextRef(), topic, key));
|
|
9270
|
+
}
|
|
9271
|
+
/** Ask for a fresh snapshot — the answer to a detected gap. */
|
|
9272
|
+
sendStateResync(topic) {
|
|
9273
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9274
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9275
|
+
this.send(encodeStateResync(joinRef, this.nextRef(), topic));
|
|
9276
|
+
}
|
|
9277
|
+
/**
|
|
9278
|
+
* Present a fresher credential on every joined channel WITHOUT rejoining.
|
|
9279
|
+
*
|
|
9280
|
+
* The alternative — dropping and rejoining — reaps this connection's
|
|
9281
|
+
* ephemeral entries (its presence) and forces a full resync on every channel.
|
|
9282
|
+
* A token rotation is not a reconnection and must not look like one.
|
|
9283
|
+
*/
|
|
9284
|
+
sendAccessToken(token) {
|
|
9285
|
+
if (!this.isOpen()) return;
|
|
9286
|
+
for (const [topic, joinRef] of this.joinRefs) {
|
|
9287
|
+
this.send(encodeAccessToken(joinRef, this.nextRef(), topic, token));
|
|
9288
|
+
}
|
|
9289
|
+
}
|
|
9160
9290
|
// MARK: helpers
|
|
9161
9291
|
/** Drop a single topic's token-expiry rejoin state (cancel its pending timer). */
|
|
9162
9292
|
clearChannelRejoin(topic) {
|
|
@@ -9182,6 +9312,60 @@ var RealtimeSocket = class {
|
|
|
9182
9312
|
}
|
|
9183
9313
|
};
|
|
9184
9314
|
|
|
9315
|
+
// src/realtime/state.ts
|
|
9316
|
+
var PRESENCE_PREFIX = "$p:";
|
|
9317
|
+
var ChannelState = class {
|
|
9318
|
+
/** The sequence this local copy is current as of. */
|
|
9319
|
+
seq = 0;
|
|
9320
|
+
/** key → value. Lifetimes are the server's business; a reader only needs the
|
|
9321
|
+
* values, and presence is derived from the key shape. */
|
|
9322
|
+
entries = /* @__PURE__ */ new Map();
|
|
9323
|
+
/** Replace everything. A snapshot is the WHOLE picture, never a merge —
|
|
9324
|
+
* merging one would keep entries the server has since removed. */
|
|
9325
|
+
applySnapshot(body) {
|
|
9326
|
+
this.entries.clear();
|
|
9327
|
+
for (const [k, e] of Object.entries(body.entries ?? {})) {
|
|
9328
|
+
this.entries.set(k, e.v);
|
|
9329
|
+
}
|
|
9330
|
+
this.seq = body.seq;
|
|
9331
|
+
}
|
|
9332
|
+
/**
|
|
9333
|
+
* Apply a diff, or report that it cannot be applied.
|
|
9334
|
+
*
|
|
9335
|
+
* `gap` when this copy is behind the diff's starting point: frames were
|
|
9336
|
+
* missed, and applying anyway would produce a state that never existed on the
|
|
9337
|
+
* server. The state is left untouched so the caller can resync from something
|
|
9338
|
+
* consistent.
|
|
9339
|
+
*
|
|
9340
|
+
* A diff at or below the current sequence is already applied — a resend after
|
|
9341
|
+
* a retry — and is silently accepted so duplicates are harmless.
|
|
9342
|
+
*/
|
|
9343
|
+
applyDiff(body) {
|
|
9344
|
+
if (this.seq < body.from_seq) return "gap";
|
|
9345
|
+
if (body.seq <= this.seq) return "applied";
|
|
9346
|
+
for (const [k, e] of Object.entries(body.set ?? {})) this.entries.set(k, e.v);
|
|
9347
|
+
for (const k of body.del ?? []) this.entries.delete(k);
|
|
9348
|
+
this.seq = body.seq;
|
|
9349
|
+
return "applied";
|
|
9350
|
+
}
|
|
9351
|
+
/**
|
|
9352
|
+
* Everyone else's presence.
|
|
9353
|
+
*
|
|
9354
|
+
* `selfConn` is this connection's id, as the join reply reported it. Its own
|
|
9355
|
+
* entry is excluded because an app rendering "who else is here" would
|
|
9356
|
+
* otherwise always show one extra face — itself.
|
|
9357
|
+
*/
|
|
9358
|
+
presence(selfConn) {
|
|
9359
|
+
const own = selfConn === void 0 ? void 0 : PRESENCE_PREFIX + selfConn;
|
|
9360
|
+
const out = [];
|
|
9361
|
+
for (const [k, v] of this.entries) {
|
|
9362
|
+
if (!k.startsWith(PRESENCE_PREFIX) || k === own) continue;
|
|
9363
|
+
out.push(v);
|
|
9364
|
+
}
|
|
9365
|
+
return out;
|
|
9366
|
+
}
|
|
9367
|
+
};
|
|
9368
|
+
|
|
9185
9369
|
// src/realtime/facade.ts
|
|
9186
9370
|
var StatusStore = class {
|
|
9187
9371
|
currentState = "idle";
|
|
@@ -9217,6 +9401,7 @@ var StatusStore = class {
|
|
|
9217
9401
|
for (const listener of this.listeners) listener(snapshot);
|
|
9218
9402
|
}
|
|
9219
9403
|
};
|
|
9404
|
+
var RETRYABLE_REASONS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
9220
9405
|
var RealtimeChannel = class {
|
|
9221
9406
|
/** The app-defined channel name (bare sub-topic, no "realtime:" prefix). */
|
|
9222
9407
|
name;
|
|
@@ -9234,6 +9419,23 @@ var RealtimeChannel = class {
|
|
|
9234
9419
|
on(event, handler) {
|
|
9235
9420
|
return this.owner.subscribe(this.name, event, handler);
|
|
9236
9421
|
}
|
|
9422
|
+
/**
|
|
9423
|
+
* Called when the server REFUSES this channel.
|
|
9424
|
+
*
|
|
9425
|
+
* A join is an authorization decision, and a decision the app never hears is
|
|
9426
|
+
* the same as no decision at all: silence looks exactly like a slow network.
|
|
9427
|
+
* This is where a refusal arrives, carrying the server's own distinction —
|
|
9428
|
+
* `retryable: false` means "you may not" and asking again will not help;
|
|
9429
|
+
* `retryable: true` means the project could not be asked, and the socket's
|
|
9430
|
+
* next reconnect retries on its own.
|
|
9431
|
+
*
|
|
9432
|
+
* pb.realtime.channel('room:42').onError((err) => {
|
|
9433
|
+
* if (!err.retryable) showJoinDenied()
|
|
9434
|
+
* })
|
|
9435
|
+
*/
|
|
9436
|
+
onError(handler) {
|
|
9437
|
+
return this.owner.subscribeError(this.name, handler);
|
|
9438
|
+
}
|
|
9237
9439
|
/**
|
|
9238
9440
|
* Broadcast `payload` to the channel's other subscribers (web addition —
|
|
9239
9441
|
* iOS is receive-only). Joins the channel if it isn't already; queued
|
|
@@ -9242,6 +9444,57 @@ var RealtimeChannel = class {
|
|
|
9242
9444
|
send(event, payload = {}) {
|
|
9243
9445
|
this.owner.send(this.name, event, payload);
|
|
9244
9446
|
}
|
|
9447
|
+
/**
|
|
9448
|
+
* Watch one key of the channel's shared state.
|
|
9449
|
+
*
|
|
9450
|
+
* The handler is called with the CURRENT value straight away when there is
|
|
9451
|
+
* one, and again on every change. That is the difference from `on()`: a
|
|
9452
|
+
* subscriber to an event learns nothing until the next one, while state is
|
|
9453
|
+
* already there when you ask.
|
|
9454
|
+
*/
|
|
9455
|
+
onState(key, handler) {
|
|
9456
|
+
return this.owner.subscribeState(this.name, key, handler);
|
|
9457
|
+
}
|
|
9458
|
+
/** Read one key without subscribing. */
|
|
9459
|
+
stateValue(key) {
|
|
9460
|
+
return this.owner.stateOf(this.name).entries.get(key);
|
|
9461
|
+
}
|
|
9462
|
+
/** Write one key. `durable` outlives this connection; `ephemeral` is reaped
|
|
9463
|
+
* when it closes, which is what presence is built on. */
|
|
9464
|
+
setState(key, value, opts = {}) {
|
|
9465
|
+
this.owner.writeState(this.name, key, value, opts.life ?? "durable");
|
|
9466
|
+
}
|
|
9467
|
+
/** Remove a key this connection wrote. */
|
|
9468
|
+
clearState(key) {
|
|
9469
|
+
this.owner.clearState(this.name, key);
|
|
9470
|
+
}
|
|
9471
|
+
/**
|
|
9472
|
+
* Announce this client on the channel. `meta` is whatever other participants
|
|
9473
|
+
* should see — a name, an avatar, a cursor.
|
|
9474
|
+
*
|
|
9475
|
+
* It is ephemeral state keyed by this connection, so it disappears when the
|
|
9476
|
+
* socket does. Nothing has to be un-announced on a crash.
|
|
9477
|
+
*/
|
|
9478
|
+
presenceEnter(meta) {
|
|
9479
|
+
this.owner.presenceWrite(this.name, meta);
|
|
9480
|
+
}
|
|
9481
|
+
/** Replace the announced value — a cursor moving is this, at whatever rate
|
|
9482
|
+
* the app likes: the server conflates writes it cannot deliver fast enough. */
|
|
9483
|
+
presenceUpdate(meta) {
|
|
9484
|
+
this.owner.presenceWrite(this.name, meta);
|
|
9485
|
+
}
|
|
9486
|
+
/** Leave early. Closing the socket does this on its own. */
|
|
9487
|
+
presenceLeave() {
|
|
9488
|
+
this.owner.presenceClear(this.name);
|
|
9489
|
+
}
|
|
9490
|
+
/** Everyone else currently announced, this client excluded. */
|
|
9491
|
+
presenceOthers() {
|
|
9492
|
+
return this.owner.presenceOthers(this.name);
|
|
9493
|
+
}
|
|
9494
|
+
/** Watch the announced set. Called on every change. */
|
|
9495
|
+
onPresence(handler) {
|
|
9496
|
+
return this.owner.subscribePresence(this.name, handler);
|
|
9497
|
+
}
|
|
9245
9498
|
};
|
|
9246
9499
|
var PalbeRealtime = class {
|
|
9247
9500
|
rt;
|
|
@@ -9251,9 +9504,21 @@ var PalbeRealtime = class {
|
|
|
9251
9504
|
// Per-(topic, event) handlers — an event may have multiple handlers
|
|
9252
9505
|
// (multiple .on calls); entry identity is the unsubscribe token.
|
|
9253
9506
|
handlers = /* @__PURE__ */ new Set();
|
|
9507
|
+
// Per-topic refusal handlers. Kept apart from `handlers` because they are not
|
|
9508
|
+
// refcounted: registering one must not join a channel, and cancelling the
|
|
9509
|
+
// last one must not leave a channel that still has event handlers on it.
|
|
9510
|
+
errorHandlers = /* @__PURE__ */ new Set();
|
|
9254
9511
|
// Joins are refcounted per topic so the socket only leaves a topic when
|
|
9255
9512
|
// its last handler cancels (iOS RealtimeClient parity).
|
|
9256
9513
|
topicRefcount = /* @__PURE__ */ new Map();
|
|
9514
|
+
// Per-topic shared state, and this connection's identity on each topic (from
|
|
9515
|
+
// the join ack). Presence is keyed by that identity, so a write before the
|
|
9516
|
+
// ack has nowhere to go and is queued until it arrives.
|
|
9517
|
+
states = /* @__PURE__ */ new Map();
|
|
9518
|
+
connIds = /* @__PURE__ */ new Map();
|
|
9519
|
+
pendingPresence = /* @__PURE__ */ new Map();
|
|
9520
|
+
stateWatchers = /* @__PURE__ */ new Set();
|
|
9521
|
+
presenceWatchers = /* @__PURE__ */ new Set();
|
|
9257
9522
|
constructor(rt) {
|
|
9258
9523
|
this.rt = rt;
|
|
9259
9524
|
}
|
|
@@ -9296,6 +9561,20 @@ var PalbeRealtime = class {
|
|
|
9296
9561
|
get status() {
|
|
9297
9562
|
return this.statusStore;
|
|
9298
9563
|
}
|
|
9564
|
+
/**
|
|
9565
|
+
* Hand the socket a fresher credential for the SAME user, without rejoining.
|
|
9566
|
+
*
|
|
9567
|
+
* Called when the app's token rotates. The alternative — letting the channels
|
|
9568
|
+
* die at expiry and rejoining — reaps this connection's ephemeral entries
|
|
9569
|
+
* (its presence, on every channel) and forces a full state resync. A token
|
|
9570
|
+
* getting older is not a disconnection and must not cost one.
|
|
9571
|
+
*
|
|
9572
|
+
* A no-op before a socket exists: the first connect resolves a fresh token
|
|
9573
|
+
* on its own.
|
|
9574
|
+
*/
|
|
9575
|
+
refreshToken(token) {
|
|
9576
|
+
this.socket?.sendAccessToken(token);
|
|
9577
|
+
}
|
|
9299
9578
|
/** @internal */
|
|
9300
9579
|
subscribe(topic, event, handler) {
|
|
9301
9580
|
const socket = this.ensureSocket();
|
|
@@ -9310,7 +9589,9 @@ var PalbeRealtime = class {
|
|
|
9310
9589
|
if (cancelled) return;
|
|
9311
9590
|
cancelled = true;
|
|
9312
9591
|
this.handlers.delete(entry);
|
|
9313
|
-
const
|
|
9592
|
+
const held = this.topicRefcount.get(topic);
|
|
9593
|
+
if (held === void 0) return;
|
|
9594
|
+
const next = held - 1;
|
|
9314
9595
|
if (next <= 0) {
|
|
9315
9596
|
this.topicRefcount.delete(topic);
|
|
9316
9597
|
socket.leaveTopic(topic);
|
|
@@ -9320,6 +9601,25 @@ var PalbeRealtime = class {
|
|
|
9320
9601
|
}
|
|
9321
9602
|
};
|
|
9322
9603
|
}
|
|
9604
|
+
/**
|
|
9605
|
+
* Register a refusal handler for one topic. Does NOT join: watching for a
|
|
9606
|
+
* refusal is not a subscription, and an app that registers one before its
|
|
9607
|
+
* first `.on()` should not be joining anything yet.
|
|
9608
|
+
*
|
|
9609
|
+
* @internal
|
|
9610
|
+
*/
|
|
9611
|
+
subscribeError(topic, handler) {
|
|
9612
|
+
const entry = { topic, handler };
|
|
9613
|
+
this.errorHandlers.add(entry);
|
|
9614
|
+
let cancelled = false;
|
|
9615
|
+
return {
|
|
9616
|
+
cancel: () => {
|
|
9617
|
+
if (cancelled) return;
|
|
9618
|
+
cancelled = true;
|
|
9619
|
+
this.errorHandlers.delete(entry);
|
|
9620
|
+
}
|
|
9621
|
+
};
|
|
9622
|
+
}
|
|
9323
9623
|
/** @internal */
|
|
9324
9624
|
send(topic, event, payload) {
|
|
9325
9625
|
const socket = this.ensureSocket();
|
|
@@ -9341,7 +9641,11 @@ var PalbeRealtime = class {
|
|
|
9341
9641
|
onReconnect: () => {
|
|
9342
9642
|
},
|
|
9343
9643
|
onStateChange: (state) => this.statusStore.setState(state),
|
|
9344
|
-
onChannelError: (topic) => this.handleChannelError(topic)
|
|
9644
|
+
onChannelError: (topic) => this.handleChannelError(topic),
|
|
9645
|
+
onChannelRefused: (topic, reason) => this.handleChannelRefused(topic, reason),
|
|
9646
|
+
onStateSnapshot: (topic, body) => this.applySnapshot(topic, body),
|
|
9647
|
+
onStateDiff: (topic, body) => this.applyDiff(topic, body),
|
|
9648
|
+
onJoinInfo: (topic, info) => this.applyJoinInfo(topic, info)
|
|
9345
9649
|
});
|
|
9346
9650
|
}
|
|
9347
9651
|
return this.socket;
|
|
@@ -9355,19 +9659,185 @@ var PalbeRealtime = class {
|
|
|
9355
9659
|
}
|
|
9356
9660
|
}
|
|
9357
9661
|
/**
|
|
9358
|
-
|
|
9359
|
-
*
|
|
9360
|
-
*
|
|
9361
|
-
*
|
|
9662
|
+
/**
|
|
9663
|
+
* The server refused a channel. Tell whoever asked, and — when the answer is
|
|
9664
|
+
* final — stop pretending the app is subscribed to it.
|
|
9665
|
+
*
|
|
9666
|
+
* `unavailable` leaves everything in place: the project could not be reached
|
|
9667
|
+
* to decide, the socket rejoins on its next reconnect, and dropping the app's
|
|
9668
|
+
* handlers would make a momentary outage look like a permanent denial.
|
|
9669
|
+
*/
|
|
9670
|
+
handleChannelRefused(topic, reason) {
|
|
9671
|
+
const retryable = RETRYABLE_REASONS.has(reason);
|
|
9672
|
+
for (const entry of this.errorHandlers) {
|
|
9673
|
+
if (entry.topic === topic) entry.handler({ topic, reason, retryable });
|
|
9674
|
+
}
|
|
9675
|
+
if (retryable) return;
|
|
9676
|
+
this.forgetTopic(topic);
|
|
9677
|
+
}
|
|
9678
|
+
/**
|
|
9679
|
+
* A channel died for good — a token-expiry rejoin that exhausted its attempts.
|
|
9680
|
+
* The app must stop expecting anything on it, and the status observable flips
|
|
9681
|
+
* to `'error'` (the React `useChannel` hook reports that).
|
|
9682
|
+
*
|
|
9683
|
+
* It clears the SAME registries a final refusal clears, and used to clear only
|
|
9684
|
+
* the event handlers. A state or presence watcher left behind here waits
|
|
9685
|
+
* forever for a snapshot that cannot arrive, and fires twice if the app ever
|
|
9686
|
+
* re-subscribes — the identical failure its sibling three functions up
|
|
9687
|
+
* describes, on a channel that was given up on rather than refused.
|
|
9362
9688
|
*/
|
|
9363
9689
|
handleChannelError(topic) {
|
|
9690
|
+
this.forgetTopic(topic);
|
|
9691
|
+
this.statusStore.setState("error");
|
|
9692
|
+
}
|
|
9693
|
+
/** Drop every registration this topic holds. One place, because "which
|
|
9694
|
+
* registries" is a question two callers must never answer differently. */
|
|
9695
|
+
forgetTopic(topic) {
|
|
9364
9696
|
for (const entry of this.handlers) {
|
|
9365
9697
|
if (entry.topic === topic) this.handlers.delete(entry);
|
|
9366
9698
|
}
|
|
9699
|
+
for (const entry of this.stateWatchers) {
|
|
9700
|
+
if (entry.topic === topic) this.stateWatchers.delete(entry);
|
|
9701
|
+
}
|
|
9702
|
+
for (const entry of this.presenceWatchers) {
|
|
9703
|
+
if (entry.topic === topic) this.presenceWatchers.delete(entry);
|
|
9704
|
+
}
|
|
9705
|
+
this.pendingPresence.delete(topic);
|
|
9706
|
+
this.states.delete(topic);
|
|
9707
|
+
this.connIds.delete(topic);
|
|
9367
9708
|
this.topicRefcount.delete(topic);
|
|
9368
|
-
|
|
9709
|
+
}
|
|
9710
|
+
// ── state ──────────────────────────────────────────────────────────────────
|
|
9711
|
+
/** @internal */
|
|
9712
|
+
stateOf(topic) {
|
|
9713
|
+
let st = this.states.get(topic);
|
|
9714
|
+
if (!st) {
|
|
9715
|
+
st = new ChannelState();
|
|
9716
|
+
this.states.set(topic, st);
|
|
9717
|
+
}
|
|
9718
|
+
return st;
|
|
9719
|
+
}
|
|
9720
|
+
/** @internal */
|
|
9721
|
+
subscribeState(topic, key, handler) {
|
|
9722
|
+
const socket = this.ensureSocket();
|
|
9723
|
+
const entry = { topic, key, handler };
|
|
9724
|
+
this.stateWatchers.add(entry);
|
|
9725
|
+
this.retainTopic(topic, socket);
|
|
9726
|
+
const known = this.stateOf(topic).entries.get(key);
|
|
9727
|
+
if (known !== void 0) handler(known);
|
|
9728
|
+
let cancelled = false;
|
|
9729
|
+
return {
|
|
9730
|
+
cancel: () => {
|
|
9731
|
+
if (cancelled) return;
|
|
9732
|
+
cancelled = true;
|
|
9733
|
+
this.stateWatchers.delete(entry);
|
|
9734
|
+
this.releaseTopic(topic, socket);
|
|
9735
|
+
}
|
|
9736
|
+
};
|
|
9737
|
+
}
|
|
9738
|
+
/** @internal */
|
|
9739
|
+
subscribePresence(topic, handler) {
|
|
9740
|
+
const socket = this.ensureSocket();
|
|
9741
|
+
const entry = { topic, handler };
|
|
9742
|
+
this.presenceWatchers.add(entry);
|
|
9743
|
+
this.retainTopic(topic, socket);
|
|
9744
|
+
handler(this.presenceOthers(topic));
|
|
9745
|
+
let cancelled = false;
|
|
9746
|
+
return {
|
|
9747
|
+
cancel: () => {
|
|
9748
|
+
if (cancelled) return;
|
|
9749
|
+
cancelled = true;
|
|
9750
|
+
this.presenceWatchers.delete(entry);
|
|
9751
|
+
this.releaseTopic(topic, socket);
|
|
9752
|
+
}
|
|
9753
|
+
};
|
|
9754
|
+
}
|
|
9755
|
+
/** @internal */
|
|
9756
|
+
writeState(topic, key, value, life) {
|
|
9757
|
+
const socket = this.ensureSocket();
|
|
9758
|
+
socket.joinTopic(topic);
|
|
9759
|
+
socket.sendStateSet(topic, key, value, life);
|
|
9760
|
+
}
|
|
9761
|
+
/** @internal */
|
|
9762
|
+
clearState(topic, key) {
|
|
9763
|
+
this.ensureSocket().sendStateDel(topic, key);
|
|
9764
|
+
}
|
|
9765
|
+
/** @internal */
|
|
9766
|
+
presenceWrite(topic, meta) {
|
|
9767
|
+
const socket = this.ensureSocket();
|
|
9768
|
+
socket.joinTopic(topic);
|
|
9769
|
+
const conn = this.connIds.get(topic);
|
|
9770
|
+
if (conn === void 0) {
|
|
9771
|
+
this.pendingPresence.set(topic, meta);
|
|
9772
|
+
return;
|
|
9773
|
+
}
|
|
9774
|
+
socket.sendStateSet(topic, PRESENCE_KEY(conn), meta, "ephemeral");
|
|
9775
|
+
}
|
|
9776
|
+
/** @internal */
|
|
9777
|
+
presenceClear(topic) {
|
|
9778
|
+
this.pendingPresence.delete(topic);
|
|
9779
|
+
const conn = this.connIds.get(topic);
|
|
9780
|
+
if (conn === void 0) return;
|
|
9781
|
+
this.ensureSocket().sendStateDel(topic, PRESENCE_KEY(conn));
|
|
9782
|
+
}
|
|
9783
|
+
/** @internal */
|
|
9784
|
+
presenceOthers(topic) {
|
|
9785
|
+
return this.stateOf(topic).presence(this.connIds.get(topic));
|
|
9786
|
+
}
|
|
9787
|
+
retainTopic(topic, socket) {
|
|
9788
|
+
const count = (this.topicRefcount.get(topic) ?? 0) + 1;
|
|
9789
|
+
this.topicRefcount.set(topic, count);
|
|
9790
|
+
if (count === 1) socket.joinTopic(topic);
|
|
9791
|
+
}
|
|
9792
|
+
releaseTopic(topic, socket) {
|
|
9793
|
+
const next = (this.topicRefcount.get(topic) ?? 1) - 1;
|
|
9794
|
+
if (next <= 0) {
|
|
9795
|
+
this.topicRefcount.delete(topic);
|
|
9796
|
+
socket.leaveTopic(topic);
|
|
9797
|
+
return;
|
|
9798
|
+
}
|
|
9799
|
+
this.topicRefcount.set(topic, next);
|
|
9800
|
+
}
|
|
9801
|
+
applySnapshot(topic, body) {
|
|
9802
|
+
this.stateOf(topic).applySnapshot(body);
|
|
9803
|
+
this.notifyState(topic);
|
|
9804
|
+
}
|
|
9805
|
+
applyDiff(topic, body) {
|
|
9806
|
+
if (this.stateOf(topic).applyDiff(body) === "gap") {
|
|
9807
|
+
this.socket?.sendStateResync(topic);
|
|
9808
|
+
return;
|
|
9809
|
+
}
|
|
9810
|
+
this.notifyState(topic);
|
|
9811
|
+
}
|
|
9812
|
+
applyJoinInfo(topic, info) {
|
|
9813
|
+
if (typeof info.conn !== "string" || info.conn === "") return;
|
|
9814
|
+
this.connIds.set(topic, info.conn);
|
|
9815
|
+
const seq = info.state_seq;
|
|
9816
|
+
if (typeof seq === "number" && seq < this.stateOf(topic).seq) {
|
|
9817
|
+
this.stateOf(topic).applySnapshot({ seq, entries: {} });
|
|
9818
|
+
}
|
|
9819
|
+
const held = this.pendingPresence.get(topic);
|
|
9820
|
+
if (held !== void 0) {
|
|
9821
|
+
this.pendingPresence.delete(topic);
|
|
9822
|
+
this.socket?.sendStateSet(topic, PRESENCE_KEY(info.conn), held, "ephemeral");
|
|
9823
|
+
}
|
|
9824
|
+
this.notifyState(topic);
|
|
9825
|
+
}
|
|
9826
|
+
notifyState(topic) {
|
|
9827
|
+
this.statusStore.recordEvent(/* @__PURE__ */ new Date());
|
|
9828
|
+
const st = this.stateOf(topic);
|
|
9829
|
+
for (const w of this.stateWatchers) {
|
|
9830
|
+
if (w.topic === topic) w.handler(st.entries.get(w.key));
|
|
9831
|
+
}
|
|
9832
|
+
if (this.presenceWatchers.size > 0) {
|
|
9833
|
+
const others = this.presenceOthers(topic);
|
|
9834
|
+
for (const w of this.presenceWatchers) {
|
|
9835
|
+
if (w.topic === topic) w.handler(others);
|
|
9836
|
+
}
|
|
9837
|
+
}
|
|
9369
9838
|
}
|
|
9370
9839
|
};
|
|
9840
|
+
var PRESENCE_KEY = (conn) => `$p:${conn}`;
|
|
9371
9841
|
|
|
9372
9842
|
// src/storage.ts
|
|
9373
9843
|
function memorySessionStorage() {
|
|
@@ -9428,7 +9898,7 @@ function defaultSessionStorage(key) {
|
|
|
9428
9898
|
}
|
|
9429
9899
|
|
|
9430
9900
|
// src/version.ts
|
|
9431
|
-
var VERSION = "7.
|
|
9901
|
+
var VERSION = "7.4.1";
|
|
9432
9902
|
|
|
9433
9903
|
// src/runtime.ts
|
|
9434
9904
|
function buildRuntime(config) {
|
|
@@ -9606,6 +10076,7 @@ async function callEndpoint(resolveRt, name, input, options) {
|
|
|
9606
10076
|
}
|
|
9607
10077
|
|
|
9608
10078
|
// src/upload.ts
|
|
10079
|
+
var UPLOAD_PATH = "/v1/storage/upload";
|
|
9609
10080
|
function abortError() {
|
|
9610
10081
|
return new BackendError("network", { code: "aborted", message: "Upload aborted" });
|
|
9611
10082
|
}
|
|
@@ -9659,7 +10130,9 @@ async function buildHeaders(rt, extra) {
|
|
|
9659
10130
|
}
|
|
9660
10131
|
function buildForm(options) {
|
|
9661
10132
|
const form = new FormData();
|
|
9662
|
-
|
|
10133
|
+
const { body, ...loose } = options.fields ?? {};
|
|
10134
|
+
if (body !== void 0) form.append("body", body);
|
|
10135
|
+
else if (Object.keys(loose).length > 0) form.append("body", JSON.stringify(loose));
|
|
9663
10136
|
const file = options.contentType && options.file.type !== options.contentType ? new Blob([options.file], { type: options.contentType }) : options.file;
|
|
9664
10137
|
const filename = options.filename ?? (typeof File !== "undefined" && options.file instanceof File ? options.file.name : "file");
|
|
9665
10138
|
form.append("file", file, filename);
|
|
@@ -9733,8 +10206,8 @@ async function uploadViaFetch(url, form, headers, options) {
|
|
|
9733
10206
|
async function uploadEndpoint(resolveRt, name, options) {
|
|
9734
10207
|
const rt = resolveRt();
|
|
9735
10208
|
checkConstraints(options);
|
|
9736
|
-
const
|
|
9737
|
-
const url = `${rt.config.url}${
|
|
10209
|
+
const route = name.startsWith("/") ? name : `/${name}`;
|
|
10210
|
+
const url = `${rt.config.url}${UPLOAD_PATH}${route}`;
|
|
9738
10211
|
const headers = await buildHeaders(rt, options.headers);
|
|
9739
10212
|
const form = buildForm(options);
|
|
9740
10213
|
const useXHR = options.onProgress !== void 0 && typeof XMLHttpRequest !== "undefined";
|