@palbase/web 7.3.9 → 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-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-WXBOPPHU.js} +502 -38
- package/dist/chunk-WXBOPPHU.js.map +1 -0
- package/dist/gen/cli.cjs +1 -1
- 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 +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/client.cjs
CHANGED
|
@@ -188,6 +188,15 @@ function parseEnvironmentRef(apiKey) {
|
|
|
188
188
|
var MAX_RETRIES = 3;
|
|
189
189
|
var INITIAL_BACKOFF_MS = 200;
|
|
190
190
|
var MAX_RETRY_DELAY_MS = 1e4;
|
|
191
|
+
function withRetryHint(body, response) {
|
|
192
|
+
if (response.status !== 429) return body;
|
|
193
|
+
const data = body?.data;
|
|
194
|
+
const alreadyStated = typeof body?.retry_after === "number" || typeof data === "object" && data !== null && "retryAfter" in data;
|
|
195
|
+
if (alreadyStated) return body;
|
|
196
|
+
const seconds = Number.parseInt(response.headers.get("Retry-After") ?? "", 10);
|
|
197
|
+
if (Number.isNaN(seconds) || seconds <= 0) return body;
|
|
198
|
+
return { ...body, retry_after: seconds };
|
|
199
|
+
}
|
|
191
200
|
var HttpClient = class _HttpClient {
|
|
192
201
|
apiKey;
|
|
193
202
|
options;
|
|
@@ -363,7 +372,7 @@ var HttpClient = class _HttpClient {
|
|
|
363
372
|
errorBody?.error ?? "unknown_error",
|
|
364
373
|
errorBody?.error_description ?? response.statusText,
|
|
365
374
|
response.status,
|
|
366
|
-
errorBody
|
|
375
|
+
withRetryHint(errorBody, response)
|
|
367
376
|
),
|
|
368
377
|
status: response.status
|
|
369
378
|
};
|
|
@@ -486,6 +495,15 @@ function isFieldErrorArray(value) {
|
|
|
486
495
|
(v) => typeof v === "object" && v !== null && typeof v.field === "string" && typeof v.message === "string"
|
|
487
496
|
);
|
|
488
497
|
}
|
|
498
|
+
function fieldErrorsOf(envelope) {
|
|
499
|
+
const scoped = pickField(pickField(envelope, "data"), "fields");
|
|
500
|
+
if (isFieldErrorArray(scoped)) return scoped;
|
|
501
|
+
const flat = pickField(envelope, "fields");
|
|
502
|
+
return isFieldErrorArray(flat) ? flat : void 0;
|
|
503
|
+
}
|
|
504
|
+
function retryAfterOf(envelope) {
|
|
505
|
+
return pickNumber(envelope, "retry_after") ?? pickNumber(pickField(envelope, "data"), "retryAfter");
|
|
506
|
+
}
|
|
489
507
|
function pickField(value, key) {
|
|
490
508
|
if (typeof value === "object" && value !== null) {
|
|
491
509
|
return value[key];
|
|
@@ -511,20 +529,15 @@ function fromPalbaseError(err) {
|
|
|
511
529
|
if (err.code === "network_error") return new BackendError("network", base);
|
|
512
530
|
if (err.status === 401) return new BackendError("unauthorized", base);
|
|
513
531
|
if (err.status === 429)
|
|
514
|
-
return new BackendError("rateLimited", {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
});
|
|
518
|
-
const nested = pickField(err.details, "details");
|
|
519
|
-
if (err.status === 400 && isFieldErrorArray(nested))
|
|
520
|
-
return new BackendError("validation", { ...base, fields: nested });
|
|
532
|
+
return new BackendError("rateLimited", { ...base, retryAfter: retryAfterOf(err.details) });
|
|
533
|
+
const fields = fieldErrorsOf(err.details);
|
|
534
|
+
if (err.status === 400 && fields) return new BackendError("validation", { ...base, fields });
|
|
521
535
|
return new BackendError("server", base);
|
|
522
536
|
}
|
|
523
537
|
function fromEnvelope(status, body) {
|
|
524
538
|
const code = pickString(body, "error") ?? "http_error";
|
|
525
539
|
const message = pickString(body, "error_description") ?? `HTTP ${status}`;
|
|
526
540
|
const requestId = pickString(body, "request_id");
|
|
527
|
-
const details = pickField(body, "details");
|
|
528
541
|
const params = {
|
|
529
542
|
code,
|
|
530
543
|
message,
|
|
@@ -534,13 +547,9 @@ function fromEnvelope(status, body) {
|
|
|
534
547
|
};
|
|
535
548
|
if (status === 401) return new BackendError("unauthorized", params);
|
|
536
549
|
if (status === 429)
|
|
537
|
-
return new BackendError("rateLimited", {
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
retryAfter: pickNumber(body, "retry_after") ?? pickNumber(details, "retry_after")
|
|
541
|
-
});
|
|
542
|
-
if (status === 400 && isFieldErrorArray(details))
|
|
543
|
-
return new BackendError("validation", { ...params, fields: details });
|
|
550
|
+
return new BackendError("rateLimited", { ...params, retryAfter: retryAfterOf(body) });
|
|
551
|
+
const fields = fieldErrorsOf(body);
|
|
552
|
+
if (status === 400 && fields) return new BackendError("validation", { ...params, fields });
|
|
544
553
|
return new BackendError("server", params);
|
|
545
554
|
}
|
|
546
555
|
function isBackendError(e) {
|
|
@@ -1666,24 +1675,29 @@ var PalbeAuth = class {
|
|
|
1666
1675
|
* Self-service account erasure ("right to be forgotten"). Calls palauth
|
|
1667
1676
|
* `DELETE /auth/user` (session-authed + fresh re-auth): password users pass
|
|
1668
1677
|
* `{ password }`; passwordless/OAuth users rely on a freshly stepped-up
|
|
1669
|
-
* session.
|
|
1670
|
-
* already revoked server-side — so we tear down
|
|
1671
|
-
* `signOut()`'s clear, minus the pointless `/logout`)
|
|
1672
|
-
* `signedOut{reason:'accountDeleted'}`.
|
|
1678
|
+
* session. The handler answers **204 with no body** — the erasure has already
|
|
1679
|
+
* finished and every session is already revoked server-side — so we tear down
|
|
1680
|
+
* local state (mirroring `signOut()`'s clear, minus the pointless `/logout`)
|
|
1681
|
+
* and emit `signedOut{reason:'accountDeleted'}`.
|
|
1682
|
+
*
|
|
1683
|
+
* Returns nothing, because there is nothing to return: the handler used to
|
|
1684
|
+
* answer 202 with an `{erasure_id}` naming an asynchronous run, and that id
|
|
1685
|
+
* was removed along with the run (`gdpr_handlers.go:221-229`). Reading it
|
|
1686
|
+
* here threw a `TypeError` AFTER the account was already gone — a success
|
|
1687
|
+
* reported as a failure.
|
|
1673
1688
|
*
|
|
1674
1689
|
* On 401 `reauth_required` / 503 `not_configured` (or `erasure_unavailable`)
|
|
1675
1690
|
* the request throws BEFORE the teardown line, so the local session is left
|
|
1676
1691
|
* fully intact — the deletion did not happen and the user is still signed in.
|
|
1677
1692
|
*/
|
|
1678
1693
|
async deleteAccount(params = {}) {
|
|
1679
|
-
|
|
1694
|
+
await palbeRequest(this.rt, "DELETE", "/auth/user", {
|
|
1680
1695
|
body: params.password ? { password: params.password } : {}
|
|
1681
1696
|
});
|
|
1682
1697
|
this.nextSignOutReason = "accountDeleted";
|
|
1683
1698
|
this.rt.tokenManager.clearSession();
|
|
1684
1699
|
this.rt.storage.clear();
|
|
1685
1700
|
this.cachedUser = null;
|
|
1686
|
-
return { erasureId: res.erasure_id };
|
|
1687
1701
|
}
|
|
1688
1702
|
/**
|
|
1689
1703
|
* @internal — invoked by the transport choke point (`request.ts`), not app code.
|
|
@@ -2170,9 +2184,15 @@ var PalbeCalls = class {
|
|
|
2170
2184
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
2171
2185
|
* @returns A `Call` in `connecting` state; transitions to `active` once the media room connects.
|
|
2172
2186
|
*
|
|
2173
|
-
*
|
|
2174
|
-
*
|
|
2175
|
-
*
|
|
2187
|
+
* Every failure below arrives as kind `'server'` — this SDK's error kinds are
|
|
2188
|
+
* `notConfigured | validation | unauthorized | rateLimited | server | network
|
|
2189
|
+
* | decode`, and only a 401, a 429 or a 400 carrying a field-error array gets
|
|
2190
|
+
* its own kind. So branch on `err.code`, never on the kind.
|
|
2191
|
+
*
|
|
2192
|
+
* @throws BackendError('server', { code: 'calling_unavailable' }) no SFU configured on this stack (503)
|
|
2193
|
+
* @throws BackendError('server', { code: 'not_group_member' }) the caller is not a member of the group (403)
|
|
2194
|
+
* @throws BackendError('server', { code: 'call_room_full' }) the call is at its participant limit (409)
|
|
2195
|
+
* @throws BackendError('server', { code: 'call_provider_error' }) the media provider failed upstream (502)
|
|
2176
2196
|
*/
|
|
2177
2197
|
async start(groupId, { media = ["audio", "video"] } = {}) {
|
|
2178
2198
|
if (!groupId) {
|
|
@@ -2193,7 +2213,11 @@ var PalbeCalls = class {
|
|
|
2193
2213
|
* Accept an incoming call in a messaging group.
|
|
2194
2214
|
*
|
|
2195
2215
|
* @param groupId - Messaging group display-id
|
|
2196
|
-
* @param callId - Call identifier
|
|
2216
|
+
* @param callId - Call identifier, carried by the `call_invite` the server
|
|
2217
|
+
* fans onto the group's conversation topic
|
|
2218
|
+
* (`pb.realtime.channel('messaging:conv:<rfcGroupId>').on('call_invite', …)`).
|
|
2219
|
+
* This SDK ships no web push: on the web the invite arrives over realtime or
|
|
2220
|
+
* over whatever signalling the app already runs.
|
|
2197
2221
|
* @param media - Which media tracks to publish (default: audio + video)
|
|
2198
2222
|
*/
|
|
2199
2223
|
async accept(groupId, callId, { media = ["audio", "video"] } = {}) {
|
|
@@ -4296,14 +4320,11 @@ var Chat = class {
|
|
|
4296
4320
|
get title() {
|
|
4297
4321
|
if (this.titleOverride) return this.titleOverride;
|
|
4298
4322
|
if (this._group?.name) return this._group.name;
|
|
4299
|
-
if (this.
|
|
4300
|
-
const
|
|
4301
|
-
|
|
4302
|
-
const peer = this.memberCache.find((m) => m.userId === mode.peerUserId);
|
|
4303
|
-
return peer?.displayName ?? "Chat";
|
|
4304
|
-
}
|
|
4305
|
-
return "Group";
|
|
4323
|
+
if (this.kind === "direct") {
|
|
4324
|
+
const peer = this.memberCache.find((m) => !m.isSelf);
|
|
4325
|
+
return peer?.displayName ?? "Chat";
|
|
4306
4326
|
}
|
|
4327
|
+
if (this._draft) return "Group";
|
|
4307
4328
|
const tail = this.id.startsWith("grp_") ? this.id.slice(4) : this.id;
|
|
4308
4329
|
const short = tail.slice(-4);
|
|
4309
4330
|
return short ? `Group ${short}` : "Group";
|
|
@@ -5277,6 +5298,11 @@ var MessageHub = class {
|
|
|
5277
5298
|
emitConv(displayId, e) {
|
|
5278
5299
|
for (const fn of this.convListeners.get(displayId) ?? []) fn(e);
|
|
5279
5300
|
}
|
|
5301
|
+
/** How many live conv listeners a group still has — the census the LAST
|
|
5302
|
+
* subscriber reads before tearing the conv topic (and its presence) down. */
|
|
5303
|
+
convListenerCount(displayId) {
|
|
5304
|
+
return this.convListeners.get(displayId)?.size ?? 0;
|
|
5305
|
+
}
|
|
5280
5306
|
add(map, key, fn) {
|
|
5281
5307
|
let set = map.get(key);
|
|
5282
5308
|
if (!set) {
|
|
@@ -5314,6 +5340,9 @@ var MessageDeliverySource = class {
|
|
|
5314
5340
|
started = false;
|
|
5315
5341
|
wakeUnsub = null;
|
|
5316
5342
|
// Per-observed-group conv subscription + heartbeat (presence/typing/read).
|
|
5343
|
+
// `teardown` is the ONE departure path: it stops the heartbeat, broadcasts the
|
|
5344
|
+
// offline frame and drops the subscriptions, so no caller can tear an
|
|
5345
|
+
// observation down without saying goodbye (see observeConversation).
|
|
5317
5346
|
observed = /* @__PURE__ */ new Map();
|
|
5318
5347
|
/** Subscribe the device wake topic + run an initial drain. Idempotent. */
|
|
5319
5348
|
async start() {
|
|
@@ -5338,10 +5367,7 @@ var MessageDeliverySource = class {
|
|
|
5338
5367
|
stop() {
|
|
5339
5368
|
this.wakeUnsub?.();
|
|
5340
5369
|
this.wakeUnsub = null;
|
|
5341
|
-
for (const [, o] of this.observed)
|
|
5342
|
-
o.unsub();
|
|
5343
|
-
if (o.heartbeat) clearInterval(o.heartbeat);
|
|
5344
|
-
}
|
|
5370
|
+
for (const [, o] of this.observed) o.teardown();
|
|
5345
5371
|
this.observed.clear();
|
|
5346
5372
|
this.started = false;
|
|
5347
5373
|
}
|
|
@@ -5572,14 +5598,26 @@ var MessageDeliverySource = class {
|
|
|
5572
5598
|
emitPresence(true);
|
|
5573
5599
|
const heartbeat = setInterval(() => emitPresence(true), 25e3);
|
|
5574
5600
|
this.observed.set(group.displayId, {
|
|
5575
|
-
|
|
5601
|
+
teardown: () => {
|
|
5602
|
+
clearInterval(heartbeat);
|
|
5603
|
+
try {
|
|
5604
|
+
emitPresence(false);
|
|
5605
|
+
} catch {
|
|
5606
|
+
}
|
|
5576
5607
|
for (const s of subs) s.cancel();
|
|
5577
|
-
}
|
|
5578
|
-
heartbeat
|
|
5608
|
+
}
|
|
5579
5609
|
});
|
|
5580
5610
|
} catch {
|
|
5581
5611
|
}
|
|
5582
5612
|
}
|
|
5613
|
+
/** Stop observing a group's conv topic: announce OFFLINE, stop the heartbeat
|
|
5614
|
+
* and drop the subscriptions. Idempotent — an unobserved group is a no-op. */
|
|
5615
|
+
unobserveConversation(group) {
|
|
5616
|
+
const o = this.observed.get(group.displayId);
|
|
5617
|
+
if (!o) return;
|
|
5618
|
+
this.observed.delete(group.displayId);
|
|
5619
|
+
o.teardown();
|
|
5620
|
+
}
|
|
5583
5621
|
/** Announce typing on a group's conv topic (the app calls per keystroke). */
|
|
5584
5622
|
setTyping(group, isTyping) {
|
|
5585
5623
|
this.observeConversation(group);
|
|
@@ -7807,7 +7845,11 @@ var MessagingCoordinator = class {
|
|
|
7807
7845
|
subscribeLive(group, chat) {
|
|
7808
7846
|
let offMsg = null;
|
|
7809
7847
|
let offConv = null;
|
|
7848
|
+
let resolved = null;
|
|
7849
|
+
let cancelled = false;
|
|
7810
7850
|
void this.resolve().then((r) => {
|
|
7851
|
+
if (cancelled) return;
|
|
7852
|
+
resolved = r;
|
|
7811
7853
|
offMsg = r.hub.onMessage(group.displayId, (m) => {
|
|
7812
7854
|
void chat.ingestLive(m);
|
|
7813
7855
|
});
|
|
@@ -7815,8 +7857,12 @@ var MessagingCoordinator = class {
|
|
|
7815
7857
|
offConv = r.hub.onConv(group.displayId, (e) => chat.applyConv(e.event, e.payload));
|
|
7816
7858
|
});
|
|
7817
7859
|
return () => {
|
|
7860
|
+
cancelled = true;
|
|
7818
7861
|
offMsg?.();
|
|
7819
7862
|
offConv?.();
|
|
7863
|
+
if (resolved && resolved.hub.convListenerCount(group.displayId) === 0) {
|
|
7864
|
+
resolved.source.unobserveConversation(group);
|
|
7865
|
+
}
|
|
7820
7866
|
};
|
|
7821
7867
|
}
|
|
7822
7868
|
async userIdForDevice(group, deviceId) {
|
|
@@ -8674,8 +8720,33 @@ function unwrapBroadcast(frame) {
|
|
|
8674
8720
|
function stripRealtimePrefix(topic) {
|
|
8675
8721
|
return topic.startsWith(REALTIME_PREFIX) ? topic.slice(REALTIME_PREFIX.length) : topic;
|
|
8676
8722
|
}
|
|
8723
|
+
function encodeStateSet(joinRef, ref, topic, key, value, life) {
|
|
8724
|
+
return JSON.stringify([
|
|
8725
|
+
joinRef,
|
|
8726
|
+
String(ref),
|
|
8727
|
+
REALTIME_PREFIX + topic,
|
|
8728
|
+
"state_set",
|
|
8729
|
+
{ key, value, life }
|
|
8730
|
+
]);
|
|
8731
|
+
}
|
|
8732
|
+
function encodeStateDel(joinRef, ref, topic, key) {
|
|
8733
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_del", { key }]);
|
|
8734
|
+
}
|
|
8735
|
+
function encodeStateResync(joinRef, ref, topic) {
|
|
8736
|
+
return JSON.stringify([joinRef, String(ref), REALTIME_PREFIX + topic, "state_resync", {}]);
|
|
8737
|
+
}
|
|
8738
|
+
function encodeAccessToken(joinRef, ref, topic, token) {
|
|
8739
|
+
return JSON.stringify([
|
|
8740
|
+
joinRef,
|
|
8741
|
+
String(ref),
|
|
8742
|
+
REALTIME_PREFIX + topic,
|
|
8743
|
+
"access_token",
|
|
8744
|
+
{ access_token: token }
|
|
8745
|
+
]);
|
|
8746
|
+
}
|
|
8677
8747
|
|
|
8678
8748
|
// src/realtime/connection.ts
|
|
8749
|
+
var RETRYABLE_REFUSALS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
8679
8750
|
var WS_OPEN = 1;
|
|
8680
8751
|
var DEFAULT_HEARTBEAT_MS = 25e3;
|
|
8681
8752
|
var DEFAULT_MAX_BACKOFF_SECONDS = 30;
|
|
@@ -8908,6 +8979,31 @@ var RealtimeSocket = class {
|
|
|
8908
8979
|
this.handleChannelClose(frame);
|
|
8909
8980
|
return;
|
|
8910
8981
|
}
|
|
8982
|
+
if (frame.event === "state_snapshot") {
|
|
8983
|
+
this.handlers?.onStateSnapshot(stripRealtimePrefix(frame.topic), frame.payload);
|
|
8984
|
+
return;
|
|
8985
|
+
}
|
|
8986
|
+
if (frame.event === "state_diff") {
|
|
8987
|
+
this.handlers?.onStateDiff(stripRealtimePrefix(frame.topic), frame.payload);
|
|
8988
|
+
return;
|
|
8989
|
+
}
|
|
8990
|
+
if (frame.event === "phx_reply") {
|
|
8991
|
+
const body = frame.payload;
|
|
8992
|
+
const topic = stripRealtimePrefix(frame.topic);
|
|
8993
|
+
if (body && body.status === "ok" && body.response && typeof body.response === "object") {
|
|
8994
|
+
this.handlers?.onJoinInfo(topic, body.response);
|
|
8995
|
+
return;
|
|
8996
|
+
}
|
|
8997
|
+
if (body && body.status === "error" && String(frame.ref) === this.joinRefs.get(topic)) {
|
|
8998
|
+
const reason = typeof body.response?.["reason"] === "string" ? body.response["reason"] : "unauthorized";
|
|
8999
|
+
if (!RETRYABLE_REFUSALS.has(reason)) {
|
|
9000
|
+
this.joinedTopics.delete(topic);
|
|
9001
|
+
this.joinRefs.delete(topic);
|
|
9002
|
+
}
|
|
9003
|
+
this.handlers?.onChannelRefused(topic, reason);
|
|
9004
|
+
}
|
|
9005
|
+
return;
|
|
9006
|
+
}
|
|
8911
9007
|
const inbound = unwrapBroadcast(frame);
|
|
8912
9008
|
if (inbound) {
|
|
8913
9009
|
this.channelRejoinAttempts.delete(inbound.topic);
|
|
@@ -8948,6 +9044,40 @@ var RealtimeSocket = class {
|
|
|
8948
9044
|
}, delayMs);
|
|
8949
9045
|
this.channelRejoinTimers.set(topic, timer);
|
|
8950
9046
|
}
|
|
9047
|
+
// MARK: state
|
|
9048
|
+
/** Write one entry on a joined channel's state. Dropped when the topic has no
|
|
9049
|
+
* live join: the server refuses state on a channel this socket is not on, and
|
|
9050
|
+
* the reconnect's join brings a fresh snapshot anyway. */
|
|
9051
|
+
sendStateSet(topic, key, value, life) {
|
|
9052
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9053
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9054
|
+
this.send(encodeStateSet(joinRef, this.nextRef(), topic, key, value, life));
|
|
9055
|
+
}
|
|
9056
|
+
/** Remove one entry. The server refuses a key this connection does not own. */
|
|
9057
|
+
sendStateDel(topic, key) {
|
|
9058
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9059
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9060
|
+
this.send(encodeStateDel(joinRef, this.nextRef(), topic, key));
|
|
9061
|
+
}
|
|
9062
|
+
/** Ask for a fresh snapshot — the answer to a detected gap. */
|
|
9063
|
+
sendStateResync(topic) {
|
|
9064
|
+
const joinRef = this.joinRefs.get(topic);
|
|
9065
|
+
if (joinRef === void 0 || !this.isOpen()) return;
|
|
9066
|
+
this.send(encodeStateResync(joinRef, this.nextRef(), topic));
|
|
9067
|
+
}
|
|
9068
|
+
/**
|
|
9069
|
+
* Present a fresher credential on every joined channel WITHOUT rejoining.
|
|
9070
|
+
*
|
|
9071
|
+
* The alternative — dropping and rejoining — reaps this connection's
|
|
9072
|
+
* ephemeral entries (its presence) and forces a full resync on every channel.
|
|
9073
|
+
* A token rotation is not a reconnection and must not look like one.
|
|
9074
|
+
*/
|
|
9075
|
+
sendAccessToken(token) {
|
|
9076
|
+
if (!this.isOpen()) return;
|
|
9077
|
+
for (const [topic, joinRef] of this.joinRefs) {
|
|
9078
|
+
this.send(encodeAccessToken(joinRef, this.nextRef(), topic, token));
|
|
9079
|
+
}
|
|
9080
|
+
}
|
|
8951
9081
|
// MARK: helpers
|
|
8952
9082
|
/** Drop a single topic's token-expiry rejoin state (cancel its pending timer). */
|
|
8953
9083
|
clearChannelRejoin(topic) {
|
|
@@ -8973,6 +9103,60 @@ var RealtimeSocket = class {
|
|
|
8973
9103
|
}
|
|
8974
9104
|
};
|
|
8975
9105
|
|
|
9106
|
+
// src/realtime/state.ts
|
|
9107
|
+
var PRESENCE_PREFIX = "$p:";
|
|
9108
|
+
var ChannelState = class {
|
|
9109
|
+
/** The sequence this local copy is current as of. */
|
|
9110
|
+
seq = 0;
|
|
9111
|
+
/** key → value. Lifetimes are the server's business; a reader only needs the
|
|
9112
|
+
* values, and presence is derived from the key shape. */
|
|
9113
|
+
entries = /* @__PURE__ */ new Map();
|
|
9114
|
+
/** Replace everything. A snapshot is the WHOLE picture, never a merge —
|
|
9115
|
+
* merging one would keep entries the server has since removed. */
|
|
9116
|
+
applySnapshot(body) {
|
|
9117
|
+
this.entries.clear();
|
|
9118
|
+
for (const [k, e] of Object.entries(body.entries ?? {})) {
|
|
9119
|
+
this.entries.set(k, e.v);
|
|
9120
|
+
}
|
|
9121
|
+
this.seq = body.seq;
|
|
9122
|
+
}
|
|
9123
|
+
/**
|
|
9124
|
+
* Apply a diff, or report that it cannot be applied.
|
|
9125
|
+
*
|
|
9126
|
+
* `gap` when this copy is behind the diff's starting point: frames were
|
|
9127
|
+
* missed, and applying anyway would produce a state that never existed on the
|
|
9128
|
+
* server. The state is left untouched so the caller can resync from something
|
|
9129
|
+
* consistent.
|
|
9130
|
+
*
|
|
9131
|
+
* A diff at or below the current sequence is already applied — a resend after
|
|
9132
|
+
* a retry — and is silently accepted so duplicates are harmless.
|
|
9133
|
+
*/
|
|
9134
|
+
applyDiff(body) {
|
|
9135
|
+
if (this.seq < body.from_seq) return "gap";
|
|
9136
|
+
if (body.seq <= this.seq) return "applied";
|
|
9137
|
+
for (const [k, e] of Object.entries(body.set ?? {})) this.entries.set(k, e.v);
|
|
9138
|
+
for (const k of body.del ?? []) this.entries.delete(k);
|
|
9139
|
+
this.seq = body.seq;
|
|
9140
|
+
return "applied";
|
|
9141
|
+
}
|
|
9142
|
+
/**
|
|
9143
|
+
* Everyone else's presence.
|
|
9144
|
+
*
|
|
9145
|
+
* `selfConn` is this connection's id, as the join reply reported it. Its own
|
|
9146
|
+
* entry is excluded because an app rendering "who else is here" would
|
|
9147
|
+
* otherwise always show one extra face — itself.
|
|
9148
|
+
*/
|
|
9149
|
+
presence(selfConn) {
|
|
9150
|
+
const own = selfConn === void 0 ? void 0 : PRESENCE_PREFIX + selfConn;
|
|
9151
|
+
const out = [];
|
|
9152
|
+
for (const [k, v] of this.entries) {
|
|
9153
|
+
if (!k.startsWith(PRESENCE_PREFIX) || k === own) continue;
|
|
9154
|
+
out.push(v);
|
|
9155
|
+
}
|
|
9156
|
+
return out;
|
|
9157
|
+
}
|
|
9158
|
+
};
|
|
9159
|
+
|
|
8976
9160
|
// src/realtime/facade.ts
|
|
8977
9161
|
var StatusStore = class {
|
|
8978
9162
|
currentState = "idle";
|
|
@@ -9008,6 +9192,7 @@ var StatusStore = class {
|
|
|
9008
9192
|
for (const listener of this.listeners) listener(snapshot);
|
|
9009
9193
|
}
|
|
9010
9194
|
};
|
|
9195
|
+
var RETRYABLE_REASONS = /* @__PURE__ */ new Set(["unavailable", "rate_limited", "too_many_channels"]);
|
|
9011
9196
|
var RealtimeChannel = class {
|
|
9012
9197
|
/** The app-defined channel name (bare sub-topic, no "realtime:" prefix). */
|
|
9013
9198
|
name;
|
|
@@ -9025,6 +9210,23 @@ var RealtimeChannel = class {
|
|
|
9025
9210
|
on(event, handler) {
|
|
9026
9211
|
return this.owner.subscribe(this.name, event, handler);
|
|
9027
9212
|
}
|
|
9213
|
+
/**
|
|
9214
|
+
* Called when the server REFUSES this channel.
|
|
9215
|
+
*
|
|
9216
|
+
* A join is an authorization decision, and a decision the app never hears is
|
|
9217
|
+
* the same as no decision at all: silence looks exactly like a slow network.
|
|
9218
|
+
* This is where a refusal arrives, carrying the server's own distinction —
|
|
9219
|
+
* `retryable: false` means "you may not" and asking again will not help;
|
|
9220
|
+
* `retryable: true` means the project could not be asked, and the socket's
|
|
9221
|
+
* next reconnect retries on its own.
|
|
9222
|
+
*
|
|
9223
|
+
* pb.realtime.channel('room:42').onError((err) => {
|
|
9224
|
+
* if (!err.retryable) showJoinDenied()
|
|
9225
|
+
* })
|
|
9226
|
+
*/
|
|
9227
|
+
onError(handler) {
|
|
9228
|
+
return this.owner.subscribeError(this.name, handler);
|
|
9229
|
+
}
|
|
9028
9230
|
/**
|
|
9029
9231
|
* Broadcast `payload` to the channel's other subscribers (web addition —
|
|
9030
9232
|
* iOS is receive-only). Joins the channel if it isn't already; queued
|
|
@@ -9033,6 +9235,57 @@ var RealtimeChannel = class {
|
|
|
9033
9235
|
send(event, payload = {}) {
|
|
9034
9236
|
this.owner.send(this.name, event, payload);
|
|
9035
9237
|
}
|
|
9238
|
+
/**
|
|
9239
|
+
* Watch one key of the channel's shared state.
|
|
9240
|
+
*
|
|
9241
|
+
* The handler is called with the CURRENT value straight away when there is
|
|
9242
|
+
* one, and again on every change. That is the difference from `on()`: a
|
|
9243
|
+
* subscriber to an event learns nothing until the next one, while state is
|
|
9244
|
+
* already there when you ask.
|
|
9245
|
+
*/
|
|
9246
|
+
onState(key, handler) {
|
|
9247
|
+
return this.owner.subscribeState(this.name, key, handler);
|
|
9248
|
+
}
|
|
9249
|
+
/** Read one key without subscribing. */
|
|
9250
|
+
stateValue(key) {
|
|
9251
|
+
return this.owner.stateOf(this.name).entries.get(key);
|
|
9252
|
+
}
|
|
9253
|
+
/** Write one key. `durable` outlives this connection; `ephemeral` is reaped
|
|
9254
|
+
* when it closes, which is what presence is built on. */
|
|
9255
|
+
setState(key, value, opts = {}) {
|
|
9256
|
+
this.owner.writeState(this.name, key, value, opts.life ?? "durable");
|
|
9257
|
+
}
|
|
9258
|
+
/** Remove a key this connection wrote. */
|
|
9259
|
+
clearState(key) {
|
|
9260
|
+
this.owner.clearState(this.name, key);
|
|
9261
|
+
}
|
|
9262
|
+
/**
|
|
9263
|
+
* Announce this client on the channel. `meta` is whatever other participants
|
|
9264
|
+
* should see — a name, an avatar, a cursor.
|
|
9265
|
+
*
|
|
9266
|
+
* It is ephemeral state keyed by this connection, so it disappears when the
|
|
9267
|
+
* socket does. Nothing has to be un-announced on a crash.
|
|
9268
|
+
*/
|
|
9269
|
+
presenceEnter(meta) {
|
|
9270
|
+
this.owner.presenceWrite(this.name, meta);
|
|
9271
|
+
}
|
|
9272
|
+
/** Replace the announced value — a cursor moving is this, at whatever rate
|
|
9273
|
+
* the app likes: the server conflates writes it cannot deliver fast enough. */
|
|
9274
|
+
presenceUpdate(meta) {
|
|
9275
|
+
this.owner.presenceWrite(this.name, meta);
|
|
9276
|
+
}
|
|
9277
|
+
/** Leave early. Closing the socket does this on its own. */
|
|
9278
|
+
presenceLeave() {
|
|
9279
|
+
this.owner.presenceClear(this.name);
|
|
9280
|
+
}
|
|
9281
|
+
/** Everyone else currently announced, this client excluded. */
|
|
9282
|
+
presenceOthers() {
|
|
9283
|
+
return this.owner.presenceOthers(this.name);
|
|
9284
|
+
}
|
|
9285
|
+
/** Watch the announced set. Called on every change. */
|
|
9286
|
+
onPresence(handler) {
|
|
9287
|
+
return this.owner.subscribePresence(this.name, handler);
|
|
9288
|
+
}
|
|
9036
9289
|
};
|
|
9037
9290
|
var PalbeRealtime = class {
|
|
9038
9291
|
rt;
|
|
@@ -9042,9 +9295,21 @@ var PalbeRealtime = class {
|
|
|
9042
9295
|
// Per-(topic, event) handlers — an event may have multiple handlers
|
|
9043
9296
|
// (multiple .on calls); entry identity is the unsubscribe token.
|
|
9044
9297
|
handlers = /* @__PURE__ */ new Set();
|
|
9298
|
+
// Per-topic refusal handlers. Kept apart from `handlers` because they are not
|
|
9299
|
+
// refcounted: registering one must not join a channel, and cancelling the
|
|
9300
|
+
// last one must not leave a channel that still has event handlers on it.
|
|
9301
|
+
errorHandlers = /* @__PURE__ */ new Set();
|
|
9045
9302
|
// Joins are refcounted per topic so the socket only leaves a topic when
|
|
9046
9303
|
// its last handler cancels (iOS RealtimeClient parity).
|
|
9047
9304
|
topicRefcount = /* @__PURE__ */ new Map();
|
|
9305
|
+
// Per-topic shared state, and this connection's identity on each topic (from
|
|
9306
|
+
// the join ack). Presence is keyed by that identity, so a write before the
|
|
9307
|
+
// ack has nowhere to go and is queued until it arrives.
|
|
9308
|
+
states = /* @__PURE__ */ new Map();
|
|
9309
|
+
connIds = /* @__PURE__ */ new Map();
|
|
9310
|
+
pendingPresence = /* @__PURE__ */ new Map();
|
|
9311
|
+
stateWatchers = /* @__PURE__ */ new Set();
|
|
9312
|
+
presenceWatchers = /* @__PURE__ */ new Set();
|
|
9048
9313
|
constructor(rt) {
|
|
9049
9314
|
this.rt = rt;
|
|
9050
9315
|
}
|
|
@@ -9087,6 +9352,20 @@ var PalbeRealtime = class {
|
|
|
9087
9352
|
get status() {
|
|
9088
9353
|
return this.statusStore;
|
|
9089
9354
|
}
|
|
9355
|
+
/**
|
|
9356
|
+
* Hand the socket a fresher credential for the SAME user, without rejoining.
|
|
9357
|
+
*
|
|
9358
|
+
* Called when the app's token rotates. The alternative — letting the channels
|
|
9359
|
+
* die at expiry and rejoining — reaps this connection's ephemeral entries
|
|
9360
|
+
* (its presence, on every channel) and forces a full state resync. A token
|
|
9361
|
+
* getting older is not a disconnection and must not cost one.
|
|
9362
|
+
*
|
|
9363
|
+
* A no-op before a socket exists: the first connect resolves a fresh token
|
|
9364
|
+
* on its own.
|
|
9365
|
+
*/
|
|
9366
|
+
refreshToken(token) {
|
|
9367
|
+
this.socket?.sendAccessToken(token);
|
|
9368
|
+
}
|
|
9090
9369
|
/** @internal */
|
|
9091
9370
|
subscribe(topic, event, handler) {
|
|
9092
9371
|
const socket = this.ensureSocket();
|
|
@@ -9101,7 +9380,9 @@ var PalbeRealtime = class {
|
|
|
9101
9380
|
if (cancelled) return;
|
|
9102
9381
|
cancelled = true;
|
|
9103
9382
|
this.handlers.delete(entry);
|
|
9104
|
-
const
|
|
9383
|
+
const held = this.topicRefcount.get(topic);
|
|
9384
|
+
if (held === void 0) return;
|
|
9385
|
+
const next = held - 1;
|
|
9105
9386
|
if (next <= 0) {
|
|
9106
9387
|
this.topicRefcount.delete(topic);
|
|
9107
9388
|
socket.leaveTopic(topic);
|
|
@@ -9111,6 +9392,25 @@ var PalbeRealtime = class {
|
|
|
9111
9392
|
}
|
|
9112
9393
|
};
|
|
9113
9394
|
}
|
|
9395
|
+
/**
|
|
9396
|
+
* Register a refusal handler for one topic. Does NOT join: watching for a
|
|
9397
|
+
* refusal is not a subscription, and an app that registers one before its
|
|
9398
|
+
* first `.on()` should not be joining anything yet.
|
|
9399
|
+
*
|
|
9400
|
+
* @internal
|
|
9401
|
+
*/
|
|
9402
|
+
subscribeError(topic, handler) {
|
|
9403
|
+
const entry = { topic, handler };
|
|
9404
|
+
this.errorHandlers.add(entry);
|
|
9405
|
+
let cancelled = false;
|
|
9406
|
+
return {
|
|
9407
|
+
cancel: () => {
|
|
9408
|
+
if (cancelled) return;
|
|
9409
|
+
cancelled = true;
|
|
9410
|
+
this.errorHandlers.delete(entry);
|
|
9411
|
+
}
|
|
9412
|
+
};
|
|
9413
|
+
}
|
|
9114
9414
|
/** @internal */
|
|
9115
9415
|
send(topic, event, payload) {
|
|
9116
9416
|
const socket = this.ensureSocket();
|
|
@@ -9132,7 +9432,11 @@ var PalbeRealtime = class {
|
|
|
9132
9432
|
onReconnect: () => {
|
|
9133
9433
|
},
|
|
9134
9434
|
onStateChange: (state) => this.statusStore.setState(state),
|
|
9135
|
-
onChannelError: (topic) => this.handleChannelError(topic)
|
|
9435
|
+
onChannelError: (topic) => this.handleChannelError(topic),
|
|
9436
|
+
onChannelRefused: (topic, reason) => this.handleChannelRefused(topic, reason),
|
|
9437
|
+
onStateSnapshot: (topic, body) => this.applySnapshot(topic, body),
|
|
9438
|
+
onStateDiff: (topic, body) => this.applyDiff(topic, body),
|
|
9439
|
+
onJoinInfo: (topic, info) => this.applyJoinInfo(topic, info)
|
|
9136
9440
|
});
|
|
9137
9441
|
}
|
|
9138
9442
|
return this.socket;
|
|
@@ -9146,19 +9450,185 @@ var PalbeRealtime = class {
|
|
|
9146
9450
|
}
|
|
9147
9451
|
}
|
|
9148
9452
|
/**
|
|
9149
|
-
|
|
9150
|
-
*
|
|
9151
|
-
*
|
|
9152
|
-
*
|
|
9453
|
+
/**
|
|
9454
|
+
* The server refused a channel. Tell whoever asked, and — when the answer is
|
|
9455
|
+
* final — stop pretending the app is subscribed to it.
|
|
9456
|
+
*
|
|
9457
|
+
* `unavailable` leaves everything in place: the project could not be reached
|
|
9458
|
+
* to decide, the socket rejoins on its next reconnect, and dropping the app's
|
|
9459
|
+
* handlers would make a momentary outage look like a permanent denial.
|
|
9460
|
+
*/
|
|
9461
|
+
handleChannelRefused(topic, reason) {
|
|
9462
|
+
const retryable = RETRYABLE_REASONS.has(reason);
|
|
9463
|
+
for (const entry of this.errorHandlers) {
|
|
9464
|
+
if (entry.topic === topic) entry.handler({ topic, reason, retryable });
|
|
9465
|
+
}
|
|
9466
|
+
if (retryable) return;
|
|
9467
|
+
this.forgetTopic(topic);
|
|
9468
|
+
}
|
|
9469
|
+
/**
|
|
9470
|
+
* A channel died for good — a token-expiry rejoin that exhausted its attempts.
|
|
9471
|
+
* The app must stop expecting anything on it, and the status observable flips
|
|
9472
|
+
* to `'error'` (the React `useChannel` hook reports that).
|
|
9473
|
+
*
|
|
9474
|
+
* It clears the SAME registries a final refusal clears, and used to clear only
|
|
9475
|
+
* the event handlers. A state or presence watcher left behind here waits
|
|
9476
|
+
* forever for a snapshot that cannot arrive, and fires twice if the app ever
|
|
9477
|
+
* re-subscribes — the identical failure its sibling three functions up
|
|
9478
|
+
* describes, on a channel that was given up on rather than refused.
|
|
9153
9479
|
*/
|
|
9154
9480
|
handleChannelError(topic) {
|
|
9481
|
+
this.forgetTopic(topic);
|
|
9482
|
+
this.statusStore.setState("error");
|
|
9483
|
+
}
|
|
9484
|
+
/** Drop every registration this topic holds. One place, because "which
|
|
9485
|
+
* registries" is a question two callers must never answer differently. */
|
|
9486
|
+
forgetTopic(topic) {
|
|
9155
9487
|
for (const entry of this.handlers) {
|
|
9156
9488
|
if (entry.topic === topic) this.handlers.delete(entry);
|
|
9157
9489
|
}
|
|
9490
|
+
for (const entry of this.stateWatchers) {
|
|
9491
|
+
if (entry.topic === topic) this.stateWatchers.delete(entry);
|
|
9492
|
+
}
|
|
9493
|
+
for (const entry of this.presenceWatchers) {
|
|
9494
|
+
if (entry.topic === topic) this.presenceWatchers.delete(entry);
|
|
9495
|
+
}
|
|
9496
|
+
this.pendingPresence.delete(topic);
|
|
9497
|
+
this.states.delete(topic);
|
|
9498
|
+
this.connIds.delete(topic);
|
|
9158
9499
|
this.topicRefcount.delete(topic);
|
|
9159
|
-
|
|
9500
|
+
}
|
|
9501
|
+
// ── state ──────────────────────────────────────────────────────────────────
|
|
9502
|
+
/** @internal */
|
|
9503
|
+
stateOf(topic) {
|
|
9504
|
+
let st = this.states.get(topic);
|
|
9505
|
+
if (!st) {
|
|
9506
|
+
st = new ChannelState();
|
|
9507
|
+
this.states.set(topic, st);
|
|
9508
|
+
}
|
|
9509
|
+
return st;
|
|
9510
|
+
}
|
|
9511
|
+
/** @internal */
|
|
9512
|
+
subscribeState(topic, key, handler) {
|
|
9513
|
+
const socket = this.ensureSocket();
|
|
9514
|
+
const entry = { topic, key, handler };
|
|
9515
|
+
this.stateWatchers.add(entry);
|
|
9516
|
+
this.retainTopic(topic, socket);
|
|
9517
|
+
const known = this.stateOf(topic).entries.get(key);
|
|
9518
|
+
if (known !== void 0) handler(known);
|
|
9519
|
+
let cancelled = false;
|
|
9520
|
+
return {
|
|
9521
|
+
cancel: () => {
|
|
9522
|
+
if (cancelled) return;
|
|
9523
|
+
cancelled = true;
|
|
9524
|
+
this.stateWatchers.delete(entry);
|
|
9525
|
+
this.releaseTopic(topic, socket);
|
|
9526
|
+
}
|
|
9527
|
+
};
|
|
9528
|
+
}
|
|
9529
|
+
/** @internal */
|
|
9530
|
+
subscribePresence(topic, handler) {
|
|
9531
|
+
const socket = this.ensureSocket();
|
|
9532
|
+
const entry = { topic, handler };
|
|
9533
|
+
this.presenceWatchers.add(entry);
|
|
9534
|
+
this.retainTopic(topic, socket);
|
|
9535
|
+
handler(this.presenceOthers(topic));
|
|
9536
|
+
let cancelled = false;
|
|
9537
|
+
return {
|
|
9538
|
+
cancel: () => {
|
|
9539
|
+
if (cancelled) return;
|
|
9540
|
+
cancelled = true;
|
|
9541
|
+
this.presenceWatchers.delete(entry);
|
|
9542
|
+
this.releaseTopic(topic, socket);
|
|
9543
|
+
}
|
|
9544
|
+
};
|
|
9545
|
+
}
|
|
9546
|
+
/** @internal */
|
|
9547
|
+
writeState(topic, key, value, life) {
|
|
9548
|
+
const socket = this.ensureSocket();
|
|
9549
|
+
socket.joinTopic(topic);
|
|
9550
|
+
socket.sendStateSet(topic, key, value, life);
|
|
9551
|
+
}
|
|
9552
|
+
/** @internal */
|
|
9553
|
+
clearState(topic, key) {
|
|
9554
|
+
this.ensureSocket().sendStateDel(topic, key);
|
|
9555
|
+
}
|
|
9556
|
+
/** @internal */
|
|
9557
|
+
presenceWrite(topic, meta) {
|
|
9558
|
+
const socket = this.ensureSocket();
|
|
9559
|
+
socket.joinTopic(topic);
|
|
9560
|
+
const conn = this.connIds.get(topic);
|
|
9561
|
+
if (conn === void 0) {
|
|
9562
|
+
this.pendingPresence.set(topic, meta);
|
|
9563
|
+
return;
|
|
9564
|
+
}
|
|
9565
|
+
socket.sendStateSet(topic, PRESENCE_KEY(conn), meta, "ephemeral");
|
|
9566
|
+
}
|
|
9567
|
+
/** @internal */
|
|
9568
|
+
presenceClear(topic) {
|
|
9569
|
+
this.pendingPresence.delete(topic);
|
|
9570
|
+
const conn = this.connIds.get(topic);
|
|
9571
|
+
if (conn === void 0) return;
|
|
9572
|
+
this.ensureSocket().sendStateDel(topic, PRESENCE_KEY(conn));
|
|
9573
|
+
}
|
|
9574
|
+
/** @internal */
|
|
9575
|
+
presenceOthers(topic) {
|
|
9576
|
+
return this.stateOf(topic).presence(this.connIds.get(topic));
|
|
9577
|
+
}
|
|
9578
|
+
retainTopic(topic, socket) {
|
|
9579
|
+
const count = (this.topicRefcount.get(topic) ?? 0) + 1;
|
|
9580
|
+
this.topicRefcount.set(topic, count);
|
|
9581
|
+
if (count === 1) socket.joinTopic(topic);
|
|
9582
|
+
}
|
|
9583
|
+
releaseTopic(topic, socket) {
|
|
9584
|
+
const next = (this.topicRefcount.get(topic) ?? 1) - 1;
|
|
9585
|
+
if (next <= 0) {
|
|
9586
|
+
this.topicRefcount.delete(topic);
|
|
9587
|
+
socket.leaveTopic(topic);
|
|
9588
|
+
return;
|
|
9589
|
+
}
|
|
9590
|
+
this.topicRefcount.set(topic, next);
|
|
9591
|
+
}
|
|
9592
|
+
applySnapshot(topic, body) {
|
|
9593
|
+
this.stateOf(topic).applySnapshot(body);
|
|
9594
|
+
this.notifyState(topic);
|
|
9595
|
+
}
|
|
9596
|
+
applyDiff(topic, body) {
|
|
9597
|
+
if (this.stateOf(topic).applyDiff(body) === "gap") {
|
|
9598
|
+
this.socket?.sendStateResync(topic);
|
|
9599
|
+
return;
|
|
9600
|
+
}
|
|
9601
|
+
this.notifyState(topic);
|
|
9602
|
+
}
|
|
9603
|
+
applyJoinInfo(topic, info) {
|
|
9604
|
+
if (typeof info.conn !== "string" || info.conn === "") return;
|
|
9605
|
+
this.connIds.set(topic, info.conn);
|
|
9606
|
+
const seq = info.state_seq;
|
|
9607
|
+
if (typeof seq === "number" && seq < this.stateOf(topic).seq) {
|
|
9608
|
+
this.stateOf(topic).applySnapshot({ seq, entries: {} });
|
|
9609
|
+
}
|
|
9610
|
+
const held = this.pendingPresence.get(topic);
|
|
9611
|
+
if (held !== void 0) {
|
|
9612
|
+
this.pendingPresence.delete(topic);
|
|
9613
|
+
this.socket?.sendStateSet(topic, PRESENCE_KEY(info.conn), held, "ephemeral");
|
|
9614
|
+
}
|
|
9615
|
+
this.notifyState(topic);
|
|
9616
|
+
}
|
|
9617
|
+
notifyState(topic) {
|
|
9618
|
+
this.statusStore.recordEvent(/* @__PURE__ */ new Date());
|
|
9619
|
+
const st = this.stateOf(topic);
|
|
9620
|
+
for (const w of this.stateWatchers) {
|
|
9621
|
+
if (w.topic === topic) w.handler(st.entries.get(w.key));
|
|
9622
|
+
}
|
|
9623
|
+
if (this.presenceWatchers.size > 0) {
|
|
9624
|
+
const others = this.presenceOthers(topic);
|
|
9625
|
+
for (const w of this.presenceWatchers) {
|
|
9626
|
+
if (w.topic === topic) w.handler(others);
|
|
9627
|
+
}
|
|
9628
|
+
}
|
|
9160
9629
|
}
|
|
9161
9630
|
};
|
|
9631
|
+
var PRESENCE_KEY = (conn) => `$p:${conn}`;
|
|
9162
9632
|
|
|
9163
9633
|
// src/storage.ts
|
|
9164
9634
|
function memorySessionStorage() {
|
|
@@ -9219,7 +9689,7 @@ function defaultSessionStorage(key) {
|
|
|
9219
9689
|
}
|
|
9220
9690
|
|
|
9221
9691
|
// src/version.ts
|
|
9222
|
-
var VERSION = "7.
|
|
9692
|
+
var VERSION = "7.4.0";
|
|
9223
9693
|
|
|
9224
9694
|
// src/runtime.ts
|
|
9225
9695
|
function buildRuntime(config) {
|