@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.js
ADDED
|
@@ -0,0 +1,985 @@
|
|
|
1
|
+
import { Socket } from 'phoenix';
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
5
|
+
var DEFAULT_BASE_BACKOFF_MS = 250;
|
|
6
|
+
var DEFAULT_MAX_BACKOFF_MS = 3e4;
|
|
7
|
+
function resolveConfig(config) {
|
|
8
|
+
if (!config.apiUrl) {
|
|
9
|
+
throw new Error("Poolse: `apiUrl` is required.");
|
|
10
|
+
}
|
|
11
|
+
if (typeof config.getToken !== "function") {
|
|
12
|
+
throw new Error("Poolse: `getToken` is required and must be a function.");
|
|
13
|
+
}
|
|
14
|
+
const rawFetch = config.fetch ?? globalThis.fetch;
|
|
15
|
+
if (typeof rawFetch !== "function") {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Poolse: no global `fetch` found. Provide one via `config.fetch` (Node <18 or a sandboxed runtime)."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const fetchFn = rawFetch.bind(globalThis);
|
|
21
|
+
return {
|
|
22
|
+
apiUrl: trimTrailingSlash(config.apiUrl),
|
|
23
|
+
getToken: config.getToken,
|
|
24
|
+
fetch: fetchFn,
|
|
25
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
26
|
+
baseBackoffMs: config.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS,
|
|
27
|
+
maxBackoffMs: config.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS,
|
|
28
|
+
generateIdempotencyKey: config.generateIdempotencyKey ?? defaultIdempotencyKey,
|
|
29
|
+
wsUrl: config.wsUrl,
|
|
30
|
+
socketPath: config.socketPath ?? "/socket",
|
|
31
|
+
onSocketError: config.onSocketError
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function trimTrailingSlash(s) {
|
|
35
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
36
|
+
}
|
|
37
|
+
function defaultIdempotencyKey() {
|
|
38
|
+
const c = globalThis.crypto;
|
|
39
|
+
if (c && typeof c.randomUUID === "function") {
|
|
40
|
+
return c.randomUUID();
|
|
41
|
+
}
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Poolse: globalThis.crypto.randomUUID() is not available; supply `config.generateIdempotencyKey` instead."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/errors.ts
|
|
48
|
+
var PoolseError = class extends Error {
|
|
49
|
+
name = "PoolseError";
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message);
|
|
52
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var NetworkError = class extends PoolseError {
|
|
56
|
+
name = "NetworkError";
|
|
57
|
+
cause;
|
|
58
|
+
constructor(message, cause) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.cause = cause;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var ApiError = class extends PoolseError {
|
|
64
|
+
name = "ApiError";
|
|
65
|
+
status;
|
|
66
|
+
code;
|
|
67
|
+
docUrl;
|
|
68
|
+
details;
|
|
69
|
+
constructor(status, envelope) {
|
|
70
|
+
super(`[${status}] ${envelope.code}: ${envelope.message}`);
|
|
71
|
+
this.status = status;
|
|
72
|
+
this.code = envelope.code;
|
|
73
|
+
this.docUrl = envelope.doc_url;
|
|
74
|
+
this.details = envelope.details;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
var RateLimitedError = class extends ApiError {
|
|
78
|
+
name = "RateLimitedError";
|
|
79
|
+
retryAfterMs;
|
|
80
|
+
constructor(envelope, retryAfterMs) {
|
|
81
|
+
super(429, envelope);
|
|
82
|
+
this.retryAfterMs = retryAfterMs;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var AuthError = class extends ApiError {
|
|
86
|
+
name = "AuthError";
|
|
87
|
+
constructor(envelope) {
|
|
88
|
+
super(401, envelope);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// src/realtime/realtime.ts
|
|
93
|
+
var PoolseRealtime = class {
|
|
94
|
+
config;
|
|
95
|
+
tokenCache;
|
|
96
|
+
socketPath;
|
|
97
|
+
wsUrl;
|
|
98
|
+
socket = null;
|
|
99
|
+
conversations = /* @__PURE__ */ new Map();
|
|
100
|
+
userChannel = null;
|
|
101
|
+
status = "idle";
|
|
102
|
+
statusListeners = /* @__PURE__ */ new Set();
|
|
103
|
+
constructor(config, tokenCache, opts = {}) {
|
|
104
|
+
this.config = config;
|
|
105
|
+
this.tokenCache = tokenCache;
|
|
106
|
+
this.socketPath = opts.socketPath ?? "/socket";
|
|
107
|
+
this.wsUrl = opts.wsUrl ?? deriveWsUrl(config.apiUrl);
|
|
108
|
+
}
|
|
109
|
+
/** Current connection status. */
|
|
110
|
+
getStatus() {
|
|
111
|
+
return this.status;
|
|
112
|
+
}
|
|
113
|
+
/** Subscribe to connection-status changes. */
|
|
114
|
+
onStatus(listener) {
|
|
115
|
+
this.statusListeners.add(listener);
|
|
116
|
+
return () => this.statusListeners.delete(listener);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Open the socket (idempotent). Synchronous construction so callers
|
|
120
|
+
* can join a channel on the next line; the underlying WebSocket
|
|
121
|
+
* connects on the next tick once the JWT pre-fetch resolves.
|
|
122
|
+
*
|
|
123
|
+
* Phoenix.js invokes the `params` callback SYNCHRONOUSLY on every
|
|
124
|
+
* (re)connect and does NOT await its return value — see
|
|
125
|
+
* `phoenix/priv/static/phoenix.mjs::endPointURL`. So `params` has
|
|
126
|
+
* to read a token that's already in hand. We:
|
|
127
|
+
*
|
|
128
|
+
* 1. Construct the Socket immediately (sets `this.socket` so
|
|
129
|
+
* concurrent `conversation()` / `user()` callers can attach
|
|
130
|
+
* channels — Phoenix buffers joins until the socket opens).
|
|
131
|
+
* 2. Pre-fetch the JWT through `TokenCache`, which fills its
|
|
132
|
+
* internal cache.
|
|
133
|
+
* 3. Call `socket.connect()` so phoenix.js's first handshake reads
|
|
134
|
+
* a primed `peekToken()`.
|
|
135
|
+
*
|
|
136
|
+
* On reconnect, Phoenix calls `params()` again; the cache is still
|
|
137
|
+
* warm (default JWT exp ~1h, refresh window 30s) so `peekToken()`
|
|
138
|
+
* returns the live token. When the token genuinely expires, our
|
|
139
|
+
* REST 401 path invalidates the cache; the next reconnect's
|
|
140
|
+
* `peekToken()` is `null` and the handshake intentionally fails so
|
|
141
|
+
* the cache can re-fill on the next iteration.
|
|
142
|
+
*/
|
|
143
|
+
connect() {
|
|
144
|
+
if (this.socket) return;
|
|
145
|
+
this.setStatus("connecting");
|
|
146
|
+
const socket = new Socket(`${this.wsUrl}${this.socketPath}`, {
|
|
147
|
+
params: () => ({ token: this.tokenCache.peekToken() ?? "" })
|
|
148
|
+
// Phoenix's default reconnect strategy: 10ms, 50ms, 100ms, 150ms,
|
|
149
|
+
// 200ms, 250ms, 500ms, 1s, 2s, 5s — perfectly reasonable for chat.
|
|
150
|
+
});
|
|
151
|
+
socket.onOpen(() => this.setStatus("connected"));
|
|
152
|
+
socket.onClose(() => {
|
|
153
|
+
this.setStatus(this.status === "closed" ? "closed" : "reconnecting");
|
|
154
|
+
});
|
|
155
|
+
socket.onError((err) => {
|
|
156
|
+
this.setStatus("reconnecting");
|
|
157
|
+
this.config.onSocketError?.(new PoolseError(`socket error: ${String(err)}`));
|
|
158
|
+
});
|
|
159
|
+
this.socket = socket;
|
|
160
|
+
void this.tokenCache.getToken().catch((err) => {
|
|
161
|
+
this.config.onSocketError?.(
|
|
162
|
+
new PoolseError(`token fetch failed before socket open: ${String(err)}`)
|
|
163
|
+
);
|
|
164
|
+
}).finally(() => {
|
|
165
|
+
socket.connect();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Close the socket and tear down every joined channel. */
|
|
169
|
+
disconnect() {
|
|
170
|
+
this.setStatus("closed");
|
|
171
|
+
this.conversations.forEach((c) => c._destroy());
|
|
172
|
+
this.conversations.clear();
|
|
173
|
+
this.userChannel?._destroy();
|
|
174
|
+
this.userChannel = null;
|
|
175
|
+
if (this.socket) {
|
|
176
|
+
this.socket.disconnect();
|
|
177
|
+
this.socket = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Subscribe to a conversation. Returns a typed handle with
|
|
182
|
+
* `onMessage`, `onTyping`, etc. Reusing the same `id` returns the
|
|
183
|
+
* same handle — re-subscribing doesn't open a second channel.
|
|
184
|
+
*/
|
|
185
|
+
conversation(conversationId) {
|
|
186
|
+
const existing = this.conversations.get(conversationId);
|
|
187
|
+
if (existing) return existing;
|
|
188
|
+
this.connect();
|
|
189
|
+
if (!this.socket) {
|
|
190
|
+
throw new PoolseError("socket not initialised \u2014 call connect() first");
|
|
191
|
+
}
|
|
192
|
+
const channel = this.socket.channel(`conversation:${conversationId}`, {});
|
|
193
|
+
const handle = new ConversationChannel(conversationId, channel);
|
|
194
|
+
this.conversations.set(conversationId, handle);
|
|
195
|
+
handle._join();
|
|
196
|
+
return handle;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Subscribe to the current user's `user:<id>` channel. Only the user
|
|
200
|
+
* matching the JWT can join — poolse's UserChannel enforces this.
|
|
201
|
+
*/
|
|
202
|
+
user(userId) {
|
|
203
|
+
if (this.userChannel) return this.userChannel;
|
|
204
|
+
this.connect();
|
|
205
|
+
if (!this.socket) {
|
|
206
|
+
throw new PoolseError("socket not initialised \u2014 call connect() first");
|
|
207
|
+
}
|
|
208
|
+
const channel = this.socket.channel(`user:${userId}`, {});
|
|
209
|
+
const handle = new UserChannel(userId, channel);
|
|
210
|
+
this.userChannel = handle;
|
|
211
|
+
handle._join();
|
|
212
|
+
return handle;
|
|
213
|
+
}
|
|
214
|
+
/** Drop a conversation handle and leave the channel. */
|
|
215
|
+
leave(conversationId) {
|
|
216
|
+
const handle = this.conversations.get(conversationId);
|
|
217
|
+
if (!handle) return;
|
|
218
|
+
handle._destroy();
|
|
219
|
+
this.conversations.delete(conversationId);
|
|
220
|
+
}
|
|
221
|
+
setStatus(status) {
|
|
222
|
+
if (this.status === status) return;
|
|
223
|
+
this.status = status;
|
|
224
|
+
this.statusListeners.forEach((l) => l(status));
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
var ConversationChannel = class {
|
|
228
|
+
conversationId;
|
|
229
|
+
channel;
|
|
230
|
+
// Map from event-name → set of listeners. We bind one Phoenix `.on(...)`
|
|
231
|
+
// per event name (no matter how many JS listeners) and fan out
|
|
232
|
+
// ourselves — much cheaper than re-binding on every subscription.
|
|
233
|
+
listeners = /* @__PURE__ */ new Map();
|
|
234
|
+
constructor(conversationId, channel) {
|
|
235
|
+
this.conversationId = conversationId;
|
|
236
|
+
this.channel = channel;
|
|
237
|
+
}
|
|
238
|
+
/** New message pushed to the conversation. */
|
|
239
|
+
onMessage(fn) {
|
|
240
|
+
return this.subscribe("message:new", fn);
|
|
241
|
+
}
|
|
242
|
+
/** Existing message edited by its sender. */
|
|
243
|
+
onMessageUpdated(fn) {
|
|
244
|
+
return this.subscribe("message:updated", fn);
|
|
245
|
+
}
|
|
246
|
+
/** Tombstone for a soft-deleted message. */
|
|
247
|
+
onMessageDeleted(fn) {
|
|
248
|
+
return this.subscribe("message:deleted", fn);
|
|
249
|
+
}
|
|
250
|
+
onTypingStart(fn) {
|
|
251
|
+
return this.subscribe("typing:start", fn);
|
|
252
|
+
}
|
|
253
|
+
onTypingStop(fn) {
|
|
254
|
+
return this.subscribe("typing:stop", fn);
|
|
255
|
+
}
|
|
256
|
+
onReactionAdded(fn) {
|
|
257
|
+
return this.subscribe("reaction:added", fn);
|
|
258
|
+
}
|
|
259
|
+
onReactionRemoved(fn) {
|
|
260
|
+
return this.subscribe("reaction:removed", fn);
|
|
261
|
+
}
|
|
262
|
+
onPresenceState(fn) {
|
|
263
|
+
return this.subscribe("presence_state", fn);
|
|
264
|
+
}
|
|
265
|
+
onPresenceDiff(fn) {
|
|
266
|
+
return this.subscribe("presence_diff", fn);
|
|
267
|
+
}
|
|
268
|
+
/** Send a typing ping to the server. Debounced server-side. */
|
|
269
|
+
sendTyping() {
|
|
270
|
+
this.channel.push("typing", {});
|
|
271
|
+
}
|
|
272
|
+
/** @internal — called by `PoolseRealtime.conversation/1`. */
|
|
273
|
+
_join() {
|
|
274
|
+
this.channel.join();
|
|
275
|
+
}
|
|
276
|
+
/** @internal — called when the consumer leaves this conversation. */
|
|
277
|
+
_destroy() {
|
|
278
|
+
this.listeners.clear();
|
|
279
|
+
this.channel.leave();
|
|
280
|
+
}
|
|
281
|
+
subscribe(event, fn) {
|
|
282
|
+
let set = this.listeners.get(event);
|
|
283
|
+
if (!set) {
|
|
284
|
+
set = /* @__PURE__ */ new Set();
|
|
285
|
+
this.listeners.set(event, set);
|
|
286
|
+
this.channel.on(event, (payload) => {
|
|
287
|
+
const listeners = this.listeners.get(event);
|
|
288
|
+
if (listeners) listeners.forEach((l) => l(payload));
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
set.add(fn);
|
|
292
|
+
return () => {
|
|
293
|
+
set.delete(fn);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var UserChannel = class {
|
|
298
|
+
userId;
|
|
299
|
+
channel;
|
|
300
|
+
mentionListeners = /* @__PURE__ */ new Set();
|
|
301
|
+
conversationCreatedListeners = /* @__PURE__ */ new Set();
|
|
302
|
+
mentionBound = false;
|
|
303
|
+
conversationCreatedBound = false;
|
|
304
|
+
constructor(userId, channel) {
|
|
305
|
+
this.userId = userId;
|
|
306
|
+
this.channel = channel;
|
|
307
|
+
}
|
|
308
|
+
onMention(fn) {
|
|
309
|
+
if (!this.mentionBound) {
|
|
310
|
+
this.mentionBound = true;
|
|
311
|
+
this.channel.on("mention:new", (payload) => {
|
|
312
|
+
this.mentionListeners.forEach((l) => l(payload));
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
this.mentionListeners.add(fn);
|
|
316
|
+
return () => {
|
|
317
|
+
this.mentionListeners.delete(fn);
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Subscribe to "you've been added to a conversation" notifications.
|
|
322
|
+
* Fires once per new membership — either because you created the
|
|
323
|
+
* conversation, or because someone added you to an existing one.
|
|
324
|
+
*
|
|
325
|
+
* Payload is the full {@link Conversation} row so consumers can
|
|
326
|
+
* prepend it to a local list without a refetch.
|
|
327
|
+
*/
|
|
328
|
+
onConversationCreated(fn) {
|
|
329
|
+
if (!this.conversationCreatedBound) {
|
|
330
|
+
this.conversationCreatedBound = true;
|
|
331
|
+
this.channel.on("conversation:created", (payload) => {
|
|
332
|
+
this.conversationCreatedListeners.forEach((l) => l(payload));
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
this.conversationCreatedListeners.add(fn);
|
|
336
|
+
return () => {
|
|
337
|
+
this.conversationCreatedListeners.delete(fn);
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** @internal */
|
|
341
|
+
_join() {
|
|
342
|
+
this.channel.join();
|
|
343
|
+
}
|
|
344
|
+
/** @internal */
|
|
345
|
+
_destroy() {
|
|
346
|
+
this.mentionListeners.clear();
|
|
347
|
+
this.conversationCreatedListeners.clear();
|
|
348
|
+
this.channel.leave();
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
function deriveWsUrl(apiUrl) {
|
|
352
|
+
return apiUrl.replace(/^http/, "ws");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/resources/attachments.ts
|
|
356
|
+
var AttachmentsResource = class {
|
|
357
|
+
/**
|
|
358
|
+
* The PUT to the presigned URL bypasses the SDK's authenticated
|
|
359
|
+
* REST client (presigned URLs encode their own auth and MUST NOT
|
|
360
|
+
* receive an `Authorization` header). It still respects
|
|
361
|
+
* `config.fetch` if the customer provided one — required for tests
|
|
362
|
+
* with a mock fetch, and for runtimes where `globalThis.fetch` is
|
|
363
|
+
* not the right transport.
|
|
364
|
+
*/
|
|
365
|
+
constructor(client, fetchFn) {
|
|
366
|
+
this.client = client;
|
|
367
|
+
this.fetchFn = fetchFn;
|
|
368
|
+
}
|
|
369
|
+
client;
|
|
370
|
+
fetchFn;
|
|
371
|
+
/**
|
|
372
|
+
* Step 1 of an upload — request a presigned PUT URL. Use this when
|
|
373
|
+
* you want to drive the PUT yourself (e.g. resumable uploads,
|
|
374
|
+
* React Native FileSystem). For the common case prefer
|
|
375
|
+
* {@link upload}, which does both steps for you.
|
|
376
|
+
*/
|
|
377
|
+
requestUpload(attrs, opts = {}) {
|
|
378
|
+
return this.client.request({
|
|
379
|
+
method: "POST",
|
|
380
|
+
path: "/v1/attachments/upload-url",
|
|
381
|
+
body: attrs,
|
|
382
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* One-call upload: request a presigned URL, PUT the bytes to it,
|
|
387
|
+
* return the attachment row. After this resolves the attachment is
|
|
388
|
+
* ready to be referenced from a message send.
|
|
389
|
+
*
|
|
390
|
+
* ```ts
|
|
391
|
+
* // Browser <input type="file">:
|
|
392
|
+
* const file = inputEl.files![0]!;
|
|
393
|
+
* const att = await chat.attachments.upload({
|
|
394
|
+
* body: file,
|
|
395
|
+
* contentType: file.type,
|
|
396
|
+
* byteSize: file.size,
|
|
397
|
+
* filename: file.name,
|
|
398
|
+
* });
|
|
399
|
+
* await chat.conversations.one(convId).messages.send({
|
|
400
|
+
* body: 'Look at this!',
|
|
401
|
+
* custom_data: { attachment_id: att.id },
|
|
402
|
+
* });
|
|
403
|
+
* ```
|
|
404
|
+
*
|
|
405
|
+
* Note: the PUT uses the runtime's bare `fetch` (NOT the SDK's
|
|
406
|
+
* authenticated REST client) — presigned URLs already encode their
|
|
407
|
+
* own auth and MUST NOT receive an `Authorization` header.
|
|
408
|
+
*/
|
|
409
|
+
async upload(input, opts = {}) {
|
|
410
|
+
const req = {
|
|
411
|
+
content_type: input.contentType,
|
|
412
|
+
byte_size: input.byteSize,
|
|
413
|
+
...input.filename !== void 0 ? { original_filename: input.filename } : {}
|
|
414
|
+
};
|
|
415
|
+
const { attachment, upload } = await this.requestUpload(req, opts);
|
|
416
|
+
const putInit = {
|
|
417
|
+
method: upload.method.toUpperCase(),
|
|
418
|
+
headers: upload.headers,
|
|
419
|
+
body: input.body,
|
|
420
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
421
|
+
};
|
|
422
|
+
const res = await this.fetchFn(upload.url, putInit);
|
|
423
|
+
if (!res.ok) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Poolse: presigned upload PUT failed (${res.status}) for attachment ${attachment.id}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return attachment;
|
|
429
|
+
}
|
|
430
|
+
/** Returns a handle for further operations on a single attachment. */
|
|
431
|
+
one(id) {
|
|
432
|
+
return new AttachmentHandle(this.client, id);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
var AttachmentHandle = class {
|
|
436
|
+
constructor(client, id) {
|
|
437
|
+
this.client = client;
|
|
438
|
+
this.id = id;
|
|
439
|
+
}
|
|
440
|
+
client;
|
|
441
|
+
id;
|
|
442
|
+
/**
|
|
443
|
+
* Request a presigned GET URL (~1h TTL). Conversation-member-gated
|
|
444
|
+
* server-side. Useful when rendering files in chat: cache the URL
|
|
445
|
+
* client-side until close to expiry, then re-fetch.
|
|
446
|
+
*/
|
|
447
|
+
downloadUrl(opts = {}) {
|
|
448
|
+
return this.client.request({
|
|
449
|
+
method: "GET",
|
|
450
|
+
path: `/v1/attachments/${this.id}/download-url`,
|
|
451
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Delete the attachment row + best-effort bucket object delete.
|
|
456
|
+
* Authz: uploader (while still `:pending`) or message-sender / conv
|
|
457
|
+
* owner-admin (once linked).
|
|
458
|
+
*/
|
|
459
|
+
delete(opts = {}) {
|
|
460
|
+
return this.client.request({
|
|
461
|
+
method: "DELETE",
|
|
462
|
+
path: `/v1/attachments/${this.id}`,
|
|
463
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// src/resources/messages.ts
|
|
469
|
+
var ConversationMessages = class {
|
|
470
|
+
constructor(client, conversationId) {
|
|
471
|
+
this.client = client;
|
|
472
|
+
this.conversationId = conversationId;
|
|
473
|
+
}
|
|
474
|
+
client;
|
|
475
|
+
conversationId;
|
|
476
|
+
list(opts = {}, signal) {
|
|
477
|
+
return this.client.request({
|
|
478
|
+
method: "GET",
|
|
479
|
+
path: `/v1/conversations/${this.conversationId}/messages`,
|
|
480
|
+
query: {
|
|
481
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {},
|
|
482
|
+
...opts.before !== void 0 ? { before: opts.before } : {}
|
|
483
|
+
},
|
|
484
|
+
...signal ? { signal } : {}
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Send a message to this conversation.
|
|
489
|
+
*
|
|
490
|
+
* If `attrs.id` is omitted the SDK generates a v4 UUID and uses it
|
|
491
|
+
* as both the wire-level idempotency key AND the literal message.id
|
|
492
|
+
* the server stores. Two side-effects that make a real-time UI
|
|
493
|
+
* trivial:
|
|
494
|
+
*
|
|
495
|
+
* * Resending the same `id` (e.g. a network-retry) returns the
|
|
496
|
+
* ORIGINAL message instead of inserting a duplicate.
|
|
497
|
+
* * The realtime `message:new` broadcast carries this same id,
|
|
498
|
+
* so an optimistic UI can pre-render the row under the final id
|
|
499
|
+
* and dedup by id alone — no client/server id swap needed.
|
|
500
|
+
*
|
|
501
|
+
* Pass an explicit `attrs.id` only when you generated it yourself
|
|
502
|
+
* upstream (e.g. you already render an optimistic row in your hook
|
|
503
|
+
* and want the server to confirm under the same key).
|
|
504
|
+
*/
|
|
505
|
+
send(attrs, signal) {
|
|
506
|
+
const body = attrs.id !== void 0 ? attrs : { ...attrs, id: generateClientMessageId() };
|
|
507
|
+
return this.client.request({
|
|
508
|
+
method: "POST",
|
|
509
|
+
path: `/v1/conversations/${this.conversationId}/messages`,
|
|
510
|
+
body,
|
|
511
|
+
...signal ? { signal } : {}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
markRead(messageId, signal) {
|
|
515
|
+
return this.client.request({
|
|
516
|
+
method: "POST",
|
|
517
|
+
path: `/v1/conversations/${this.conversationId}/read`,
|
|
518
|
+
body: { message_id: messageId },
|
|
519
|
+
...signal ? { signal } : {}
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
var MessageHandle = class {
|
|
524
|
+
constructor(client, id) {
|
|
525
|
+
this.client = client;
|
|
526
|
+
this.id = id;
|
|
527
|
+
}
|
|
528
|
+
client;
|
|
529
|
+
id;
|
|
530
|
+
update(attrs, signal) {
|
|
531
|
+
return this.client.request({
|
|
532
|
+
method: "PATCH",
|
|
533
|
+
path: `/v1/messages/${this.id}`,
|
|
534
|
+
body: attrs,
|
|
535
|
+
...signal ? { signal } : {}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
delete(signal) {
|
|
539
|
+
return this.client.request({
|
|
540
|
+
method: "DELETE",
|
|
541
|
+
path: `/v1/messages/${this.id}`,
|
|
542
|
+
...signal ? { signal } : {}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
replies(opts = {}, signal) {
|
|
546
|
+
return this.client.request({
|
|
547
|
+
method: "GET",
|
|
548
|
+
path: `/v1/messages/${this.id}/replies`,
|
|
549
|
+
query: {
|
|
550
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {},
|
|
551
|
+
...opts.after !== void 0 ? { after: opts.after } : {}
|
|
552
|
+
},
|
|
553
|
+
...signal ? { signal } : {}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
addReaction(emoji, signal) {
|
|
557
|
+
const body = { emoji };
|
|
558
|
+
return this.client.request({
|
|
559
|
+
method: "POST",
|
|
560
|
+
path: `/v1/messages/${this.id}/reactions`,
|
|
561
|
+
body,
|
|
562
|
+
...signal ? { signal } : {}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
removeReaction(emoji, signal) {
|
|
566
|
+
return this.client.request({
|
|
567
|
+
method: "DELETE",
|
|
568
|
+
path: `/v1/messages/${this.id}/reactions/${encodeURIComponent(emoji)}`,
|
|
569
|
+
...signal ? { signal } : {}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var MessagesResource = class {
|
|
574
|
+
constructor(client) {
|
|
575
|
+
this.client = client;
|
|
576
|
+
}
|
|
577
|
+
client;
|
|
578
|
+
one(id) {
|
|
579
|
+
return new MessageHandle(this.client, id);
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
function generateClientMessageId() {
|
|
583
|
+
const c = globalThis.crypto;
|
|
584
|
+
if (c && typeof c.randomUUID === "function") {
|
|
585
|
+
return c.randomUUID();
|
|
586
|
+
}
|
|
587
|
+
throw new Error(
|
|
588
|
+
"Poolse: globalThis.crypto.randomUUID() unavailable \u2014 pass `id` explicitly in MessageCreateRequest (your env lacks a built-in UUID generator)."
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/resources/conversations.ts
|
|
593
|
+
var ConversationHandle = class {
|
|
594
|
+
constructor(client, id) {
|
|
595
|
+
this.client = client;
|
|
596
|
+
this.id = id;
|
|
597
|
+
this.messages = new ConversationMessages(this.client, this.id);
|
|
598
|
+
}
|
|
599
|
+
client;
|
|
600
|
+
id;
|
|
601
|
+
/**
|
|
602
|
+
* Message ops scoped to this conversation: `list`, `send`, `markRead`.
|
|
603
|
+
* Lazy: constructed on first access so an idle handle stays cheap.
|
|
604
|
+
*/
|
|
605
|
+
messages;
|
|
606
|
+
show(signal) {
|
|
607
|
+
return this.client.request({
|
|
608
|
+
method: "GET",
|
|
609
|
+
path: `/v1/conversations/${this.id}`,
|
|
610
|
+
...signal ? { signal } : {}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
update(attrs, signal) {
|
|
614
|
+
return this.client.request({
|
|
615
|
+
method: "PATCH",
|
|
616
|
+
path: `/v1/conversations/${this.id}`,
|
|
617
|
+
body: attrs,
|
|
618
|
+
...signal ? { signal } : {}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
// ── members ────────────────────────────────────────────────────────────
|
|
622
|
+
listMembers(signal) {
|
|
623
|
+
return this.client.request({
|
|
624
|
+
method: "GET",
|
|
625
|
+
path: `/v1/conversations/${this.id}/members`,
|
|
626
|
+
...signal ? { signal } : {}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Add multiple users to this conversation in one round-trip.
|
|
631
|
+
*
|
|
632
|
+
* `externalIds` are the stable customer-side identifiers you passed
|
|
633
|
+
* to `POST /v1/users` when creating each user — the server resolves
|
|
634
|
+
* them to internal user_ids and creates one membership row per id.
|
|
635
|
+
*
|
|
636
|
+
* Requires `:manage_members` on this conversation (owner or admin).
|
|
637
|
+
*
|
|
638
|
+
* ```ts
|
|
639
|
+
* await chat.conversations.one(convId).addMembers(['alice', 'bob']);
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
addMembers(externalIds, opts = {}) {
|
|
643
|
+
return this.client.request({
|
|
644
|
+
method: "POST",
|
|
645
|
+
path: `/v1/conversations/${this.id}/members`,
|
|
646
|
+
body: {
|
|
647
|
+
external_ids: externalIds,
|
|
648
|
+
...opts.role !== void 0 ? { role: opts.role } : {}
|
|
649
|
+
},
|
|
650
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Add a single user. Convenience wrapper around {@link addMembers}
|
|
655
|
+
* that unwraps the returned list to the single membership row.
|
|
656
|
+
*
|
|
657
|
+
* ```ts
|
|
658
|
+
* const m = await chat.conversations.one(convId).addMember('alice');
|
|
659
|
+
* ```
|
|
660
|
+
*/
|
|
661
|
+
async addMember(externalId, opts = {}) {
|
|
662
|
+
const list = await this.addMembers([externalId], opts);
|
|
663
|
+
const row = list.data[0];
|
|
664
|
+
if (!row) {
|
|
665
|
+
throw new Error("Poolse: addMember succeeded but server returned no membership row.");
|
|
666
|
+
}
|
|
667
|
+
return row;
|
|
668
|
+
}
|
|
669
|
+
removeMember(userId, signal) {
|
|
670
|
+
return this.client.request({
|
|
671
|
+
method: "DELETE",
|
|
672
|
+
path: `/v1/conversations/${this.id}/members/${userId}`,
|
|
673
|
+
...signal ? { signal } : {}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
var ConversationsResource = class {
|
|
678
|
+
constructor(client) {
|
|
679
|
+
this.client = client;
|
|
680
|
+
}
|
|
681
|
+
client;
|
|
682
|
+
list(signal) {
|
|
683
|
+
return this.client.request({
|
|
684
|
+
method: "GET",
|
|
685
|
+
path: "/v1/conversations",
|
|
686
|
+
...signal ? { signal } : {}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
create(attrs, signal) {
|
|
690
|
+
return this.client.request({
|
|
691
|
+
method: "POST",
|
|
692
|
+
path: "/v1/conversations",
|
|
693
|
+
body: attrs,
|
|
694
|
+
...signal ? { signal } : {}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/** Returns a handle for further operations on a single conversation. */
|
|
698
|
+
one(id) {
|
|
699
|
+
return new ConversationHandle(this.client, id);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// src/resources/me.ts
|
|
704
|
+
var MeResource = class {
|
|
705
|
+
constructor(client) {
|
|
706
|
+
this.client = client;
|
|
707
|
+
}
|
|
708
|
+
client;
|
|
709
|
+
/** GET /v1/me */
|
|
710
|
+
show(signal) {
|
|
711
|
+
return this.client.request({
|
|
712
|
+
method: "GET",
|
|
713
|
+
path: "/v1/me",
|
|
714
|
+
...signal ? { signal } : {}
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
// src/rest-client.ts
|
|
720
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET"]);
|
|
721
|
+
var RestClient = class {
|
|
722
|
+
config;
|
|
723
|
+
tokenCache;
|
|
724
|
+
constructor(config, tokenCache) {
|
|
725
|
+
this.config = config;
|
|
726
|
+
this.tokenCache = tokenCache;
|
|
727
|
+
}
|
|
728
|
+
async request(opts) {
|
|
729
|
+
const url = this.buildUrl(opts.path, opts.query);
|
|
730
|
+
const maxRetries = opts.maxRetries ?? this.config.maxRetries;
|
|
731
|
+
const idempotencyKey = this.resolveIdempotencyKey(opts);
|
|
732
|
+
let attempt = 0;
|
|
733
|
+
let triedAuthRefresh = false;
|
|
734
|
+
for (; ; ) {
|
|
735
|
+
const body = opts.body === void 0 ? void 0 : JSON.stringify(opts.body);
|
|
736
|
+
const headers = await this.buildHeaders(opts.method, idempotencyKey, body !== void 0);
|
|
737
|
+
let response;
|
|
738
|
+
try {
|
|
739
|
+
const init = { method: opts.method, headers };
|
|
740
|
+
if (body !== void 0) init.body = body;
|
|
741
|
+
if (opts.signal) init.signal = opts.signal;
|
|
742
|
+
response = await this.config.fetch(url, init);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
if (attempt < maxRetries && isRetryableNetworkError(err)) {
|
|
745
|
+
await sleep(this.backoffDelay(attempt));
|
|
746
|
+
attempt += 1;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
throw new NetworkError("Network request failed", err);
|
|
750
|
+
}
|
|
751
|
+
if (response.status >= 200 && response.status < 300) {
|
|
752
|
+
return await parseJsonOrNull(response);
|
|
753
|
+
}
|
|
754
|
+
if (response.status === 401) {
|
|
755
|
+
if (!triedAuthRefresh) {
|
|
756
|
+
this.tokenCache.invalidate();
|
|
757
|
+
triedAuthRefresh = true;
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
throw new AuthError(await parseEnvelope(response));
|
|
761
|
+
}
|
|
762
|
+
if (response.status === 429) {
|
|
763
|
+
const envelope = await parseEnvelope(response);
|
|
764
|
+
const retryAfterMs = retryAfterHeaderMs(response);
|
|
765
|
+
if (attempt < maxRetries) {
|
|
766
|
+
await sleep(Math.max(retryAfterMs ?? 0, this.backoffDelay(attempt)));
|
|
767
|
+
attempt += 1;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
throw new RateLimitedError(envelope, retryAfterMs ?? 0);
|
|
771
|
+
}
|
|
772
|
+
if (response.status >= 500 && attempt < maxRetries) {
|
|
773
|
+
await sleep(this.backoffDelay(attempt));
|
|
774
|
+
attempt += 1;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
throw new ApiError(response.status, await parseEnvelope(response));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// ── helpers ────────────────────────────────────────────────────────────
|
|
781
|
+
buildUrl(path, query) {
|
|
782
|
+
const base = `${this.config.apiUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
783
|
+
if (!query) return base;
|
|
784
|
+
const params = new URLSearchParams();
|
|
785
|
+
for (const [k, v] of Object.entries(query)) {
|
|
786
|
+
if (v !== void 0 && v !== null) {
|
|
787
|
+
params.append(k, String(v));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const qs = params.toString();
|
|
791
|
+
return qs ? `${base}?${qs}` : base;
|
|
792
|
+
}
|
|
793
|
+
async buildHeaders(method, idempotencyKey, hasBody) {
|
|
794
|
+
const headers = { Accept: "application/json" };
|
|
795
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
796
|
+
const token = await this.config.getToken();
|
|
797
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
798
|
+
if (!IDEMPOTENT_METHODS.has(method) && idempotencyKey) {
|
|
799
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
800
|
+
}
|
|
801
|
+
return headers;
|
|
802
|
+
}
|
|
803
|
+
resolveIdempotencyKey(opts) {
|
|
804
|
+
if (opts.idempotencyKey === null) return null;
|
|
805
|
+
if (opts.idempotencyKey) return opts.idempotencyKey;
|
|
806
|
+
if (IDEMPOTENT_METHODS.has(opts.method)) return null;
|
|
807
|
+
return this.config.generateIdempotencyKey();
|
|
808
|
+
}
|
|
809
|
+
backoffDelay(attempt) {
|
|
810
|
+
const exp = this.config.baseBackoffMs * 2 ** attempt;
|
|
811
|
+
const capped = Math.min(this.config.maxBackoffMs, exp);
|
|
812
|
+
return Math.floor(Math.random() * capped);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
async function parseEnvelope(response) {
|
|
816
|
+
try {
|
|
817
|
+
const json = await response.json();
|
|
818
|
+
if (json?.error?.code) return json.error;
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
code: "unknown_error",
|
|
823
|
+
message: `HTTP ${response.status}`,
|
|
824
|
+
doc_url: ""
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async function parseJsonOrNull(response) {
|
|
828
|
+
if (response.status === 204) return null;
|
|
829
|
+
const text = await response.text();
|
|
830
|
+
if (!text) return null;
|
|
831
|
+
return JSON.parse(text);
|
|
832
|
+
}
|
|
833
|
+
function retryAfterHeaderMs(response) {
|
|
834
|
+
const raw = response.headers.get("retry-after");
|
|
835
|
+
if (!raw) return null;
|
|
836
|
+
const seconds = Number.parseInt(raw, 10);
|
|
837
|
+
return Number.isFinite(seconds) ? seconds * 1e3 : null;
|
|
838
|
+
}
|
|
839
|
+
function isRetryableNetworkError(err) {
|
|
840
|
+
if (err instanceof DOMException && err.name === "AbortError") return false;
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
function sleep(ms) {
|
|
844
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/token-cache.ts
|
|
848
|
+
var REFRESH_BUFFER_MS = 3e4;
|
|
849
|
+
var FALLBACK_TTL_MS = 6e4;
|
|
850
|
+
var TokenCache = class {
|
|
851
|
+
constructor(fetcher) {
|
|
852
|
+
this.fetcher = fetcher;
|
|
853
|
+
}
|
|
854
|
+
fetcher;
|
|
855
|
+
token = null;
|
|
856
|
+
expMs = null;
|
|
857
|
+
inFlight = null;
|
|
858
|
+
/**
|
|
859
|
+
* Synchronously return the cached token without triggering a fetch.
|
|
860
|
+
* Returns `null` if the cache is empty OR if the cached token is
|
|
861
|
+
* within the refresh window (treating near-expiry tokens as stale
|
|
862
|
+
* keeps the realtime layer from handshaking with an about-to-expire
|
|
863
|
+
* JWT when a refresh is already due).
|
|
864
|
+
*
|
|
865
|
+
* Exists for callers like Phoenix.js's `params` callback that the
|
|
866
|
+
* library invokes synchronously and does NOT await — see
|
|
867
|
+
* `phoenix/priv/static/phoenix.mjs::endPointURL()`.
|
|
868
|
+
*/
|
|
869
|
+
peekToken() {
|
|
870
|
+
if (this.token === null || this.expMs === null) return this.token;
|
|
871
|
+
return Date.now() < this.expMs - REFRESH_BUFFER_MS ? this.token : null;
|
|
872
|
+
}
|
|
873
|
+
async getToken(opts = {}) {
|
|
874
|
+
if (opts.forceRefresh) this.invalidate();
|
|
875
|
+
const now = Date.now();
|
|
876
|
+
if (this.token && this.expMs !== null && now < this.expMs - REFRESH_BUFFER_MS) {
|
|
877
|
+
return this.token;
|
|
878
|
+
}
|
|
879
|
+
if (this.inFlight) return this.inFlight;
|
|
880
|
+
this.inFlight = this.fetchAndStore().finally(() => {
|
|
881
|
+
this.inFlight = null;
|
|
882
|
+
});
|
|
883
|
+
return this.inFlight;
|
|
884
|
+
}
|
|
885
|
+
invalidate() {
|
|
886
|
+
this.token = null;
|
|
887
|
+
this.expMs = null;
|
|
888
|
+
}
|
|
889
|
+
async fetchAndStore() {
|
|
890
|
+
const token = await this.fetcher();
|
|
891
|
+
if (!token) {
|
|
892
|
+
this.token = null;
|
|
893
|
+
this.expMs = null;
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
const expSec = parseJwtExp(token);
|
|
897
|
+
if (expSec !== null) {
|
|
898
|
+
const expMs = expSec * 1e3;
|
|
899
|
+
this.expMs = expMs <= Date.now() ? Date.now() + FALLBACK_TTL_MS : expMs;
|
|
900
|
+
} else {
|
|
901
|
+
this.expMs = Date.now() + FALLBACK_TTL_MS;
|
|
902
|
+
}
|
|
903
|
+
this.token = token;
|
|
904
|
+
return token;
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
function parseJwtExp(token) {
|
|
908
|
+
const parts = token.split(".");
|
|
909
|
+
if (parts.length !== 3) return null;
|
|
910
|
+
try {
|
|
911
|
+
const json = decodeBase64Url(parts[1]);
|
|
912
|
+
const payload = JSON.parse(json);
|
|
913
|
+
return typeof payload.exp === "number" && Number.isFinite(payload.exp) ? payload.exp : null;
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
function decodeBase64Url(s) {
|
|
919
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
920
|
+
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
921
|
+
if (typeof atob === "function") return atob(padded);
|
|
922
|
+
const g = globalThis;
|
|
923
|
+
if (g.Buffer) return g.Buffer.from(padded, "base64").toString("binary");
|
|
924
|
+
throw new Error("No base64 decoder available");
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/poolse.ts
|
|
928
|
+
var Poolse = class {
|
|
929
|
+
/** `/v1/me` — current End User. */
|
|
930
|
+
me;
|
|
931
|
+
/** `/v1/conversations` collection + per-conversation handle factory. */
|
|
932
|
+
conversations;
|
|
933
|
+
/** `/v1/messages/:id/*` — accessed via `chat.messages.one(id)`. */
|
|
934
|
+
messages;
|
|
935
|
+
/** `/v1/attachments/*` — presigned-URL uploads/downloads. */
|
|
936
|
+
attachments;
|
|
937
|
+
/**
|
|
938
|
+
* Low-level REST client. Exposed for advanced use cases (custom endpoints,
|
|
939
|
+
* raw retry/headers control). Most callers should use the resources above.
|
|
940
|
+
*/
|
|
941
|
+
rest;
|
|
942
|
+
/**
|
|
943
|
+
* WebSocket / Phoenix Channels client. Lazily connects on the first
|
|
944
|
+
* `poolse.realtime.conversation(id)` / `poolse.realtime.user(id)`
|
|
945
|
+
* call — passing `config.apiUrl` (with `http(s)://` swapped to
|
|
946
|
+
* `ws(s)://`) for the socket URL by default, overridable via
|
|
947
|
+
* `config.wsUrl`.
|
|
948
|
+
*/
|
|
949
|
+
realtime;
|
|
950
|
+
resolved;
|
|
951
|
+
tokenCache;
|
|
952
|
+
constructor(config) {
|
|
953
|
+
this.resolved = resolveConfig(config);
|
|
954
|
+
this.tokenCache = new TokenCache(this.resolved.getToken);
|
|
955
|
+
const cachedConfig = {
|
|
956
|
+
...this.resolved,
|
|
957
|
+
getToken: () => this.tokenCache.getToken()
|
|
958
|
+
};
|
|
959
|
+
this.rest = new RestClient(cachedConfig, this.tokenCache);
|
|
960
|
+
this.me = new MeResource(this.rest);
|
|
961
|
+
this.conversations = new ConversationsResource(this.rest);
|
|
962
|
+
this.messages = new MessagesResource(this.rest);
|
|
963
|
+
this.attachments = new AttachmentsResource(this.rest, cachedConfig.fetch);
|
|
964
|
+
this.realtime = new PoolseRealtime(cachedConfig, this.tokenCache, {
|
|
965
|
+
...this.resolved.wsUrl !== void 0 ? { wsUrl: this.resolved.wsUrl } : {},
|
|
966
|
+
socketPath: this.resolved.socketPath
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Tear down the SDK: close the WebSocket, drop all channels.
|
|
971
|
+
* No-op for REST — fetch() doesn't keep persistent state.
|
|
972
|
+
* Call this when the user signs out or the SDK instance is
|
|
973
|
+
* being replaced.
|
|
974
|
+
*/
|
|
975
|
+
destroy() {
|
|
976
|
+
this.realtime.disconnect();
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
// src/version.ts
|
|
981
|
+
var version = "0.0.1";
|
|
982
|
+
|
|
983
|
+
export { ApiError, AttachmentHandle, AttachmentsResource, AuthError, ConversationChannel, ConversationHandle, ConversationMessages, ConversationsResource, MeResource, MessageHandle, MessagesResource, NetworkError, Poolse, PoolseError, PoolseRealtime, RateLimitedError, UserChannel, version };
|
|
984
|
+
//# sourceMappingURL=index.js.map
|
|
985
|
+
//# sourceMappingURL=index.js.map
|