@pmate/chat-sdk 0.11.9 → 0.12.0-next.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.
@@ -0,0 +1,81 @@
1
+ import { Dexie } from "dexie";
2
+ export const messageScopeKey = (scope) => JSON.stringify([scope.appId, scope.accountId, scope.profileId, scope.teamId]);
3
+ /** Normalized browser cache. Data and coverage advance together in each commit.
4
+ * Merge runs inside the write transaction against the latest cross-tab state. */
5
+ export class IndexedDbMessageStore {
6
+ merge;
7
+ db;
8
+ rows;
9
+ coverage;
10
+ key;
11
+ constructor(scope, merge, options = {}) {
12
+ this.merge = merge;
13
+ this.key = messageScopeKey(scope);
14
+ this.db = new Dexie(options.name ?? "pmate-chat-messages", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
15
+ this.db.version(1).stores({ messages: "[scope+thread+id],[scope+thread+time+id],[scope+thread],scope", coverage: "[scope+thread],scope" });
16
+ this.rows = this.db.table("messages");
17
+ this.coverage = this.db.table("coverage");
18
+ this.db.on("versionchange", () => { this.db.close({ disableAutoOpen: false }); return false; });
19
+ }
20
+ async read(thread, limit = 100, before) {
21
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
22
+ throw new Error("INVALID_MESSAGE_LIMIT");
23
+ return this.db.transaction("r", this.rows, this.coverage, async () => {
24
+ const range = this.rows.where("[scope+thread+time+id]").between([this.key, thread, Dexie.minKey, Dexie.minKey], [this.key, thread, before?.timestamp ?? Dexie.maxKey, before?.id ?? Dexie.maxKey], true, !before);
25
+ const records = await range.reverse().limit(limit + 1).toArray();
26
+ return { messages: records.slice(0, limit).reverse().map(row => row.value), hasEarlier: records.length > limit, coverage: await this.coverage.get([this.key, thread]) };
27
+ });
28
+ }
29
+ async commit(thread, messages, coverage) {
30
+ for (const message of messages)
31
+ if (message.threadId !== thread || !message.id || !Number.isFinite(message.timestamp))
32
+ throw new Error("MESSAGE_SCOPE_MISMATCH");
33
+ if (messages.length > 1000)
34
+ throw new Error("MESSAGE_BATCH_TOO_LARGE");
35
+ return this.db.transaction("rw", this.rows, this.coverage, async () => {
36
+ const ids = [...new Set(messages.map(message => message.id))];
37
+ const previous = await this.rows.bulkGet(ids.map(id => [this.key, thread, id]));
38
+ const latest = new Map(previous.flatMap(row => row ? [[row.id, row.value]] : []));
39
+ const changed = new Map();
40
+ for (const message of messages) {
41
+ const old = latest.get(message.id), value = this.merge(old, message);
42
+ if (value.id !== message.id || value.threadId !== thread || !Number.isFinite(value.timestamp))
43
+ throw new Error("MESSAGE_SCOPE_MISMATCH");
44
+ latest.set(message.id, value);
45
+ if (JSON.stringify(old) !== JSON.stringify(value))
46
+ changed.set(message.id, { scope: this.key, thread, id: value.id, time: value.timestamp, value });
47
+ }
48
+ if (changed.size)
49
+ await this.rows.bulkPut([...changed.values()]);
50
+ if (coverage) {
51
+ const old = await this.coverage.get([this.key, thread]), next = { ...coverage, scope: this.key, thread };
52
+ if (JSON.stringify(old) !== JSON.stringify(next))
53
+ await this.coverage.put(next);
54
+ }
55
+ return changed.size;
56
+ });
57
+ }
58
+ async removeThread(thread) {
59
+ await this.db.transaction("rw", this.rows, this.coverage, async () => {
60
+ await this.rows.where("[scope+thread]").equals([this.key, thread]).delete();
61
+ await this.coverage.delete([this.key, thread]);
62
+ });
63
+ }
64
+ async clear() {
65
+ await this.db.transaction("rw", this.rows, this.coverage, async () => {
66
+ await this.rows.where("scope").equals(this.key).delete();
67
+ await this.coverage.where("scope").equals(this.key).delete();
68
+ });
69
+ }
70
+ close() { this.db.close(); }
71
+ }
72
+ /** Keep tombstones monotonic; applications can supply stricter domain reducers. */
73
+ export function mergeStoredMessage(previous, incoming) {
74
+ if (!previous)
75
+ return incoming;
76
+ if (previous.recalled && !incoming.recalled)
77
+ return previous;
78
+ if ((previous.revision ?? 0) > (incoming.revision ?? 0))
79
+ return previous;
80
+ return incoming;
81
+ }
@@ -102,8 +102,9 @@ function resolveObservedReads(current) {
102
102
  }
103
103
  if (message.recalled || message.from === next.profileId || !message.recipients.includes(next.profileId))
104
104
  continue;
105
+ // Visibility may include voice rows; incompatible observations never acknowledge them.
105
106
  if (message.readPolicy !== intent.readPolicy)
106
- throw new Error("READ_POLICY_MISMATCH");
107
+ continue;
107
108
  next = intent.readPolicy === "explicit" ? recordExplicitRead(next, intent.threadId, intent.messageId, intent.operationId)
108
109
  : recordVisibleRead(next, intent.threadId, intent.messageId, intent.operationId);
109
110
  }
@@ -206,10 +207,12 @@ export class IndexedDbReadIndex {
206
207
  }
207
208
  async update(appId, profileId, transform) {
208
209
  return this.transaction("rw", async () => {
209
- const next = transform(await this.profiles.get([appId, profileId]) ?? createReadIndex(appId, profileId));
210
+ const previous = await this.profiles.get([appId, profileId]);
211
+ const next = transform(previous ?? createReadIndex(appId, profileId));
210
212
  if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
211
213
  throw new Error("READ_IDENTITY_MISMATCH");
212
- await this.profiles.put(next, [appId, profileId]);
214
+ if (JSON.stringify(next) !== JSON.stringify(previous))
215
+ await this.profiles.put(next, [appId, profileId]);
213
216
  return next;
214
217
  });
215
218
  }
@@ -0,0 +1,40 @@
1
+ import { type ReadIndexPersistence, type ReadIndexState } from "./readIndex.js";
2
+ export type ReadingClientOptions = {
3
+ appId: string;
4
+ profileId: string;
5
+ sessionToken: string;
6
+ hubHttpUrl: string;
7
+ persistence?: ReadIndexPersistence;
8
+ fetch?: typeof fetch;
9
+ onError?: (error: unknown) => void;
10
+ onRecovered?: () => void;
11
+ };
12
+ /** Owns durable local reads, network coalescing, retries and state notifications.
13
+ * Hosts report actual visibility; loading or subscribing never marks messages read. */
14
+ export declare class ReadingClient {
15
+ private options;
16
+ private storage;
17
+ private persistence;
18
+ private sync;
19
+ private tasks;
20
+ private listeners;
21
+ private state?;
22
+ private closed;
23
+ private retry?;
24
+ private retryDelay;
25
+ private offWake?;
26
+ private generation;
27
+ constructor(options: ReadingClientOptions);
28
+ getSnapshot: () => ReadIndexState | undefined;
29
+ subscribe: (listener: () => void) => () => void;
30
+ private publish;
31
+ private failed;
32
+ start(): Promise<void>;
33
+ refresh: () => void;
34
+ synchronize(targetCursor?: string, profileId?: string): Promise<ReadIndexState>;
35
+ update(transform: (state: ReadIndexState) => ReadIndexState): Promise<ReadIndexState>;
36
+ recordVisible(threadId: string, messageIds: string[]): Promise<ReadIndexState>;
37
+ recordExplicit(threadId: string, messageIds: string[]): Promise<ReadIndexState>;
38
+ private record;
39
+ close(): Promise<void>;
40
+ }
@@ -0,0 +1,115 @@
1
+ import { BatchedReadIndex } from "./batchedReadIndex.js";
2
+ import { WindowedTasks } from "./coalescer.js";
3
+ import { IndexedDbReadIndex, recordReadIntent } from "./readIndex.js";
4
+ import { ReadSyncClient } from "./readSync.js";
5
+ /** Owns durable local reads, network coalescing, retries and state notifications.
6
+ * Hosts report actual visibility; loading or subscribing never marks messages read. */
7
+ export class ReadingClient {
8
+ options;
9
+ storage;
10
+ persistence;
11
+ sync;
12
+ tasks;
13
+ listeners = new Set();
14
+ state;
15
+ closed = false;
16
+ retry;
17
+ retryDelay = 1000;
18
+ offWake;
19
+ generation = 0;
20
+ constructor(options) {
21
+ this.options = options;
22
+ const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex());
23
+ this.persistence = new BatchedReadIndex(storage);
24
+ this.tasks = new WindowedTasks(1000, error => this.failed(error));
25
+ this.sync = new ReadSyncClient({ ...options, persistence: this.persistence, onChange: state => this.publish(state) });
26
+ }
27
+ getSnapshot = () => this.state;
28
+ subscribe = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
29
+ publish(state) {
30
+ if (this.closed || JSON.stringify(state) === JSON.stringify(this.state))
31
+ return;
32
+ this.state = state;
33
+ this.listeners.forEach(listener => listener());
34
+ }
35
+ failed(error) {
36
+ if (this.closed)
37
+ return;
38
+ this.options.onError?.(error);
39
+ if (/HTTP_(400|401|403)/.test(String(error)) || (typeof navigator !== "undefined" && !navigator.onLine) || this.retry)
40
+ return;
41
+ this.retry = setTimeout(() => { this.retry = undefined; this.refresh(); }, this.retryDelay);
42
+ this.retryDelay = Math.min(this.retryDelay * 2, 30_000);
43
+ }
44
+ async start() {
45
+ if (this.closed)
46
+ throw new Error("READING_CLIENT_CLOSED");
47
+ this.publish(await this.sync.loadLocal());
48
+ if (this.closed)
49
+ return;
50
+ if (typeof window !== "undefined" && !this.offWake) {
51
+ const wake = () => this.refresh();
52
+ window.addEventListener("online", wake);
53
+ window.addEventListener("focus", wake);
54
+ this.offWake = () => { window.removeEventListener("online", wake); window.removeEventListener("focus", wake); };
55
+ }
56
+ this.refresh();
57
+ }
58
+ refresh = () => {
59
+ if (!this.closed)
60
+ this.tasks.schedule("reading", async () => { await this.synchronize(); });
61
+ };
62
+ async synchronize(targetCursor, profileId) {
63
+ if (this.closed)
64
+ throw new Error("READING_CLIENT_CLOSED");
65
+ if (profileId !== undefined && profileId !== this.options.profileId)
66
+ throw new Error("Reading reach identity mismatch");
67
+ if (targetCursor !== undefined && !/^v1:\d+$/.test(targetCursor))
68
+ throw new Error("Invalid reading reach cursor");
69
+ const generation = this.generation;
70
+ for (let attempt = 0; attempt < 2; attempt++) {
71
+ const next = await this.sync.synchronize();
72
+ if (this.closed || generation !== this.generation)
73
+ throw new Error("READING_CLIENT_CLOSED");
74
+ const cursor = /^(?:teams:[0-9a-f]{64}:)?v1:(\d+)$/.exec(next.syncCursor);
75
+ if (targetCursor === undefined || (cursor && BigInt(cursor[1]) >= BigInt(targetCursor.slice(3)))) {
76
+ clearTimeout(this.retry);
77
+ this.retry = undefined;
78
+ this.retryDelay = 1000;
79
+ this.options.onRecovered?.();
80
+ if (next.pending.length)
81
+ this.refresh();
82
+ return next;
83
+ }
84
+ }
85
+ throw new Error("Reading projection has not reached the announced version");
86
+ }
87
+ async update(transform) {
88
+ if (this.closed)
89
+ throw new Error("READING_CLIENT_CLOSED");
90
+ const next = await this.persistence.update(this.options.appId, this.options.profileId, transform);
91
+ if (!this.closed) {
92
+ this.publish(next);
93
+ if (next.pending.length || next.observedReadIntents?.length)
94
+ this.refresh();
95
+ }
96
+ return next;
97
+ }
98
+ recordVisible(threadId, messageIds) { return this.record(threadId, messageIds, "visible"); }
99
+ recordExplicit(threadId, messageIds) { return this.record(threadId, messageIds, "explicit"); }
100
+ record(threadId, ids, readPolicy) {
101
+ const messageIds = [...new Set(ids)];
102
+ return this.update(state => messageIds.reduce((next, messageId) => recordReadIntent(next, { threadId, messageId, readPolicy, operationId: crypto.randomUUID() }), state));
103
+ }
104
+ async close() {
105
+ this.closed = true;
106
+ this.generation++;
107
+ this.offWake?.();
108
+ this.tasks.close();
109
+ clearTimeout(this.retry);
110
+ this.sync.stop();
111
+ this.listeners.clear();
112
+ await this.persistence.close();
113
+ await this.storage?.close();
114
+ }
115
+ }
@@ -0,0 +1,41 @@
1
+ export type RecordCacheOptions<T> = {
2
+ scope: string;
3
+ key: (value: T) => string;
4
+ merge: (previous: T | undefined, incoming: T) => T;
5
+ indexedDB?: IDBFactory;
6
+ name?: string;
7
+ onError?: (error: unknown) => void;
8
+ };
9
+ /** In-memory control-event index backed by bounded asynchronous IndexedDB batches.
10
+ * Legacy storage is read only during open; the hot path never uses localStorage. */
11
+ export declare class IndexedDbRecordCache<T> {
12
+ private options;
13
+ private db;
14
+ private records;
15
+ private metadata;
16
+ private values;
17
+ private snapshotValue;
18
+ private pending;
19
+ private waiters;
20
+ private tasks;
21
+ private ready?;
22
+ private closed;
23
+ private closing;
24
+ private initialized;
25
+ private running?;
26
+ private channel?;
27
+ private listeners;
28
+ constructor(options: RecordCacheOptions<T>);
29
+ open(legacy?: () => {
30
+ values: T[];
31
+ clear: () => void;
32
+ }): Promise<void>;
33
+ snapshot(): readonly T[];
34
+ subscribe(listener: () => void): () => void;
35
+ private reload;
36
+ upsert(incoming: T[]): Promise<void>;
37
+ private write;
38
+ flush(): Promise<void>;
39
+ private runFlush;
40
+ close(): Promise<void>;
41
+ }
@@ -0,0 +1,163 @@
1
+ import { Dexie } from "dexie";
2
+ import { WindowedTasks } from "./coalescer.js";
3
+ /** In-memory control-event index backed by bounded asynchronous IndexedDB batches.
4
+ * Legacy storage is read only during open; the hot path never uses localStorage. */
5
+ export class IndexedDbRecordCache {
6
+ options;
7
+ db;
8
+ records;
9
+ metadata;
10
+ values = new Map();
11
+ snapshotValue = [];
12
+ pending = new Map();
13
+ waiters = [];
14
+ tasks;
15
+ ready;
16
+ closed = false;
17
+ closing = false;
18
+ initialized = false;
19
+ running;
20
+ channel;
21
+ listeners = new Set();
22
+ constructor(options) {
23
+ this.options = options;
24
+ this.db = new Dexie(options.name ?? "pmate-chat-records", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
25
+ this.db.version(1).stores({ records: "[scope+id],scope", metadata: "scope" });
26
+ this.records = this.db.table("records");
27
+ this.metadata = this.db.table("metadata");
28
+ this.tasks = new WindowedTasks(16, error => options.onError?.(error));
29
+ if (typeof BroadcastChannel !== "undefined") {
30
+ this.channel = new BroadcastChannel(`pmate-chat-records:${options.scope}`);
31
+ this.channel.onmessage = () => this.tasks.schedule("reload", () => this.reload());
32
+ }
33
+ }
34
+ open(legacy) {
35
+ if (this.closed)
36
+ return Promise.reject(new Error("RECORD_CACHE_CLOSED"));
37
+ if (this.ready)
38
+ return this.ready;
39
+ this.ready = (async () => {
40
+ if (legacy && !(await this.metadata.get(this.options.scope))?.migrated) {
41
+ const previous = legacy();
42
+ // Idempotent batches: the completion marker is written only after every row.
43
+ for (let offset = 0; offset < previous.values.length; offset += 128)
44
+ await this.write(previous.values.slice(offset, offset + 128));
45
+ await this.metadata.put({ scope: this.options.scope, migrated: true });
46
+ previous.clear();
47
+ }
48
+ await this.reload();
49
+ this.initialized = true;
50
+ this.listeners.forEach(listener => listener());
51
+ })().catch(error => { this.ready = undefined; throw error; });
52
+ return this.ready;
53
+ }
54
+ snapshot() {
55
+ if (!this.initialized)
56
+ throw new Error("RECORD_CACHE_NOT_OPEN");
57
+ return this.snapshotValue;
58
+ }
59
+ subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
60
+ async reload() {
61
+ const rows = await this.records.where("scope").equals(this.options.scope).toArray();
62
+ if (this.closed)
63
+ return;
64
+ let changed = false;
65
+ for (const row of rows) {
66
+ const old = this.values.get(row.id), next = this.options.merge(old, row.value);
67
+ if (JSON.stringify(old) !== JSON.stringify(next)) {
68
+ this.values.set(row.id, next);
69
+ changed = true;
70
+ }
71
+ }
72
+ if (changed) {
73
+ this.snapshotValue = [...this.values.values()];
74
+ if (this.initialized)
75
+ this.listeners.forEach(listener => listener());
76
+ }
77
+ }
78
+ async upsert(incoming) {
79
+ if (this.closed || this.closing)
80
+ throw new Error("RECORD_CACHE_CLOSED");
81
+ if (!this.ready)
82
+ throw new Error("RECORD_CACHE_NOT_OPEN");
83
+ await this.ready;
84
+ if (this.closed || this.closing)
85
+ throw new Error("RECORD_CACHE_CLOSED");
86
+ if (incoming.length + this.pending.size > 4096)
87
+ throw new Error("RECORD_CACHE_BACKPRESSURE");
88
+ let changed = false;
89
+ for (const value of incoming) {
90
+ const key = this.options.key(value), old = this.values.get(key), next = this.options.merge(old, value);
91
+ if (JSON.stringify(old) === JSON.stringify(next))
92
+ continue;
93
+ this.values.set(key, next);
94
+ this.pending.set(key, next);
95
+ changed = true;
96
+ }
97
+ if (changed)
98
+ this.snapshotValue = [...this.values.values()];
99
+ if (!changed && !this.pending.size)
100
+ return;
101
+ const committed = new Promise((resolve, reject) => this.waiters.push({ resolve, reject }));
102
+ this.tasks.schedule("write", () => this.flush());
103
+ return committed;
104
+ }
105
+ async write(values) {
106
+ await this.db.transaction("rw", this.records, async () => {
107
+ const previous = await this.records.bulkGet(values.map(value => [this.options.scope, this.options.key(value)]));
108
+ const records = values.map((value, index) => ({ scope: this.options.scope, id: this.options.key(value), value: this.options.merge(previous[index]?.value, value) }));
109
+ const changed = records.filter((row, index) => JSON.stringify(previous[index]?.value) !== JSON.stringify(row.value));
110
+ if (changed.length)
111
+ await this.records.bulkPut(changed);
112
+ });
113
+ }
114
+ async flush() {
115
+ if (this.running) {
116
+ await this.running;
117
+ if (this.pending.size)
118
+ return this.flush();
119
+ return;
120
+ }
121
+ const task = this.runFlush().finally(() => { if (this.running === task)
122
+ this.running = undefined; });
123
+ this.running = task;
124
+ return task;
125
+ }
126
+ async runFlush() {
127
+ const values = [...this.pending.values()], waiters = this.waiters.splice(0);
128
+ this.pending.clear();
129
+ if (!values.length)
130
+ return;
131
+ try {
132
+ for (let offset = 0; offset < values.length; offset += 128)
133
+ await this.write(values.slice(offset, offset + 128));
134
+ await this.reload();
135
+ this.listeners.forEach(listener => listener());
136
+ this.channel?.postMessage("changed");
137
+ waiters.forEach(waiter => waiter.resolve());
138
+ }
139
+ catch (error) {
140
+ // Keep failed records dirty so a later operation can retry them.
141
+ for (const value of values) {
142
+ const key = this.options.key(value);
143
+ this.pending.set(key, this.options.merge(value, this.pending.get(key) ?? value));
144
+ }
145
+ waiters.forEach(waiter => waiter.reject(error));
146
+ throw error;
147
+ }
148
+ }
149
+ async close() {
150
+ this.closing = true;
151
+ await this.ready;
152
+ this.tasks.close();
153
+ try {
154
+ await this.flush();
155
+ }
156
+ finally {
157
+ this.closed = true;
158
+ this.channel?.close();
159
+ this.listeners.clear();
160
+ this.db.close();
161
+ }
162
+ }
163
+ }
@@ -6,6 +6,13 @@ export type SocialReadClientOptions = {
6
6
  hubHttpUrl?: string;
7
7
  fetch?: typeof fetch;
8
8
  };
9
+ export type SocialMessagePage = {
10
+ messages: ChatReaderSocialMessage[];
11
+ checkpoint: string;
12
+ nextBefore: string | null;
13
+ hasMoreChanges: boolean;
14
+ resetRequired: boolean;
15
+ };
9
16
  export type SocialGroup = {
10
17
  appId: string;
11
18
  groupId: string;
@@ -44,6 +51,13 @@ export declare class SocialReadClient {
44
51
  threadId: string;
45
52
  teamId?: string;
46
53
  }): Promise<ChatReaderSocialMessage[]>;
54
+ messagePage(input: {
55
+ threadId: string;
56
+ teamId?: string;
57
+ limit?: number;
58
+ before?: string;
59
+ after?: string;
60
+ }, signal?: AbortSignal): Promise<SocialMessagePage>;
47
61
  group(input: {
48
62
  groupId: string;
49
63
  teamId?: string;
@@ -24,6 +24,21 @@ export class SocialReadClient {
24
24
  this.requireTeam(input.teamId);
25
25
  return this.list("messages", { threadId: input.threadId, ...this.teamQuery(input.teamId) });
26
26
  }
27
+ async messagePage(input, signal) {
28
+ this.requireTeam(input.teamId);
29
+ const limit = input.limit ?? 100;
30
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500 || (input.before !== undefined && input.after !== undefined))
31
+ throw new SocialReadError(400, "INVALID_MESSAGE_PAGE");
32
+ const value = await this.request("messages", { threadId: input.threadId, ...this.teamQuery(input.teamId), limit: String(limit),
33
+ ...(input.before === undefined ? {} : { before: input.before }), ...(input.after === undefined ? {} : { after: input.after }) }, signal);
34
+ const cursor = (token) => typeof token === "string" && /^mp1:\d+:[0-9a-f]{64}$/.test(token);
35
+ if (!value || !Array.isArray(value.messages) || value.messages.length > limit || !cursor(value.checkpoint)
36
+ || (value.nextBefore !== null && !cursor(value.nextBefore)) || typeof value.hasMoreChanges !== "boolean" || typeof value.resetRequired !== "boolean"
37
+ || value.messages.some(row => row.appId !== this.options.appId || row.threadId !== input.threadId || row.teamId !== input.teamId || !row.messageId || !Number.isFinite(row.serverCreatedAt))
38
+ || (value.resetRequired && (value.messages.length || value.hasMoreChanges || value.nextBefore !== null)))
39
+ throw new SocialReadError(502, "INVALID_MESSAGE_PAGE_RESPONSE");
40
+ return value;
41
+ }
27
42
  group(input) {
28
43
  this.requireTeam(input.teamId);
29
44
  return this.request("group", { groupId: input.groupId, ...this.teamQuery(input.teamId) });
@@ -45,11 +60,11 @@ export class SocialReadClient {
45
60
  throw new SocialReadError(502, "INVALID_SOCIAL_READ_RESPONSE");
46
61
  return value;
47
62
  }
48
- async request(action, query) {
63
+ async request(action, query, signal) {
49
64
  const search = new URLSearchParams({ ...query, appId: this.options.appId, profileId: this.options.profileId });
50
65
  const result = await (this.options.fetch ?? fetch)(`${(this.options.hubHttpUrl ?? "https://hub.pmate.chat").replace(/\/+$/, "")}/api/social/${action}?${search}`, {
51
66
  headers: { authorization: `Bearer ${this.options.token}`, accept: "application/json" },
52
- cache: "no-store", redirect: "error", signal: AbortSignal.timeout(15_000),
67
+ cache: "no-store", redirect: "error", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
53
68
  });
54
69
  const envelope = await result.json().catch(() => null);
55
70
  if (!result.ok || envelope?.success !== true || !Object.hasOwn(envelope, "data")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.11.9",
3
+ "version": "0.12.0-next.1",
4
4
  "description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -28,15 +28,6 @@
28
28
  "publishConfig": {
29
29
  "access": "public"
30
30
  },
31
- "scripts": {
32
- "clean": "rimraf lib",
33
- "build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
34
- "prepack": "pnpm run build",
35
- "npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
36
- "test": "vitest run",
37
- "storybook": "storybook dev -p 6009",
38
- "build-storybook": "storybook build --output-dir dist"
39
- },
40
31
  "dependencies": {
41
32
  "@emoji-mart/data": "^1.2.1",
42
33
  "@emoji-mart/react": "^1.1.1",
@@ -52,16 +43,24 @@
52
43
  "devDependencies": {
53
44
  "@storybook/addon-docs": "^9.0.17",
54
45
  "@storybook/react-vite": "9.0.17",
55
- "@tailwindcss/vite": "catalog:",
56
- "@types/react": "catalog:",
57
- "@types/react-dom": "catalog:",
46
+ "@tailwindcss/vite": "^4.1.3",
47
+ "@types/react": "^19.2.7",
48
+ "@types/react-dom": "^19.2.3",
58
49
  "fake-indexeddb": "^6.2.5",
59
- "react": "catalog:",
60
- "react-dom": "catalog:",
50
+ "react": "^19.1.1",
51
+ "react-dom": "^19.1.1",
61
52
  "rimraf": "^6.1.2",
62
53
  "storybook": "^9.0.17",
63
- "tailwindcss": "catalog:",
54
+ "tailwindcss": "^4.0.14",
64
55
  "typescript": "5.9.2",
65
56
  "vitest": "^4.0.17"
57
+ },
58
+ "scripts": {
59
+ "clean": "rimraf lib",
60
+ "build": "pnpm run clean && tsc -p tsconfig.build.json && cp src/voice/recorder-processor.js lib/voice/recorder-processor.js",
61
+ "npm:publish": "pnpm run build && npm publish --access public --registry https://registry.npmjs.org/",
62
+ "test": "vitest run",
63
+ "storybook": "storybook dev -p 6009",
64
+ "build-storybook": "storybook build --output-dir dist"
66
65
  }
67
- }
66
+ }