@poolse/sdk 0.1.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/index.cjs +1004 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +775 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +985 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
import { Channel } from 'phoenix';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SDK configuration passed to `new Poolse(config)`.
|
|
5
|
+
*/
|
|
6
|
+
interface PoolseConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Base URL of the poolse REST API, e.g. `https://chat.example.com`.
|
|
9
|
+
* MUST NOT include the `/v1` path — the SDK adds that itself.
|
|
10
|
+
*/
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
/**
|
|
13
|
+
* Async hook the SDK calls every time it needs an `Authorization:
|
|
14
|
+
* Bearer <jwt>` header. Most apps refresh the JWT from their own
|
|
15
|
+
* backend here — the SDK never talks to poolse's `POST
|
|
16
|
+
* /v1/users/:user_id/tokens` itself (that endpoint is API-key-authed
|
|
17
|
+
* and lives on the Customer's BACKEND, not the End User's device).
|
|
18
|
+
*
|
|
19
|
+
* Return `null` to deliberately make an unauthenticated request — the
|
|
20
|
+
* server will reject it, but the SDK won't error inside `getToken`.
|
|
21
|
+
*/
|
|
22
|
+
getToken: () => Promise<string | null> | string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Optional fetch override. Browsers and Node 22+ both ship a global
|
|
25
|
+
* `fetch`, but tests can inject a mock here; bundlers in restricted
|
|
26
|
+
* environments can supply a polyfill.
|
|
27
|
+
*/
|
|
28
|
+
fetch?: typeof globalThis.fetch;
|
|
29
|
+
/**
|
|
30
|
+
* Retry budget for transient failures (network + 5xx + 429). Defaults
|
|
31
|
+
* to 3 attempts after the initial request. Set to 0 to disable.
|
|
32
|
+
*/
|
|
33
|
+
maxRetries?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Base for the exponential backoff, in milliseconds. Each retry waits
|
|
36
|
+
* `min(maxBackoffMs, baseBackoffMs * 2^attempt)` plus jitter, OR honours
|
|
37
|
+
* the `Retry-After` header if present. Default 250 ms.
|
|
38
|
+
*/
|
|
39
|
+
baseBackoffMs?: number;
|
|
40
|
+
/** Hard cap on a single retry delay. Default 30_000 ms. */
|
|
41
|
+
maxBackoffMs?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Override the idempotency-key generator. Defaults to
|
|
44
|
+
* `crypto.randomUUID()`. Most apps don't need to override this — the
|
|
45
|
+
* generator is exposed mainly for deterministic tests.
|
|
46
|
+
*/
|
|
47
|
+
generateIdempotencyKey?: () => string;
|
|
48
|
+
/**
|
|
49
|
+
* Override the WebSocket URL. Defaults to `apiUrl` with `http(s)://`
|
|
50
|
+
* swapped to `ws(s)://`, suitable when the realtime gateway shares
|
|
51
|
+
* its origin with the REST API. Set explicitly for split-host
|
|
52
|
+
* deployments (`https://api.example.com` REST + `wss://realtime.example.com` WS).
|
|
53
|
+
*/
|
|
54
|
+
wsUrl?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Path the WebSocket is mounted on. Defaults to `/socket` — matches
|
|
57
|
+
* `CaasRealtimeWeb.UserSocket`'s mount point.
|
|
58
|
+
*/
|
|
59
|
+
socketPath?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Called when the underlying socket encounters a non-fatal error
|
|
62
|
+
* (Phoenix retries internally). Useful for surfacing reconnect
|
|
63
|
+
* banners in the UI without coupling to socket internals.
|
|
64
|
+
*/
|
|
65
|
+
onSocketError?: (err: Error) => void;
|
|
66
|
+
}
|
|
67
|
+
/** Internal resolved config — all the defaults filled in. */
|
|
68
|
+
interface ResolvedConfig {
|
|
69
|
+
apiUrl: string;
|
|
70
|
+
getToken: PoolseConfig['getToken'];
|
|
71
|
+
fetch: typeof globalThis.fetch;
|
|
72
|
+
maxRetries: number;
|
|
73
|
+
baseBackoffMs: number;
|
|
74
|
+
maxBackoffMs: number;
|
|
75
|
+
generateIdempotencyKey: () => string;
|
|
76
|
+
wsUrl: string | undefined;
|
|
77
|
+
socketPath: string;
|
|
78
|
+
onSocketError: ((err: Error) => void) | undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type Fetcher = PoolseConfig['getToken'];
|
|
82
|
+
interface GetTokenOptions {
|
|
83
|
+
/** Bypass the cache and force a fresh call to the consumer's `getToken`. */
|
|
84
|
+
forceRefresh?: boolean;
|
|
85
|
+
}
|
|
86
|
+
declare class TokenCache {
|
|
87
|
+
private readonly fetcher;
|
|
88
|
+
private token;
|
|
89
|
+
private expMs;
|
|
90
|
+
private inFlight;
|
|
91
|
+
constructor(fetcher: Fetcher);
|
|
92
|
+
/**
|
|
93
|
+
* Synchronously return the cached token without triggering a fetch.
|
|
94
|
+
* Returns `null` if the cache is empty OR if the cached token is
|
|
95
|
+
* within the refresh window (treating near-expiry tokens as stale
|
|
96
|
+
* keeps the realtime layer from handshaking with an about-to-expire
|
|
97
|
+
* JWT when a refresh is already due).
|
|
98
|
+
*
|
|
99
|
+
* Exists for callers like Phoenix.js's `params` callback that the
|
|
100
|
+
* library invokes synchronously and does NOT await — see
|
|
101
|
+
* `phoenix/priv/static/phoenix.mjs::endPointURL()`.
|
|
102
|
+
*/
|
|
103
|
+
peekToken(): string | null;
|
|
104
|
+
getToken(opts?: GetTokenOptions): Promise<string | null>;
|
|
105
|
+
invalidate(): void;
|
|
106
|
+
private fetchAndStore;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type Uuid = string;
|
|
110
|
+
type IsoDateTime = string;
|
|
111
|
+
interface Me {
|
|
112
|
+
id: Uuid;
|
|
113
|
+
tenant_id: Uuid;
|
|
114
|
+
external_id: string;
|
|
115
|
+
display_name: string | null;
|
|
116
|
+
custom_data: Record<string, unknown>;
|
|
117
|
+
is_blocked: boolean;
|
|
118
|
+
inserted_at: IsoDateTime;
|
|
119
|
+
updated_at: IsoDateTime;
|
|
120
|
+
}
|
|
121
|
+
type ConversationType = 'direct' | 'group';
|
|
122
|
+
interface Conversation {
|
|
123
|
+
id: Uuid;
|
|
124
|
+
tenant_id: Uuid;
|
|
125
|
+
type: ConversationType;
|
|
126
|
+
/** Display name (for group chats); null for unnamed conversations. */
|
|
127
|
+
name: string | null;
|
|
128
|
+
/** Public avatar URL; null when none uploaded. */
|
|
129
|
+
avatar_url: string | null;
|
|
130
|
+
/** User that created the conversation; null for system-created rows. */
|
|
131
|
+
created_by_user_id: Uuid | null;
|
|
132
|
+
/** Hard cap on memberships; null = unlimited (tenant default). */
|
|
133
|
+
member_limit: number | null;
|
|
134
|
+
/**
|
|
135
|
+
* Customer-supplied free-form data — anything you want to attach to
|
|
136
|
+
* the conversation that doesn't fit the schema (UI flags, tags, etc).
|
|
137
|
+
* Server treats it as opaque JSON.
|
|
138
|
+
*/
|
|
139
|
+
custom_data: Record<string, unknown>;
|
|
140
|
+
/**
|
|
141
|
+
* Server-defined behavioral knobs (notification rules, retention,
|
|
142
|
+
* etc). Defaults to `{}` and is updateable.
|
|
143
|
+
*/
|
|
144
|
+
settings: Record<string, unknown>;
|
|
145
|
+
/** Most recent message's `inserted_at`; null until first message. */
|
|
146
|
+
last_message_at: IsoDateTime | null;
|
|
147
|
+
/** Monotonic per-conversation sequence counter (last message's `sequence`). */
|
|
148
|
+
last_sequence: number;
|
|
149
|
+
inserted_at: IsoDateTime;
|
|
150
|
+
updated_at: IsoDateTime;
|
|
151
|
+
}
|
|
152
|
+
interface ConversationList {
|
|
153
|
+
data: Conversation[];
|
|
154
|
+
}
|
|
155
|
+
interface ConversationCreateRequest {
|
|
156
|
+
type: ConversationType;
|
|
157
|
+
name?: string | null;
|
|
158
|
+
avatar_url?: string | null;
|
|
159
|
+
member_limit?: number | null;
|
|
160
|
+
/**
|
|
161
|
+
* Customer-side user IDs to add as members on creation — saves a
|
|
162
|
+
* round-trip vs creating then calling `addMembers`.
|
|
163
|
+
*/
|
|
164
|
+
member_external_ids?: string[];
|
|
165
|
+
custom_data?: Record<string, unknown>;
|
|
166
|
+
settings?: Record<string, unknown>;
|
|
167
|
+
}
|
|
168
|
+
interface ConversationUpdateRequest {
|
|
169
|
+
name?: string | null;
|
|
170
|
+
avatar_url?: string | null;
|
|
171
|
+
member_limit?: number | null;
|
|
172
|
+
custom_data?: Record<string, unknown>;
|
|
173
|
+
settings?: Record<string, unknown>;
|
|
174
|
+
}
|
|
175
|
+
type MemberRole = 'owner' | 'admin' | 'member';
|
|
176
|
+
interface Membership {
|
|
177
|
+
id: Uuid;
|
|
178
|
+
conversation_id: Uuid;
|
|
179
|
+
user_id: Uuid;
|
|
180
|
+
role: MemberRole;
|
|
181
|
+
last_read_message_id: Uuid | null;
|
|
182
|
+
last_read_at: IsoDateTime | null;
|
|
183
|
+
inserted_at: IsoDateTime;
|
|
184
|
+
updated_at: IsoDateTime;
|
|
185
|
+
}
|
|
186
|
+
interface MembershipList {
|
|
187
|
+
data: Membership[];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Server accepts a batch of `external_ids` (the customer's stable user
|
|
191
|
+
* identifiers — what was passed to `POST /v1/users`). The server
|
|
192
|
+
* resolves each to its internal user_id and creates a membership row
|
|
193
|
+
* per external_id, all in one round-trip. Optional `role` defaults to
|
|
194
|
+
* `"member"` server-side.
|
|
195
|
+
*
|
|
196
|
+
* Most callers use the higher-level
|
|
197
|
+
* `chat.conversations.one(id).addMember(externalId)` /
|
|
198
|
+
* `addMembers([externalIds])` methods which build this shape for you.
|
|
199
|
+
*/
|
|
200
|
+
interface MembershipCreateRequest {
|
|
201
|
+
external_ids: string[];
|
|
202
|
+
role?: MemberRole;
|
|
203
|
+
}
|
|
204
|
+
type MessageType = 'text' | 'system' | 'custom';
|
|
205
|
+
interface Message {
|
|
206
|
+
id: Uuid;
|
|
207
|
+
tenant_id: Uuid;
|
|
208
|
+
conversation_id: Uuid;
|
|
209
|
+
sender_id: Uuid | null;
|
|
210
|
+
type: MessageType;
|
|
211
|
+
body: string | null;
|
|
212
|
+
reply_to_id: Uuid | null;
|
|
213
|
+
thread_root_id: Uuid | null;
|
|
214
|
+
mentions: Uuid[];
|
|
215
|
+
reactions: Record<string, Uuid[]>;
|
|
216
|
+
edited_at: IsoDateTime | null;
|
|
217
|
+
deleted_at: IsoDateTime | null;
|
|
218
|
+
sequence: number;
|
|
219
|
+
inserted_at: IsoDateTime;
|
|
220
|
+
updated_at: IsoDateTime;
|
|
221
|
+
}
|
|
222
|
+
interface MessageList {
|
|
223
|
+
data: Message[];
|
|
224
|
+
}
|
|
225
|
+
interface MessageCreateRequest {
|
|
226
|
+
body: string;
|
|
227
|
+
type?: MessageType;
|
|
228
|
+
reply_to_id?: Uuid;
|
|
229
|
+
mentions?: Uuid[];
|
|
230
|
+
id?: Uuid;
|
|
231
|
+
}
|
|
232
|
+
interface MessageUpdateRequest {
|
|
233
|
+
body: string;
|
|
234
|
+
}
|
|
235
|
+
interface ReadRequest {
|
|
236
|
+
message_id: Uuid;
|
|
237
|
+
}
|
|
238
|
+
interface ReactionRequest {
|
|
239
|
+
emoji: string;
|
|
240
|
+
}
|
|
241
|
+
type AttachmentStatus = 'pending' | 'ready';
|
|
242
|
+
interface Attachment {
|
|
243
|
+
id: Uuid;
|
|
244
|
+
tenant_id: Uuid;
|
|
245
|
+
/** Linked message id; null while the attachment is still `:pending`. */
|
|
246
|
+
message_id: Uuid | null;
|
|
247
|
+
/** Uploader; null for system-created attachments. */
|
|
248
|
+
sender_id: Uuid | null;
|
|
249
|
+
content_type: string;
|
|
250
|
+
byte_size: number;
|
|
251
|
+
/** Server-computed SHA-256 of the uploaded bytes; populated after the PUT completes. */
|
|
252
|
+
sha256: string | null;
|
|
253
|
+
original_filename: string | null;
|
|
254
|
+
status: AttachmentStatus;
|
|
255
|
+
inserted_at: IsoDateTime;
|
|
256
|
+
updated_at: IsoDateTime;
|
|
257
|
+
}
|
|
258
|
+
interface AttachmentUploadRequest {
|
|
259
|
+
content_type: string;
|
|
260
|
+
byte_size: number;
|
|
261
|
+
original_filename?: string;
|
|
262
|
+
}
|
|
263
|
+
/** Presigned PUT payload returned by `POST /v1/attachments/upload-url`. */
|
|
264
|
+
interface AttachmentUploadResponse {
|
|
265
|
+
attachment: Attachment;
|
|
266
|
+
upload: {
|
|
267
|
+
url: string;
|
|
268
|
+
method: 'put';
|
|
269
|
+
/** Headers the client MUST include on the PUT (signed into the URL). */
|
|
270
|
+
headers: Record<string, string>;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Presigned GET payload returned by `GET /v1/attachments/:id/download-url`. */
|
|
274
|
+
interface AttachmentDownloadResponse {
|
|
275
|
+
url: string;
|
|
276
|
+
method: 'get';
|
|
277
|
+
}
|
|
278
|
+
interface ErrorEnvelope {
|
|
279
|
+
error: {
|
|
280
|
+
code: string;
|
|
281
|
+
message: string;
|
|
282
|
+
doc_url: string;
|
|
283
|
+
details?: Record<string, unknown>;
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** `message:new` / `message:updated` push payloads. */
|
|
288
|
+
type MessageNewEvent = Message;
|
|
289
|
+
type MessageUpdatedEvent = Message;
|
|
290
|
+
/** `message:deleted` push payload (just the tombstone, no body). */
|
|
291
|
+
interface MessageDeletedEvent {
|
|
292
|
+
id: Uuid;
|
|
293
|
+
conversation_id: Uuid;
|
|
294
|
+
deleted_at: string | null;
|
|
295
|
+
}
|
|
296
|
+
/** `typing:start` / `typing:stop`. */
|
|
297
|
+
interface TypingEvent {
|
|
298
|
+
user_id: Uuid;
|
|
299
|
+
}
|
|
300
|
+
/** `reaction:added` / `reaction:removed`. */
|
|
301
|
+
interface ReactionEvent {
|
|
302
|
+
message_id: Uuid;
|
|
303
|
+
conversation_id: Uuid;
|
|
304
|
+
emoji: string;
|
|
305
|
+
user_id: Uuid;
|
|
306
|
+
}
|
|
307
|
+
/** Per-user mention push on the `user:<id>` channel. */
|
|
308
|
+
interface MentionEvent {
|
|
309
|
+
message_id: Uuid;
|
|
310
|
+
conversation_id: Uuid;
|
|
311
|
+
sender_id: Uuid | null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Pushed on `user:<id>` when the user is added to a conversation —
|
|
315
|
+
* either as creator (via `POST /v1/conversations`) or as additional
|
|
316
|
+
* member (via `POST /v1/conversations/:id/members`). Payload is the
|
|
317
|
+
* full conversation row.
|
|
318
|
+
*/
|
|
319
|
+
type ConversationCreatedEvent = Conversation;
|
|
320
|
+
/** Phoenix Presence list shape. */
|
|
321
|
+
type PresenceSnapshot = Record<Uuid, {
|
|
322
|
+
metas: Array<{
|
|
323
|
+
phx_ref: string;
|
|
324
|
+
online_at: number;
|
|
325
|
+
external_id?: string;
|
|
326
|
+
}>;
|
|
327
|
+
}>;
|
|
328
|
+
/**
|
|
329
|
+
* Connection status. Reflects the underlying socket lifecycle plus
|
|
330
|
+
* channel join state — easier for UIs than tracking both separately.
|
|
331
|
+
*/
|
|
332
|
+
type RealtimeStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
|
|
333
|
+
/** Unsubscribe handle returned by `onX` listener registrations. */
|
|
334
|
+
type Unsubscribe = () => void;
|
|
335
|
+
|
|
336
|
+
interface RealtimeOptions {
|
|
337
|
+
/**
|
|
338
|
+
* WebSocket endpoint path (mounted in caas_realtime). Defaults to
|
|
339
|
+
* `'/socket'` — matches the path served by `CaasRealtimeWeb.UserSocket`.
|
|
340
|
+
*/
|
|
341
|
+
socketPath?: string;
|
|
342
|
+
/**
|
|
343
|
+
* Override the Phoenix-derived WebSocket URL. Use when the realtime
|
|
344
|
+
* gateway lives on a different host than the REST API (most common
|
|
345
|
+
* in prod: `https://api.example.com` for REST, `wss://realtime.example.com`
|
|
346
|
+
* for WebSocket). When unset, the WS URL is derived from `apiUrl` by
|
|
347
|
+
* swapping http(s) → ws(s).
|
|
348
|
+
*/
|
|
349
|
+
wsUrl?: string;
|
|
350
|
+
}
|
|
351
|
+
declare class PoolseRealtime {
|
|
352
|
+
private readonly config;
|
|
353
|
+
private readonly tokenCache;
|
|
354
|
+
private readonly socketPath;
|
|
355
|
+
private readonly wsUrl;
|
|
356
|
+
private socket;
|
|
357
|
+
private readonly conversations;
|
|
358
|
+
private userChannel;
|
|
359
|
+
private status;
|
|
360
|
+
private readonly statusListeners;
|
|
361
|
+
constructor(config: ResolvedConfig, tokenCache: TokenCache, opts?: RealtimeOptions);
|
|
362
|
+
/** Current connection status. */
|
|
363
|
+
getStatus(): RealtimeStatus;
|
|
364
|
+
/** Subscribe to connection-status changes. */
|
|
365
|
+
onStatus(listener: (status: RealtimeStatus) => void): Unsubscribe;
|
|
366
|
+
/**
|
|
367
|
+
* Open the socket (idempotent). Synchronous construction so callers
|
|
368
|
+
* can join a channel on the next line; the underlying WebSocket
|
|
369
|
+
* connects on the next tick once the JWT pre-fetch resolves.
|
|
370
|
+
*
|
|
371
|
+
* Phoenix.js invokes the `params` callback SYNCHRONOUSLY on every
|
|
372
|
+
* (re)connect and does NOT await its return value — see
|
|
373
|
+
* `phoenix/priv/static/phoenix.mjs::endPointURL`. So `params` has
|
|
374
|
+
* to read a token that's already in hand. We:
|
|
375
|
+
*
|
|
376
|
+
* 1. Construct the Socket immediately (sets `this.socket` so
|
|
377
|
+
* concurrent `conversation()` / `user()` callers can attach
|
|
378
|
+
* channels — Phoenix buffers joins until the socket opens).
|
|
379
|
+
* 2. Pre-fetch the JWT through `TokenCache`, which fills its
|
|
380
|
+
* internal cache.
|
|
381
|
+
* 3. Call `socket.connect()` so phoenix.js's first handshake reads
|
|
382
|
+
* a primed `peekToken()`.
|
|
383
|
+
*
|
|
384
|
+
* On reconnect, Phoenix calls `params()` again; the cache is still
|
|
385
|
+
* warm (default JWT exp ~1h, refresh window 30s) so `peekToken()`
|
|
386
|
+
* returns the live token. When the token genuinely expires, our
|
|
387
|
+
* REST 401 path invalidates the cache; the next reconnect's
|
|
388
|
+
* `peekToken()` is `null` and the handshake intentionally fails so
|
|
389
|
+
* the cache can re-fill on the next iteration.
|
|
390
|
+
*/
|
|
391
|
+
connect(): void;
|
|
392
|
+
/** Close the socket and tear down every joined channel. */
|
|
393
|
+
disconnect(): void;
|
|
394
|
+
/**
|
|
395
|
+
* Subscribe to a conversation. Returns a typed handle with
|
|
396
|
+
* `onMessage`, `onTyping`, etc. Reusing the same `id` returns the
|
|
397
|
+
* same handle — re-subscribing doesn't open a second channel.
|
|
398
|
+
*/
|
|
399
|
+
conversation(conversationId: string): ConversationChannel;
|
|
400
|
+
/**
|
|
401
|
+
* Subscribe to the current user's `user:<id>` channel. Only the user
|
|
402
|
+
* matching the JWT can join — poolse's UserChannel enforces this.
|
|
403
|
+
*/
|
|
404
|
+
user(userId: string): UserChannel;
|
|
405
|
+
/** Drop a conversation handle and leave the channel. */
|
|
406
|
+
leave(conversationId: string): void;
|
|
407
|
+
private setStatus;
|
|
408
|
+
}
|
|
409
|
+
declare class ConversationChannel {
|
|
410
|
+
readonly conversationId: string;
|
|
411
|
+
private readonly channel;
|
|
412
|
+
private readonly listeners;
|
|
413
|
+
constructor(conversationId: string, channel: Channel);
|
|
414
|
+
/** New message pushed to the conversation. */
|
|
415
|
+
onMessage(fn: (msg: MessageNewEvent) => void): Unsubscribe;
|
|
416
|
+
/** Existing message edited by its sender. */
|
|
417
|
+
onMessageUpdated(fn: (msg: MessageUpdatedEvent) => void): Unsubscribe;
|
|
418
|
+
/** Tombstone for a soft-deleted message. */
|
|
419
|
+
onMessageDeleted(fn: (evt: MessageDeletedEvent) => void): Unsubscribe;
|
|
420
|
+
onTypingStart(fn: (evt: TypingEvent) => void): Unsubscribe;
|
|
421
|
+
onTypingStop(fn: (evt: TypingEvent) => void): Unsubscribe;
|
|
422
|
+
onReactionAdded(fn: (evt: ReactionEvent) => void): Unsubscribe;
|
|
423
|
+
onReactionRemoved(fn: (evt: ReactionEvent) => void): Unsubscribe;
|
|
424
|
+
onPresenceState(fn: (state: PresenceSnapshot) => void): Unsubscribe;
|
|
425
|
+
onPresenceDiff(fn: (diff: PresenceSnapshot) => void): Unsubscribe;
|
|
426
|
+
/** Send a typing ping to the server. Debounced server-side. */
|
|
427
|
+
sendTyping(): void;
|
|
428
|
+
/** @internal — called by `PoolseRealtime.conversation/1`. */
|
|
429
|
+
_join(): void;
|
|
430
|
+
/** @internal — called when the consumer leaves this conversation. */
|
|
431
|
+
_destroy(): void;
|
|
432
|
+
private subscribe;
|
|
433
|
+
}
|
|
434
|
+
declare class UserChannel {
|
|
435
|
+
readonly userId: string;
|
|
436
|
+
private readonly channel;
|
|
437
|
+
private readonly mentionListeners;
|
|
438
|
+
private readonly conversationCreatedListeners;
|
|
439
|
+
private mentionBound;
|
|
440
|
+
private conversationCreatedBound;
|
|
441
|
+
constructor(userId: string, channel: Channel);
|
|
442
|
+
onMention(fn: (evt: MentionEvent) => void): Unsubscribe;
|
|
443
|
+
/**
|
|
444
|
+
* Subscribe to "you've been added to a conversation" notifications.
|
|
445
|
+
* Fires once per new membership — either because you created the
|
|
446
|
+
* conversation, or because someone added you to an existing one.
|
|
447
|
+
*
|
|
448
|
+
* Payload is the full {@link Conversation} row so consumers can
|
|
449
|
+
* prepend it to a local list without a refetch.
|
|
450
|
+
*/
|
|
451
|
+
onConversationCreated(fn: (conv: ConversationCreatedEvent) => void): Unsubscribe;
|
|
452
|
+
/** @internal */
|
|
453
|
+
_join(): void;
|
|
454
|
+
/** @internal */
|
|
455
|
+
_destroy(): void;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
|
|
459
|
+
interface RequestOptions {
|
|
460
|
+
method: HttpMethod;
|
|
461
|
+
path: string;
|
|
462
|
+
/** JSON-serialisable body for non-GETs. Omit for GET/DELETE. */
|
|
463
|
+
body?: unknown;
|
|
464
|
+
/** Query string params; values are stringified, nullish entries dropped. */
|
|
465
|
+
query?: Record<string, string | number | boolean | null | undefined>;
|
|
466
|
+
/**
|
|
467
|
+
* Idempotency-Key override. Defaults to a fresh UUID for non-GETs.
|
|
468
|
+
* Pass `null` to deliberately omit the header (e.g. when the caller's
|
|
469
|
+
* own retry logic supplies a deterministic one).
|
|
470
|
+
*/
|
|
471
|
+
idempotencyKey?: string | null;
|
|
472
|
+
/**
|
|
473
|
+
* Per-request override for total retry budget. Defaults to
|
|
474
|
+
* `config.maxRetries`.
|
|
475
|
+
*/
|
|
476
|
+
maxRetries?: number;
|
|
477
|
+
/** AbortSignal for caller-driven cancellation. */
|
|
478
|
+
signal?: AbortSignal;
|
|
479
|
+
}
|
|
480
|
+
declare class RestClient {
|
|
481
|
+
private readonly config;
|
|
482
|
+
private readonly tokenCache;
|
|
483
|
+
constructor(config: ResolvedConfig, tokenCache: TokenCache);
|
|
484
|
+
request<T>(opts: RequestOptions): Promise<T>;
|
|
485
|
+
private buildUrl;
|
|
486
|
+
private buildHeaders;
|
|
487
|
+
private resolveIdempotencyKey;
|
|
488
|
+
private backoffDelay;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Input accepted by {@link AttachmentsResource.upload}. */
|
|
492
|
+
interface AttachmentUploadInput {
|
|
493
|
+
/**
|
|
494
|
+
* The bytes to upload. Browser: pass a `File` or `Blob`. Node /
|
|
495
|
+
* Workers / Deno: a `Uint8Array`, `ArrayBuffer`, or any
|
|
496
|
+
* `BodyInit`-compatible value the runtime's `fetch` accepts as a
|
|
497
|
+
* PUT body.
|
|
498
|
+
*/
|
|
499
|
+
body: BodyInit;
|
|
500
|
+
/**
|
|
501
|
+
* MIME type. MUST match what you pass as `content_type` on the
|
|
502
|
+
* upload-URL request (the storage backend signs it into the URL —
|
|
503
|
+
* a mismatch makes the PUT fail with 403).
|
|
504
|
+
*/
|
|
505
|
+
contentType: string;
|
|
506
|
+
/** Total bytes — must match the bytes you actually PUT. */
|
|
507
|
+
byteSize: number;
|
|
508
|
+
/** Surfaced in download UX so saved files keep a sensible name. */
|
|
509
|
+
filename?: string;
|
|
510
|
+
}
|
|
511
|
+
/** Options accepted by every attachment method. */
|
|
512
|
+
interface AttachmentOptions {
|
|
513
|
+
signal?: AbortSignal;
|
|
514
|
+
}
|
|
515
|
+
/** Top-level `/v1/attachments` collection. */
|
|
516
|
+
declare class AttachmentsResource {
|
|
517
|
+
private readonly client;
|
|
518
|
+
private readonly fetchFn;
|
|
519
|
+
/**
|
|
520
|
+
* The PUT to the presigned URL bypasses the SDK's authenticated
|
|
521
|
+
* REST client (presigned URLs encode their own auth and MUST NOT
|
|
522
|
+
* receive an `Authorization` header). It still respects
|
|
523
|
+
* `config.fetch` if the customer provided one — required for tests
|
|
524
|
+
* with a mock fetch, and for runtimes where `globalThis.fetch` is
|
|
525
|
+
* not the right transport.
|
|
526
|
+
*/
|
|
527
|
+
constructor(client: RestClient, fetchFn: typeof globalThis.fetch);
|
|
528
|
+
/**
|
|
529
|
+
* Step 1 of an upload — request a presigned PUT URL. Use this when
|
|
530
|
+
* you want to drive the PUT yourself (e.g. resumable uploads,
|
|
531
|
+
* React Native FileSystem). For the common case prefer
|
|
532
|
+
* {@link upload}, which does both steps for you.
|
|
533
|
+
*/
|
|
534
|
+
requestUpload(attrs: AttachmentUploadRequest, opts?: AttachmentOptions): Promise<AttachmentUploadResponse>;
|
|
535
|
+
/**
|
|
536
|
+
* One-call upload: request a presigned URL, PUT the bytes to it,
|
|
537
|
+
* return the attachment row. After this resolves the attachment is
|
|
538
|
+
* ready to be referenced from a message send.
|
|
539
|
+
*
|
|
540
|
+
* ```ts
|
|
541
|
+
* // Browser <input type="file">:
|
|
542
|
+
* const file = inputEl.files![0]!;
|
|
543
|
+
* const att = await chat.attachments.upload({
|
|
544
|
+
* body: file,
|
|
545
|
+
* contentType: file.type,
|
|
546
|
+
* byteSize: file.size,
|
|
547
|
+
* filename: file.name,
|
|
548
|
+
* });
|
|
549
|
+
* await chat.conversations.one(convId).messages.send({
|
|
550
|
+
* body: 'Look at this!',
|
|
551
|
+
* custom_data: { attachment_id: att.id },
|
|
552
|
+
* });
|
|
553
|
+
* ```
|
|
554
|
+
*
|
|
555
|
+
* Note: the PUT uses the runtime's bare `fetch` (NOT the SDK's
|
|
556
|
+
* authenticated REST client) — presigned URLs already encode their
|
|
557
|
+
* own auth and MUST NOT receive an `Authorization` header.
|
|
558
|
+
*/
|
|
559
|
+
upload(input: AttachmentUploadInput, opts?: AttachmentOptions): Promise<Attachment>;
|
|
560
|
+
/** Returns a handle for further operations on a single attachment. */
|
|
561
|
+
one(id: Uuid): AttachmentHandle;
|
|
562
|
+
}
|
|
563
|
+
/** Wraps an attachment id for download-url + delete. */
|
|
564
|
+
declare class AttachmentHandle {
|
|
565
|
+
private readonly client;
|
|
566
|
+
readonly id: Uuid;
|
|
567
|
+
constructor(client: RestClient, id: Uuid);
|
|
568
|
+
/**
|
|
569
|
+
* Request a presigned GET URL (~1h TTL). Conversation-member-gated
|
|
570
|
+
* server-side. Useful when rendering files in chat: cache the URL
|
|
571
|
+
* client-side until close to expiry, then re-fetch.
|
|
572
|
+
*/
|
|
573
|
+
downloadUrl(opts?: AttachmentOptions): Promise<AttachmentDownloadResponse>;
|
|
574
|
+
/**
|
|
575
|
+
* Delete the attachment row + best-effort bucket object delete.
|
|
576
|
+
* Authz: uploader (while still `:pending`) or message-sender / conv
|
|
577
|
+
* owner-admin (once linked).
|
|
578
|
+
*/
|
|
579
|
+
delete(opts?: AttachmentOptions): Promise<void>;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Per-conversation message collection: send, list, mark-read. */
|
|
583
|
+
declare class ConversationMessages {
|
|
584
|
+
private readonly client;
|
|
585
|
+
private readonly conversationId;
|
|
586
|
+
constructor(client: RestClient, conversationId: Uuid);
|
|
587
|
+
list(opts?: {
|
|
588
|
+
limit?: number;
|
|
589
|
+
before?: number;
|
|
590
|
+
}, signal?: AbortSignal): Promise<MessageList>;
|
|
591
|
+
/**
|
|
592
|
+
* Send a message to this conversation.
|
|
593
|
+
*
|
|
594
|
+
* If `attrs.id` is omitted the SDK generates a v4 UUID and uses it
|
|
595
|
+
* as both the wire-level idempotency key AND the literal message.id
|
|
596
|
+
* the server stores. Two side-effects that make a real-time UI
|
|
597
|
+
* trivial:
|
|
598
|
+
*
|
|
599
|
+
* * Resending the same `id` (e.g. a network-retry) returns the
|
|
600
|
+
* ORIGINAL message instead of inserting a duplicate.
|
|
601
|
+
* * The realtime `message:new` broadcast carries this same id,
|
|
602
|
+
* so an optimistic UI can pre-render the row under the final id
|
|
603
|
+
* and dedup by id alone — no client/server id swap needed.
|
|
604
|
+
*
|
|
605
|
+
* Pass an explicit `attrs.id` only when you generated it yourself
|
|
606
|
+
* upstream (e.g. you already render an optimistic row in your hook
|
|
607
|
+
* and want the server to confirm under the same key).
|
|
608
|
+
*/
|
|
609
|
+
send(attrs: MessageCreateRequest, signal?: AbortSignal): Promise<Message>;
|
|
610
|
+
markRead(messageId: Uuid, signal?: AbortSignal): Promise<void>;
|
|
611
|
+
}
|
|
612
|
+
/** Per-message operations: edit, delete, react, list replies. */
|
|
613
|
+
declare class MessageHandle {
|
|
614
|
+
private readonly client;
|
|
615
|
+
readonly id: Uuid;
|
|
616
|
+
constructor(client: RestClient, id: Uuid);
|
|
617
|
+
update(attrs: MessageUpdateRequest, signal?: AbortSignal): Promise<Message>;
|
|
618
|
+
delete(signal?: AbortSignal): Promise<void>;
|
|
619
|
+
replies(opts?: {
|
|
620
|
+
limit?: number;
|
|
621
|
+
after?: number;
|
|
622
|
+
}, signal?: AbortSignal): Promise<MessageList>;
|
|
623
|
+
addReaction(emoji: string, signal?: AbortSignal): Promise<Message>;
|
|
624
|
+
removeReaction(emoji: string, signal?: AbortSignal): Promise<Message>;
|
|
625
|
+
}
|
|
626
|
+
/** Top-level `/v1/messages` namespace — accessed via `chat.messages(id)`. */
|
|
627
|
+
declare class MessagesResource {
|
|
628
|
+
private readonly client;
|
|
629
|
+
constructor(client: RestClient);
|
|
630
|
+
one(id: Uuid): MessageHandle;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Optional knobs accepted by every member-add call. */
|
|
634
|
+
interface AddMemberOptions {
|
|
635
|
+
/** Membership role — defaults to `"member"` server-side. */
|
|
636
|
+
role?: MemberRole;
|
|
637
|
+
/** AbortSignal for caller-driven cancellation. */
|
|
638
|
+
signal?: AbortSignal;
|
|
639
|
+
}
|
|
640
|
+
/** Wraps a single conversation by id — used as the entry point for sub-resources. */
|
|
641
|
+
declare class ConversationHandle {
|
|
642
|
+
private readonly client;
|
|
643
|
+
readonly id: Uuid;
|
|
644
|
+
/**
|
|
645
|
+
* Message ops scoped to this conversation: `list`, `send`, `markRead`.
|
|
646
|
+
* Lazy: constructed on first access so an idle handle stays cheap.
|
|
647
|
+
*/
|
|
648
|
+
readonly messages: ConversationMessages;
|
|
649
|
+
constructor(client: RestClient, id: Uuid);
|
|
650
|
+
show(signal?: AbortSignal): Promise<Conversation>;
|
|
651
|
+
update(attrs: ConversationUpdateRequest, signal?: AbortSignal): Promise<Conversation>;
|
|
652
|
+
listMembers(signal?: AbortSignal): Promise<MembershipList>;
|
|
653
|
+
/**
|
|
654
|
+
* Add multiple users to this conversation in one round-trip.
|
|
655
|
+
*
|
|
656
|
+
* `externalIds` are the stable customer-side identifiers you passed
|
|
657
|
+
* to `POST /v1/users` when creating each user — the server resolves
|
|
658
|
+
* them to internal user_ids and creates one membership row per id.
|
|
659
|
+
*
|
|
660
|
+
* Requires `:manage_members` on this conversation (owner or admin).
|
|
661
|
+
*
|
|
662
|
+
* ```ts
|
|
663
|
+
* await chat.conversations.one(convId).addMembers(['alice', 'bob']);
|
|
664
|
+
* ```
|
|
665
|
+
*/
|
|
666
|
+
addMembers(externalIds: string[], opts?: AddMemberOptions): Promise<MembershipList>;
|
|
667
|
+
/**
|
|
668
|
+
* Add a single user. Convenience wrapper around {@link addMembers}
|
|
669
|
+
* that unwraps the returned list to the single membership row.
|
|
670
|
+
*
|
|
671
|
+
* ```ts
|
|
672
|
+
* const m = await chat.conversations.one(convId).addMember('alice');
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
addMember(externalId: string, opts?: AddMemberOptions): Promise<Membership>;
|
|
676
|
+
removeMember(userId: Uuid, signal?: AbortSignal): Promise<void>;
|
|
677
|
+
}
|
|
678
|
+
/** Top-level `/v1/conversations` collection. */
|
|
679
|
+
declare class ConversationsResource {
|
|
680
|
+
private readonly client;
|
|
681
|
+
constructor(client: RestClient);
|
|
682
|
+
list(signal?: AbortSignal): Promise<ConversationList>;
|
|
683
|
+
create(attrs: ConversationCreateRequest, signal?: AbortSignal): Promise<Conversation>;
|
|
684
|
+
/** Returns a handle for further operations on a single conversation. */
|
|
685
|
+
one(id: Uuid): ConversationHandle;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** `/v1/me` — the End-User identity behind the presented JWT. */
|
|
689
|
+
declare class MeResource {
|
|
690
|
+
private readonly client;
|
|
691
|
+
constructor(client: RestClient);
|
|
692
|
+
/** GET /v1/me */
|
|
693
|
+
show(signal?: AbortSignal): Promise<Me>;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
declare class Poolse {
|
|
697
|
+
/** `/v1/me` — current End User. */
|
|
698
|
+
readonly me: MeResource;
|
|
699
|
+
/** `/v1/conversations` collection + per-conversation handle factory. */
|
|
700
|
+
readonly conversations: ConversationsResource;
|
|
701
|
+
/** `/v1/messages/:id/*` — accessed via `chat.messages.one(id)`. */
|
|
702
|
+
readonly messages: MessagesResource;
|
|
703
|
+
/** `/v1/attachments/*` — presigned-URL uploads/downloads. */
|
|
704
|
+
readonly attachments: AttachmentsResource;
|
|
705
|
+
/**
|
|
706
|
+
* Low-level REST client. Exposed for advanced use cases (custom endpoints,
|
|
707
|
+
* raw retry/headers control). Most callers should use the resources above.
|
|
708
|
+
*/
|
|
709
|
+
readonly rest: RestClient;
|
|
710
|
+
/**
|
|
711
|
+
* WebSocket / Phoenix Channels client. Lazily connects on the first
|
|
712
|
+
* `poolse.realtime.conversation(id)` / `poolse.realtime.user(id)`
|
|
713
|
+
* call — passing `config.apiUrl` (with `http(s)://` swapped to
|
|
714
|
+
* `ws(s)://`) for the socket URL by default, overridable via
|
|
715
|
+
* `config.wsUrl`.
|
|
716
|
+
*/
|
|
717
|
+
readonly realtime: PoolseRealtime;
|
|
718
|
+
private readonly resolved;
|
|
719
|
+
private readonly tokenCache;
|
|
720
|
+
constructor(config: PoolseConfig);
|
|
721
|
+
/**
|
|
722
|
+
* Tear down the SDK: close the WebSocket, drop all channels.
|
|
723
|
+
* No-op for REST — fetch() doesn't keep persistent state.
|
|
724
|
+
* Call this when the user signs out or the SDK instance is
|
|
725
|
+
* being replaced.
|
|
726
|
+
*/
|
|
727
|
+
destroy(): void;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** Base for any error originating in the SDK. */
|
|
731
|
+
declare class PoolseError extends Error {
|
|
732
|
+
readonly name: string;
|
|
733
|
+
constructor(message: string);
|
|
734
|
+
}
|
|
735
|
+
/** fetch() rejected or the server returned no response at all. */
|
|
736
|
+
declare class NetworkError extends PoolseError {
|
|
737
|
+
readonly name: string;
|
|
738
|
+
readonly cause: unknown;
|
|
739
|
+
constructor(message: string, cause: unknown);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Server returned a non-2xx status with the canonical error envelope.
|
|
743
|
+
* `code` is poolse's snake_case error code (e.g. `"invalid_user_token"`).
|
|
744
|
+
*/
|
|
745
|
+
declare class ApiError extends PoolseError {
|
|
746
|
+
readonly name: string;
|
|
747
|
+
readonly status: number;
|
|
748
|
+
readonly code: string;
|
|
749
|
+
readonly docUrl: string;
|
|
750
|
+
readonly details: Record<string, unknown> | undefined;
|
|
751
|
+
constructor(status: number, envelope: ErrorEnvelope['error']);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* 429 specifically. Surfaced as its own subclass so callers can back off
|
|
755
|
+
* politely without parsing the generic ApiError.
|
|
756
|
+
*/
|
|
757
|
+
declare class RateLimitedError extends ApiError {
|
|
758
|
+
readonly name: string;
|
|
759
|
+
readonly retryAfterMs: number;
|
|
760
|
+
constructor(envelope: ErrorEnvelope['error'], retryAfterMs: number);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Auth failure (401, including expired/revoked tokens). Callers should
|
|
764
|
+
* refresh the JWT (via `getToken`) and retry — the SDK does NOT do this
|
|
765
|
+
* automatically, because the failure could also mean "the user signed
|
|
766
|
+
* out on another tab" and silent re-auth would hide that.
|
|
767
|
+
*/
|
|
768
|
+
declare class AuthError extends ApiError {
|
|
769
|
+
readonly name: string;
|
|
770
|
+
constructor(envelope: ErrorEnvelope['error']);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
declare const version = "0.0.1";
|
|
774
|
+
|
|
775
|
+
export { type AddMemberOptions, ApiError, type Attachment, type AttachmentDownloadResponse, AttachmentHandle, type AttachmentOptions, type AttachmentStatus, type AttachmentUploadInput, type AttachmentUploadRequest, type AttachmentUploadResponse, AttachmentsResource, AuthError, type Conversation, ConversationChannel, type ConversationCreateRequest, type ConversationCreatedEvent, ConversationHandle, type ConversationList, ConversationMessages, type ConversationType, type ConversationUpdateRequest, ConversationsResource, type ErrorEnvelope, type IsoDateTime, type Me, MeResource, type MemberRole, type Membership, type MembershipCreateRequest, type MembershipList, type MentionEvent, type Message, type MessageCreateRequest, type MessageDeletedEvent, MessageHandle, type MessageList, type MessageNewEvent, type MessageType, type MessageUpdateRequest, type MessageUpdatedEvent, MessagesResource, NetworkError, Poolse, type PoolseConfig, PoolseError, PoolseRealtime, type PresenceSnapshot, RateLimitedError, type ReactionEvent, type ReactionRequest, type ReadRequest, type RealtimeStatus, type TypingEvent, type Unsubscribe, UserChannel, type Uuid, version };
|