@poolse/sdk 1.1.0 → 2.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/CHANGELOG.md +43 -0
- package/README.md +15 -13
- package/dist/index.cjs +55 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -22
- package/dist/index.d.ts +47 -22
- package/dist/index.js +55 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable changes to `@poolse/sdk` are documented here. Format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
5
|
[semver](https://semver.org).
|
|
6
6
|
|
|
7
|
+
## [2.0.0] — 2026-06-03
|
|
8
|
+
|
|
9
|
+
Lockstep release with `@poolse/react@2.0.0` and `@poolse/react-ui@2.0.0`.
|
|
10
|
+
Single load-bearing change: the SDK no longer surfaces poolse-internal
|
|
11
|
+
user uuids in identity-shaped APIs — everything is keyed by the
|
|
12
|
+
tenant's own `external_id` instead.
|
|
13
|
+
|
|
14
|
+
### Breaking
|
|
15
|
+
|
|
16
|
+
- **`PoolseConfig.userResolver` signature changes.** Was
|
|
17
|
+
`(userId: string) => UserProfile | null`; now
|
|
18
|
+
`(externalId: string) => UserProfile | null`. Your resolver receives
|
|
19
|
+
YOUR user id (the same string you pass when minting JWTs and as
|
|
20
|
+
`member_external_ids`), so no `poolse_user_id` column is needed.
|
|
21
|
+
- **`Message.sender_external_id`** added (string | null, required on
|
|
22
|
+
every wire payload that carries a sender).
|
|
23
|
+
- **`Membership.external_id`** added (string, required).
|
|
24
|
+
- **`QuotedMessagePreview.sender_external_id`** added.
|
|
25
|
+
- **`TypingEvent.external_id`** added — `useTyping` returns
|
|
26
|
+
`Set<externalId>` instead of `Set<userId>`.
|
|
27
|
+
- **`UsersResource.{peek, get, subscribe, invalidate}`** all keyed by
|
|
28
|
+
`externalId` (was `userId`). Same semantics; same caching.
|
|
29
|
+
|
|
30
|
+
### Migration
|
|
31
|
+
|
|
32
|
+
See `MIGRATING.md` at the repo root. For most apps it's a rename of
|
|
33
|
+
the resolver argument plus deleting the `poolse_user_id` column if
|
|
34
|
+
you stored one.
|
|
35
|
+
|
|
36
|
+
### Backend coupling
|
|
37
|
+
|
|
38
|
+
Pairs with the `poolse-server` change that:
|
|
39
|
+
|
|
40
|
+
- lazy-provisions unknown `external_id`s referenced in
|
|
41
|
+
`POST /v1/conversations` (`member_external_ids`) and
|
|
42
|
+
`POST /v1/conversations/:id/members` (`external_ids`), abuse-capped
|
|
43
|
+
at 50/hour per JWT,
|
|
44
|
+
- emits `sender_external_id` / `external_id` on every user-shaped
|
|
45
|
+
wire payload (REST, webhook, realtime).
|
|
46
|
+
|
|
47
|
+
Self-hosted `@poolse/sdk@2.0.0` against an older backend will get
|
|
48
|
+
`sender_external_id: null` everywhere — degraded display, but no crash.
|
|
49
|
+
|
|
7
50
|
## [1.1.0] — 2026-06-02
|
|
8
51
|
|
|
9
52
|
Lockstep release with `@poolse/react@1.1.0` and `@poolse/react-ui@1.1.0`.
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Headless TypeScript SDK for [poolse](https://poolse.dev) — REST + WebSocket cl
|
|
|
4
4
|
|
|
5
5
|
If you're using React, you'll usually want [`@poolse/react`](https://www.npmjs.com/package/@poolse/react) (hooks) or [`@poolse/react-ui`](https://www.npmjs.com/package/@poolse/react-ui) (prebuilt chat surface). Both sit on top of this package; you can drop down to it whenever you outgrow them.
|
|
6
6
|
|
|
7
|
+
> **⚠️ Upgrading from 1.x?** See [MIGRATING.md](https://github.com/poolse-hq/js-sdk/blob/main/MIGRATING.md). 2.0 is a breaking change: identity APIs are now keyed by your `external_id` instead of poolse uuids — `userResolver`, `useUser`, member operations all flip.
|
|
8
|
+
|
|
7
9
|
## Install
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -61,19 +63,19 @@ chat.destroy(); // closes WebSocket, leaves every joined channel
|
|
|
61
63
|
|
|
62
64
|
`new Poolse(config)` — every field but `getToken` has a default.
|
|
63
65
|
|
|
64
|
-
| Field | Type
|
|
65
|
-
| ------------------------ |
|
|
66
|
-
| `getToken` | `() => Promise<string \| null> \| string \| null`
|
|
67
|
-
| `apiUrl` | `string`
|
|
68
|
-
| `wsUrl` | `string`
|
|
69
|
-
| `socketPath` | `string`
|
|
70
|
-
| `fetch` | `typeof fetch`
|
|
71
|
-
| `maxRetries` | `number`
|
|
72
|
-
| `baseBackoffMs` | `number`
|
|
73
|
-
| `maxBackoffMs` | `number`
|
|
74
|
-
| `generateIdempotencyKey` | `() => string`
|
|
75
|
-
| `onSocketError` | `(err: Error) => void`
|
|
76
|
-
| `userResolver` | `(
|
|
66
|
+
| Field | Type | Default | Notes |
|
|
67
|
+
| ------------------------ | ----------------------------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
68
|
+
| `getToken` | `() => Promise<string \| null> \| string \| null` | — _required_ | Called when the SDK needs a JWT. Return `null` to make an unauthenticated request (rare — the server requires a JWT for most routes). |
|
|
69
|
+
| `apiUrl` | `string` | `https://api.poolse.dev` | Base URL **without** `/v1`. Strip the trailing slash if present (the SDK does this too). |
|
|
70
|
+
| `wsUrl` | `string` | `apiUrl` with `http(s)` → `ws(s)` | Override for split-host deployments where the WebSocket gateway is on a different origin. |
|
|
71
|
+
| `socketPath` | `string` | `/socket` | Phoenix Channels mount point. |
|
|
72
|
+
| `fetch` | `typeof fetch` | `globalThis.fetch` | Inject a fetch implementation (tests, restricted environments). Must be bound to `globalThis` if you pass the global one explicitly. |
|
|
73
|
+
| `maxRetries` | `number` | `3` | Retry budget for transient failures, per request. |
|
|
74
|
+
| `baseBackoffMs` | `number` | `250` | Base of the exponential backoff. |
|
|
75
|
+
| `maxBackoffMs` | `number` | `30000` | Hard ceiling on a single retry delay. |
|
|
76
|
+
| `generateIdempotencyKey` | `() => string` | `crypto.randomUUID()` | Override the key generator. Defaults throws at construction time if no `crypto.randomUUID` is available. |
|
|
77
|
+
| `onSocketError` | `(err: Error) => void` | — | Fired on non-fatal socket errors (Phoenix handles reconnect internally; this is for surface-level banners). |
|
|
78
|
+
| `userResolver` | `(externalId: string) => Promise<PoolseUserProfile \| null> \| PoolseUserProfile \| null` | — | Optional. Resolve the tenant's own user identifier (`external_id` — same string you pass when minting JWTs) to `{ displayName, avatarUrl }` from your app's user data. The UI packages pick this up automatically. |
|
|
77
79
|
|
|
78
80
|
## REST surface
|
|
79
81
|
|
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
var phoenix = require('phoenix');
|
|
4
4
|
|
|
5
|
+
// src/uuid.ts
|
|
6
|
+
function safeUuid() {
|
|
7
|
+
const c = globalThis.crypto;
|
|
8
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
9
|
+
const bytes = new Uint8Array(16);
|
|
10
|
+
if (c?.getRandomValues) {
|
|
11
|
+
c.getRandomValues(bytes);
|
|
12
|
+
} else {
|
|
13
|
+
for (let i = 0; i < 16; i++) {
|
|
14
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
18
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
19
|
+
const hex = [];
|
|
20
|
+
for (let i = 0; i < 16; i++) {
|
|
21
|
+
hex.push(bytes[i].toString(16).padStart(2, "0"));
|
|
22
|
+
}
|
|
23
|
+
return hex.slice(0, 4).join("") + "-" + hex.slice(4, 6).join("") + "-" + hex.slice(6, 8).join("") + "-" + hex.slice(8, 10).join("") + "-" + hex.slice(10).join("");
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
// src/config.ts
|
|
6
27
|
var POOLSE_API_URL = "https://api.poolse.dev";
|
|
7
28
|
var DEFAULT_MAX_RETRIES = 3;
|
|
@@ -36,13 +57,7 @@ function trimTrailingSlash(s) {
|
|
|
36
57
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
37
58
|
}
|
|
38
59
|
function defaultIdempotencyKey() {
|
|
39
|
-
|
|
40
|
-
if (c && typeof c.randomUUID === "function") {
|
|
41
|
-
return c.randomUUID();
|
|
42
|
-
}
|
|
43
|
-
throw new Error(
|
|
44
|
-
"Poolse: globalThis.crypto.randomUUID() is not available; supply `config.generateIdempotencyKey` instead."
|
|
45
|
-
);
|
|
60
|
+
return safeUuid();
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
// src/errors.ts
|
|
@@ -665,13 +680,7 @@ var MessagesResource = class {
|
|
|
665
680
|
}
|
|
666
681
|
};
|
|
667
682
|
function generateClientMessageId() {
|
|
668
|
-
|
|
669
|
-
if (c && typeof c.randomUUID === "function") {
|
|
670
|
-
return c.randomUUID();
|
|
671
|
-
}
|
|
672
|
-
throw new Error(
|
|
673
|
-
"Poolse: globalThis.crypto.randomUUID() unavailable \u2014 pass `id` explicitly in MessageCreateRequest (your env lacks a built-in UUID generator)."
|
|
674
|
-
);
|
|
683
|
+
return safeUuid();
|
|
675
684
|
}
|
|
676
685
|
|
|
677
686
|
// src/resources/conversations.ts
|
|
@@ -815,55 +824,55 @@ var UsersResource = class {
|
|
|
815
824
|
* "not in cache yet" (different from `null`, which means "resolver
|
|
816
825
|
* ran and the user wasn't found").
|
|
817
826
|
*/
|
|
818
|
-
peek(
|
|
819
|
-
return this.cache.get(
|
|
827
|
+
peek(externalId) {
|
|
828
|
+
return this.cache.get(externalId);
|
|
820
829
|
}
|
|
821
830
|
/**
|
|
822
831
|
* Resolve a user, hitting the customer's `userResolver` on cache
|
|
823
|
-
* miss. Concurrent calls for the same
|
|
832
|
+
* miss. Concurrent calls for the same external_id share one Promise.
|
|
824
833
|
*/
|
|
825
|
-
async get(
|
|
826
|
-
if (this.cache.has(
|
|
827
|
-
return this.cache.get(
|
|
834
|
+
async get(externalId) {
|
|
835
|
+
if (this.cache.has(externalId)) {
|
|
836
|
+
return this.cache.get(externalId) ?? null;
|
|
828
837
|
}
|
|
829
|
-
const existingPending = this.pending.get(
|
|
838
|
+
const existingPending = this.pending.get(externalId);
|
|
830
839
|
if (existingPending) return existingPending;
|
|
831
840
|
const resolver = this.config.userResolver;
|
|
832
841
|
if (!resolver) {
|
|
833
|
-
this.cache.set(
|
|
834
|
-
this.notify(
|
|
842
|
+
this.cache.set(externalId, null);
|
|
843
|
+
this.notify(externalId);
|
|
835
844
|
return null;
|
|
836
845
|
}
|
|
837
|
-
const promise = Promise.resolve().then(() => resolver(
|
|
846
|
+
const promise = Promise.resolve().then(() => resolver(externalId)).then(
|
|
838
847
|
(profile) => {
|
|
839
|
-
this.cache.set(
|
|
840
|
-
this.pending.delete(
|
|
841
|
-
this.notify(
|
|
848
|
+
this.cache.set(externalId, profile ?? null);
|
|
849
|
+
this.pending.delete(externalId);
|
|
850
|
+
this.notify(externalId);
|
|
842
851
|
return profile ?? null;
|
|
843
852
|
},
|
|
844
853
|
(err) => {
|
|
845
|
-
console.error("[poolse] userResolver failed for",
|
|
846
|
-
this.cache.set(
|
|
847
|
-
this.pending.delete(
|
|
848
|
-
this.notify(
|
|
854
|
+
console.error("[poolse] userResolver failed for", externalId, err);
|
|
855
|
+
this.cache.set(externalId, null);
|
|
856
|
+
this.pending.delete(externalId);
|
|
857
|
+
this.notify(externalId);
|
|
849
858
|
return null;
|
|
850
859
|
}
|
|
851
860
|
);
|
|
852
|
-
this.pending.set(
|
|
861
|
+
this.pending.set(externalId, promise);
|
|
853
862
|
return promise;
|
|
854
863
|
}
|
|
855
864
|
/**
|
|
856
|
-
* Subscribe to changes for a single
|
|
865
|
+
* Subscribe to changes for a single external_id. The listener fires
|
|
857
866
|
* when the resolver lands (or when the entry is invalidated).
|
|
858
867
|
* Returns an unsubscribe.
|
|
859
868
|
*
|
|
860
869
|
* `useUser` in @poolse/react uses this with `useSyncExternalStore`.
|
|
861
870
|
*/
|
|
862
|
-
subscribe(
|
|
863
|
-
let set = this.listeners.get(
|
|
871
|
+
subscribe(externalId, listener) {
|
|
872
|
+
let set = this.listeners.get(externalId);
|
|
864
873
|
if (!set) {
|
|
865
874
|
set = /* @__PURE__ */ new Set();
|
|
866
|
-
this.listeners.set(
|
|
875
|
+
this.listeners.set(externalId, set);
|
|
867
876
|
}
|
|
868
877
|
set.add(listener);
|
|
869
878
|
return () => {
|
|
@@ -871,10 +880,10 @@ var UsersResource = class {
|
|
|
871
880
|
};
|
|
872
881
|
}
|
|
873
882
|
/** Drop a single cached entry — next `get` re-fetches via the resolver. */
|
|
874
|
-
invalidate(
|
|
875
|
-
this.cache.delete(
|
|
876
|
-
this.pending.delete(
|
|
877
|
-
this.notify(
|
|
883
|
+
invalidate(externalId) {
|
|
884
|
+
this.cache.delete(externalId);
|
|
885
|
+
this.pending.delete(externalId);
|
|
886
|
+
this.notify(externalId);
|
|
878
887
|
}
|
|
879
888
|
/**
|
|
880
889
|
* Drop the entire cache. Use after a sign-out, tenant swap, or any
|
|
@@ -884,12 +893,12 @@ var UsersResource = class {
|
|
|
884
893
|
invalidateAll() {
|
|
885
894
|
this.cache.clear();
|
|
886
895
|
this.pending.clear();
|
|
887
|
-
for (const
|
|
888
|
-
this.notify(
|
|
896
|
+
for (const externalId of this.listeners.keys()) {
|
|
897
|
+
this.notify(externalId);
|
|
889
898
|
}
|
|
890
899
|
}
|
|
891
|
-
notify(
|
|
892
|
-
const set = this.listeners.get(
|
|
900
|
+
notify(externalId) {
|
|
901
|
+
const set = this.listeners.get(externalId);
|
|
893
902
|
if (!set) return;
|
|
894
903
|
for (const l of set) l();
|
|
895
904
|
}
|
|
@@ -1172,7 +1181,7 @@ var Poolse = class {
|
|
|
1172
1181
|
};
|
|
1173
1182
|
|
|
1174
1183
|
// src/version.ts
|
|
1175
|
-
var version = "
|
|
1184
|
+
var version = "2.0.1";
|
|
1176
1185
|
|
|
1177
1186
|
exports.ApiError = ApiError;
|
|
1178
1187
|
exports.AttachmentHandle = AttachmentHandle;
|
|
@@ -1193,6 +1202,7 @@ exports.PoolseRealtime = PoolseRealtime;
|
|
|
1193
1202
|
exports.RateLimitedError = RateLimitedError;
|
|
1194
1203
|
exports.UserChannel = UserChannel;
|
|
1195
1204
|
exports.UsersResource = UsersResource;
|
|
1205
|
+
exports.safeUuid = safeUuid;
|
|
1196
1206
|
exports.version = version;
|
|
1197
1207
|
//# sourceMappingURL=index.cjs.map
|
|
1198
1208
|
//# sourceMappingURL=index.cjs.map
|