@protobase/client 0.59.1 → 0.59.3

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,29 @@
1
+ //#region src/channel-binary-frame.ts
2
+ const encoder = new TextEncoder();
3
+ const decoder = new TextDecoder();
4
+ const lengthBytes = 4;
5
+ /**
6
+ * A binary frame of `/api/channels`, which carries a broadcast of bytes: the byte length of a JSON header, the header,
7
+ * the message without its payload, then the payload's bytes.
8
+ */
9
+ const encodeBinaryFrame = (header, bytes) => {
10
+ const head = encoder.encode(JSON.stringify(header));
11
+ const body = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12
+ const frame = new Uint8Array(lengthBytes + head.length + body.length);
13
+ new DataView(frame.buffer).setUint32(0, head.length);
14
+ frame.set(head, lengthBytes);
15
+ frame.set(body, lengthBytes + head.length);
16
+ return frame.buffer;
17
+ };
18
+ /** The message a binary frame from the server carries, its bytes as its `payload`. */
19
+ const decodeBinaryFrame = (frame) => {
20
+ const end = lengthBytes + new DataView(frame).getUint32(0);
21
+ return {
22
+ ...JSON.parse(decoder.decode(new Uint8Array(frame, lengthBytes, end - lengthBytes))),
23
+ payload: frame.slice(end)
24
+ };
25
+ };
26
+ /** Whether a payload is bytes, sent as a binary frame. */
27
+ const isBytes = (payload) => payload instanceof ArrayBuffer || ArrayBuffer.isView(payload);
28
+ //#endregion
29
+ export { decodeBinaryFrame, encodeBinaryFrame, isBytes };
@@ -2,7 +2,8 @@
2
2
  /** The part of the browser's `WebSocket` a connection uses, which a test hands its own of. */
3
3
  export type SocketLike = {
4
4
  readonly readyState: number;
5
- send(data: string): void;
5
+ binaryType: string;
6
+ send(data: string | ArrayBuffer): void;
6
7
  close(code?: number): void;
7
8
  onopen: ((event: unknown) => void) | null;
8
9
  onmessage: ((event: {
@@ -1,3 +1,4 @@
1
+ import { decodeBinaryFrame, encodeBinaryFrame } from "./channel-binary-frame.js";
1
2
  import { ChannelRefusal } from "./channel-types.js";
2
3
  //#region src/channel-connection.ts
3
4
  /** How often the client pings, the server's `channelBounds.pingMs`; no pong in twice that drops the connection. */
@@ -38,6 +39,7 @@ const channelConnection = ({ url, token, socket: open, retryDelay, onOpen, onDow
38
39
  sentToken = offered;
39
40
  const current = open(url(), offered === void 0 ? [protocol] : [protocol, `bearer.${offered}`]);
40
41
  socket = current;
42
+ current.binaryType = "arraybuffer";
41
43
  current.onopen = () => {
42
44
  failures = 0;
43
45
  heardAt = Date.now();
@@ -50,8 +52,8 @@ const channelConnection = ({ url, token, socket: open, retryDelay, onOpen, onDow
50
52
  };
51
53
  current.onmessage = ({ data }) => {
52
54
  heardAt = Date.now();
53
- if (typeof data !== "string") return;
54
- const message = JSON.parse(data);
55
+ if (typeof data !== "string" && !(data instanceof ArrayBuffer)) return;
56
+ const message = typeof data === "string" ? JSON.parse(data) : decodeBinaryFrame(data);
55
57
  if (message.type !== "reply") return onMessage(message);
56
58
  const waiting = pending.get(message.ref);
57
59
  pending.delete(message.ref);
@@ -71,9 +73,9 @@ const channelConnection = ({ url, token, socket: open, retryDelay, onOpen, onDow
71
73
  }, retryDelay(failures++));
72
74
  };
73
75
  };
74
- const sendNow = (message) => {
76
+ const sendNow = (message, bytes) => {
75
77
  if (socket?.readyState !== openState) return false;
76
- socket.send(JSON.stringify(message));
78
+ socket.send(bytes === void 0 ? JSON.stringify(message) : encodeBinaryFrame(message, bytes));
77
79
  return true;
78
80
  };
79
81
  return {
@@ -93,19 +95,19 @@ const channelConnection = ({ url, token, socket: open, retryDelay, onOpen, onDow
93
95
  dropPending();
94
96
  current?.close(1e3);
95
97
  },
96
- /** Sends a message with a ref, and resolves with its reply, or rejects with its refusal. */
97
- request: (message) => new Promise((resolve, reject) => {
98
+ /** Sends a message with a ref, and bytes as its payload if any, and resolves with its reply, or rejects with its refusal. */
99
+ request: (message, bytes) => new Promise((resolve, reject) => {
98
100
  const ref = String(++lastRef);
99
101
  if (!sendNow({
100
102
  ...message,
101
103
  ref
102
- })) return reject(new ChannelRefusal("disconnected", "The connection is down"));
104
+ }, bytes)) return reject(new ChannelRefusal("disconnected", "The connection is down"));
103
105
  pending.set(ref, {
104
106
  resolve,
105
107
  reject
106
108
  });
107
109
  }),
108
- /** Sends a message that gets no reply; `false` when the socket is not open. */
110
+ /** Sends a message that gets no reply, and bytes as its payload if any; `false` when the socket is not open. */
109
111
  post: sendNow
110
112
  };
111
113
  };
@@ -18,7 +18,7 @@ export type ChannelError = {
18
18
  code: string;
19
19
  message: string;
20
20
  };
21
- /** A broadcast as a channel hears it: with `server`, the app's own code sent it, and `user` is `null`. */
21
+ /** A broadcast as a channel hears it, its payload JSON or, for bytes, an `ArrayBuffer`: with `server`, the app's own code sent it, and `user` is `null`. */
22
22
  export type ChannelMessage = {
23
23
  event: string;
24
24
  payload: unknown;
@@ -14,8 +14,9 @@ export type Channel = {
14
14
  track: (state: unknown) => Promise<void>;
15
15
  untrack: () => Promise<void>;
16
16
  /**
17
- * Broadcasts to the others on the channel; with `self` this connection hears it too. With `ack` it resolves once the
18
- * server took it, or rejects with its `ChannelRefusal`; without, once it was sent.
17
+ * Broadcasts to the others on the channel: a JSON payload, or bytes in an `ArrayBuffer` or typed array, which the
18
+ * others hear as an `ArrayBuffer`. With `self` this connection hears it too. With `ack` it resolves once the server
19
+ * took it, or rejects with its `ChannelRefusal`; without, once it was sent.
19
20
  */
20
21
  send: (event: string, payload: unknown, options?: {
21
22
  self?: boolean;
package/dist/channels.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isBytes } from "./channel-binary-frame.js";
1
2
  import { ChannelRefusal } from "./channel-types.js";
2
3
  import { channelConnection } from "./channel-connection.js";
3
4
  import { encodeKey } from "@protobase/schema/internal";
@@ -145,21 +146,22 @@ const channelsClient = (options) => {
145
146
  send: async (event, payload, { self, ack } = {}) => {
146
147
  const name = joinedName();
147
148
  if (name === void 0) throw notJoined();
149
+ const bytes = isBytes(payload) ? payload : void 0;
148
150
  const message = {
149
151
  type: "send",
150
152
  channel: name,
151
153
  event,
152
- payload,
154
+ ...bytes === void 0 && { payload },
153
155
  ...self && { self }
154
156
  };
155
157
  if (ack) {
156
158
  await connection.request({
157
159
  ...message,
158
160
  ack
159
- });
161
+ }, bytes);
160
162
  return;
161
163
  }
162
- if (!connection.post(message)) throw new ChannelRefusal("disconnected", "The connection is down");
164
+ if (!connection.post(message, bytes)) throw new ChannelRefusal("disconnected", "The connection is down");
163
165
  },
164
166
  on: (event, listener) => {
165
167
  const listeners = channel.eventListeners.get(event) ?? /* @__PURE__ */ new Set();
package/dist/client.d.ts CHANGED
@@ -112,6 +112,12 @@ export declare const createClient: (options?: ClientOptions) => {
112
112
  markRead: (id: string) => Promise<void>;
113
113
  markAllRead: (upTo: string) => Promise<void>;
114
114
  };
115
+ status: {
116
+ own: () => Promise<import("@protobase/schema/internal").PersonStatus | null>;
117
+ set: (status: import("@protobase/schema/internal").StatusInput) => Promise<import("@protobase/schema/internal").PersonStatus | null>;
118
+ clear: () => Promise<void>;
119
+ people: () => Promise<import("@protobase/schema/internal").PersonStatus[]>;
120
+ };
115
121
  };
116
122
  export type Client = ReturnType<typeof createClient>;
117
123
  //#endregion
package/dist/client.js CHANGED
@@ -6,6 +6,7 @@ import { followLive } from "./live.js";
6
6
  import { sharedLive } from "./shared-live.js";
7
7
  import { channelsClient } from "./channels.js";
8
8
  import { devicesClient } from "./devices.js";
9
+ import { statusClient } from "./status.js";
9
10
  import { notificationHistoryClient } from "./notification-history.js";
10
11
  import { encodeKey } from "@protobase/schema/internal";
11
12
  //#region src/client.ts
@@ -460,7 +461,8 @@ const createClient = (options = {}) => {
460
461
  live,
461
462
  channel,
462
463
  devices: devicesClient(send),
463
- notifications: notificationHistoryClient(send)
464
+ notifications: notificationHistoryClient(send),
465
+ status: statusClient(send)
464
466
  };
465
467
  };
466
468
  //#endregion
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ import { UploadProgress } from "./upload-request.js";
9
9
  import { LiveChange, LiveHandlers } from "./live.js";
10
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
11
  import { Client, InvokeOptions, UploadOptions, UploadResult, createClient } from "./client.js";
12
+ import { Availability, PersonStatus, StatusClient, StatusInput } from "./status.js";
12
13
  import { ApiError, PreconditionFailedError, ProblemDetails, ProblemError, isApiError } from "./problem.js";
13
14
  import { fileUrl } from "./file-urls.js";
14
15
  import { routedClient } from "./routed-client.js";
@@ -27,4 +28,5 @@ import { AuthSession, AuthSessionOptions, createAuthSession, tokenExpiry } from
27
28
  import { createStaticSession } from "./static-session.js";
28
29
  import { AssistantClient, AssistantClientOptions, AssistantConnection, createAssistantClient } from "./assistant.js";
29
30
  import { RuntimeClient, RuntimeClientOptions, RuntimeLastUpdate, RuntimePolicy, RuntimeStatus, RuntimeUpdateStage, createRuntimeClient } from "./runtime.js";
30
- 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 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 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 Stored, type StoredFile, type StoredFileVariant, type TwoFactorMethod, type UploadOptions, type UploadProgress, type UploadResult, type UserInvitation, type UserInvitationSent, type UsersClient, type UsersFound, type WebDeviceRegistration, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, fileUrl, filterText, isApiError, listWire, routedClient, sourceMeta, tokenExpiry };
31
+ 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 };
package/dist/index.js CHANGED
@@ -10,4 +10,5 @@ import { createAuthSession, tokenExpiry } from "./auth.js";
10
10
  import { createStaticSession } from "./static-session.js";
11
11
  import { createAssistantClient } from "./assistant.js";
12
12
  import { createRuntimeClient } from "./runtime.js";
13
- export { ApiError, AuthError, ChannelRefusal, PreconditionFailedError, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, fileUrl, filterText, isApiError, listWire, routedClient, sourceMeta, tokenExpiry };
13
+ import { availabilities, heldStatus, maxStatusText, statusTextProblem } from "@protobase/schema/internal";
14
+ export { ApiError, AuthError, ChannelRefusal, PreconditionFailedError, availabilities, createAssistantClient, createAuthSession, createClient, createRuntimeClient, createStaticSession, fileUrl, filterText, heldStatus, isApiError, listWire, maxStatusText, routedClient, sourceMeta, statusTextProblem, tokenExpiry };
@@ -3,7 +3,7 @@ import { Client } from "./client.js";
3
3
  /**
4
4
  * One client over the app's own API and the sources it draws resources from: a call about a resource goes to the API
5
5
  * that serves it (`sourceOf` gives a source's client, or `undefined` for the app's own), and `meta`, `invoke`, the
6
- * public forms, `live` and the user's devices and notifications go to the app's own. A batch goes to the one API all its writes are for; one that writes to two cannot be atomic, so it is
6
+ * public forms, `live` and the user's devices, notifications and status go to the app's own. A batch goes to the one API all its writes are for; one that writes to two cannot be atomic, so it is
7
7
  * refused before anything is sent.
8
8
  */
9
9
  export declare const routedClient: (own: Client, sourceOf: (resource: string) => Client | undefined) => Client;
@@ -3,7 +3,7 @@ const nameOf = (resource) => typeof resource === "string" ? resource : resource.
3
3
  /**
4
4
  * One client over the app's own API and the sources it draws resources from: a call about a resource goes to the API
5
5
  * that serves it (`sourceOf` gives a source's client, or `undefined` for the app's own), and `meta`, `invoke`, the
6
- * public forms, `live` and the user's devices and notifications go to the app's own. A batch goes to the one API all its writes are for; one that writes to two cannot be atomic, so it is
6
+ * public forms, `live` and the user's devices, notifications and status go to the app's own. A batch goes to the one API all its writes are for; one that writes to two cannot be atomic, so it is
7
7
  * refused before anything is sent.
8
8
  */
9
9
  const routedClient = (own, sourceOf) => {
@@ -42,7 +42,8 @@ const routedClient = (own, sourceOf) => {
42
42
  live: (handlers) => own.live(handlers),
43
43
  channel: (resource, key) => own.channel(resource, key),
44
44
  devices: own.devices,
45
- notifications: own.notifications
45
+ notifications: own.notifications,
46
+ status: own.status
46
47
  };
47
48
  };
48
49
  //#endregion
@@ -0,0 +1,19 @@
1
+ import { Transport } from "./transport.js";
2
+ import { Availability, PersonStatus, PersonStatus as PersonStatus$1, StatusInput, StatusInput as StatusInput$1 } from "@protobase/schema/internal";
3
+ //#region src/status.d.ts
4
+ /**
5
+ * People's own status (`/api/status`), in an app with `status: {}`: `own` the signed-in person's, `set` replaces it
6
+ * and answers it, `clear` removes it, and `people` the statuses of everyone who shares an organization with them, those
7
+ * without one left out. A status past its clear time is none. `set` rejects with an `ApiError` of slug
8
+ * `invalid-status-text` for a text with control characters or over `maxStatusText` characters, and
9
+ * `own-sign-in-only` for staff signed in as the person.
10
+ */
11
+ export declare const statusClient: (send: Transport) => {
12
+ own: () => Promise<PersonStatus | null>;
13
+ set: (status: StatusInput) => Promise<PersonStatus | null>;
14
+ clear: () => Promise<void>;
15
+ people: () => Promise<PersonStatus[]>;
16
+ };
17
+ export type StatusClient = ReturnType<typeof statusClient>;
18
+ //#endregion
19
+ export type { Availability, PersonStatus$1 as PersonStatus, StatusInput$1 as StatusInput };
package/dist/status.js ADDED
@@ -0,0 +1,36 @@
1
+ import { statusPath } from "@protobase/schema/internal";
2
+ //#region src/status.ts
3
+ /**
4
+ * People's own status (`/api/status`), in an app with `status: {}`: `own` the signed-in person's, `set` replaces it
5
+ * and answers it, `clear` removes it, and `people` the statuses of everyone who shares an organization with them, those
6
+ * without one left out. A status past its clear time is none. `set` rejects with an `ApiError` of slug
7
+ * `invalid-status-text` for a text with control characters or over `maxStatusText` characters, and
8
+ * `own-sign-in-only` for staff signed in as the person.
9
+ */
10
+ const statusClient = (send) => ({
11
+ own: async () => (await (await send({
12
+ method: "GET",
13
+ path: statusPath(""),
14
+ system: true
15
+ })).json()).status,
16
+ set: async (status) => (await (await send({
17
+ method: "PUT",
18
+ path: statusPath(""),
19
+ system: true,
20
+ body: status
21
+ })).json()).status,
22
+ clear: async () => {
23
+ await send({
24
+ method: "DELETE",
25
+ path: statusPath(""),
26
+ system: true
27
+ });
28
+ },
29
+ people: async () => (await (await send({
30
+ method: "GET",
31
+ path: statusPath("", "people"),
32
+ system: true
33
+ })).json()).statuses
34
+ });
35
+ //#endregion
36
+ export { statusClient };
package/dist/types.d.ts CHANGED
@@ -284,6 +284,7 @@ export type Meta = {
284
284
  bell?: true;
285
285
  webPushKey?: string;
286
286
  };
287
+ status?: true;
287
288
  locale?: 'en' | 'nl';
288
289
  files?: {
289
290
  trustedHosts: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/client",
3
- "version": "0.59.1",
3
+ "version": "0.59.3",
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.59.1",
35
- "@protobase/schema": "0.59.1",
34
+ "@protobase/layout": "0.59.3",
35
+ "@protobase/schema": "0.59.3",
36
36
  "@simplewebauthn/browser": "^13.3.0",
37
37
  "better-auth": "^1.7.12"
38
38
  }