@pmate/chat-sdk 0.12.0-next.38 → 0.12.0-next.39

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.
@@ -67,7 +67,7 @@ export class ChatSession {
67
67
  return options.connection.onReach(event);
68
68
  throw new Error("REACH_HANDLER_UNAVAILABLE");
69
69
  } });
70
- this.reading = new ReadingClient({ ...options.reading, teamId: scope.teamId, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true });
70
+ this.reading = new ReadingClient({ ...options.reading, teamId: scope.teamId, storageScope: JSON.stringify([scope.accountId]), migrateLegacy: true, metadataOnly: true });
71
71
  this.readers = new MessageReadersClient({ appId: scope.appId, profileId: scope.profileId, token: options.reading.sessionToken, scope, access: () => this.messages.scopeAccess(), onError: options.onError, hubHttpUrl: options.reading.hubHttpUrl, fetch: options.reading.fetch });
72
72
  this.sender = new DurableChatSender(this.connection, scope, options.onError, { access: () => this.messages.scopeAccess() });
73
73
  this.catchUp = new WindowedTasks(1000, error => { if (!this.closed)
@@ -7,9 +7,16 @@ export type IndexedReadMessage = {
7
7
  mentions: string[];
8
8
  readPolicy: "visible" | "explicit";
9
9
  recalled: boolean;
10
- /** Cached payload is retained so an offline reload can render the message. */
10
+ /** Legacy standalone readers may retain payloads; unified sessions use the message store. */
11
11
  payload: unknown;
12
+ /** Body-free control classification retained by metadata-only reading indexes. */
13
+ readingControl?: boolean;
12
14
  };
15
+ /** Retain read semantics without a second durable copy of message bodies. */
16
+ export declare function readingMetadata<T extends {
17
+ appId: string;
18
+ threads: IndexedReadThread[];
19
+ }>(state: T): T;
13
20
  export type ReadingOperation = {
14
21
  operationId: string;
15
22
  threadId: string;
@@ -98,6 +105,7 @@ export interface ReadIndexPersistence {
98
105
  export declare class IndexedDbReadIndex implements ReadIndexPersistence {
99
106
  private storageScope?;
100
107
  private migrateLegacy;
108
+ private metadataOnly;
101
109
  private database;
102
110
  private profiles;
103
111
  private threadRows;
@@ -106,11 +114,12 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
106
114
  private normalizedStates;
107
115
  private versions;
108
116
  private closed;
109
- constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean);
117
+ constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean, metadataOnly?: boolean);
110
118
  private transaction;
111
119
  private key;
112
120
  private hydrate;
113
121
  private loadCurrent;
122
+ private loadStored;
114
123
  /** Message bodies are immutable for local read observations: keep them out of
115
124
  * profile/cursor writes, and update only changed normalized rows on sync. */
116
125
  private persist;
@@ -1,6 +1,24 @@
1
1
  import { Dexie } from "dexie";
2
2
  import { isReadingControl } from "./readingContent.js";
3
3
  import { isValidChatTeamId } from "./thread.js";
4
+ /** Retain read semantics without a second durable copy of message bodies. */
5
+ export function readingMetadata(state) {
6
+ let changed = false;
7
+ const threads = state.threads.map(thread => {
8
+ let different = false;
9
+ const messages = thread.messages.map(message => {
10
+ if (message.payload === null && typeof message.readingControl === "boolean")
11
+ return message;
12
+ different = true;
13
+ return { ...message, payload: null, readingControl: Boolean(message.readingControl) || isReadingControl(message.payload, state.appId) };
14
+ });
15
+ if (!different)
16
+ return thread;
17
+ changed = true;
18
+ return { ...thread, messages };
19
+ });
20
+ return changed ? { ...state, threads } : state;
21
+ }
4
22
  export class ReadSyncGap extends Error {
5
23
  cursor;
6
24
  constructor(cursor) {
@@ -159,7 +177,7 @@ export function unreadInThread(state, threadId, hidden) {
159
177
  if (!thread)
160
178
  throw new Error("UNKNOWN_READ_THREAD");
161
179
  const explicit = new Set(thread.explicitReadMessageIds);
162
- const messages = thread.messages.filter(message => !message.recalled && !hidden?.get(message.id)?.has(message.from) && !isReadingControl(message.payload, state.appId) && message.from !== state.profileId && message.recipients.includes(state.profileId)
180
+ const messages = thread.messages.filter(message => !message.recalled && !hidden?.get(message.id)?.has(message.from) && !(message.readingControl || isReadingControl(message.payload, state.appId)) && message.from !== state.profileId && message.recipients.includes(state.profileId)
163
181
  && (message.readPolicy === "explicit" ? !explicit.has(message.id) : message.seq > thread.readThroughSeq));
164
182
  const unreadMessageCount = messages.length;
165
183
  const unreadMentionCount = messages.filter(message => message.mentions.includes(state.profileId)).length;
@@ -189,6 +207,7 @@ export function removeReadTeam(state, teamId, knownThreadIds = []) {
189
207
  export class IndexedDbReadIndex {
190
208
  storageScope;
191
209
  migrateLegacy;
210
+ metadataOnly;
192
211
  database;
193
212
  profiles;
194
213
  threadRows;
@@ -197,9 +216,10 @@ export class IndexedDbReadIndex {
197
216
  normalizedStates = new WeakSet();
198
217
  versions;
199
218
  closed = false;
200
- constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false) {
219
+ constructor(factory = globalThis.indexedDB, name = "pmate-social-read-index", storageScope, migrateLegacy = false, metadataOnly = false) {
201
220
  this.storageScope = storageScope;
202
221
  this.migrateLegacy = migrateLegacy;
222
+ this.metadataOnly = metadataOnly;
203
223
  if (!factory)
204
224
  throw new Error("READ_STORAGE_UNAVAILABLE");
205
225
  this.database = new Dexie(name, { indexedDB: factory, IDBKeyRange: globalThis.IDBKeyRange });
@@ -274,13 +294,23 @@ export class IndexedDbReadIndex {
274
294
  return current;
275
295
  }
276
296
  async loadCurrent(appId, profileId) {
297
+ const current = await this.loadStored(appId, profileId);
298
+ if (!current || !this.metadataOnly)
299
+ return current;
300
+ const metadata = readingMetadata(current);
301
+ if (metadata !== current)
302
+ await this.persist(appId, profileId, current, metadata);
303
+ return metadata;
304
+ }
305
+ async loadStored(appId, profileId) {
277
306
  const current = await this.profiles.get(this.key(appId, profileId));
278
307
  if (current?.normalized)
279
308
  return this.hydrate(current, this.key(appId, profileId));
280
309
  if (current) {
281
- if (this.migrateLegacy)
282
- await this.persist(appId, profileId, undefined, current, true);
283
- return current;
310
+ const next = this.metadataOnly ? readingMetadata(current) : current;
311
+ if (this.migrateLegacy || this.metadataOnly)
312
+ await this.persist(appId, profileId, undefined, next, true);
313
+ return next;
284
314
  }
285
315
  if (!this.migrateLegacy || this.storageScope === undefined)
286
316
  return undefined;
@@ -291,7 +321,8 @@ export class IndexedDbReadIndex {
291
321
  const legacyRow = await this.profiles.get([appId, profileId]);
292
322
  if (!legacyRow)
293
323
  return undefined;
294
- const legacy = await this.hydrate(legacyRow, [appId, profileId]);
324
+ const hydrated = await this.hydrate(legacyRow, [appId, profileId]);
325
+ const legacy = this.metadataOnly ? readingMetadata(hydrated) : hydrated;
295
326
  if (legacy.appId !== appId || legacy.profileId !== profileId || legacy.schemaVersion !== 1)
296
327
  throw new Error("READ_IDENTITY_MISMATCH");
297
328
  const ownership = { ...createReadIndex(appId, profileId), legacyOwner: this.storageScope };
@@ -380,10 +411,10 @@ export class IndexedDbReadIndex {
380
411
  this.normalizedStates.add(next);
381
412
  }
382
413
  async load(appId, profileId) {
383
- return this.transaction(this.migrateLegacy ? "rw" : "r", () => this.loadCurrent(appId, profileId));
414
+ return this.transaction(this.migrateLegacy || this.metadataOnly ? "rw" : "r", () => this.loadCurrent(appId, profileId));
384
415
  }
385
416
  async loadVersioned(appId, profileId) {
386
- return this.transaction(this.migrateLegacy ? "rw" : "r", async () => ({
417
+ return this.transaction(this.migrateLegacy || this.metadataOnly ? "rw" : "r", async () => ({
387
418
  state: await this.loadCurrent(appId, profileId), versions: (await this.versions.get(this.key(appId, profileId)))?.teams ?? {},
388
419
  }));
389
420
  }
@@ -407,7 +438,8 @@ export class IndexedDbReadIndex {
407
438
  async update(appId, profileId, transform) {
408
439
  return this.transaction("rw", async () => {
409
440
  const previous = await this.loadCurrent(appId, profileId);
410
- const next = transform(previous ?? createReadIndex(appId, profileId), (await this.versions.get(this.key(appId, profileId)))?.teams ?? {});
441
+ const transformed = transform(previous ?? createReadIndex(appId, profileId), (await this.versions.get(this.key(appId, profileId)))?.teams ?? {});
442
+ const next = this.metadataOnly ? readingMetadata(transformed) : transformed;
411
443
  if (next.appId !== appId || next.profileId !== profileId || next.schemaVersion !== 1)
412
444
  throw new Error("READ_IDENTITY_MISMATCH");
413
445
  await this.persist(appId, profileId, previous, next);
@@ -4,6 +4,7 @@ export type ReadSyncOptions = {
4
4
  profileId: string;
5
5
  sessionToken: string;
6
6
  hubHttpUrl: string;
7
+ metadataOnly?: boolean;
7
8
  persistence: ReadIndexPersistence;
8
9
  fetch?: typeof globalThis.fetch;
9
10
  onChange?: (state: ReadIndexState) => void;
@@ -1,4 +1,4 @@
1
- import { assertReadIndexVersions, applyReadChange, ReadSyncGap, createReadIndex, installReadSnapshot } from "./readIndex.js";
1
+ import { readingMetadata, assertReadIndexVersions, applyReadChange, ReadSyncGap, createReadIndex, installReadSnapshot } from "./readIndex.js";
2
2
  /** Keeps local operations until an authoritative projection covers them. */
3
3
  export class ReadSyncClient {
4
4
  options;
@@ -9,8 +9,11 @@ export class ReadSyncClient {
9
9
  this.options = options;
10
10
  }
11
11
  async loadLocal() {
12
- return await this.options.persistence.load(this.options.appId, this.options.profileId)
13
- ?? createReadIndex(this.options.appId, this.options.profileId);
12
+ const { appId, profileId, persistence, metadataOnly } = this.options;
13
+ const state = await persistence.load(appId, profileId) ?? createReadIndex(appId, profileId);
14
+ if (!metadataOnly || readingMetadata(state) === state)
15
+ return state;
16
+ return persistence.update(appId, profileId, readingMetadata);
14
17
  }
15
18
  stop() { this.controller.abort(); }
16
19
  synchronize() {
@@ -84,7 +87,7 @@ export class ReadSyncClient {
84
87
  throw new Error("READ_SYNC_STOPPED");
85
88
  const next = await persistence.update(appId, profileId, (state, currentVersions) => {
86
89
  assertReadIndexVersions(versions, currentVersions);
87
- const applied = delta.changes.reduce(applyReadChange, state);
90
+ const applied = delta.changes.reduce((current, change) => applyReadChange(current, this.options.metadataOnly ? { ...change, thread: readingMetadata({ appId, threads: [change.thread] }).threads[0] } : change), state);
88
91
  if (applied.syncCursor !== delta.syncCursor)
89
92
  throw new ReadSyncGap(applied.syncCursor);
90
93
  return applied;
@@ -107,7 +110,7 @@ export class ReadSyncClient {
107
110
  // Merge into the latest transaction, including reads made while HTTP was pending.
108
111
  const next = await persistence.update(appId, profileId, (state, currentVersions) => {
109
112
  assertReadIndexVersions(versions, currentVersions);
110
- return installReadSnapshot(state, snapshot);
113
+ return installReadSnapshot(state, this.options.metadataOnly ? readingMetadata(snapshot) : snapshot);
111
114
  });
112
115
  if (!next.pending.length)
113
116
  this.readAfter = 0;
@@ -6,6 +6,7 @@ export type ReadingClientOptions = {
6
6
  sessionToken: string;
7
7
  hubHttpUrl: string;
8
8
  teamId?: string;
9
+ metadataOnly?: boolean;
9
10
  persistence?: ReadIndexPersistence;
10
11
  storageScope?: string;
11
12
  migrateLegacy?: boolean;
@@ -3,7 +3,7 @@ import { isOffline, isOfflineNetworkFailure } from "./connectivity.js";
3
3
  import { recordConversationRead } from "./conversationRead.js";
4
4
  import { BatchedReadIndex } from "./batchedReadIndex.js";
5
5
  import { WindowedTasks } from "./coalescer.js";
6
- import { IndexedDbReadIndex, recordReadIntent, assertReadIndexVersions } from "./readIndex.js";
6
+ import { IndexedDbReadIndex, readingMetadata, recordReadIntent, assertReadIndexVersions } from "./readIndex.js";
7
7
  import { ReadSyncClient } from "./readSync.js";
8
8
  /** Owns durable local reads, network coalescing, retries and state notifications.
9
9
  * Hosts report actual visibility; loading or subscribing never marks messages read. */
@@ -30,7 +30,7 @@ export class ReadingClient {
30
30
  this.options = options;
31
31
  if (options.teamId !== undefined && !isValidChatTeamId(options.teamId))
32
32
  throw new Error("INVALID_READ_TEAM");
33
- const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex(globalThis.indexedDB, "pmate-social-read-index", options.storageScope, options.migrateLegacy));
33
+ const storage = options.persistence ?? (this.storage = new IndexedDbReadIndex(globalThis.indexedDB, "pmate-social-read-index", options.storageScope, options.migrateLegacy, options.metadataOnly));
34
34
  this.backing = storage;
35
35
  this.persistence = new BatchedReadIndex(storage);
36
36
  this.tasks = new WindowedTasks(1000, error => this.failed(error));
@@ -112,7 +112,8 @@ export class ReadingClient {
112
112
  const expected = await versions;
113
113
  const next = await this.persistence.update(this.options.appId, this.options.profileId, (state, currentVersions) => {
114
114
  assertReadIndexVersions(expected, currentVersions, teamId);
115
- return transform(state);
115
+ const next = transform(state);
116
+ return this.options.metadataOnly ? readingMetadata(next) : next;
116
117
  });
117
118
  if (!this.closed) {
118
119
  this.publish(next);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.38",
3
+ "version": "0.12.0-next.39",
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",