@poolse/sdk 1.0.0-beta.0 → 1.0.0-rc.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/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
@@ -28,7 +28,8 @@ function resolveConfig(config) {
28
28
  generateIdempotencyKey: config.generateIdempotencyKey ?? defaultIdempotencyKey,
29
29
  wsUrl: config.wsUrl,
30
30
  socketPath: config.socketPath ?? "/socket",
31
- onSocketError: config.onSocketError
31
+ onSocketError: config.onSocketError,
32
+ userResolver: config.userResolver
32
33
  };
33
34
  }
34
35
  function trimTrailingSlash(s) {
@@ -420,6 +421,14 @@ var AttachmentsResource = class {
420
421
  ...input.filename !== void 0 ? { original_filename: input.filename } : {}
421
422
  };
422
423
  const { attachment, upload } = await this.requestUpload(req, opts);
424
+ if (opts.onProgress && typeof XMLHttpRequest !== "undefined") {
425
+ await xhrPut(upload.url, upload.method.toUpperCase(), upload.headers, input.body, {
426
+ byteSize: input.byteSize,
427
+ onProgress: opts.onProgress,
428
+ ...opts.signal ? { signal: opts.signal } : {}
429
+ });
430
+ return attachment;
431
+ }
423
432
  const putInit = {
424
433
  method: upload.method.toUpperCase(),
425
434
  headers: upload.headers,
@@ -471,6 +480,39 @@ var AttachmentHandle = class {
471
480
  });
472
481
  }
473
482
  };
483
+ function xhrPut(url, method, headers, body, opts) {
484
+ return new Promise((resolve, reject) => {
485
+ const xhr = new XMLHttpRequest();
486
+ xhr.open(method, url);
487
+ for (const [k, v] of Object.entries(headers)) {
488
+ try {
489
+ xhr.setRequestHeader(k, v);
490
+ } catch {
491
+ }
492
+ }
493
+ xhr.upload.onprogress = (e) => {
494
+ const total = e.lengthComputable ? e.total : opts.byteSize;
495
+ opts.onProgress({ loaded: e.loaded, total });
496
+ };
497
+ xhr.onload = () => {
498
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
499
+ else
500
+ reject(new Error(`Poolse: presigned upload PUT failed (${xhr.status} ${xhr.statusText})`));
501
+ };
502
+ xhr.onerror = () => reject(new Error("Poolse: presigned upload PUT failed (network error)"));
503
+ xhr.onabort = () => {
504
+ reject(new DOMException("Upload aborted", "AbortError"));
505
+ };
506
+ if (opts.signal) {
507
+ if (opts.signal.aborted) {
508
+ xhr.abort();
509
+ return;
510
+ }
511
+ opts.signal.addEventListener("abort", () => xhr.abort(), { once: true });
512
+ }
513
+ xhr.send(body);
514
+ });
515
+ }
474
516
 
475
517
  // src/resources/messages.ts
476
518
  var ConversationMessages = class {
@@ -723,6 +765,100 @@ var MeResource = class {
723
765
  }
724
766
  };
725
767
 
768
+ // src/resources/users.ts
769
+ var UsersResource = class {
770
+ constructor(config) {
771
+ this.config = config;
772
+ }
773
+ config;
774
+ cache = /* @__PURE__ */ new Map();
775
+ pending = /* @__PURE__ */ new Map();
776
+ listeners = /* @__PURE__ */ new Map();
777
+ /**
778
+ * Get the cached value if present. Returns `undefined` to mean
779
+ * "not in cache yet" (different from `null`, which means "resolver
780
+ * ran and the user wasn't found").
781
+ */
782
+ peek(userId) {
783
+ return this.cache.get(userId);
784
+ }
785
+ /**
786
+ * Resolve a user, hitting the customer's `userResolver` on cache
787
+ * miss. Concurrent calls for the same id share one Promise.
788
+ */
789
+ async get(userId) {
790
+ if (this.cache.has(userId)) {
791
+ return this.cache.get(userId) ?? null;
792
+ }
793
+ const existingPending = this.pending.get(userId);
794
+ if (existingPending) return existingPending;
795
+ const resolver = this.config.userResolver;
796
+ if (!resolver) {
797
+ this.cache.set(userId, null);
798
+ this.notify(userId);
799
+ return null;
800
+ }
801
+ const promise = Promise.resolve().then(() => resolver(userId)).then(
802
+ (profile) => {
803
+ this.cache.set(userId, profile ?? null);
804
+ this.pending.delete(userId);
805
+ this.notify(userId);
806
+ return profile ?? null;
807
+ },
808
+ (err) => {
809
+ console.error("[poolse] userResolver failed for", userId, err);
810
+ this.cache.set(userId, null);
811
+ this.pending.delete(userId);
812
+ this.notify(userId);
813
+ return null;
814
+ }
815
+ );
816
+ this.pending.set(userId, promise);
817
+ return promise;
818
+ }
819
+ /**
820
+ * Subscribe to changes for a single user id. The listener fires
821
+ * when the resolver lands (or when the entry is invalidated).
822
+ * Returns an unsubscribe.
823
+ *
824
+ * `useUser` in @poolse/react uses this with `useSyncExternalStore`.
825
+ */
826
+ subscribe(userId, listener) {
827
+ let set = this.listeners.get(userId);
828
+ if (!set) {
829
+ set = /* @__PURE__ */ new Set();
830
+ this.listeners.set(userId, set);
831
+ }
832
+ set.add(listener);
833
+ return () => {
834
+ set?.delete(listener);
835
+ };
836
+ }
837
+ /** Drop a single cached entry — next `get` re-fetches via the resolver. */
838
+ invalidate(userId) {
839
+ this.cache.delete(userId);
840
+ this.pending.delete(userId);
841
+ this.notify(userId);
842
+ }
843
+ /**
844
+ * Drop the entire cache. Use after a sign-out, tenant swap, or any
845
+ * other event that invalidates every cached profile (e.g., the
846
+ * customer just renamed every user in bulk).
847
+ */
848
+ invalidateAll() {
849
+ this.cache.clear();
850
+ this.pending.clear();
851
+ for (const userId of this.listeners.keys()) {
852
+ this.notify(userId);
853
+ }
854
+ }
855
+ notify(userId) {
856
+ const set = this.listeners.get(userId);
857
+ if (!set) return;
858
+ for (const l of set) l();
859
+ }
860
+ };
861
+
726
862
  // src/rest-client.ts
727
863
  var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set(["GET"]);
728
864
  var RestClient = class {
@@ -944,6 +1080,17 @@ var Poolse = class {
944
1080
  messages;
945
1081
  /** `/v1/attachments/*` — presigned-URL uploads/downloads. */
946
1082
  attachments;
1083
+ /**
1084
+ * Customer-supplied user metadata, cached + dedup'd.
1085
+ * `chat.users.get(userId)` returns `{ displayName, avatarUrl }`
1086
+ * via the optional `config.userResolver`. UI components
1087
+ * (`MessageBubble`, `MemberList`, `TypingIndicator`) pick this up
1088
+ * automatically via the `useUser` hook in `@poolse/react`.
1089
+ *
1090
+ * If no resolver is configured, `get` always returns `null` and
1091
+ * UI falls back to the userId slice + initials avatar.
1092
+ */
1093
+ users;
947
1094
  /**
948
1095
  * Low-level REST client. Exposed for advanced use cases (custom endpoints,
949
1096
  * raw retry/headers control). Most callers should use the resources above.
@@ -971,6 +1118,7 @@ var Poolse = class {
971
1118
  this.conversations = new ConversationsResource(this.rest);
972
1119
  this.messages = new MessagesResource(this.rest);
973
1120
  this.attachments = new AttachmentsResource(this.rest, cachedConfig.fetch);
1121
+ this.users = new UsersResource(cachedConfig);
974
1122
  this.realtime = new PoolseRealtime(cachedConfig, this.tokenCache, {
975
1123
  ...this.resolved.wsUrl !== void 0 ? { wsUrl: this.resolved.wsUrl } : {},
976
1124
  socketPath: this.resolved.socketPath
@@ -988,7 +1136,7 @@ var Poolse = class {
988
1136
  };
989
1137
 
990
1138
  // src/version.ts
991
- var version = "0.0.1";
1139
+ var version = "1.0.0-rc.0";
992
1140
 
993
1141
  exports.ApiError = ApiError;
994
1142
  exports.AttachmentHandle = AttachmentHandle;
@@ -1008,6 +1156,7 @@ exports.PoolseError = PoolseError;
1008
1156
  exports.PoolseRealtime = PoolseRealtime;
1009
1157
  exports.RateLimitedError = RateLimitedError;
1010
1158
  exports.UserChannel = UserChannel;
1159
+ exports.UsersResource = UsersResource;
1011
1160
  exports.version = version;
1012
1161
  //# sourceMappingURL=index.cjs.map
1013
1162
  //# sourceMappingURL=index.cjs.map