@protobase/client 0.62.1 → 0.62.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ //#region src/calls/connection-stats.d.ts
2
+ /**
3
+ * How one of the member's connections fares: to another member (`member`), or to the media server (no `member`). Its
4
+ * state, and once connected, the round trip of its network path and the share of the media it received that was lost.
5
+ */
6
+ export type CallConnection = {
7
+ member?: string;
8
+ state: RTCPeerConnectionState;
9
+ roundTripMs?: number;
10
+ lostRatio?: number;
11
+ };
12
+ //#endregion
@@ -0,0 +1,31 @@
1
+ //#region src/calls/connection-stats.ts
2
+ /** What `connectionOf` reads of a connection's stats: the selected candidate pair's round trip, and the received media's losses. */
3
+ const statsOf = (report) => {
4
+ const stats = [...report];
5
+ const transport = stats.find((stat) => stat.type === "transport" && typeof stat.selectedCandidatePairId === "string");
6
+ const pair = stats.find((stat) => stat.type === "candidate-pair" && (stat.id === transport?.selectedCandidatePairId || !transport && stat.selected === true || !transport && stat.nominated === true && stat.state === "succeeded"));
7
+ let lost = 0;
8
+ let received = 0;
9
+ for (const stat of stats) {
10
+ if (stat.type !== "inbound-rtp") continue;
11
+ lost += Math.max(0, Number(stat.packetsLost ?? 0));
12
+ received += Number(stat.packetsReceived ?? 0);
13
+ }
14
+ const roundTrip = typeof pair?.currentRoundTripTime === "number" ? pair.currentRoundTripTime * 1e3 : void 0;
15
+ return {
16
+ ...roundTrip !== void 0 && { roundTripMs: roundTrip },
17
+ ...lost + received > 0 && { lostRatio: lost / (lost + received) }
18
+ };
19
+ };
20
+ /** How the connection fares now, as `CallConnection`. */
21
+ const connectionOf = async (pc, member) => {
22
+ const state = pc.connectionState;
23
+ const report = state === "connected" ? statsOf((await pc.getStats()).values()) : {};
24
+ return {
25
+ ...member !== void 0 && { member },
26
+ state,
27
+ ...report
28
+ };
29
+ };
30
+ //#endregion
31
+ export { connectionOf, statsOf };
@@ -1,4 +1,6 @@
1
+ import { CallConnection } from "./connection-stats.js";
1
2
  import { CallTrack, PublishOptions } from "./media-link.js";
2
3
  import { Call, CallState, JoinCallOptions, joinCall } from "./join-call.js";
4
+ import { PublishRefused } from "./publish-refused.js";
3
5
  import { CallLeftReason, CallMode, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
4
- export { type Call, type CallLeftReason, type CallMode, type CallRosterMember, type CallState, type CallTrack, type CallTrackSource, type JoinCallOptions, type PublishOptions, joinCall };
6
+ export { type Call, type CallConnection, type CallLeftReason, type CallMode, type CallRosterMember, type CallState, type CallTrack, type CallTrackSource, type JoinCallOptions, type PublishOptions, PublishRefused, joinCall };
@@ -1,2 +1,3 @@
1
+ import { PublishRefused } from "./publish-refused.js";
1
2
  import { joinCall } from "./join-call.js";
2
- export { joinCall };
3
+ export { PublishRefused, joinCall };
@@ -1,3 +1,4 @@
1
+ import { CallConnection } from "./connection-stats.js";
1
2
  import { CallTrack, PublishOptions } from "./media-link.js";
2
3
  import { CallLeftReason, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
3
4
  //#region src/calls/join-call.d.ts
@@ -59,6 +60,8 @@ export type Call = {
59
60
  leave: () => void;
60
61
  /** The tracks of other members the member hears now. */
61
62
  tracks: () => CallTrack[];
63
+ /** How each of the member's connections fares now: to each other member, or to the media server. */
64
+ connections: () => Promise<CallConnection[]>;
62
65
  };
63
66
  /**
64
67
  * Joins a call through the app's function socket and resolves once the member is in it. Media flows over plain
@@ -181,7 +181,8 @@ const joinCall = (options) => {
181
181
  send({ type: "leave" });
182
182
  end("ended");
183
183
  },
184
- tracks: () => remote
184
+ tracks: () => remote,
185
+ connections: async () => link ? link.connections() : []
185
186
  };
186
187
  setState({ state: "connecting" });
187
188
  connect("join");
@@ -1,10 +1,20 @@
1
+ import { PublishRefused } from "./publish-refused.js";
1
2
  import { createPeerConnection } from "./media-link.js";
3
+ import { connectionOf } from "./connection-stats.js";
2
4
  import { midsByStreamId } from "./msid-tracks.js";
3
5
  //#region src/calls/peer-link.ts
6
+ const firefoxMade = (sdp) => /^o=mozilla/m.test(sdp);
7
+ const inFirefox = typeof navigator !== "undefined" && /Firefox\//.test(navigator.userAgent);
8
+ /**
9
+ * Who gives way when this side's offer crosses the other's: Firefox, when only one of the two is, since an offer
10
+ * Chromium rolls back for Firefox's leaves their header extension ids clashing and Chromium's next offer fails;
11
+ * otherwise the member whose id sorts first.
12
+ */
13
+ const politeTo = (peer, offer) => inFirefox === firefoxMade(offer) ? peer.polite : inFirefox;
4
14
  /**
5
15
  * The media link without a media server: one `RTCPeerConnection` to each other member, whose browser answers, so either
6
16
  * side may offer. Protobase passes the descriptions and candidates between the two ("perfect negotiation": the member
7
- * whose id sorts first gives way when both offer at once) and says who is in the call and which tracks each member
17
+ * whose id sorts first gives way when both offer at once, or Firefox to Chromium) and says who is in the call and which tracks each member
8
18
  * hears. A track goes by the id of the stream the sending browser sends it on, which the receiving browser sees too.
9
19
  */
10
20
  const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: createPeerConnection$1 = createPeerConnection }) => {
@@ -72,7 +82,7 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
72
82
  const { pc } = peer;
73
83
  if (description) {
74
84
  const collision = description.type === "offer" && (peer.makingOffer || pc.signalingState !== "stable");
75
- peer.ignoreOffer = !peer.polite && collision;
85
+ peer.ignoreOffer = !politeTo(peer, description.sdp) && collision;
76
86
  if (peer.ignoreOffer) return;
77
87
  await pc.setRemoteDescription(description);
78
88
  if (description.type === "offer") {
@@ -106,7 +116,7 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
106
116
  publishing?.resolve();
107
117
  return;
108
118
  case "publish-refused":
109
- publishing?.reject(new Error(message.reason));
119
+ publishing?.reject(new PublishRefused(message.reason));
110
120
  return;
111
121
  }
112
122
  };
@@ -192,6 +202,7 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
192
202
  track,
193
203
  simulcast: false
194
204
  }])),
205
+ connections: () => Promise.all([...peers].map(([member, peer]) => connectionOf(peer.pc, member))),
195
206
  close: () => {
196
207
  publishing?.reject(/* @__PURE__ */ new Error("The call ended"));
197
208
  for (const member of [...peers.keys()]) disconnect(member);
@@ -0,0 +1,6 @@
1
+ //#region src/calls/publish-refused.d.ts
2
+ /** The server refused tracks the member may not send, such as a camera for a member who may only listen. */
3
+ export declare class PublishRefused extends Error {
4
+ constructor(reason: string);
5
+ }
6
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/calls/publish-refused.ts
2
+ /** The server refused tracks the member may not send, such as a camera for a member who may only listen. */
3
+ var PublishRefused = class extends Error {
4
+ constructor(reason) {
5
+ super(reason);
6
+ this.name = "PublishRefused";
7
+ }
8
+ };
9
+ //#endregion
10
+ export { PublishRefused };
@@ -1,4 +1,6 @@
1
+ import { PublishRefused } from "./publish-refused.js";
1
2
  import { createPeerConnection } from "./media-link.js";
3
+ import { connectionOf } from "./connection-stats.js";
2
4
  import { negotiation } from "./negotiation.js";
3
5
  //#region src/calls/sfu-link.ts
4
6
  const simulcastLayers = [
@@ -156,7 +158,7 @@ const sfuLink = ({ iceServers, send, onTracks, onUnpublished, createPeerConnecti
156
158
  publishing?.resolve(message.tracks);
157
159
  return;
158
160
  case "publish-refused":
159
- publishing?.reject(new Error(message.reason));
161
+ publishing?.reject(new PublishRefused(message.reason));
160
162
  return;
161
163
  }
162
164
  };
@@ -250,6 +252,7 @@ const sfuLink = ({ iceServers, send, onTracks, onUnpublished, createPeerConnecti
250
252
  track: track.track,
251
253
  simulcast: track.simulcast
252
254
  }])),
255
+ connections: async () => [await connectionOf(pc)],
253
256
  close: () => {
254
257
  publishing?.reject(/* @__PURE__ */ new Error("The call ended"));
255
258
  offers.reset();
package/dist/client.d.ts CHANGED
@@ -121,6 +121,10 @@ export declare const createClient: (options?: ClientOptions) => {
121
121
  };
122
122
  calls: (resource: ResourceRef, key: KeyValue | readonly KeyValue[]) => Promise<RecordCall[]>;
123
123
  watchCalls: (resource: ResourceRef, key: KeyValue | readonly KeyValue[], handlers: RecordCallsHandlers) => () => void;
124
+ rings: {
125
+ list: () => Promise<import("@protobase/schema/internal").CallRing[]>;
126
+ decline: (call: string) => Promise<void>;
127
+ };
124
128
  };
125
129
  export type Client = ReturnType<typeof createClient>;
126
130
  //#endregion
package/dist/client.js CHANGED
@@ -9,6 +9,7 @@ import { devicesClient } from "./devices.js";
9
9
  import { statusClient } from "./status.js";
10
10
  import { notificationHistoryClient } from "./notification-history.js";
11
11
  import { recordCallsClient } from "./record-calls.js";
12
+ import { ringsClient } from "./rings.js";
12
13
  import { encodeKey } from "@protobase/schema/internal";
13
14
  //#region src/client.ts
14
15
  const nameOf = (resource) => typeof resource === "string" ? resource : resource.toModel().name;
@@ -473,7 +474,8 @@ const createClient = (options = {}) => {
473
474
  notifications,
474
475
  status: statusClient(send),
475
476
  calls,
476
- watchCalls
477
+ watchCalls,
478
+ rings: ringsClient(send)
477
479
  };
478
480
  };
479
481
  //#endregion
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ import { UploadProgress } from "./upload-request.js";
11
11
  import { LiveChange, LiveHandlers } from "./live.js";
12
12
  import { Client, InvokeOptions, UploadOptions, UploadResult, createClient } from "./client.js";
13
13
  import { Availability, PersonStatus, StatusClient, StatusInput } from "./status.js";
14
+ import { CallRing, RingsClient } from "./rings.js";
14
15
  import { ApiError, PreconditionFailedError, ProblemDetails, ProblemError, isApiError } from "./problem.js";
15
16
  import { fileUrl } from "./file-urls.js";
16
17
  import { routedClient } from "./routed-client.js";
@@ -30,4 +31,4 @@ import { createStaticSession } from "./static-session.js";
30
31
  import { AssistantClient, AssistantClientOptions, AssistantConnection, createAssistantClient } from "./assistant.js";
31
32
  import { RuntimeClient, RuntimeClientOptions, RuntimeLastUpdate, RuntimePolicy, RuntimeStatus, RuntimeUpdateStage, createRuntimeClient } from "./runtime.js";
32
33
  import { availabilities, heldStatus, maxStatusText, statusTextProblem } from "@protobase/schema/internal";
33
- export { type AccountSecurity, type AccountSignIn, type ActiveRole, type Aggregate, type AggregateGroup, type AggregateParams, type AggregateSplitTotal, ApiError, type AppUser, type AssistantClient, type AssistantClientOptions, type AssistantConnection, AuthError, type AuthSession, type AuthSessionOptions, type AuthUser, type AuthenticatorSetup, type Availability, type BatchOp, type BatchRef, type BatchResult, type Channel, type ChannelError, type ChannelMessage, ChannelRefusal, type ChannelStatus, type ChannelUser, type Client, type ClientOptions, type CommentPage, type CommentPerson, type ConnectClient, type ConnectedApp, type ConnectingClient, type CurrentOrganization, type DeviceSession, type DevicesClient, type Facet, type FilterInput, type FormChallenge, type FormProof, type Histogram, type HistogramBucket, type HistoryChange, type HistoryPage, type InboxNotification, type InboxPage, type InvitationPreview, type InvitationSent, type InvokeOptions, type ListPage, type ListParams, type LiveChange, type LiveHandlers, type MapBounds, type MapBucket, type MapLayerModel, type MapParams, type MapResult, type MentionableUser, type Meta, type MetaResult, type NotificationHistoryClient, type Organization, type OrganizationInvitation, type OrganizationMember, type OrganizationPerson, type OrganizationRole, type OrganizationsClient, type OrganizationsFound, type Passkey, type PersonStatus, type PlatformSignIn, type PluginRef, PreconditionFailedError, type PresenceChange, type PresenceEntry, type ProblemDetails, type ProblemError, type PublicForm, type RecordCall, type RecordCallsHandlers, type RecordComment, type RecordOf, type RecordPermissions, type RecordPhotos, type RegisteredDevice, type RelatedLabels, type RequiredSetup, type ResourcePermissions, type ResourceRef, type RoleLabel, type RoleOption, type RolesClient, type RuntimeClient, type RuntimeClientOptions, type RuntimeLastUpdate, type RuntimePolicy, type RuntimeStatus, type RuntimeUpdateStage, type SamlSignIn, type Series, type SeriesParams, type SeriesPoint, type SetupStatus, type SignInMethod, type SignInPolicy, type SignInPolicyClient, type SignInPolicyState, type SignInResult, type SignInRule, type SocketLike, type SourceMeta, type SourceRef, type StaffAccessRule, type StaffSignIn, type StaffSignInClient, type StatusClient, type StatusInput, type Stored, type StoredFile, type StoredFileVariant, type TwoFactorMethod, type UploadOptions, type UploadProgress, type UploadResult, type UserInvitation, type UserInvitationSent, type UsersClient, type UsersFound, type WebDeviceRegistration, availabilities, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, fileUrl, filterText, heldStatus, isApiError, listWire, maxStatusText, routedClient, sourceMeta, statusTextProblem, tokenExpiry };
34
+ export { type AccountSecurity, type AccountSignIn, type ActiveRole, type Aggregate, type AggregateGroup, type AggregateParams, type AggregateSplitTotal, ApiError, type AppUser, type AssistantClient, type AssistantClientOptions, type AssistantConnection, AuthError, type AuthSession, type AuthSessionOptions, type AuthUser, type AuthenticatorSetup, type Availability, type BatchOp, type BatchRef, type BatchResult, type CallRing, type Channel, type ChannelError, type ChannelMessage, ChannelRefusal, type ChannelStatus, type ChannelUser, type Client, type ClientOptions, type CommentPage, type CommentPerson, type ConnectClient, type ConnectedApp, type ConnectingClient, type CurrentOrganization, type DeviceSession, type DevicesClient, type Facet, type FilterInput, type FormChallenge, type FormProof, type Histogram, type HistogramBucket, type HistoryChange, type HistoryPage, type InboxNotification, type InboxPage, type InvitationPreview, type InvitationSent, type InvokeOptions, type ListPage, type ListParams, type LiveChange, type LiveHandlers, type MapBounds, type MapBucket, type MapLayerModel, type MapParams, type MapResult, type MentionableUser, type Meta, type MetaResult, type NotificationHistoryClient, type Organization, type OrganizationInvitation, type OrganizationMember, type OrganizationPerson, type OrganizationRole, type OrganizationsClient, type OrganizationsFound, type Passkey, type PersonStatus, type PlatformSignIn, type PluginRef, PreconditionFailedError, type PresenceChange, type PresenceEntry, type ProblemDetails, type ProblemError, type PublicForm, type RecordCall, type RecordCallsHandlers, type RecordComment, type RecordOf, type RecordPermissions, type RecordPhotos, type RegisteredDevice, type RelatedLabels, type RequiredSetup, type ResourcePermissions, type ResourceRef, type RingsClient, type RoleLabel, type RoleOption, type RolesClient, type RuntimeClient, type RuntimeClientOptions, type RuntimeLastUpdate, type RuntimePolicy, type RuntimeStatus, type RuntimeUpdateStage, type SamlSignIn, type Series, type SeriesParams, type SeriesPoint, type SetupStatus, type SignInMethod, type SignInPolicy, type SignInPolicyClient, type SignInPolicyState, type SignInResult, type SignInRule, type SocketLike, type SourceMeta, type SourceRef, type StaffAccessRule, type StaffSignIn, type StaffSignInClient, type StatusClient, type StatusInput, type Stored, type StoredFile, type StoredFileVariant, type TwoFactorMethod, type UploadOptions, type UploadProgress, type UploadResult, type UserInvitation, type UserInvitationSent, type UsersClient, type UsersFound, type WebDeviceRegistration, availabilities, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, fileUrl, filterText, heldStatus, isApiError, listWire, maxStatusText, routedClient, sourceMeta, statusTextProblem, tokenExpiry };
@@ -0,0 +1,14 @@
1
+ import { Transport } from "./transport.js";
2
+ import { CallRing, CallRing as CallRing$1 } from "@protobase/schema/internal";
3
+ //#region src/rings.d.ts
4
+ /**
5
+ * The calls that ring the signed-in user (`/api/calls/rings`), in an app with calls: `list` those ringing them now,
6
+ * newest first, and `decline` stops one ringing them on every page they have open. Joining the call stops it too.
7
+ */
8
+ export declare const ringsClient: (send: Transport) => {
9
+ list: () => Promise<CallRing[]>;
10
+ decline: (call: string) => Promise<void>;
11
+ };
12
+ export type RingsClient = ReturnType<typeof ringsClient>;
13
+ //#endregion
14
+ export type { CallRing$1 as CallRing };
package/dist/rings.js ADDED
@@ -0,0 +1,23 @@
1
+ import { ringsPath } from "@protobase/schema/internal";
2
+ //#region src/rings.ts
3
+ /**
4
+ * The calls that ring the signed-in user (`/api/calls/rings`), in an app with calls: `list` those ringing them now,
5
+ * newest first, and `decline` stops one ringing them on every page they have open. Joining the call stops it too.
6
+ */
7
+ const ringsClient = (send) => ({
8
+ list: async () => (await (await send({
9
+ method: "GET",
10
+ path: ringsPath(""),
11
+ system: true
12
+ })).json()).items,
13
+ decline: async (call) => {
14
+ await send({
15
+ method: "POST",
16
+ path: ringsPath("", { decline: call }),
17
+ system: true,
18
+ body: {}
19
+ });
20
+ }
21
+ });
22
+ //#endregion
23
+ export { ringsClient };
@@ -44,6 +44,7 @@ const routedClient = (own, sourceOf) => {
44
44
  devices: own.devices,
45
45
  notifications: own.notifications,
46
46
  status: own.status,
47
+ rings: own.rings,
47
48
  calls: (resource, key) => to(resource).calls(resource, key),
48
49
  watchCalls: (resource, key, handlers) => to(resource).watchCalls(resource, key, handlers)
49
50
  };
package/dist/types.d.ts CHANGED
@@ -285,6 +285,7 @@ export type Meta = {
285
285
  webPushKey?: string;
286
286
  };
287
287
  status?: true;
288
+ calls?: true;
288
289
  locale?: 'en' | 'nl';
289
290
  files?: {
290
291
  trustedHosts: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/client",
3
- "version": "0.62.1",
3
+ "version": "0.62.2",
4
4
  "description": "Typed fetch client for the Protobase REST API.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@better-auth/passkey": "^1.7.12",
34
- "@protobase/layout": "0.62.1",
35
- "@protobase/schema": "0.62.1",
34
+ "@protobase/layout": "0.62.2",
35
+ "@protobase/schema": "0.62.2",
36
36
  "@simplewebauthn/browser": "^13.3.0",
37
37
  "better-auth": "^1.7.12"
38
38
  }