@poolse/sdk 1.0.1-beta.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,12 +48,14 @@ chat.destroy();
48
48
 
49
49
  ## What it covers
50
50
 
51
- - **REST resources**: `me`, `conversations` (incl. members), `messages` (send / edit / delete / replies / reactions / mark-read), `attachments` (presigned upload + download + delete).
51
+ - **REST resources**: `me`, `conversations` (incl. members), `messages` (send / edit / delete / replies / reactions / mark-read / quote-replies), `attachments` (presigned upload + download + delete with upload progress), `users` (customer-supplied profile cache).
52
52
  - **Realtime channels**: `message:new`, `message:updated`, `message:deleted`, `typing:start/stop`, `reaction:added/removed`, presence state + diff, per-user `mention:new` + `conversation:created`.
53
53
  - **Token caching** — `getToken` is called once per JWT lifetime, not per request. Auto-invalidates on 401 and re-issues.
54
54
  - **Idempotency** — every non-GET request carries an auto-generated `Idempotency-Key`, so retries (network or 5xx) never insert duplicates.
55
55
  - **Backoff** — exponential with full jitter, honors `Retry-After` on 429, never retries `AbortError`.
56
56
  - **Typed errors** — `AuthError` / `ApiError` / `NetworkError` / `RateLimitedError`, all `instanceof`-checkable.
57
+ - **Upload progress** — pass `onProgress` to `attachments.upload(...)` and the SDK switches to XHR for the PUT so byte-level progress events fire during the upload to your storage backend.
58
+ - **User resolution** — pass `userResolver(userId)` and `chat.users.get(userId)` returns customer-supplied `{ displayName, avatarUrl }` (in-memory cached + dedup'd). The React UI components pick this up automatically; vanilla callers can read it directly.
57
59
 
58
60
  ## Architecture pattern
59
61
 
package/dist/index.cjs CHANGED
@@ -232,9 +232,32 @@ var ConversationChannel = class {
232
232
  // per event name (no matter how many JS listeners) and fan out
233
233
  // ourselves — much cheaper than re-binding on every subscription.
234
234
  listeners = /* @__PURE__ */ new Map();
235
+ // Phoenix.Presence pushes `presence_state` exactly ONCE right after
236
+ // join, then `presence_diff` for every change. Late subscribers
237
+ // (MemberList mounted after ConversationView has already joined the
238
+ // channel) would otherwise never see the initial snapshot and stay
239
+ // empty until somebody else joins or leaves. We cache the running
240
+ // state here and replay it on subscribe.
241
+ presenceState = {};
242
+ presenceStateSeen = false;
235
243
  constructor(conversationId, channel) {
236
244
  this.conversationId = conversationId;
237
245
  this.channel = channel;
246
+ channel.on("presence_state", (payload) => {
247
+ this.presenceState = payload ?? {};
248
+ this.presenceStateSeen = true;
249
+ const listeners = this.listeners.get("presence_state");
250
+ if (listeners) listeners.forEach((l) => l(this.presenceState));
251
+ });
252
+ channel.on("presence_diff", (payload) => {
253
+ const diff = payload ?? {};
254
+ const next = { ...this.presenceState };
255
+ if (diff.joins) for (const [k, v] of Object.entries(diff.joins)) next[k] = v;
256
+ if (diff.leaves) for (const k of Object.keys(diff.leaves)) delete next[k];
257
+ this.presenceState = next;
258
+ const listeners = this.listeners.get("presence_diff");
259
+ if (listeners) listeners.forEach((l) => l(payload));
260
+ });
238
261
  }
239
262
  /** New message pushed to the conversation. */
240
263
  onMessage(fn) {
@@ -268,11 +291,24 @@ var ConversationChannel = class {
268
291
  return this.subscribe("member:read", fn);
269
292
  }
270
293
  onPresenceState(fn) {
271
- return this.subscribe("presence_state", fn);
294
+ let set = this.listeners.get("presence_state");
295
+ if (!set) {
296
+ set = /* @__PURE__ */ new Set();
297
+ this.listeners.set("presence_state", set);
298
+ }
299
+ set.add(fn);
300
+ if (this.presenceStateSeen) fn(this.presenceState);
301
+ return () => {
302
+ set.delete(fn);
303
+ };
272
304
  }
273
305
  onPresenceDiff(fn) {
274
306
  return this.subscribe("presence_diff", fn);
275
307
  }
308
+ /** Current presence snapshot for sync access — usually used in tests. */
309
+ getPresenceState() {
310
+ return this.presenceState;
311
+ }
276
312
  /** Send a typing ping to the server. Debounced server-side. */
277
313
  sendTyping() {
278
314
  this.channel.push("typing", {});
@@ -421,6 +457,14 @@ var AttachmentsResource = class {
421
457
  ...input.filename !== void 0 ? { original_filename: input.filename } : {}
422
458
  };
423
459
  const { attachment, upload } = await this.requestUpload(req, opts);
460
+ if (opts.onProgress && typeof XMLHttpRequest !== "undefined") {
461
+ await xhrPut(upload.url, upload.method.toUpperCase(), upload.headers, input.body, {
462
+ byteSize: input.byteSize,
463
+ onProgress: opts.onProgress,
464
+ ...opts.signal ? { signal: opts.signal } : {}
465
+ });
466
+ return attachment;
467
+ }
424
468
  const putInit = {
425
469
  method: upload.method.toUpperCase(),
426
470
  headers: upload.headers,
@@ -472,6 +516,39 @@ var AttachmentHandle = class {
472
516
  });
473
517
  }
474
518
  };
519
+ function xhrPut(url, method, headers, body, opts) {
520
+ return new Promise((resolve, reject) => {
521
+ const xhr = new XMLHttpRequest();
522
+ xhr.open(method, url);
523
+ for (const [k, v] of Object.entries(headers)) {
524
+ try {
525
+ xhr.setRequestHeader(k, v);
526
+ } catch {
527
+ }
528
+ }
529
+ xhr.upload.onprogress = (e) => {
530
+ const total = e.lengthComputable ? e.total : opts.byteSize;
531
+ opts.onProgress({ loaded: e.loaded, total });
532
+ };
533
+ xhr.onload = () => {
534
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
535
+ else
536
+ reject(new Error(`Poolse: presigned upload PUT failed (${xhr.status} ${xhr.statusText})`));
537
+ };
538
+ xhr.onerror = () => reject(new Error("Poolse: presigned upload PUT failed (network error)"));
539
+ xhr.onabort = () => {
540
+ reject(new DOMException("Upload aborted", "AbortError"));
541
+ };
542
+ if (opts.signal) {
543
+ if (opts.signal.aborted) {
544
+ xhr.abort();
545
+ return;
546
+ }
547
+ opts.signal.addEventListener("abort", () => xhr.abort(), { once: true });
548
+ }
549
+ xhr.send(body);
550
+ });
551
+ }
475
552
 
476
553
  // src/resources/messages.ts
477
554
  var ConversationMessages = class {
@@ -1095,7 +1172,7 @@ var Poolse = class {
1095
1172
  };
1096
1173
 
1097
1174
  // src/version.ts
1098
- var version = "0.0.1";
1175
+ var version = "1.0.1";
1099
1176
 
1100
1177
  exports.ApiError = ApiError;
1101
1178
  exports.AttachmentHandle = AttachmentHandle;