@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/index.cjs
CHANGED
|
@@ -197,6 +197,15 @@ function isFieldErrorArray(value) {
|
|
|
197
197
|
(v) => typeof v === "object" && v !== null && typeof v.field === "string" && typeof v.message === "string"
|
|
198
198
|
);
|
|
199
199
|
}
|
|
200
|
+
function fieldErrorsOf(envelope) {
|
|
201
|
+
const scoped = pickField(pickField(envelope, "data"), "fields");
|
|
202
|
+
if (isFieldErrorArray(scoped)) return scoped;
|
|
203
|
+
const flat = pickField(envelope, "fields");
|
|
204
|
+
return isFieldErrorArray(flat) ? flat : void 0;
|
|
205
|
+
}
|
|
206
|
+
function retryAfterOf(envelope) {
|
|
207
|
+
return pickNumber(envelope, "retry_after") ?? pickNumber(pickField(envelope, "data"), "retryAfter");
|
|
208
|
+
}
|
|
200
209
|
function pickField(value, key) {
|
|
201
210
|
if (typeof value === "object" && value !== null) {
|
|
202
211
|
return value[key];
|
|
@@ -222,20 +231,15 @@ function fromPalbaseError(err) {
|
|
|
222
231
|
if (err.code === "network_error") return new BackendError("network", base);
|
|
223
232
|
if (err.status === 401) return new BackendError("unauthorized", base);
|
|
224
233
|
if (err.status === 429)
|
|
225
|
-
return new BackendError("rateLimited", {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
});
|
|
229
|
-
const nested = pickField(err.details, "details");
|
|
230
|
-
if (err.status === 400 && isFieldErrorArray(nested))
|
|
231
|
-
return new BackendError("validation", { ...base, fields: nested });
|
|
234
|
+
return new BackendError("rateLimited", { ...base, retryAfter: retryAfterOf(err.details) });
|
|
235
|
+
const fields = fieldErrorsOf(err.details);
|
|
236
|
+
if (err.status === 400 && fields) return new BackendError("validation", { ...base, fields });
|
|
232
237
|
return new BackendError("server", base);
|
|
233
238
|
}
|
|
234
239
|
function fromEnvelope(status, body) {
|
|
235
240
|
const code = pickString(body, "error") ?? "http_error";
|
|
236
241
|
const message = pickString(body, "error_description") ?? `HTTP ${status}`;
|
|
237
242
|
const requestId = pickString(body, "request_id");
|
|
238
|
-
const details = pickField(body, "details");
|
|
239
243
|
const params = {
|
|
240
244
|
code,
|
|
241
245
|
message,
|
|
@@ -245,13 +249,9 @@ function fromEnvelope(status, body) {
|
|
|
245
249
|
};
|
|
246
250
|
if (status === 401) return new BackendError("unauthorized", params);
|
|
247
251
|
if (status === 429)
|
|
248
|
-
return new BackendError("rateLimited", {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
retryAfter: pickNumber(body, "retry_after") ?? pickNumber(details, "retry_after")
|
|
252
|
-
});
|
|
253
|
-
if (status === 400 && isFieldErrorArray(details))
|
|
254
|
-
return new BackendError("validation", { ...params, fields: details });
|
|
252
|
+
return new BackendError("rateLimited", { ...params, retryAfter: retryAfterOf(body) });
|
|
253
|
+
const fields = fieldErrorsOf(body);
|
|
254
|
+
if (status === 400 && fields) return new BackendError("validation", { ...params, fields });
|
|
255
255
|
return new BackendError("server", params);
|
|
256
256
|
}
|
|
257
257
|
function isBackendError(e) {
|
|
@@ -811,24 +811,29 @@ var PalbeAuth = class {
|
|
|
811
811
|
* Self-service account erasure ("right to be forgotten"). Calls palauth
|
|
812
812
|
* `DELETE /auth/user` (session-authed + fresh re-auth): password users pass
|
|
813
813
|
* `{ password }`; passwordless/OAuth users rely on a freshly stepped-up
|
|
814
|
-
* session.
|
|
815
|
-
* already revoked server-side — so we tear down
|
|
816
|
-
* `signOut()`'s clear, minus the pointless `/logout`)
|
|
817
|
-
* `signedOut{reason:'accountDeleted'}`.
|
|
814
|
+
* session. The handler answers **204 with no body** — the erasure has already
|
|
815
|
+
* finished and every session is already revoked server-side — so we tear down
|
|
816
|
+
* local state (mirroring `signOut()`'s clear, minus the pointless `/logout`)
|
|
817
|
+
* and emit `signedOut{reason:'accountDeleted'}`.
|
|
818
|
+
*
|
|
819
|
+
* Returns nothing, because there is nothing to return: the handler used to
|
|
820
|
+
* answer 202 with an `{erasure_id}` naming an asynchronous run, and that id
|
|
821
|
+
* was removed along with the run (`gdpr_handlers.go:221-229`). Reading it
|
|
822
|
+
* here threw a `TypeError` AFTER the account was already gone — a success
|
|
823
|
+
* reported as a failure.
|
|
818
824
|
*
|
|
819
825
|
* On 401 `reauth_required` / 503 `not_configured` (or `erasure_unavailable`)
|
|
820
826
|
* the request throws BEFORE the teardown line, so the local session is left
|
|
821
827
|
* fully intact — the deletion did not happen and the user is still signed in.
|
|
822
828
|
*/
|
|
823
829
|
async deleteAccount(params = {}) {
|
|
824
|
-
|
|
830
|
+
await palbeRequest(this.rt, "DELETE", "/auth/user", {
|
|
825
831
|
body: params.password ? { password: params.password } : {}
|
|
826
832
|
});
|
|
827
833
|
this.nextSignOutReason = "accountDeleted";
|
|
828
834
|
this.rt.tokenManager.clearSession();
|
|
829
835
|
this.rt.storage.clear();
|
|
830
836
|
this.cachedUser = null;
|
|
831
|
-
return { erasureId: res.erasure_id };
|
|
832
837
|
}
|
|
833
838
|
/**
|
|
834
839
|
* @internal — invoked by the transport choke point (`request.ts`), not app code.
|
|
@@ -1315,9 +1320,15 @@ var PalbeCalls = class {
|
|
|
1315
1320
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
1316
1321
|
* @returns A `Call` in `connecting` state; transitions to `active` once the media room connects.
|
|
1317
1322
|
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1323
|
+
* Every failure below arrives as kind `'server'` — this SDK's error kinds are
|
|
1324
|
+
* `notConfigured | validation | unauthorized | rateLimited | server | network
|
|
1325
|
+
* | decode`, and only a 401, a 429 or a 400 carrying a field-error array gets
|
|
1326
|
+
* its own kind. So branch on `err.code`, never on the kind.
|
|
1327
|
+
*
|
|
1328
|
+
* @throws BackendError('server', { code: 'calling_unavailable' }) no SFU configured on this stack (503)
|
|
1329
|
+
* @throws BackendError('server', { code: 'not_group_member' }) the caller is not a member of the group (403)
|
|
1330
|
+
* @throws BackendError('server', { code: 'call_room_full' }) the call is at its participant limit (409)
|
|
1331
|
+
* @throws BackendError('server', { code: 'call_provider_error' }) the media provider failed upstream (502)
|
|
1321
1332
|
*/
|
|
1322
1333
|
async start(groupId, { media = ["audio", "video"] } = {}) {
|
|
1323
1334
|
if (!groupId) {
|
|
@@ -1338,7 +1349,11 @@ var PalbeCalls = class {
|
|
|
1338
1349
|
* Accept an incoming call in a messaging group.
|
|
1339
1350
|
*
|
|
1340
1351
|
* @param groupId - Messaging group display-id
|
|
1341
|
-
* @param callId - Call identifier
|
|
1352
|
+
* @param callId - Call identifier, carried by the `call_invite` the server
|
|
1353
|
+
* fans onto the group's conversation topic
|
|
1354
|
+
* (`pb.realtime.channel('messaging:conv:<rfcGroupId>').on('call_invite', …)`).
|
|
1355
|
+
* This SDK ships no web push: on the web the invite arrives over realtime or
|
|
1356
|
+
* over whatever signalling the app already runs.
|
|
1342
1357
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
1343
1358
|
*/
|
|
1344
1359
|
async accept(groupId, callId, { media = ["audio", "video"] } = {}) {
|
|
@@ -3441,14 +3456,11 @@ var Chat = class {
|
|
|
3441
3456
|
get title() {
|
|
3442
3457
|
if (this.titleOverride) return this.titleOverride;
|
|
3443
3458
|
if (this._group?.name) return this._group.name;
|
|
3444
|
-
if (this.
|
|
3445
|
-
const
|
|
3446
|
-
|
|
3447
|
-
const peer = this.memberCache.find((m) => m.userId === mode.peerUserId);
|
|
3448
|
-
return peer?.displayName ?? "Chat";
|
|
3449
|
-
}
|
|
3450
|
-
return "Group";
|
|
3459
|
+
if (this.kind === "direct") {
|
|
3460
|
+
const peer = this.memberCache.find((m) => !m.isSelf);
|
|
3461
|
+
return peer?.displayName ?? "Chat";
|
|
3451
3462
|
}
|
|
3463
|
+
if (this._draft) return "Group";
|
|
3452
3464
|
const tail = this.id.startsWith("grp_") ? this.id.slice(4) : this.id;
|
|
3453
3465
|
const short = tail.slice(-4);
|
|
3454
3466
|
return short ? `Group ${short}` : "Group";
|
|
@@ -4422,6 +4434,11 @@ var MessageHub = class {
|
|
|
4422
4434
|
emitConv(displayId, e) {
|
|
4423
4435
|
for (const fn of this.convListeners.get(displayId) ?? []) fn(e);
|
|
4424
4436
|
}
|
|
4437
|
+
/** How many live conv listeners a group still has — the census the LAST
|
|
4438
|
+
* subscriber reads before tearing the conv topic (and its presence) down. */
|
|
4439
|
+
convListenerCount(displayId) {
|
|
4440
|
+
return this.convListeners.get(displayId)?.size ?? 0;
|
|
4441
|
+
}
|
|
4425
4442
|
add(map, key, fn) {
|
|
4426
4443
|
let set = map.get(key);
|
|
4427
4444
|
if (!set) {
|
|
@@ -4459,6 +4476,9 @@ var MessageDeliverySource = class {
|
|
|
4459
4476
|
started = false;
|
|
4460
4477
|
wakeUnsub = null;
|
|
4461
4478
|
// Per-observed-group conv subscription + heartbeat (presence/typing/read).
|
|
4479
|
+
// `teardown` is the ONE departure path: it stops the heartbeat, broadcasts the
|
|
4480
|
+
// offline frame and drops the subscriptions, so no caller can tear an
|
|
4481
|
+
// observation down without saying goodbye (see observeConversation).
|
|
4462
4482
|
observed = /* @__PURE__ */ new Map();
|
|
4463
4483
|
/** Subscribe the device wake topic + run an initial drain. Idempotent. */
|
|
4464
4484
|
async start() {
|
|
@@ -4483,10 +4503,7 @@ var MessageDeliverySource = class {
|
|
|
4483
4503
|
stop() {
|
|
4484
4504
|
this.wakeUnsub?.();
|
|
4485
4505
|
this.wakeUnsub = null;
|
|
4486
|
-
for (const [, o] of this.observed)
|
|
4487
|
-
o.unsub();
|
|
4488
|
-
if (o.heartbeat) clearInterval(o.heartbeat);
|
|
4489
|
-
}
|
|
4506
|
+
for (const [, o] of this.observed) o.teardown();
|
|
4490
4507
|
this.observed.clear();
|
|
4491
4508
|
this.started = false;
|
|
4492
4509
|
}
|
|
@@ -4717,14 +4734,26 @@ var MessageDeliverySource = class {
|
|
|
4717
4734
|
emitPresence(true);
|
|
4718
4735
|
const heartbeat = setInterval(() => emitPresence(true), 25e3);
|
|
4719
4736
|
this.observed.set(group.displayId, {
|
|
4720
|
-
|
|
4737
|
+
teardown: () => {
|
|
4738
|
+
clearInterval(heartbeat);
|
|
4739
|
+
try {
|
|
4740
|
+
emitPresence(false);
|
|
4741
|
+
} catch {
|
|
4742
|
+
}
|
|
4721
4743
|
for (const s of subs) s.cancel();
|
|
4722
|
-
}
|
|
4723
|
-
heartbeat
|
|
4744
|
+
}
|
|
4724
4745
|
});
|
|
4725
4746
|
} catch {
|
|
4726
4747
|
}
|
|
4727
4748
|
}
|
|
4749
|
+
/** Stop observing a group's conv topic: announce OFFLINE, stop the heartbeat
|
|
4750
|
+
* and drop the subscriptions. Idempotent — an unobserved group is a no-op. */
|
|
4751
|
+
unobserveConversation(group) {
|
|
4752
|
+
const o = this.observed.get(group.displayId);
|
|
4753
|
+
if (!o) return;
|
|
4754
|
+
this.observed.delete(group.displayId);
|
|
4755
|
+
o.teardown();
|
|
4756
|
+
}
|
|
4728
4757
|
/** Announce typing on a group's conv topic (the app calls per keystroke). */
|
|
4729
4758
|
setTyping(group, isTyping) {
|
|
4730
4759
|
this.observeConversation(group);
|
|
@@ -6952,7 +6981,11 @@ var MessagingCoordinator = class {
|
|
|
6952
6981
|
subscribeLive(group, chat) {
|
|
6953
6982
|
let offMsg = null;
|
|
6954
6983
|
let offConv = null;
|
|
6984
|
+
let resolved = null;
|
|
6985
|
+
let cancelled = false;
|
|
6955
6986
|
void this.resolve().then((r) => {
|
|
6987
|
+
if (cancelled) return;
|
|
6988
|
+
resolved = r;
|
|
6956
6989
|
offMsg = r.hub.onMessage(group.displayId, (m) => {
|
|
6957
6990
|
void chat.ingestLive(m);
|
|
6958
6991
|
});
|
|
@@ -6960,8 +6993,12 @@ var MessagingCoordinator = class {
|
|
|
6960
6993
|
offConv = r.hub.onConv(group.displayId, (e) => chat.applyConv(e.event, e.payload));
|
|
6961
6994
|
});
|
|
6962
6995
|
return () => {
|
|
6996
|
+
cancelled = true;
|
|
6963
6997
|
offMsg?.();
|
|
6964
6998
|
offConv?.();
|
|
6999
|
+
if (resolved && resolved.hub.convListenerCount(group.displayId) === 0) {
|
|
7000
|
+
resolved.source.unobserveConversation(group);
|
|
7001
|
+
}
|
|
6965
7002
|
};
|
|
6966
7003
|
}
|
|
6967
7004
|
async userIdForDevice(group, deviceId) {
|
|
@@ -7479,8 +7516,33 @@ function unwrapBroadcast(frame) {
|
|
|
7479
7516
|
function stripRealtimePrefix(topic) {
|
|
7480
7517
|
return topic.startsWith(REALTIME_PREFIX) ? topic.slice(REALTIME_PREFIX.length) : topic;
|
|
7481
7518
|
}
|
|
7519
|
+
function encodeStateSet(joinRef, ref, topic, key, value, life) {
|
|
7520
|
+
return JSON.stringify([
|
|
7521
|
+
joinRef,
|
|
7522
|
+
String(ref),
|
|
7523
|
+
REALTIME_PREFIX + topic,
|
|
7524
|
+
"state_set",
|
|
7525
|
+
{ key, value, life }
|
|
7526
|
+
]);
|
|
7527
|
+
}
|
|
7528
|
+
function encodeStateDel(joinRef, ref, topic, key) {
|
|
7529
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_del", { key }]);
|
|
7530
|
+
}
|
|
7531
|
+
function encodeStateResync(joinRef, ref, topic) {
|
|
7532
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_resync", {}]);
|
|
7533
|
+
}
|
|
7534
|
+
function encodeAccessToken(joinRef, ref, topic, token) {
|
|
7535
|
+
return JSON.stringify([
|
|
7536
|
+
joinRef,
|
|
7537
|
+
String(ref),
|
|
7538
|
+
REALTIME_PREFIX + topic,
|
|
7539
|
+
"access_token",
|
|
7540
|
+
{ access_token: token }
|
|
7541
|
+
]);
|
|
7542
|
+
}
|
|
7482
7543
|
|
|
7483
7544
|
// src/realtime/connection.ts
|
|
7545
|
+
var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
7484
7546
|
var WS_OPEN = 1;
|
|
7485
7547
|
var DEFAULT_HEARTBEAT_MS = 25e3;
|
|
7486
7548
|
var DEFAULT_MAX_BACKOFF_SECONDS = 30;
|
|
@@ -7713,6 +7775,31 @@ var RealtimeSocket = class {
|
|
|
7713
7775
|
this.handleChannelClose(frame);
|
|
7714
7776
|
return;
|
|
7715
7777
|
}
|
|
7778
|
+
if (frame.event === "state_snapshot") {
|
|
7779
|
+
this.handlers?.onStateSnapshot(stripRealtimePrefix(frame.topic), frame.payload);
|
|
7780
|
+
return;
|
|
7781
|
+
}
|
|
7782
|
+
if (frame.event === "state_diff") {
|
|
7783
|
+
this.handlers?.onStateDiff(stripRealtimePrefix(frame.topic), frame.payload);
|
|
7784
|
+
return;
|
|
7785
|
+
}
|
|
7786
|
+
if (frame.event === "phx_reply") {
|
|
7787
|
+
const body = frame.payload;
|
|
7788
|
+
const topic = stripRealtimePrefix(frame.topic);
|
|
7789
|
+
if (body && body.status === "ok" && body.response && typeof body.response === "object") {
|
|
7790
|
+
this.handlers?.onJoinInfo(topic, body.response);
|
|
7791
|
+
return;
|
|
7792
|
+
}
|
|
7793
|
+
if (body && body.status === "error" && String(frame.ref) === this.joinRefs.get(topic)) {
|
|
7794
|
+
const reason = typeof body.response?.["reason"] === "string" ? body.response["reason"] : "unauthorized";
|
|
7795
|
+
if (!RETRYABLE_REFUSALS.has(reason)) {
|
|
7796
|
+
this.joinedTopics.delete(topic);
|
|
7797
|
+
this.joinRefs.delete(topic);
|
|
7798
|
+
}
|
|
7799
|
+
this.handlers?.onChannelRefused(topic, reason);
|
|
7800
|
+
}
|
|
7801
|
+
return;
|
|
7802
|
+
}
|
|
7716
7803
|
const inbound = unwrapBroadcast(frame);
|
|
7717
7804
|
if (inbound) {
|
|
7718
7805
|
this.channelRejoinAttempts.delete(inbound.topic);
|
|
@@ -7753,6 +7840,40 @@ var RealtimeSocket = class {
|
|
|
7753
7840
|
}, delayMs);
|
|
7754
7841
|
this.channelRejoinTimers.set(topic, timer);
|
|
7755
7842
|
}
|
|
7843
|
+
// MARK: state
|
|
7844
|
+
/** Write one entry on a joined channel's state. Dropped when the topic has no
|
|
7845
|
+
* live join: the server refuses state on a channel this socket is not on, and
|
|
7846
|
+
* the reconnect's join brings a fresh snapshot anyway. */
|
|
7847
|
+
sendStateSet(topic, key, value, life) {
|
|
7848
|
+
const joinRef = this.joinRefs.get(topic);
|
|
7849
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
7850
|
+
this.send(encodeStateSet(joinRef, this.nextRef(), topic, key, value, life));
|
|
7851
|
+
}
|
|
7852
|
+
/** Remove one entry. The server refuses a key this connection does not own. */
|
|
7853
|
+
sendStateDel(topic, key) {
|
|
7854
|
+
const joinRef = this.joinRefs.get(topic);
|
|
7855
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
7856
|
+
this.send(encodeStateDel(joinRef, this.nextRef(), topic, key));
|
|
7857
|
+
}
|
|
7858
|
+
/** Ask for a fresh snapshot — the answer to a detected gap. */
|
|
7859
|
+
sendStateResync(topic) {
|
|
7860
|
+
const joinRef = this.joinRefs.get(topic);
|
|
7861
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
7862
|
+
this.send(encodeStateResync(joinRef, this.nextRef(), topic));
|
|
7863
|
+
}
|
|
7864
|
+
/**
|
|
7865
|
+
* Present a fresher credential on every joined channel WITHOUT rejoining.
|
|
7866
|
+
*
|
|
7867
|
+
* The alternative — dropping and rejoining — reaps this connection's
|
|
7868
|
+
* ephemeral entries (its presence) and forces a full resync on every channel.
|
|
7869
|
+
* A token rotation is not a reconnection and must not look like one.
|
|
7870
|
+
*/
|
|
7871
|
+
sendAccessToken(token) {
|
|
7872
|
+
if (!this.isOpen()) return;
|
|
7873
|
+
for (const [topic, joinRef] of this.joinRefs) {
|
|
7874
|
+
this.send(encodeAccessToken(joinRef, this.nextRef(), topic, token));
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7756
7877
|
// MARK: helpers
|
|
7757
7878
|
/** Drop a single topic's token-expiry rejoin state (cancel its pending timer). */
|
|
7758
7879
|
clearChannelRejoin(topic) {
|
|
@@ -7778,6 +7899,60 @@ var RealtimeSocket = class {
|
|
|
7778
7899
|
}
|
|
7779
7900
|
};
|
|
7780
7901
|
|
|
7902
|
+
// src/realtime/state.ts
|
|
7903
|
+
var PRESENCE_PREFIX = "$p:";
|
|
7904
|
+
var ChannelState = class {
|
|
7905
|
+
/** The sequence this local copy is current as of. */
|
|
7906
|
+
seq = 0;
|
|
7907
|
+
/** key → value. Lifetimes are the server's business; a reader only needs the
|
|
7908
|
+
* values, and presence is derived from the key shape. */
|
|
7909
|
+
entries = /* @__PURE__ */ new Map();
|
|
7910
|
+
/** Replace everything. A snapshot is the WHOLE picture, never a merge —
|
|
7911
|
+
* merging one would keep entries the server has since removed. */
|
|
7912
|
+
applySnapshot(body) {
|
|
7913
|
+
this.entries.clear();
|
|
7914
|
+
for (const [k, e] of Object.entries(body.entries ?? {})) {
|
|
7915
|
+
this.entries.set(k, e.v);
|
|
7916
|
+
}
|
|
7917
|
+
this.seq = body.seq;
|
|
7918
|
+
}
|
|
7919
|
+
/**
|
|
7920
|
+
* Apply a diff, or report that it cannot be applied.
|
|
7921
|
+
*
|
|
7922
|
+
* `gap` when this copy is behind the diff's starting point: frames were
|
|
7923
|
+
* missed, and applying anyway would produce a state that never existed on the
|
|
7924
|
+
* server. The state is left untouched so the caller can resync from something
|
|
7925
|
+
* consistent.
|
|
7926
|
+
*
|
|
7927
|
+
* A diff at or below the current sequence is already applied — a resend after
|
|
7928
|
+
* a retry — and is silently accepted so duplicates are harmless.
|
|
7929
|
+
*/
|
|
7930
|
+
applyDiff(body) {
|
|
7931
|
+
if (this.seq < body.from_seq) return "gap";
|
|
7932
|
+
if (body.seq <= this.seq) return "applied";
|
|
7933
|
+
for (const [k, e] of Object.entries(body.set ?? {})) this.entries.set(k, e.v);
|
|
7934
|
+
for (const k of body.del ?? []) this.entries.delete(k);
|
|
7935
|
+
this.seq = body.seq;
|
|
7936
|
+
return "applied";
|
|
7937
|
+
}
|
|
7938
|
+
/**
|
|
7939
|
+
* Everyone else's presence.
|
|
7940
|
+
*
|
|
7941
|
+
* `selfConn` is this connection's id, as the join reply reported it. Its own
|
|
7942
|
+
* entry is excluded because an app rendering "who else is here" would
|
|
7943
|
+
* otherwise always show one extra face — itself.
|
|
7944
|
+
*/
|
|
7945
|
+
presence(selfConn) {
|
|
7946
|
+
const own = selfConn === void 0 ? void 0 : PRESENCE_PREFIX + selfConn;
|
|
7947
|
+
const out = [];
|
|
7948
|
+
for (const [k, v] of this.entries) {
|
|
7949
|
+
if (!k.startsWith(PRESENCE_PREFIX) || k === own) continue;
|
|
7950
|
+
out.push(v);
|
|
7951
|
+
}
|
|
7952
|
+
return out;
|
|
7953
|
+
}
|
|
7954
|
+
};
|
|
7955
|
+
|
|
7781
7956
|
// src/realtime/facade.ts
|
|
7782
7957
|
var StatusStore = class {
|
|
7783
7958
|
currentState = "idle";
|
|
@@ -7813,6 +7988,7 @@ var StatusStore = class {
|
|
|
7813
7988
|
for (const listener of this.listeners) listener(snapshot);
|
|
7814
7989
|
}
|
|
7815
7990
|
};
|
|
7991
|
+
var RETRYABLE_REASONS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
7816
7992
|
var RealtimeChannel = class {
|
|
7817
7993
|
/** The app-defined channel name (bare sub-topic, no "realtime:" prefix). */
|
|
7818
7994
|
name;
|
|
@@ -7830,6 +8006,23 @@ var RealtimeChannel = class {
|
|
|
7830
8006
|
on(event, handler) {
|
|
7831
8007
|
return this.owner.subscribe(this.name, event, handler);
|
|
7832
8008
|
}
|
|
8009
|
+
/**
|
|
8010
|
+
* Called when the server REFUSES this channel.
|
|
8011
|
+
*
|
|
8012
|
+
* A join is an authorization decision, and a decision the app never hears is
|
|
8013
|
+
* the same as no decision at all: silence looks exactly like a slow network.
|
|
8014
|
+
* This is where a refusal arrives, carrying the server's own distinction —
|
|
8015
|
+
* `retryable: false` means "you may not" and asking again will not help;
|
|
8016
|
+
* `retryable: true` means the project could not be asked, and the socket's
|
|
8017
|
+
* next reconnect retries on its own.
|
|
8018
|
+
*
|
|
8019
|
+
* pb.realtime.channel('room:42').onError((err) => {
|
|
8020
|
+
* if (!err.retryable) showJoinDenied()
|
|
8021
|
+
* })
|
|
8022
|
+
*/
|
|
8023
|
+
onError(handler) {
|
|
8024
|
+
return this.owner.subscribeError(this.name, handler);
|
|
8025
|
+
}
|
|
7833
8026
|
/**
|
|
7834
8027
|
* Broadcast `payload` to the channel's other subscribers (web addition —
|
|
7835
8028
|
* iOS is receive-only). Joins the channel if it isn't already; queued
|
|
@@ -7838,6 +8031,57 @@ var RealtimeChannel = class {
|
|
|
7838
8031
|
send(event, payload = {}) {
|
|
7839
8032
|
this.owner.send(this.name, event, payload);
|
|
7840
8033
|
}
|
|
8034
|
+
/**
|
|
8035
|
+
* Watch one key of the channel's shared state.
|
|
8036
|
+
*
|
|
8037
|
+
* The handler is called with the CURRENT value straight away when there is
|
|
8038
|
+
* one, and again on every change. That is the difference from `on()`: a
|
|
8039
|
+
* subscriber to an event learns nothing until the next one, while state is
|
|
8040
|
+
* already there when you ask.
|
|
8041
|
+
*/
|
|
8042
|
+
onState(key, handler) {
|
|
8043
|
+
return this.owner.subscribeState(this.name, key, handler);
|
|
8044
|
+
}
|
|
8045
|
+
/** Read one key without subscribing. */
|
|
8046
|
+
stateValue(key) {
|
|
8047
|
+
return this.owner.stateOf(this.name).entries.get(key);
|
|
8048
|
+
}
|
|
8049
|
+
/** Write one key. `durable` outlives this connection; `ephemeral` is reaped
|
|
8050
|
+
* when it closes, which is what presence is built on. */
|
|
8051
|
+
setState(key, value, opts = {}) {
|
|
8052
|
+
this.owner.writeState(this.name, key, value, opts.life ?? "durable");
|
|
8053
|
+
}
|
|
8054
|
+
/** Remove a key this connection wrote. */
|
|
8055
|
+
clearState(key) {
|
|
8056
|
+
this.owner.clearState(this.name, key);
|
|
8057
|
+
}
|
|
8058
|
+
/**
|
|
8059
|
+
* Announce this client on the channel. `meta` is whatever other participants
|
|
8060
|
+
* should see — a name, an avatar, a cursor.
|
|
8061
|
+
*
|
|
8062
|
+
* It is ephemeral state keyed by this connection, so it disappears when the
|
|
8063
|
+
* socket does. Nothing has to be un-announced on a crash.
|
|
8064
|
+
*/
|
|
8065
|
+
presenceEnter(meta) {
|
|
8066
|
+
this.owner.presenceWrite(this.name, meta);
|
|
8067
|
+
}
|
|
8068
|
+
/** Replace the announced value — a cursor moving is this, at whatever rate
|
|
8069
|
+
* the app likes: the server conflates writes it cannot deliver fast enough. */
|
|
8070
|
+
presenceUpdate(meta) {
|
|
8071
|
+
this.owner.presenceWrite(this.name, meta);
|
|
8072
|
+
}
|
|
8073
|
+
/** Leave early. Closing the socket does this on its own. */
|
|
8074
|
+
presenceLeave() {
|
|
8075
|
+
this.owner.presenceClear(this.name);
|
|
8076
|
+
}
|
|
8077
|
+
/** Everyone else currently announced, this client excluded. */
|
|
8078
|
+
presenceOthers() {
|
|
8079
|
+
return this.owner.presenceOthers(this.name);
|
|
8080
|
+
}
|
|
8081
|
+
/** Watch the announced set. Called on every change. */
|
|
8082
|
+
onPresence(handler) {
|
|
8083
|
+
return this.owner.subscribePresence(this.name, handler);
|
|
8084
|
+
}
|
|
7841
8085
|
};
|
|
7842
8086
|
var PalbeRealtime = class {
|
|
7843
8087
|
rt;
|
|
@@ -7847,9 +8091,21 @@ var PalbeRealtime = class {
|
|
|
7847
8091
|
// Per-(topic, event) handlers — an event may have multiple handlers
|
|
7848
8092
|
// (multiple .on calls); entry identity is the unsubscribe token.
|
|
7849
8093
|
handlers = /* @__PURE__ */ new Set();
|
|
8094
|
+
// Per-topic refusal handlers. Kept apart from `handlers` because they are not
|
|
8095
|
+
// refcounted: registering one must not join a channel, and cancelling the
|
|
8096
|
+
// last one must not leave a channel that still has event handlers on it.
|
|
8097
|
+
errorHandlers = /* @__PURE__ */ new Set();
|
|
7850
8098
|
// Joins are refcounted per topic so the socket only leaves a topic when
|
|
7851
8099
|
// its last handler cancels (iOS RealtimeClient parity).
|
|
7852
8100
|
topicRefcount = /* @__PURE__ */ new Map();
|
|
8101
|
+
// Per-topic shared state, and this connection's identity on each topic (from
|
|
8102
|
+
// the join ack). Presence is keyed by that identity, so a write before the
|
|
8103
|
+
// ack has nowhere to go and is queued until it arrives.
|
|
8104
|
+
states = /* @__PURE__ */ new Map();
|
|
8105
|
+
connIds = /* @__PURE__ */ new Map();
|
|
8106
|
+
pendingPresence = /* @__PURE__ */ new Map();
|
|
8107
|
+
stateWatchers = /* @__PURE__ */ new Set();
|
|
8108
|
+
presenceWatchers = /* @__PURE__ */ new Set();
|
|
7853
8109
|
constructor(rt) {
|
|
7854
8110
|
this.rt = rt;
|
|
7855
8111
|
}
|
|
@@ -7892,6 +8148,20 @@ var PalbeRealtime = class {
|
|
|
7892
8148
|
get status() {
|
|
7893
8149
|
return this.statusStore;
|
|
7894
8150
|
}
|
|
8151
|
+
/**
|
|
8152
|
+
* Hand the socket a fresher credential for the SAME user, without rejoining.
|
|
8153
|
+
*
|
|
8154
|
+
* Called when the app's token rotates. The alternative — letting the channels
|
|
8155
|
+
* die at expiry and rejoining — reaps this connection's ephemeral entries
|
|
8156
|
+
* (its presence, on every channel) and forces a full state resync. A token
|
|
8157
|
+
* getting older is not a disconnection and must not cost one.
|
|
8158
|
+
*
|
|
8159
|
+
* A no-op before a socket exists: the first connect resolves a fresh token
|
|
8160
|
+
* on its own.
|
|
8161
|
+
*/
|
|
8162
|
+
refreshToken(token) {
|
|
8163
|
+
this.socket?.sendAccessToken(token);
|
|
8164
|
+
}
|
|
7895
8165
|
/** @internal */
|
|
7896
8166
|
subscribe(topic, event, handler) {
|
|
7897
8167
|
const socket = this.ensureSocket();
|
|
@@ -7906,7 +8176,9 @@ var PalbeRealtime = class {
|
|
|
7906
8176
|
if (cancelled) return;
|
|
7907
8177
|
cancelled = true;
|
|
7908
8178
|
this.handlers.delete(entry);
|
|
7909
|
-
const
|
|
8179
|
+
const held = this.topicRefcount.get(topic);
|
|
8180
|
+
if (held === void 0) return;
|
|
8181
|
+
const next = held - 1;
|
|
7910
8182
|
if (next <= 0) {
|
|
7911
8183
|
this.topicRefcount.delete(topic);
|
|
7912
8184
|
socket.leaveTopic(topic);
|
|
@@ -7916,6 +8188,25 @@ var PalbeRealtime = class {
|
|
|
7916
8188
|
}
|
|
7917
8189
|
};
|
|
7918
8190
|
}
|
|
8191
|
+
/**
|
|
8192
|
+
* Register a refusal handler for one topic. Does NOT join: watching for a
|
|
8193
|
+
* refusal is not a subscription, and an app that registers one before its
|
|
8194
|
+
* first `.on()` should not be joining anything yet.
|
|
8195
|
+
*
|
|
8196
|
+
* @internal
|
|
8197
|
+
*/
|
|
8198
|
+
subscribeError(topic, handler) {
|
|
8199
|
+
const entry = { topic, handler };
|
|
8200
|
+
this.errorHandlers.add(entry);
|
|
8201
|
+
let cancelled = false;
|
|
8202
|
+
return {
|
|
8203
|
+
cancel: () => {
|
|
8204
|
+
if (cancelled) return;
|
|
8205
|
+
cancelled = true;
|
|
8206
|
+
this.errorHandlers.delete(entry);
|
|
8207
|
+
}
|
|
8208
|
+
};
|
|
8209
|
+
}
|
|
7919
8210
|
/** @internal */
|
|
7920
8211
|
send(topic, event, payload) {
|
|
7921
8212
|
const socket = this.ensureSocket();
|
|
@@ -7937,7 +8228,11 @@ var PalbeRealtime = class {
|
|
|
7937
8228
|
onReconnect: () => {
|
|
7938
8229
|
},
|
|
7939
8230
|
onStateChange: (state) => this.statusStore.setState(state),
|
|
7940
|
-
onChannelError: (topic) => this.handleChannelError(topic)
|
|
8231
|
+
onChannelError: (topic) => this.handleChannelError(topic),
|
|
8232
|
+
onChannelRefused: (topic, reason) => this.handleChannelRefused(topic, reason),
|
|
8233
|
+
onStateSnapshot: (topic, body) => this.applySnapshot(topic, body),
|
|
8234
|
+
onStateDiff: (topic, body) => this.applyDiff(topic, body),
|
|
8235
|
+
onJoinInfo: (topic, info) => this.applyJoinInfo(topic, info)
|
|
7941
8236
|
});
|
|
7942
8237
|
}
|
|
7943
8238
|
return this.socket;
|
|
@@ -7951,19 +8246,185 @@ var PalbeRealtime = class {
|
|
|
7951
8246
|
}
|
|
7952
8247
|
}
|
|
7953
8248
|
/**
|
|
7954
|
-
|
|
7955
|
-
*
|
|
7956
|
-
*
|
|
7957
|
-
*
|
|
8249
|
+
/**
|
|
8250
|
+
* The server refused a channel. Tell whoever asked, and — when the answer is
|
|
8251
|
+
* final — stop pretending the app is subscribed to it.
|
|
8252
|
+
*
|
|
8253
|
+
* `unavailable` leaves everything in place: the project could not be reached
|
|
8254
|
+
* to decide, the socket rejoins on its next reconnect, and dropping the app's
|
|
8255
|
+
* handlers would make a momentary outage look like a permanent denial.
|
|
8256
|
+
*/
|
|
8257
|
+
handleChannelRefused(topic, reason) {
|
|
8258
|
+
const retryable = RETRYABLE_REASONS.has(reason);
|
|
8259
|
+
for (const entry of this.errorHandlers) {
|
|
8260
|
+
if (entry.topic === topic) entry.handler({ topic, reason, retryable });
|
|
8261
|
+
}
|
|
8262
|
+
if (retryable) return;
|
|
8263
|
+
this.forgetTopic(topic);
|
|
8264
|
+
}
|
|
8265
|
+
/**
|
|
8266
|
+
* A channel died for good — a token-expiry rejoin that exhausted its attempts.
|
|
8267
|
+
* The app must stop expecting anything on it, and the status observable flips
|
|
8268
|
+
* to `'error'` (the React `useChannel` hook reports that).
|
|
8269
|
+
*
|
|
8270
|
+
* It clears the SAME registries a final refusal clears, and used to clear only
|
|
8271
|
+
* the event handlers. A state or presence watcher left behind here waits
|
|
8272
|
+
* forever for a snapshot that cannot arrive, and fires twice if the app ever
|
|
8273
|
+
* re-subscribes — the identical failure its sibling three functions up
|
|
8274
|
+
* describes, on a channel that was given up on rather than refused.
|
|
7958
8275
|
*/
|
|
7959
8276
|
handleChannelError(topic) {
|
|
8277
|
+
this.forgetTopic(topic);
|
|
8278
|
+
this.statusStore.setState("error");
|
|
8279
|
+
}
|
|
8280
|
+
/** Drop every registration this topic holds. One place, because "which
|
|
8281
|
+
* registries" is a question two callers must never answer differently. */
|
|
8282
|
+
forgetTopic(topic) {
|
|
7960
8283
|
for (const entry of this.handlers) {
|
|
7961
8284
|
if (entry.topic === topic) this.handlers.delete(entry);
|
|
7962
8285
|
}
|
|
8286
|
+
for (const entry of this.stateWatchers) {
|
|
8287
|
+
if (entry.topic === topic) this.stateWatchers.delete(entry);
|
|
8288
|
+
}
|
|
8289
|
+
for (const entry of this.presenceWatchers) {
|
|
8290
|
+
if (entry.topic === topic) this.presenceWatchers.delete(entry);
|
|
8291
|
+
}
|
|
8292
|
+
this.pendingPresence.delete(topic);
|
|
8293
|
+
this.states.delete(topic);
|
|
8294
|
+
this.connIds.delete(topic);
|
|
7963
8295
|
this.topicRefcount.delete(topic);
|
|
7964
|
-
|
|
8296
|
+
}
|
|
8297
|
+
// ── state ──────────────────────────────────────────────────────────────────
|
|
8298
|
+
/** @internal */
|
|
8299
|
+
stateOf(topic) {
|
|
8300
|
+
let st = this.states.get(topic);
|
|
8301
|
+
if (!st) {
|
|
8302
|
+
st = new ChannelState();
|
|
8303
|
+
this.states.set(topic, st);
|
|
8304
|
+
}
|
|
8305
|
+
return st;
|
|
8306
|
+
}
|
|
8307
|
+
/** @internal */
|
|
8308
|
+
subscribeState(topic, key, handler) {
|
|
8309
|
+
const socket = this.ensureSocket();
|
|
8310
|
+
const entry = { topic, key, handler };
|
|
8311
|
+
this.stateWatchers.add(entry);
|
|
8312
|
+
this.retainTopic(topic, socket);
|
|
8313
|
+
const known = this.stateOf(topic).entries.get(key);
|
|
8314
|
+
if (known !== void 0) handler(known);
|
|
8315
|
+
let cancelled = false;
|
|
8316
|
+
return {
|
|
8317
|
+
cancel: () => {
|
|
8318
|
+
if (cancelled) return;
|
|
8319
|
+
cancelled = true;
|
|
8320
|
+
this.stateWatchers.delete(entry);
|
|
8321
|
+
this.releaseTopic(topic, socket);
|
|
8322
|
+
}
|
|
8323
|
+
};
|
|
8324
|
+
}
|
|
8325
|
+
/** @internal */
|
|
8326
|
+
subscribePresence(topic, handler) {
|
|
8327
|
+
const socket = this.ensureSocket();
|
|
8328
|
+
const entry = { topic, handler };
|
|
8329
|
+
this.presenceWatchers.add(entry);
|
|
8330
|
+
this.retainTopic(topic, socket);
|
|
8331
|
+
handler(this.presenceOthers(topic));
|
|
8332
|
+
let cancelled = false;
|
|
8333
|
+
return {
|
|
8334
|
+
cancel: () => {
|
|
8335
|
+
if (cancelled) return;
|
|
8336
|
+
cancelled = true;
|
|
8337
|
+
this.presenceWatchers.delete(entry);
|
|
8338
|
+
this.releaseTopic(topic, socket);
|
|
8339
|
+
}
|
|
8340
|
+
};
|
|
8341
|
+
}
|
|
8342
|
+
/** @internal */
|
|
8343
|
+
writeState(topic, key, value, life) {
|
|
8344
|
+
const socket = this.ensureSocket();
|
|
8345
|
+
socket.joinTopic(topic);
|
|
8346
|
+
socket.sendStateSet(topic, key, value, life);
|
|
8347
|
+
}
|
|
8348
|
+
/** @internal */
|
|
8349
|
+
clearState(topic, key) {
|
|
8350
|
+
this.ensureSocket().sendStateDel(topic, key);
|
|
8351
|
+
}
|
|
8352
|
+
/** @internal */
|
|
8353
|
+
presenceWrite(topic, meta) {
|
|
8354
|
+
const socket = this.ensureSocket();
|
|
8355
|
+
socket.joinTopic(topic);
|
|
8356
|
+
const conn = this.connIds.get(topic);
|
|
8357
|
+
if (conn === void 0) {
|
|
8358
|
+
this.pendingPresence.set(topic, meta);
|
|
8359
|
+
return;
|
|
8360
|
+
}
|
|
8361
|
+
socket.sendStateSet(topic, PRESENCE_KEY(conn), meta, "ephemeral");
|
|
8362
|
+
}
|
|
8363
|
+
/** @internal */
|
|
8364
|
+
presenceClear(topic) {
|
|
8365
|
+
this.pendingPresence.delete(topic);
|
|
8366
|
+
const conn = this.connIds.get(topic);
|
|
8367
|
+
if (conn === void 0) return;
|
|
8368
|
+
this.ensureSocket().sendStateDel(topic, PRESENCE_KEY(conn));
|
|
8369
|
+
}
|
|
8370
|
+
/** @internal */
|
|
8371
|
+
presenceOthers(topic) {
|
|
8372
|
+
return this.stateOf(topic).presence(this.connIds.get(topic));
|
|
8373
|
+
}
|
|
8374
|
+
retainTopic(topic, socket) {
|
|
8375
|
+
const count = (this.topicRefcount.get(topic) ?? 0) + 1;
|
|
8376
|
+
this.topicRefcount.set(topic, count);
|
|
8377
|
+
if (count === 1) socket.joinTopic(topic);
|
|
8378
|
+
}
|
|
8379
|
+
releaseTopic(topic, socket) {
|
|
8380
|
+
const next = (this.topicRefcount.get(topic) ?? 1) - 1;
|
|
8381
|
+
if (next <= 0) {
|
|
8382
|
+
this.topicRefcount.delete(topic);
|
|
8383
|
+
socket.leaveTopic(topic);
|
|
8384
|
+
return;
|
|
8385
|
+
}
|
|
8386
|
+
this.topicRefcount.set(topic, next);
|
|
8387
|
+
}
|
|
8388
|
+
applySnapshot(topic, body) {
|
|
8389
|
+
this.stateOf(topic).applySnapshot(body);
|
|
8390
|
+
this.notifyState(topic);
|
|
8391
|
+
}
|
|
8392
|
+
applyDiff(topic, body) {
|
|
8393
|
+
if (this.stateOf(topic).applyDiff(body) === "gap") {
|
|
8394
|
+
this.socket?.sendStateResync(topic);
|
|
8395
|
+
return;
|
|
8396
|
+
}
|
|
8397
|
+
this.notifyState(topic);
|
|
8398
|
+
}
|
|
8399
|
+
applyJoinInfo(topic, info) {
|
|
8400
|
+
if (typeof info.conn !== "string" || info.conn === "") return;
|
|
8401
|
+
this.connIds.set(topic, info.conn);
|
|
8402
|
+
const seq = info.state_seq;
|
|
8403
|
+
if (typeof seq === "number" && seq < this.stateOf(topic).seq) {
|
|
8404
|
+
this.stateOf(topic).applySnapshot({ seq, entries: {} });
|
|
8405
|
+
}
|
|
8406
|
+
const held = this.pendingPresence.get(topic);
|
|
8407
|
+
if (held !== void 0) {
|
|
8408
|
+
this.pendingPresence.delete(topic);
|
|
8409
|
+
this.socket?.sendStateSet(topic, PRESENCE_KEY(info.conn), held, "ephemeral");
|
|
8410
|
+
}
|
|
8411
|
+
this.notifyState(topic);
|
|
8412
|
+
}
|
|
8413
|
+
notifyState(topic) {
|
|
8414
|
+
this.statusStore.recordEvent(/* @__PURE__ */ new Date());
|
|
8415
|
+
const st = this.stateOf(topic);
|
|
8416
|
+
for (const w of this.stateWatchers) {
|
|
8417
|
+
if (w.topic === topic) w.handler(st.entries.get(w.key));
|
|
8418
|
+
}
|
|
8419
|
+
if (this.presenceWatchers.size > 0) {
|
|
8420
|
+
const others = this.presenceOthers(topic);
|
|
8421
|
+
for (const w of this.presenceWatchers) {
|
|
8422
|
+
if (w.topic === topic) w.handler(others);
|
|
8423
|
+
}
|
|
8424
|
+
}
|
|
7965
8425
|
}
|
|
7966
8426
|
};
|
|
8427
|
+
var PRESENCE_KEY = (conn) => `$p:${conn}`;
|
|
7967
8428
|
|
|
7968
8429
|
// src/storage.ts
|
|
7969
8430
|
function memorySessionStorage() {
|
|
@@ -8020,7 +8481,7 @@ function localStorageSessionStorage(key = DEFAULT_KEY) {
|
|
|
8020
8481
|
}
|
|
8021
8482
|
|
|
8022
8483
|
// src/version.ts
|
|
8023
|
-
var VERSION = "7.
|
|
8484
|
+
var VERSION = "7.4.0";
|
|
8024
8485
|
|
|
8025
8486
|
// src/internal.ts
|
|
8026
8487
|
function getRuntime() {
|
|
@@ -8030,6 +8491,7 @@ function getRuntime() {
|
|
|
8030
8491
|
}
|
|
8031
8492
|
|
|
8032
8493
|
// src/upload.ts
|
|
8494
|
+
var UPLOAD_PATH = "/v1/storage/upload";
|
|
8033
8495
|
function abortError() {
|
|
8034
8496
|
return new BackendError("network", { code: "aborted", message: "Upload aborted" });
|
|
8035
8497
|
}
|
|
@@ -8083,7 +8545,9 @@ async function buildHeaders(rt, extra) {
|
|
|
8083
8545
|
}
|
|
8084
8546
|
function buildForm(options) {
|
|
8085
8547
|
const form = new FormData();
|
|
8086
|
-
|
|
8548
|
+
const { body, ...loose } = options.fields ?? {};
|
|
8549
|
+
if (body !== void 0) form.append("body", body);
|
|
8550
|
+
else if (Object.keys(loose).length > 0) form.append("body", JSON.stringify(loose));
|
|
8087
8551
|
const file = options.contentType && options.file.type !== options.contentType ? new Blob([options.file], { type: options.contentType }) : options.file;
|
|
8088
8552
|
const filename = options.filename ?? (typeof File !== "undefined" && options.file instanceof File ? options.file.name : "file");
|
|
8089
8553
|
form.append("file", file, filename);
|
|
@@ -8157,8 +8621,8 @@ async function uploadViaFetch(url, form, headers, options) {
|
|
|
8157
8621
|
async function uploadEndpoint(resolveRt, name, options) {
|
|
8158
8622
|
const rt = resolveRt();
|
|
8159
8623
|
checkConstraints(options);
|
|
8160
|
-
const
|
|
8161
|
-
const url = `${rt.config.url}${
|
|
8624
|
+
const route = name.startsWith("/") ? name : `/${name}`;
|
|
8625
|
+
const url = `${rt.config.url}${UPLOAD_PATH}${route}`;
|
|
8162
8626
|
const headers = await buildHeaders(rt, options.headers);
|
|
8163
8627
|
const form = buildForm(options);
|
|
8164
8628
|
const useXHR = options.onProgress !== void 0 && typeof XMLHttpRequest !== "undefined";
|