@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 +3 -1
- package/dist/index.cjs +151 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +235 -117
- package/dist/index.d.ts +235 -117
- package/dist/index.js +151 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|