- Add paginated thread history with bounded snapshots and replay limits - Trim oversized wire payloads and support progressive mobile history loading
Showing the changes to apps/server/src/orchestration-v2/ThreadStream.ts (1 of 43 files). Show all
Unified · Split
| @@ -0,0 +1,51 @@ | ||
| 1 | +/** Maximum number of reducer applications allowed during a thread resume. */ | |
| 2 | +export const THREAD_RESUME_MAX_REPLAY_EVENTS = 128; | |
| 3 | + | |
| 4 | +/** Maximum encoded event JSON allowed during a thread resume. */ | |
| 5 | +export const THREAD_RESUME_MAX_REPLAY_ENCODED_BYTES = 1_048_576; | |
| 6 | + | |
| 7 | +/** Encoded payload cost after events have been projected for the wire. */ | |
| 8 | +export function threadReplayEncodedBytes(items: ReadonlyArray<unknown>): number { | |
| 9 | + let total = 0; | |
| 10 | + for (const item of items) { | |
| 11 | + const encoded = JSON.stringify(item); | |
| 12 | + total += Buffer.byteLength(encoded ?? "", "utf8"); | |
| 13 | + } | |
| 14 | + return total; | |
| 15 | +} | |
| 16 | + | |
| 17 | +export type ThreadResumePlan = | |
| 18 | + | { | |
| 19 | + readonly mode: "replay"; | |
| 20 | + readonly afterSequence: number; | |
| 21 | + readonly throughSequence: number; | |
| 22 | + } | |
| 23 | + | { readonly mode: "snapshot" }; | |
| 24 | + | |
| 25 | +/** | |
| 26 | + * Decide whether a thread subscription should replay the event gap after the | |
| 27 | + * client's cursor or send a fresh snapshot instead. | |
| 28 | + * | |
| 29 | + * A client cursor above the high water mark is stale or invalid. Event count | |
| 30 | + * limits reducer churn while encoded bytes limit a small number of large | |
| 31 | + * updates. Either excess is cheaper to replace with one current snapshot. | |
| 32 | + */ | |
| 33 | +export function decideThreadResume(input: { | |
| 34 | + readonly afterSequence: number; | |
| 35 | + readonly highWater: number; | |
| 36 | + readonly replayEventCount: number; | |
| 37 | + readonly replayEncodedBytes: number; | |
| 38 | +}): ThreadResumePlan { | |
| 39 | + if ( | |
| 40 | + input.afterSequence > input.highWater || | |
| 41 | + input.replayEventCount > THREAD_RESUME_MAX_REPLAY_EVENTS || | |
| 42 | + input.replayEncodedBytes > THREAD_RESUME_MAX_REPLAY_ENCODED_BYTES | |
| 43 | + ) { | |
| 44 | + return { mode: "snapshot" }; | |
| 45 | + } | |
| 46 | + return { | |
| 47 | + mode: "replay", | |
| 48 | + afterSequence: input.afterSequence, | |
| 49 | + throughSequence: input.highWater, | |
| 50 | + }; | |
| 51 | +} | |