@protobase/client 0.59.3 → 0.60.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calls/index.d.ts +2 -2
- package/dist/calls/join-call.d.ts +8 -2
- package/dist/calls/join-call.js +17 -3
- package/dist/calls/msid-tracks.js +6 -6
- package/dist/calls/peer-link.js +21 -13
- package/dist/channels.js +22 -4
- package/dist/client.d.ts +4 -1
- package/dist/client.js +15 -4
- package/dist/index.d.ts +3 -2
- package/dist/record-calls.d.ts +26 -0
- package/dist/record-calls.js +49 -0
- package/dist/routed-client.js +3 -1
- package/package.json +3 -3
package/dist/calls/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { CallTrack, PublishOptions } from "./media-link.js";
|
|
2
2
|
import { Call, CallState, JoinCallOptions, joinCall } from "./join-call.js";
|
|
3
|
-
import { CallLeftReason, CallMode, CallTrackSource } from "@protobase/schema/internal";
|
|
4
|
-
export { type Call, type CallLeftReason, type CallMode, type CallState, type CallTrack, type CallTrackSource, type JoinCallOptions, type PublishOptions, joinCall };
|
|
3
|
+
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 };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CallTrack, PublishOptions } from "./media-link.js";
|
|
2
|
-
import { CallLeftReason, CallTrackSource } from "@protobase/schema/internal";
|
|
2
|
+
import { CallLeftReason, CallRosterMember, CallTrackSource } from "@protobase/schema/internal";
|
|
3
3
|
//#region src/calls/join-call.d.ts
|
|
4
4
|
/** Where a call stands: `resuming` while the link to Protobase is restored, with media still flowing. */
|
|
5
5
|
export type CallState = {
|
|
@@ -21,6 +21,8 @@ export type JoinCallOptions = {
|
|
|
21
21
|
url: string;
|
|
22
22
|
/** Every track of other members the member hears, whenever one comes, goes or its media arrives. */
|
|
23
23
|
onTracks?: (tracks: CallTrack[]) => void;
|
|
24
|
+
/** Everyone in the call, the member too, in the order they came, whenever that changes. */
|
|
25
|
+
onMembers?: (members: CallRosterMember[]) => void;
|
|
24
26
|
/** Who speaks how loudly, from 0 to 1, for the members whose level changed. */
|
|
25
27
|
onSpeakers?: (speakers: {
|
|
26
28
|
member: string;
|
|
@@ -31,14 +33,18 @@ export type JoinCallOptions = {
|
|
|
31
33
|
onUnpublished?: (source: CallTrackSource) => void;
|
|
32
34
|
/** How long to wait before the `attempt`th try to restore a lost link. */
|
|
33
35
|
retryDelayMs?: (attempt: number) => number;
|
|
34
|
-
/** How many tries in a row to restore a lost link before the call is left. Default 8. */
|
|
36
|
+
/** How many tries in a row to restore a lost link, or to get in while the server fails, before the call is left. Default 8. */
|
|
35
37
|
maxRetries?: number;
|
|
36
38
|
WebSocket?: typeof WebSocket;
|
|
37
39
|
createPeerConnection?: (configuration: RTCConfiguration) => RTCPeerConnection;
|
|
38
40
|
};
|
|
39
41
|
export type Call = {
|
|
42
|
+
/** The call, as the record's calls list it and `calls.remove` takes it. */
|
|
43
|
+
readonly id: string;
|
|
40
44
|
/** The member's participant in the media server. */
|
|
41
45
|
readonly participant: string;
|
|
46
|
+
/** Everyone in the call now, the member too. */
|
|
47
|
+
members: () => CallRosterMember[];
|
|
42
48
|
/** Sends tracks under the sources they carry, such as `{ camera, microphone }`; rejects when the member may not. */
|
|
43
49
|
publish: (tracks: Partial<Record<CallTrackSource, MediaStreamTrack>>, options?: PublishOptions) => Promise<void>;
|
|
44
50
|
unpublish: (source: CallTrackSource) => Promise<void>;
|
package/dist/calls/join-call.js
CHANGED
|
@@ -12,6 +12,8 @@ const leftCodes = {
|
|
|
12
12
|
1e3: "ended",
|
|
13
13
|
1008: "removed"
|
|
14
14
|
};
|
|
15
|
+
/** Close codes of a server that failed, or was too busy, for now: getting in is tried again. */
|
|
16
|
+
const failedCodes = /* @__PURE__ */ new Set([1011, 1013]);
|
|
15
17
|
/**
|
|
16
18
|
* Joins a call through the app's function socket and resolves once the member is in it. Media flows over plain
|
|
17
19
|
* `RTCPeerConnection`s, to the media server or straight to the other members, as the app's calls are configured;
|
|
@@ -26,6 +28,8 @@ const joinCall = (options) => {
|
|
|
26
28
|
let socket;
|
|
27
29
|
let link;
|
|
28
30
|
let participant = "";
|
|
31
|
+
let callId = "";
|
|
32
|
+
let members = [];
|
|
29
33
|
let remote = [];
|
|
30
34
|
let ended = false;
|
|
31
35
|
let rejoin = false;
|
|
@@ -74,6 +78,7 @@ const joinCall = (options) => {
|
|
|
74
78
|
const before = link?.sending();
|
|
75
79
|
link?.close();
|
|
76
80
|
participant = message.participant;
|
|
81
|
+
callId = message.call;
|
|
77
82
|
link = newLink(message);
|
|
78
83
|
failures = 0;
|
|
79
84
|
setState({ state: "joined" });
|
|
@@ -90,6 +95,10 @@ const joinCall = (options) => {
|
|
|
90
95
|
case "speakers":
|
|
91
96
|
options.onSpeakers?.(message.speakers);
|
|
92
97
|
return;
|
|
98
|
+
case "members":
|
|
99
|
+
members = message.members;
|
|
100
|
+
options.onMembers?.(members);
|
|
101
|
+
return;
|
|
93
102
|
case "left":
|
|
94
103
|
if (message.reason === "rejoin") rejoin = true;
|
|
95
104
|
else end(message.reason);
|
|
@@ -107,6 +116,7 @@ const joinCall = (options) => {
|
|
|
107
116
|
const resume = mode === "resume" && link ? await link.resumeState(participant) : void 0;
|
|
108
117
|
send(resume ? {
|
|
109
118
|
type: "resume",
|
|
119
|
+
call: callId,
|
|
110
120
|
...resume
|
|
111
121
|
} : { type: "join" });
|
|
112
122
|
});
|
|
@@ -127,19 +137,23 @@ const joinCall = (options) => {
|
|
|
127
137
|
}
|
|
128
138
|
const left = leftCodes[code];
|
|
129
139
|
if (left) return end(left);
|
|
130
|
-
if (!participant) return end("refused");
|
|
140
|
+
if (!participant && !failedCodes.has(code)) return end("refused");
|
|
131
141
|
failures++;
|
|
132
|
-
if (failures > maxRetries) return end("ended");
|
|
133
|
-
setState({ state: "resuming" });
|
|
142
|
+
if (failures > maxRetries) return end(participant ? "ended" : "unavailable");
|
|
143
|
+
setState(participant ? { state: "resuming" } : { state: "connecting" });
|
|
134
144
|
const next = mode === "resume" && opened && !resumedSinceOpen ? "join" : "resume";
|
|
135
145
|
setTimeout(() => {
|
|
136
146
|
if (!ended) connect(next);
|
|
137
147
|
}, retryDelay(failures - 1));
|
|
138
148
|
};
|
|
139
149
|
const call = {
|
|
150
|
+
get id() {
|
|
151
|
+
return callId;
|
|
152
|
+
},
|
|
140
153
|
get participant() {
|
|
141
154
|
return participant;
|
|
142
155
|
},
|
|
156
|
+
members: () => members,
|
|
143
157
|
publish: async (tracks, publishOptions) => {
|
|
144
158
|
if (!link) throw new Error("The call was left");
|
|
145
159
|
await link.publish(tracks, publishOptions);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
//#region src/calls/msid-tracks.ts
|
|
2
2
|
/**
|
|
3
|
-
* The `mid` of each media section of a description by the
|
|
4
|
-
*
|
|
3
|
+
* The `mid` of each media section of a description by the id of the stream its `a=msid` line names. Unlike the track
|
|
4
|
+
* id there, which Firefox makes up for each sender, both browsers write the sending page's stream id.
|
|
5
5
|
*/
|
|
6
|
-
const
|
|
6
|
+
const midsByStreamId = (sdp) => {
|
|
7
7
|
const mids = /* @__PURE__ */ new Map();
|
|
8
8
|
for (const section of sdp.split(/\r?\nm=/).slice(1)) {
|
|
9
9
|
const mid = /^a=mid:(\S+)/m.exec(section)?.[1];
|
|
10
|
-
const
|
|
11
|
-
if (mid !== void 0 &&
|
|
10
|
+
const stream = /^a=msid:(\S+) \S+/m.exec(section)?.[1];
|
|
11
|
+
if (mid !== void 0 && stream !== void 0 && stream !== "-") mids.set(stream, mid);
|
|
12
12
|
}
|
|
13
13
|
return mids;
|
|
14
14
|
};
|
|
15
15
|
//#endregion
|
|
16
|
-
export {
|
|
16
|
+
export { midsByStreamId };
|
package/dist/calls/peer-link.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createPeerConnection } from "./media-link.js";
|
|
2
|
-
import {
|
|
2
|
+
import { midsByStreamId } from "./msid-tracks.js";
|
|
3
3
|
//#region src/calls/peer-link.ts
|
|
4
4
|
/**
|
|
5
5
|
* The media link without a media server: one `RTCPeerConnection` to each other member, whose browser answers, so either
|
|
6
6
|
* side may offer. Protobase passes the descriptions and candidates between the two ("perfect negotiation": the member
|
|
7
7
|
* whose id sorts first gives way when both offer at once) and says who is in the call and which tracks each member
|
|
8
|
-
* hears. A track goes by the id the sending browser
|
|
8
|
+
* hears. A track goes by the id of the stream the sending browser sends it on, which the receiving browser sees too.
|
|
9
9
|
*/
|
|
10
10
|
const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: createPeerConnection$1 = createPeerConnection }) => {
|
|
11
11
|
const peers = /* @__PURE__ */ new Map();
|
|
@@ -13,10 +13,13 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
|
|
|
13
13
|
let remote = [];
|
|
14
14
|
let publishing;
|
|
15
15
|
let publishes = Promise.resolve();
|
|
16
|
-
const sendOn = (pc, track) => pc.addTransceiver(track, {
|
|
16
|
+
const sendOn = (pc, { track, stream }) => pc.addTransceiver(track, {
|
|
17
|
+
direction: "sendonly",
|
|
18
|
+
streams: [stream]
|
|
19
|
+
}).sender;
|
|
17
20
|
const receivedTrack = ({ member, sid }) => {
|
|
18
21
|
const pc = peers.get(member)?.pc;
|
|
19
|
-
const mid = pc?.remoteDescription &&
|
|
22
|
+
const mid = pc?.remoteDescription && midsByStreamId(pc.remoteDescription.sdp).get(sid);
|
|
20
23
|
return mid === void 0 ? void 0 : pc.getTransceivers().find((transceiver) => transceiver.mid === mid)?.receiver.track;
|
|
21
24
|
};
|
|
22
25
|
const emitTracks = () => onTracks(remote.map((track) => ({
|
|
@@ -57,7 +60,7 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
|
|
|
57
60
|
if (pc.iceConnectionState === "failed") pc.restartIce();
|
|
58
61
|
};
|
|
59
62
|
pc.ontrack = () => emitTracks();
|
|
60
|
-
for (const [source,
|
|
63
|
+
for (const [source, sending] of local) peer.senders.set(source, sendOn(pc, sending));
|
|
61
64
|
return peer;
|
|
62
65
|
};
|
|
63
66
|
const disconnect = (member) => {
|
|
@@ -111,14 +114,22 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
|
|
|
111
114
|
const added = Object.entries(tracks);
|
|
112
115
|
for (const [source] of added) if (local.has(source)) throw new Error(`The call already sends a ${source} track: unpublish it first`);
|
|
113
116
|
if (added.length === 0) return;
|
|
117
|
+
const sending = added.map(([source, track]) => {
|
|
118
|
+
const stream = new MediaStream([track]);
|
|
119
|
+
return [source, {
|
|
120
|
+
track,
|
|
121
|
+
sid: stream.id,
|
|
122
|
+
stream
|
|
123
|
+
}];
|
|
124
|
+
});
|
|
114
125
|
const answered = new Promise((resolve, reject) => publishing = {
|
|
115
126
|
resolve,
|
|
116
127
|
reject
|
|
117
128
|
});
|
|
118
129
|
send({
|
|
119
130
|
type: "publish",
|
|
120
|
-
tracks:
|
|
121
|
-
cid:
|
|
131
|
+
tracks: sending.map(([source, { track, sid }]) => ({
|
|
132
|
+
cid: sid,
|
|
122
133
|
kind: track.kind,
|
|
123
134
|
source
|
|
124
135
|
}))
|
|
@@ -128,11 +139,8 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
|
|
|
128
139
|
} finally {
|
|
129
140
|
publishing = void 0;
|
|
130
141
|
}
|
|
131
|
-
for (const [source, track] of
|
|
132
|
-
local.set(source,
|
|
133
|
-
track,
|
|
134
|
-
sid: track.id
|
|
135
|
-
});
|
|
142
|
+
for (const [source, track] of sending) {
|
|
143
|
+
local.set(source, track);
|
|
136
144
|
for (const peer of peers.values()) peer.senders.set(source, sendOn(peer.pc, track));
|
|
137
145
|
}
|
|
138
146
|
};
|
|
@@ -173,7 +181,7 @@ const peerLink = (self, { iceServers, send, onTracks, createPeerConnection: crea
|
|
|
173
181
|
resumeState: async (participant) => ({
|
|
174
182
|
participant,
|
|
175
183
|
tracks: [...local].map(([source, { track, sid }]) => ({
|
|
176
|
-
cid:
|
|
184
|
+
cid: sid,
|
|
177
185
|
sid,
|
|
178
186
|
kind: track.kind,
|
|
179
187
|
source
|
package/dist/channels.js
CHANGED
|
@@ -23,13 +23,25 @@ const channelsClient = (options) => {
|
|
|
23
23
|
const entries = [...channel.presence.values()];
|
|
24
24
|
for (const listener of channel.presenceListeners) listener(entries, change);
|
|
25
25
|
};
|
|
26
|
+
const forget = (id) => {
|
|
27
|
+
joined.delete(id);
|
|
28
|
+
if (joined.size === 0) connection.stop();
|
|
29
|
+
};
|
|
26
30
|
const join = (channel) => {
|
|
31
|
+
const id = `${channel.resource}/${channel.key}`;
|
|
27
32
|
connection.request({
|
|
28
33
|
type: "join",
|
|
29
34
|
resource: channel.resource,
|
|
30
35
|
key: channel.key
|
|
31
36
|
}).then((reply) => {
|
|
32
|
-
if (joined.get(
|
|
37
|
+
if (joined.get(id) !== channel) return;
|
|
38
|
+
if (channel.holders === 0) {
|
|
39
|
+
connection.post({
|
|
40
|
+
type: "leave",
|
|
41
|
+
channel: reply.channel
|
|
42
|
+
});
|
|
43
|
+
return forget(id);
|
|
44
|
+
}
|
|
33
45
|
channel.name = reply.channel;
|
|
34
46
|
byName.set(channel.name, channel);
|
|
35
47
|
setStatus(channel, { state: "joined" });
|
|
@@ -39,6 +51,7 @@ const channelsClient = (options) => {
|
|
|
39
51
|
state: channel.tracked.state
|
|
40
52
|
}).catch(unlessDropped);
|
|
41
53
|
}).catch((error) => {
|
|
54
|
+
if (joined.get(id) === channel && channel.holders === 0 && error instanceof ChannelRefusal) return forget(id);
|
|
42
55
|
if (!(error instanceof ChannelRefusal) || error.code === "disconnected") return unlessDropped(error);
|
|
43
56
|
setStatus(channel, {
|
|
44
57
|
state: "refused",
|
|
@@ -87,7 +100,11 @@ const channelsClient = (options) => {
|
|
|
87
100
|
const connection = channelConnection({
|
|
88
101
|
...options,
|
|
89
102
|
onOpen: () => {
|
|
90
|
-
for (const channel of joined
|
|
103
|
+
for (const [id, channel] of joined) {
|
|
104
|
+
if (channel.status.state !== "joining") continue;
|
|
105
|
+
if (channel.holders === 0) forget(id);
|
|
106
|
+
else join(channel);
|
|
107
|
+
}
|
|
91
108
|
},
|
|
92
109
|
onDown: () => {
|
|
93
110
|
byName.clear();
|
|
@@ -98,7 +115,8 @@ const channelsClient = (options) => {
|
|
|
98
115
|
const release = (id, channel) => {
|
|
99
116
|
channel.holders -= 1;
|
|
100
117
|
if (channel.holders > 0) return;
|
|
101
|
-
|
|
118
|
+
channel.tracked = void 0;
|
|
119
|
+
if (channel.status.state === "joining") return;
|
|
102
120
|
if (channel.name !== void 0) {
|
|
103
121
|
byName.delete(channel.name);
|
|
104
122
|
connection.post({
|
|
@@ -106,7 +124,7 @@ const channelsClient = (options) => {
|
|
|
106
124
|
channel: channel.name
|
|
107
125
|
});
|
|
108
126
|
}
|
|
109
|
-
|
|
127
|
+
forget(id);
|
|
110
128
|
};
|
|
111
129
|
const handleOf = (id, channel) => {
|
|
112
130
|
let left = false;
|
package/dist/client.d.ts
CHANGED
|
@@ -3,9 +3,10 @@ import { ClientOptions } from "./transport.js";
|
|
|
3
3
|
import { RegisteredDevice, WebDeviceRegistration } from "./devices.js";
|
|
4
4
|
import { InboxPage } from "./notification-history.js";
|
|
5
5
|
import { FilterInput, ListParams, MapParams } from "./query-string.js";
|
|
6
|
+
import { Aggregate, AggregateParams, BatchOp, BatchResult, CommentPage, Facet, FormChallenge, FormProof, Histogram, HistoryPage, ListPage, MapResult, MentionableUser, MetaResult, PublicForm, RecordComment, RecordOf, ResourceRef, Series, SeriesParams, Stored } from "./types.js";
|
|
7
|
+
import { RecordCall, RecordCallsHandlers } from "./record-calls.js";
|
|
6
8
|
import { UploadProgress } from "./upload-request.js";
|
|
7
9
|
import { LiveHandlers } from "./live.js";
|
|
8
|
-
import { Aggregate, AggregateParams, BatchOp, BatchResult, CommentPage, Facet, FormChallenge, FormProof, Histogram, HistoryPage, ListPage, MapResult, MentionableUser, MetaResult, PublicForm, RecordComment, RecordOf, ResourceRef, Series, SeriesParams, Stored } from "./types.js";
|
|
9
10
|
import { KeyValue } from "@protobase/schema";
|
|
10
11
|
//#region src/client.d.ts
|
|
11
12
|
/** How `invoke` calls a function: GET without a body, POST with one, unless `method` says otherwise. */
|
|
@@ -118,6 +119,8 @@ export declare const createClient: (options?: ClientOptions) => {
|
|
|
118
119
|
clear: () => Promise<void>;
|
|
119
120
|
people: () => Promise<import("@protobase/schema/internal").PersonStatus[]>;
|
|
120
121
|
};
|
|
122
|
+
calls: (resource: ResourceRef, key: KeyValue | readonly KeyValue[]) => Promise<RecordCall[]>;
|
|
123
|
+
watchCalls: (resource: ResourceRef, key: KeyValue | readonly KeyValue[], handlers: RecordCallsHandlers) => () => void;
|
|
121
124
|
};
|
|
122
125
|
export type Client = ReturnType<typeof createClient>;
|
|
123
126
|
//#endregion
|
package/dist/client.js
CHANGED
|
@@ -8,6 +8,7 @@ import { channelsClient } from "./channels.js";
|
|
|
8
8
|
import { devicesClient } from "./devices.js";
|
|
9
9
|
import { statusClient } from "./status.js";
|
|
10
10
|
import { notificationHistoryClient } from "./notification-history.js";
|
|
11
|
+
import { recordCallsClient } from "./record-calls.js";
|
|
11
12
|
import { encodeKey } from "@protobase/schema/internal";
|
|
12
13
|
//#region src/client.ts
|
|
13
14
|
const nameOf = (resource) => typeof resource === "string" ? resource : resource.toModel().name;
|
|
@@ -17,6 +18,8 @@ const channelsUrl = (baseUrl = "/api/v1") => {
|
|
|
17
18
|
return url.toString();
|
|
18
19
|
};
|
|
19
20
|
const keyPath = (resource, key) => `/${nameOf(resource)}/${encodeKey(key)}`;
|
|
21
|
+
/** Where a record's calls are read and followed. */
|
|
22
|
+
const callsPath = (resource, key) => `${keyPath(resource, key)}:calls`;
|
|
20
23
|
const toPage = (body, response, origin) => ({
|
|
21
24
|
items: body.items.map((item) => withFileOrigin(item, origin)),
|
|
22
25
|
nextPageToken: body.next_page_token,
|
|
@@ -431,6 +434,12 @@ const createClient = (options = {}) => {
|
|
|
431
434
|
socket: options.webSocket ?? ((url, protocols) => new WebSocket(url, protocols)),
|
|
432
435
|
retryDelay: (attempt) => Math.min(3e4, 1e3 * 2 ** attempt)
|
|
433
436
|
});
|
|
437
|
+
/** The signed-in user's own devices, where they receive push (see `devicesClient`). */
|
|
438
|
+
const devices = devicesClient(send);
|
|
439
|
+
/** The signed-in user's own notification history, their inbox (see `notificationHistoryClient`). */
|
|
440
|
+
const notifications = notificationHistoryClient(send);
|
|
441
|
+
/** A record's calls, live with `watchCalls`, for whoever may read the record (see `recordCallsClient`). */
|
|
442
|
+
const { calls, watchCalls } = recordCallsClient(send, callsPath);
|
|
434
443
|
return {
|
|
435
444
|
list,
|
|
436
445
|
search,
|
|
@@ -460,10 +469,12 @@ const createClient = (options = {}) => {
|
|
|
460
469
|
submitForm,
|
|
461
470
|
live,
|
|
462
471
|
channel,
|
|
463
|
-
devices
|
|
464
|
-
notifications
|
|
465
|
-
status: statusClient(send)
|
|
472
|
+
devices,
|
|
473
|
+
notifications,
|
|
474
|
+
status: statusClient(send),
|
|
475
|
+
calls,
|
|
476
|
+
watchCalls
|
|
466
477
|
};
|
|
467
478
|
};
|
|
468
479
|
//#endregion
|
|
469
|
-
export { createClient };
|
|
480
|
+
export { callsPath, createClient };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ import { ClientOptions } from "./transport.js";
|
|
|
5
5
|
import { DevicesClient, RegisteredDevice, WebDeviceRegistration } from "./devices.js";
|
|
6
6
|
import { InboxNotification, InboxPage, NotificationHistoryClient } from "./notification-history.js";
|
|
7
7
|
import { FilterInput, ListParams, MapParams, filterText, listWire } from "./query-string.js";
|
|
8
|
+
import { Aggregate, AggregateGroup, AggregateParams, AggregateSplitTotal, BatchOp, BatchRef, BatchResult, CommentPage, CommentPerson, Facet, FormChallenge, FormProof, Histogram, HistogramBucket, HistoryChange, HistoryPage, ListPage, MapBounds, MapBucket, MapLayerModel, MapResult, MentionableUser, Meta, MetaResult, PluginRef, PublicForm, RecordComment, RecordOf, RecordPermissions, RecordPhotos, RelatedLabels, ResourcePermissions, ResourceRef, Series, SeriesParams, SeriesPoint, SourceRef, Stored, StoredFile, StoredFileVariant } from "./types.js";
|
|
9
|
+
import { RecordCall, RecordCallsHandlers } from "./record-calls.js";
|
|
8
10
|
import { UploadProgress } from "./upload-request.js";
|
|
9
11
|
import { LiveChange, LiveHandlers } from "./live.js";
|
|
10
|
-
import { Aggregate, AggregateGroup, AggregateParams, AggregateSplitTotal, BatchOp, BatchRef, BatchResult, CommentPage, CommentPerson, Facet, FormChallenge, FormProof, Histogram, HistogramBucket, HistoryChange, HistoryPage, ListPage, MapBounds, MapBucket, MapLayerModel, MapResult, MentionableUser, Meta, MetaResult, PluginRef, PublicForm, RecordComment, RecordOf, RecordPermissions, RecordPhotos, RelatedLabels, ResourcePermissions, ResourceRef, Series, SeriesParams, SeriesPoint, SourceRef, Stored, StoredFile, StoredFileVariant } from "./types.js";
|
|
11
12
|
import { Client, InvokeOptions, UploadOptions, UploadResult, createClient } from "./client.js";
|
|
12
13
|
import { Availability, PersonStatus, StatusClient, StatusInput } from "./status.js";
|
|
13
14
|
import { ApiError, PreconditionFailedError, ProblemDetails, ProblemError, isApiError } from "./problem.js";
|
|
@@ -29,4 +30,4 @@ import { createStaticSession } from "./static-session.js";
|
|
|
29
30
|
import { AssistantClient, AssistantClientOptions, AssistantConnection, createAssistantClient } from "./assistant.js";
|
|
30
31
|
import { RuntimeClient, RuntimeClientOptions, RuntimeLastUpdate, RuntimePolicy, RuntimeStatus, RuntimeUpdateStage, createRuntimeClient } from "./runtime.js";
|
|
31
32
|
import { availabilities, heldStatus, maxStatusText, statusTextProblem } from "@protobase/schema/internal";
|
|
32
|
-
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 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 };
|
|
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 };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "./types.js";
|
|
2
|
+
import { KeyValue } from "@protobase/schema";
|
|
3
|
+
//#region src/record-calls.d.ts
|
|
4
|
+
/** A call on a record, as its readers see it: who started it, when, when it ended, who is in it and who was removed. */
|
|
5
|
+
export type RecordCall = {
|
|
6
|
+
id: string;
|
|
7
|
+
started_by: string;
|
|
8
|
+
started_at: string;
|
|
9
|
+
/** `null` while it goes on. */
|
|
10
|
+
ended_at: string | null;
|
|
11
|
+
members: {
|
|
12
|
+
member: string;
|
|
13
|
+
joined_at: string;
|
|
14
|
+
}[];
|
|
15
|
+
removed: {
|
|
16
|
+
member: string;
|
|
17
|
+
removed_at: string;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
20
|
+
export type RecordCallsHandlers = {
|
|
21
|
+
/** The record's latest calls, newest first, the one going on first: at once, then whenever they change. */
|
|
22
|
+
onCalls: (calls: RecordCall[]) => void;
|
|
23
|
+
/** The stream failed, with why, or ended (`undefined`); it is followed again after a pause. */
|
|
24
|
+
onRetry?: (error: unknown) => void;
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readEventStream } from "@protobase/schema/internal";
|
|
2
|
+
//#region src/record-calls.ts
|
|
3
|
+
const backoff = (attempt) => Math.min(3e4, 1e3 * 2 ** attempt);
|
|
4
|
+
const pause = (ms, signal) => new Promise((resolve) => {
|
|
5
|
+
const timer = setTimeout(resolve, ms);
|
|
6
|
+
signal.addEventListener("abort", () => {
|
|
7
|
+
clearTimeout(timer);
|
|
8
|
+
resolve();
|
|
9
|
+
}, { once: true });
|
|
10
|
+
});
|
|
11
|
+
/** A record's calls, for whoever may read the record (`GET /api/{resource}/{key}:calls`). `path` names the record's calls. */
|
|
12
|
+
const recordCallsClient = (send, path, retryDelay = backoff) => ({
|
|
13
|
+
/** The record's latest calls, newest first, the one going on first. */
|
|
14
|
+
calls: async (resource, key) => (await (await send({
|
|
15
|
+
method: "GET",
|
|
16
|
+
path: path(resource, key)
|
|
17
|
+
})).json()).items,
|
|
18
|
+
/** Follows the record's calls until the returned function is called; a stream that ends or fails is followed again. */
|
|
19
|
+
watchCalls: (resource, key, handlers) => {
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const { signal } = controller;
|
|
22
|
+
let failures = 0;
|
|
23
|
+
const follow = async () => {
|
|
24
|
+
const response = await send({
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: path(resource, key),
|
|
27
|
+
query: "?watch=true",
|
|
28
|
+
headers: { accept: "text/event-stream" },
|
|
29
|
+
signal
|
|
30
|
+
});
|
|
31
|
+
if (!response.body) throw new Error("The server answered the calls stream without a body");
|
|
32
|
+
await readEventStream(response.body, (data) => {
|
|
33
|
+
failures = 0;
|
|
34
|
+
handlers.onCalls(JSON.parse(data).items);
|
|
35
|
+
}, signal);
|
|
36
|
+
};
|
|
37
|
+
(async () => {
|
|
38
|
+
while (!signal.aborted) {
|
|
39
|
+
const error = await follow().then(() => void 0, (reason) => reason);
|
|
40
|
+
if (signal.aborted) return;
|
|
41
|
+
handlers.onRetry?.(error);
|
|
42
|
+
await pause(retryDelay(failures++), signal);
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
return () => controller.abort();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
//#endregion
|
|
49
|
+
export { recordCallsClient };
|
package/dist/routed-client.js
CHANGED
|
@@ -43,7 +43,9 @@ const routedClient = (own, sourceOf) => {
|
|
|
43
43
|
channel: (resource, key) => own.channel(resource, key),
|
|
44
44
|
devices: own.devices,
|
|
45
45
|
notifications: own.notifications,
|
|
46
|
-
status: own.status
|
|
46
|
+
status: own.status,
|
|
47
|
+
calls: (resource, key) => to(resource).calls(resource, key),
|
|
48
|
+
watchCalls: (resource, key, handlers) => to(resource).watchCalls(resource, key, handlers)
|
|
47
49
|
};
|
|
48
50
|
};
|
|
49
51
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.60.1",
|
|
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.
|
|
35
|
-
"@protobase/schema": "0.
|
|
34
|
+
"@protobase/layout": "0.60.1",
|
|
35
|
+
"@protobase/schema": "0.60.1",
|
|
36
36
|
"@simplewebauthn/browser": "^13.3.0",
|
|
37
37
|
"better-auth": "^1.7.12"
|
|
38
38
|
}
|