@pmate/chat-sdk 0.12.0-next.45 → 0.12.0-next.47

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.
@@ -1,5 +1,7 @@
1
- import { type ReadIndexPersistence, type ReadIndexState, type ReadIndexTransform } from "./readIndex.js";
1
+ import { type ReadIndexPersistence, type ReadIndexState, type ReadIndexTransform, type ReadObservationScope } from "./readIndex.js";
2
2
  type Change = {
3
+ weight: number;
4
+ observation?: ReadObservationScope;
3
5
  transform: ReadIndexTransform;
4
6
  resolve: (state: ReadIndexState) => void;
5
7
  reject: (error: unknown) => void;
@@ -13,6 +15,7 @@ export declare class BatchedReadIndex implements ReadIndexPersistence {
13
15
  private queueLimit;
14
16
  private queues;
15
17
  private closed;
18
+ private sourceCounts;
16
19
  constructor(storage: ReadIndexPersistence, windowMs?: number, batchSize?: number, queueLimit?: number);
17
20
  load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
18
21
  loadVersioned(appId: string, profileId: string): Promise<{
@@ -24,7 +27,8 @@ export declare class BatchedReadIndex implements ReadIndexPersistence {
24
27
  }>;
25
28
  loadVersions(appId: string, profileId: string): Promise<{}>;
26
29
  invalidateTeam(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
27
- update(appId: string, profileId: string, transform: Change["transform"]): Promise<ReadIndexState>;
30
+ private weight;
31
+ update(appId: string, profileId: string, transform: Change["transform"], observation?: ReadObservationScope): Promise<ReadIndexState>;
28
32
  private drain;
29
33
  flush(appId: string, profileId: string): Promise<void>;
30
34
  close(): Promise<void>;
@@ -8,6 +8,7 @@ export class BatchedReadIndex {
8
8
  queueLimit;
9
9
  queues = new Map();
10
10
  closed = false;
11
+ sourceCounts = new WeakMap();
11
12
  constructor(storage, windowMs = 16, batchSize = 128, queueLimit = 4096) {
12
13
  this.storage = storage;
13
14
  this.windowMs = windowMs;
@@ -42,7 +43,22 @@ export class BatchedReadIndex {
42
43
  // barrier; any queued transform sees it inside its eventual transaction.
43
44
  return this.storage.invalidateTeam(appId, profileId, teamId, version, knownThreadIds);
44
45
  }
45
- update(appId, profileId, transform) {
46
+ weight(observation) {
47
+ if (!observation)
48
+ return 1;
49
+ let counts = this.sourceCounts.get(observation.base);
50
+ if (!counts) {
51
+ counts = new Map();
52
+ for (const thread of observation.base.threads) {
53
+ counts.set(thread.id, (counts.get(thread.id) ?? 0) + 1);
54
+ if (thread.conversationId && thread.conversationId !== thread.id)
55
+ counts.set(thread.conversationId, (counts.get(thread.conversationId) ?? 0) + 1);
56
+ }
57
+ this.sourceCounts.set(observation.base, counts);
58
+ }
59
+ return observation.targets.reduce((sum, target) => sum + (counts.get(target.threadId) ?? 1), 0);
60
+ }
61
+ update(appId, profileId, transform, observation) {
46
62
  if (this.closed)
47
63
  return Promise.reject(new Error("READ_STORAGE_CLOSED"));
48
64
  const key = JSON.stringify([appId, profileId]);
@@ -53,7 +69,7 @@ export class BatchedReadIndex {
53
69
  }
54
70
  if (queue.pending.length >= this.queueLimit)
55
71
  return Promise.reject(new Error("READ_STORAGE_BACKPRESSURE"));
56
- const promise = new Promise((resolve, reject) => queue.pending.push({ transform, resolve, reject }));
72
+ const promise = new Promise((resolve, reject) => queue.pending.push({ transform, observation, weight: this.weight(observation), resolve, reject }));
57
73
  if (!queue.running && queue.timer === undefined)
58
74
  queue.timer = setTimeout(() => { queue.timer = undefined; void this.drain(key, queue); }, this.windowMs);
59
75
  return promise;
@@ -63,12 +79,22 @@ export class BatchedReadIndex {
63
79
  return queue.running;
64
80
  clearTimeout(queue.timer);
65
81
  queue.timer = undefined;
66
- const batch = queue.pending.splice(0, this.batchSize);
82
+ const batch = [];
83
+ let weight = 0;
84
+ while (queue.pending.length && batch.length < this.batchSize) {
85
+ const nextWeight = queue.pending[0].weight;
86
+ if (batch.length && weight + nextWeight > 128)
87
+ break;
88
+ weight += nextWeight;
89
+ batch.push(queue.pending.shift());
90
+ }
67
91
  if (!batch.length)
68
92
  return;
69
93
  const run = (async () => {
70
94
  try {
71
- const next = await this.storage.update(queue.appId, queue.profileId, (state, versions) => batch.reduce((current, change) => change.transform(current, versions), state));
95
+ const first = batch[0].observation;
96
+ const observation = first && batch.every(change => change.observation?.base === first.base) ? { base: first.base, targets: batch.flatMap(change => change.observation.targets) } : undefined;
97
+ const next = await this.storage.update(queue.appId, queue.profileId, (state, versions) => batch.reduce((current, change) => change.transform(current, versions), state), observation);
72
98
  batch.forEach(change => change.resolve(next));
73
99
  }
74
100
  catch (error) {
@@ -90,6 +90,7 @@ export declare class MessageClient<T extends StoredMessage> {
90
90
  private release;
91
91
  private flushWrites;
92
92
  private runWrites;
93
+ private assertNetworkAvailable;
93
94
  synchronize(thread: string): Promise<void>;
94
95
  /** Keep an explicitly requested history range in the shared subscribed view. */
95
96
  expandWindow(thread: string, page: MessageSnapshot<T>): Promise<void>;
@@ -168,7 +168,8 @@ export class MessageClient {
168
168
  const fallback = await this.fallback(thread, limit);
169
169
  if (!fallback.messages.length)
170
170
  throw error;
171
- this.options.onError?.(error, thread);
171
+ if (!isOfflineNetworkFailure(error))
172
+ this.options.onError?.(error, thread);
172
173
  return fallback;
173
174
  }
174
175
  }
@@ -188,7 +189,8 @@ export class MessageClient {
188
189
  const cached = await this.fallback(thread, limit);
189
190
  if (!cached.messages.length)
190
191
  throw error;
191
- this.options.onError?.(error, thread);
192
+ if (!isOfflineNetworkFailure(error))
193
+ this.options.onError?.(error, thread);
192
194
  return cached;
193
195
  }
194
196
  const snapshot = await this.reload(thread);
@@ -433,6 +435,12 @@ export class MessageClient {
433
435
  this.release(queue);
434
436
  }
435
437
  }
438
+ assertNetworkAvailable(thread) {
439
+ if (this.closed || this.closing || this.scopeDenied || this.revoked.has(thread))
440
+ throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
441
+ if (isOffline())
442
+ throw new TypeError("MESSAGE_OFFLINE_UNAVAILABLE");
443
+ }
436
444
  synchronize(thread) {
437
445
  if (this.closed || this.closing || this.scopeDenied || this.revoked.has(thread))
438
446
  return Promise.reject(new Error("MESSAGE_CLIENT_UNAVAILABLE"));
@@ -449,6 +457,7 @@ export class MessageClient {
449
457
  }
450
458
  let version = coverage?.resetVersion ?? 0;
451
459
  let after = coverage?.complete ? coverage.checkpoint : undefined;
460
+ this.assertNetworkAvailable(thread);
452
461
  let page = after && this.options.transport.changes
453
462
  ? await this.options.transport.changes(thread, after, this.abort.signal)
454
463
  : await this.options.transport.load(thread, this.abort.signal);
@@ -457,6 +466,7 @@ export class MessageClient {
457
466
  throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
458
467
  version = await this.storage.resetThread(thread, false, scopeVersion);
459
468
  after = undefined;
469
+ this.assertNetworkAvailable(thread);
460
470
  page = await this.options.transport.load(thread, this.abort.signal);
461
471
  }
462
472
  for (;;) {
@@ -470,6 +480,7 @@ export class MessageClient {
470
480
  if (!page.checkpoint || page.checkpoint === after || !this.options.transport.changes)
471
481
  throw new Error("MESSAGE_SYNC_NO_PROGRESS");
472
482
  after = page.checkpoint;
483
+ this.assertNetworkAvailable(thread);
473
484
  page = await this.options.transport.changes(thread, after, this.abort.signal);
474
485
  }
475
486
  })().catch(async (error) => {
@@ -530,7 +541,8 @@ export class MessageClient {
530
541
  const fallback = await this.fallback(thread, limit, before);
531
542
  if (!fallback.messages.length)
532
543
  throw error;
533
- this.options.onError?.(error, thread);
544
+ if (!isOfflineNetworkFailure(error))
545
+ this.options.onError?.(error, thread);
534
546
  return fallback;
535
547
  }
536
548
  }
@@ -546,6 +558,7 @@ export class MessageClient {
546
558
  }
547
559
  const token = page.coverage.nextBefore, version = page.coverage.resetVersion ?? 0, scopeVersion = page.coverage.scopeVersion ?? 0;
548
560
  const task = (async () => {
561
+ this.assertNetworkAvailable(thread);
549
562
  const remote = await this.options.transport.older(thread, token, this.abort.signal);
550
563
  if (this.closed || this.scopeDenied || this.revoked.has(thread))
551
564
  throw new Error("MESSAGE_CLIENT_UNAVAILABLE");
@@ -90,6 +90,14 @@ export declare function assertReadIndexVersions(expected: ReadIndexVersions, cur
90
90
  /** Remove only the withdrawn team's original sources, logical conversations and
91
91
  * queued observations. Other teams' pending operations must survive. */
92
92
  export declare function removeReadTeam(state: ReadIndexState, teamId: string, knownThreadIds?: readonly string[]): ReadIndexState;
93
+ /** Internal hint for typed reading observations; arbitrary transforms omit it. */
94
+ export type ReadObservationScope = {
95
+ base: ReadIndexState;
96
+ targets: readonly {
97
+ threadId: string;
98
+ messageId: string;
99
+ }[];
100
+ };
93
101
  export interface ReadIndexPersistence {
94
102
  load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
95
103
  loadVersioned?(appId: string, profileId: string): Promise<{
@@ -99,7 +107,7 @@ export interface ReadIndexPersistence {
99
107
  loadVersions?(appId: string, profileId: string): Promise<ReadIndexVersions>;
100
108
  invalidateTeam?(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
101
109
  /** Transform and persist atomically; UI publishes only the committed result. */
102
- update(appId: string, profileId: string, transform: ReadIndexTransform): Promise<ReadIndexState>;
110
+ update(appId: string, profileId: string, transform: ReadIndexTransform, observation?: ReadObservationScope): Promise<ReadIndexState>;
103
111
  }
104
112
  /** Dexie preserves the legacy database/store and outbound [appId, profileId] keys. */
105
113
  export declare class IndexedDbReadIndex implements ReadIndexPersistence {
@@ -112,6 +120,7 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
112
120
  private messageRows;
113
121
  private positionRows;
114
122
  private normalizedStates;
123
+ private observationMessages;
115
124
  private versions;
116
125
  private closed;
117
126
  constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean, metadataOnly?: boolean);
@@ -130,7 +139,8 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
130
139
  }>;
131
140
  loadVersions(appId: string, profileId: string): Promise<ReadIndexVersions>;
132
141
  invalidateTeam(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
133
- update(appId: string, profileId: string, transform: ReadIndexTransform): Promise<ReadIndexState>;
142
+ private observe;
143
+ update(appId: string, profileId: string, transform: ReadIndexTransform, observation?: ReadObservationScope): Promise<ReadIndexState>;
134
144
  close(): Promise<void>;
135
145
  }
136
146
  export declare function sourcesInConversation(state: ReadIndexState, conversationId: string): IndexedReadThread[];
@@ -214,6 +214,7 @@ export class IndexedDbReadIndex {
214
214
  messageRows;
215
215
  positionRows;
216
216
  normalizedStates = new WeakSet();
217
+ observationMessages = new WeakMap();
217
218
  versions;
218
219
  closed = false;
219
220
  constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false, metadataOnly = false) {
@@ -227,6 +228,9 @@ export class IndexedDbReadIndex {
227
228
  this.database.version(1).stores({ profiles: "" });
228
229
  this.database.version(2).stores({ profiles: "", versions: "" });
229
230
  this.database.version(3).stores({ readThreads: "[owner+id],owner", readMessages: "[owner+thread+id],[owner+thread],owner", readPositions: "[owner+id],owner" });
231
+ // Native indexes track scalar source identity even when an older writer
232
+ // changes rows. Reading these keys does not deserialize messageIds arrays.
233
+ this.database.version(4).stores({ readThreads: "[owner+id],owner,[owner+id+value.kind+value.teamId],[owner+value.conversationId+value.kind+value.teamId+id],[owner+id+value.conversationId]" });
230
234
  this.positionRows = this.database.table("readPositions");
231
235
  this.threadRows = this.database.table("readThreads");
232
236
  this.messageRows = this.database.table("readMessages");
@@ -435,8 +439,113 @@ export class IndexedDbReadIndex {
435
439
  return next;
436
440
  });
437
441
  }
438
- async update(appId, profileId, transform) {
442
+ async observe(appId, profileId, transform, scope) {
443
+ if (!this.metadataOnly || scope.targets.length > 128)
444
+ return undefined;
445
+ const key = this.key(appId, profileId), owner = JSON.stringify(key), base = scope.base;
446
+ const header = await this.profiles.get(key);
447
+ if (header && (header.appId !== appId || header.profileId !== profileId || header.schemaVersion !== 1))
448
+ throw new Error("READ_IDENTITY_MISMATCH");
449
+ if (!header?.normalized || base.schemaVersion !== 1 || base.appId !== appId || base.profileId !== profileId || base.syncCursor !== header.syncCursor ||
450
+ header.threadOrder?.length !== base.threads.length || header.threadOrder.some((id, i) => id !== base.threads[i].id))
451
+ return undefined;
452
+ const sources = new Map();
453
+ for (const threadId of new Set(scope.targets.map(target => target.threadId))) {
454
+ const direct = await this.threadRows.where("[owner+id+value.kind+value.teamId]").between([owner, threadId, Dexie.minKey], [owner, threadId, Dexie.maxKey], true, true).limit(129).keys();
455
+ const assigned = await this.threadRows.where("[owner+value.conversationId+value.kind+value.teamId+id]").between([owner, threadId, Dexie.minKey], [owner, threadId, Dexie.maxKey], true, true).limit(129).keys();
456
+ if (direct.length + assigned.length > 128)
457
+ return undefined;
458
+ const identities = [...direct.map(value => ({ id: value[1], kind: value[2], teamId: value[3] })), ...assigned.map(value => ({ id: value[4], kind: value[2], teamId: value[3] }))];
459
+ if (!identities.length && base.threads.some(thread => thread.id === threadId || thread.conversationId === threadId))
460
+ return undefined;
461
+ for (const identity of identities) {
462
+ const cached = base.threads.find(thread => thread.id === identity.id);
463
+ if (!cached || cached.kind !== identity.kind || cached.teamId !== identity.teamId)
464
+ return undefined;
465
+ const mapping = await this.threadRows.where("[owner+id+value.conversationId]").between([owner, identity.id, Dexie.minKey], [owner, identity.id, Dexie.maxKey], true, true).limit(2).keys();
466
+ if (mapping.length > 1 || cached.conversationId !== mapping[0]?.[2])
467
+ return undefined;
468
+ let source = sources.get(identity.id);
469
+ if (!source) {
470
+ source = { cached, ids: new Set() };
471
+ sources.set(identity.id, source);
472
+ }
473
+ for (const target of scope.targets)
474
+ if (target.threadId === threadId)
475
+ source.ids.add(target.messageId);
476
+ }
477
+ }
478
+ if ([...sources.values()].reduce((sum, source) => sum + source.ids.size, 0) > 128)
479
+ return undefined;
480
+ const sourceIds = [...sources.keys()];
481
+ // Cursors are the mutable cross-document view. Refresh their small rows,
482
+ // while message metadata remains targeted; otherwise a later batch based
483
+ // on an older snapshot could regress a different conversation's progress.
484
+ const allPositions = await this.positionRows.where("owner").equals(owner).toArray();
485
+ const positionById = new Map(allPositions.map(position => [position.id, position]));
486
+ if (base.threads.some(thread => !positionById.has(thread.id)))
487
+ return undefined;
488
+ const positions = sourceIds.map(id => positionById.get(id));
489
+ const partialThreads = [];
490
+ for (let i = 0; i < sourceIds.length; i++) {
491
+ const id = sourceIds[i], source = sources.get(id), position = positions[i];
492
+ if (!position)
493
+ return undefined;
494
+ let cached = this.observationMessages.get(source.cached.messages);
495
+ if (!cached) {
496
+ cached = new Map(source.cached.messages.map(message => [message.id, message]));
497
+ this.observationMessages.set(source.cached.messages, cached);
498
+ }
499
+ const ids = [...source.ids], rows = await this.messageRows.bulkGet(ids.map(messageId => [owner, id, messageId]));
500
+ const messages = [];
501
+ for (let j = 0; j < ids.length; j++) {
502
+ const previous = cached.get(ids[j]), message = rows[j]?.value;
503
+ if (message && (message.payload !== null || typeof message.readingControl !== "boolean"))
504
+ return undefined;
505
+ if (JSON.stringify(previous) !== JSON.stringify(message))
506
+ return undefined;
507
+ if (message)
508
+ messages.push(previous);
509
+ }
510
+ partialThreads.push({ ...source.cached, messages, readThroughSeq: position.readThroughSeq, explicitReadMessageIds: position.explicitReadMessageIds });
511
+ }
512
+ const partial = { schemaVersion: 1, appId, profileId, syncCursor: header.syncCursor, pending: header.pending, observedReadIntents: header.observedReadIntents, threads: partialThreads };
513
+ const result = transform(partial, (await this.versions.get(key))?.teams ?? {});
514
+ if (result.appId !== appId || result.profileId !== profileId || result.schemaVersion !== 1)
515
+ throw new Error("READ_IDENTITY_MISMATCH");
516
+ if (result.syncCursor !== partial.syncCursor || result.threads.length !== partialThreads.length || result.threads.some((thread, i) => thread.id !== partialThreads[i].id || thread.kind !== partialThreads[i].kind || thread.teamId !== partialThreads[i].teamId || thread.conversationId !== partialThreads[i].conversationId || thread.messages !== partialThreads[i].messages))
517
+ return undefined;
518
+ const writes = [];
519
+ for (let i = 0; i < result.threads.length; i++) {
520
+ const thread = result.threads[i], before = positions[i];
521
+ if (thread.readThroughSeq !== before.readThroughSeq || JSON.stringify(thread.explicitReadMessageIds) !== JSON.stringify(before.explicitReadMessageIds))
522
+ writes.push({ owner, id: thread.id, readThroughSeq: thread.readThroughSeq, explicitReadMessageIds: thread.explicitReadMessageIds });
523
+ }
524
+ if (writes.length)
525
+ await this.positionRows.bulkPut(writes);
526
+ if (JSON.stringify(header.pending) !== JSON.stringify(result.pending) || JSON.stringify(header.observedReadIntents) !== JSON.stringify(result.observedReadIntents))
527
+ await this.profiles.put({ ...header, pending: result.pending, observedReadIntents: result.observedReadIntents }, key);
528
+ const byId = new Map(allPositions.map(position => [position.id, position]));
529
+ result.threads.forEach(thread => byId.set(thread.id, thread));
530
+ let changed = false;
531
+ const threads = base.threads.map(thread => {
532
+ const position = byId.get(thread.id);
533
+ if (!position || thread.readThroughSeq === position.readThroughSeq && JSON.stringify(thread.explicitReadMessageIds) === JSON.stringify(position.explicitReadMessageIds))
534
+ return thread;
535
+ changed = true;
536
+ return { ...thread, readThroughSeq: position.readThroughSeq, explicitReadMessageIds: position.explicitReadMessageIds };
537
+ });
538
+ if (!changed && JSON.stringify(base.pending) === JSON.stringify(result.pending) && JSON.stringify(base.observedReadIntents) === JSON.stringify(result.observedReadIntents))
539
+ return base;
540
+ return { ...base, threads, pending: result.pending, observedReadIntents: result.observedReadIntents };
541
+ }
542
+ async update(appId, profileId, transform, observation) {
439
543
  return this.transaction("rw", async () => {
544
+ if (observation) {
545
+ const next = await this.observe(appId, profileId, transform, observation);
546
+ if (next)
547
+ return next;
548
+ }
440
549
  const previous = await this.loadCurrent(appId, profileId);
441
550
  const transformed = transform(previous ?? createReadIndex(appId, profileId), (await this.versions.get(this.key(appId, profileId)))?.teams ?? {});
442
551
  const next = this.metadataOnly ? readingMetadata(transformed) : transformed;
@@ -39,7 +39,7 @@ export class ReadingClient {
39
39
  getSnapshot = () => this.state;
40
40
  subscribe = (listener) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; };
41
41
  publish(state) {
42
- if (this.closed || JSON.stringify(state) === JSON.stringify(this.state))
42
+ if (this.closed || state === this.state || JSON.stringify(state) === JSON.stringify(this.state))
43
43
  return;
44
44
  this.state = state;
45
45
  this.listeners.forEach(listener => listener());
@@ -102,7 +102,7 @@ export class ReadingClient {
102
102
  // methods below only derive changes for their configured team.
103
103
  return this.updateScoped(transform);
104
104
  }
105
- updateScoped(transform, teamId) {
105
+ updateScoped(transform, teamId, observation) {
106
106
  if (this.closed)
107
107
  return Promise.reject(new Error("READING_CLIENT_CLOSED"));
108
108
  // One shared metadata read per local authority generation, rather than one
@@ -114,7 +114,7 @@ export class ReadingClient {
114
114
  assertReadIndexVersions(expected, currentVersions, teamId);
115
115
  const next = transform(state);
116
116
  return this.options.metadataOnly ? readingMetadata(next) : next;
117
- });
117
+ }, observation);
118
118
  if (!this.closed) {
119
119
  this.publish(next);
120
120
  if (next.pending.length || next.observedReadIntents?.length)
@@ -162,7 +162,16 @@ export class ReadingClient {
162
162
  recordExplicit(threadId, messageIds) { return this.record(threadId, messageIds, "explicit"); }
163
163
  record(threadId, ids, readPolicy) {
164
164
  const messageIds = [...new Set(ids)];
165
- return this.updateScoped(state => { this.assertThreadTeam(state, threadId); return messageIds.reduce((next, messageId) => recordReadIntent(next, { threadId, messageId, readPolicy, operationId: crypto.randomUUID() }), state); }, this.options.teamId);
165
+ if (!messageIds.length && this.state) {
166
+ try {
167
+ this.assertThreadTeam(this.state, threadId);
168
+ return Promise.resolve(this.state);
169
+ }
170
+ catch (error) {
171
+ return Promise.reject(error);
172
+ }
173
+ }
174
+ return this.updateScoped(state => { this.assertThreadTeam(state, threadId); return messageIds.reduce((next, messageId) => recordReadIntent(next, { threadId, messageId, readPolicy, operationId: crypto.randomUUID() }), state); }, this.options.teamId, this.state && messageIds.length ? { base: this.state, targets: messageIds.map(messageId => ({ threadId, messageId })) } : undefined);
166
175
  }
167
176
  withdraw() { this.stop(); }
168
177
  stop() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.45",
3
+ "version": "0.12.0-next.47",
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",