{"version":3,"file":"in-memory-storage-state.d.ts","sourceRoot":"","sources":["../../../src/harness/session/in-memory-storage-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,cAAc,EAAiD,MAAM,aAAa,CAAC;AAEtH,YAAY,EACX,mBAAmB,EACnB,wBAAwB,EACxB,wBAAwB,EACxB,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,cAAc,EACd,cAAc,GACd,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EACX,KAAK,EACL,SAAS,EACT,cAAc,EACd,YAAY,EACZ,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,KAAK,EACL,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,eAAe,EAGpB,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,SAAS,EAEd,MAAM,aAAa,CAAC;AAsBrB;;;;;GAKG;AACH,qBAAa,oBAAoB;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAoC;IACjE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAkC;IAC7D,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,OAAO,CAAS;IAExB,cAQC;IAED,aAAa,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,CAIhE;IAED,iBAAiB,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI,CAKzD;IAED,6FAA6F;IAC7F,cAAc,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,GAAG,YAAY,CAmD9D;IAED,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAKpC;IAED,UAAU,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAOrD;IAED,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAEzD;IAED,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC,EAAE,CAIhD;IAED,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,WAAW,CAAC,CAAC,CAAC,EAAE,CAS9E;IAED,UAAU,CAAC,KAAK,EAAE,iBAAiB,GAAG,KAAK,EAAE,CA4B5C;IAED,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,GAAG,cAAc,EAAE,CAS9D;IAED,WAAW,CAAC,KAAK,EAAE,SAAS,GAAG,KAAK,EAAE,CAkBrC;IAED,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,QAAQ,EAAE,CAMtC;IAED,QAAQ,IAAI,YAAY,CAEvB;IAED,wBAAwB,IAAI;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QAAC,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,CAAA;KAAE,CAKrF;CACD","sourcesContent":["import { addUsage, emptyUsage } from \"../utils/usage.ts\";\nimport { type CommittedWrite, type PreparedCommit, prepareStorageCommit, validateCommittedWrites } from \"./commit.ts\";\n\nexport type {\n\tCommittedEntryWrite,\n\tCommittedListAppendWrite,\n\tCommittedListDeleteWrite,\n\tCommittedUsageWrite,\n\tCommittedValueDeleteWrite,\n\tCommittedValueSetWrite,\n\tCommittedWrite,\n\tPreparedCommit,\n} from \"./commit.ts\";\n\nimport type {\n\tEntry,\n\tEntryScan,\n\tEntryStructure,\n\tSessionStats,\n\tStorageBranchScan,\n\tUsageRow,\n\tUsageScan,\n\tWrite,\n} from \"./types.ts\";\nimport {\n\ttype ListElement,\n\ttype ListReadOptions,\n\tlist,\n\tresolveListReadOptions,\n\ttype StoredValue,\n\ttype Value,\n\ttype ValueList,\n\tvalue,\n} from \"./values.ts\";\n\ninterface StoredListSnapshot {\n\taddress: ValueList<unknown>;\n\telements: ListElement<unknown>[];\n}\n\nfunction physicalKey(namespace: string, key: string): string {\n\treturn `${namespace}\\u0000${key}`;\n}\n\nfunction compareKeys(left: string, right: string): number {\n\tconst leftCodePoints = Array.from(left, (character) => character.codePointAt(0)!);\n\tconst rightCodePoints = Array.from(right, (character) => character.codePointAt(0)!);\n\tconst length = Math.min(leftCodePoints.length, rightCodePoints.length);\n\tfor (let index = 0; index < length; index++) {\n\t\tconst difference = leftCodePoints[index]! - rightCodePoints[index]!;\n\t\tif (difference !== 0) return difference;\n\t}\n\treturn leftCodePoints.length - rightCodePoints.length;\n}\n\n/**\n * Complete materialized session state for MemoryStorage and JsonlStorage.\n *\n * This is intentionally unsuitable for database backends and long-running sessions that may not fit in memory.\n * Those backends should query indexed durable state and update durable aggregates within each commit transaction.\n */\nexport class InMemoryStorageState {\n\tprivate readonly entries: Map<string, Entry>;\n\tprivate readonly entriesBySeq: Entry[];\n\tprivate readonly scalarValues: Map<string, StoredValue<unknown>>;\n\tprivate readonly listValues: Map<string, StoredListSnapshot>;\n\tprivate readonly usage: Map<string, UsageRow>;\n\tprivate stats: SessionStats;\n\tprivate nextSeq: number;\n\n\tconstructor() {\n\t\tthis.entries = new Map();\n\t\tthis.entriesBySeq = [];\n\t\tthis.scalarValues = new Map();\n\t\tthis.listValues = new Map();\n\t\tthis.usage = new Map();\n\t\tthis.stats = { messageCount: 0, usage: emptyUsage() };\n\t\tthis.nextSeq = 1;\n\t}\n\n\tprepareCommit(writes: Write[], timestamp: number): PreparedCommit {\n\t\tconst prepared = prepareStorageCommit(writes, this.nextSeq, timestamp);\n\t\tthis.validateCommitted(prepared.writes);\n\t\treturn prepared;\n\t}\n\n\tvalidateCommitted(writes: readonly CommittedWrite[]): void {\n\t\tvalidateCommittedWrites(writes, this.nextSeq, {\n\t\t\thasEntryOrUsageId: (id) => this.entries.has(id) || this.usage.has(id),\n\t\t\thasEntryId: (id) => this.entries.has(id),\n\t\t});\n\t}\n\n\t/** Apply writes already accepted by validateCommitted() and return the post-apply totals. */\n\tapplyValidated(writes: readonly CommittedWrite[]): SessionStats {\n\t\tfor (const write of writes) {\n\t\t\tswitch (write.kind) {\n\t\t\t\tcase \"entry\": {\n\t\t\t\t\tconst { kind: _kind, ...entry } = write;\n\t\t\t\t\tthis.entries.set(entry.id, entry);\n\t\t\t\t\tthis.entriesBySeq.push(entry);\n\t\t\t\t\tif (entry.type === \"message\") this.stats = { ...this.stats, messageCount: this.stats.messageCount + 1 };\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"usage\": {\n\t\t\t\t\tconst { kind: _kind, ...row } = write;\n\t\t\t\t\tthis.usage.set(row.id, row);\n\t\t\t\t\tthis.stats = { ...this.stats, usage: addUsage(this.stats.usage, row.usage) };\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"value\": {\n\t\t\t\t\tconst key = physicalKey(write.namespace, write.key);\n\t\t\t\t\tif (write.op === \"delete\") {\n\t\t\t\t\t\tthis.scalarValues.delete(key);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tthis.scalarValues.set(key, {\n\t\t\t\t\t\t\taddress: value<unknown>(write.namespace, write.key),\n\t\t\t\t\t\t\tvalue: write.value,\n\t\t\t\t\t\t\tseq: write.seq,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tcase \"list\": {\n\t\t\t\t\tconst key = physicalKey(write.namespace, write.key);\n\t\t\t\t\tif (write.op === \"delete\") {\n\t\t\t\t\t\tthis.listValues.delete(key);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconst stored = this.listValues.get(key);\n\t\t\t\t\t\tconst element = { seq: write.seq, value: write.value };\n\t\t\t\t\t\tif (stored === undefined) {\n\t\t\t\t\t\t\tthis.listValues.set(key, {\n\t\t\t\t\t\t\t\taddress: list<unknown>(write.namespace, write.key),\n\t\t\t\t\t\t\t\telements: [element],\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tstored.elements.push(element);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.nextSeq = write.seq + 1;\n\t\t}\n\t\treturn this.stats;\n\t}\n\n\tadvanceNextSeq(nextSeq: number): void {\n\t\tif (!Number.isSafeInteger(nextSeq) || nextSeq < 1) {\n\t\t\tthrow new Error(`Invalid storage sequence high-water mark: ${nextSeq}`);\n\t\t}\n\t\tthis.nextSeq = Math.max(this.nextSeq, nextSeq);\n\t}\n\n\tgetEntries(ids: readonly string[]): Map<string, Entry> {\n\t\tconst found = new Map<string, Entry>();\n\t\tfor (const id of ids) {\n\t\t\tconst entry = this.entries.get(id);\n\t\t\tif (entry !== undefined) found.set(id, entry);\n\t\t}\n\t\treturn found;\n\t}\n\n\tgetValue<T>(address: Value<T>): StoredValue<T> | undefined {\n\t\treturn this.scalarValues.get(physicalKey(address.namespace, address.key)) as StoredValue<T> | undefined;\n\t}\n\n\tscanValues<T>(prefix: Value<T>): StoredValue<T>[] {\n\t\treturn [...this.scalarValues.values()]\n\t\t\t.filter((stored) => stored.address.namespace === prefix.namespace && stored.address.key.startsWith(prefix.key))\n\t\t\t.sort((left, right) => compareKeys(left.address.key, right.address.key)) as StoredValue<T>[];\n\t}\n\n\treadList<T>(address: ValueList<T>, options?: ListReadOptions): ListElement<T>[] {\n\t\tconst resolved = resolveListReadOptions(options);\n\t\tconst elements = this.listValues.get(physicalKey(address.namespace, address.key))?.elements ?? [];\n\t\tconst filtered = elements.filter((element) => {\n\t\t\tif (resolved.cursor === undefined) return true;\n\t\t\treturn resolved.order === \"asc\" ? element.seq > resolved.cursor.seq : element.seq < resolved.cursor.seq;\n\t\t});\n\t\tconst ordered = resolved.order === \"asc\" ? filtered : [...filtered].reverse();\n\t\treturn ordered.slice(0, resolved.limit) as ListElement<T>[];\n\t}\n\n\tscanBranch(query: StorageBranchScan): Entry[] {\n\t\tconst start = this.entries.get(query.start);\n\t\tif (start === undefined) throw new Error(`Unknown branch start: ${query.start}`);\n\n\t\tconst path: Entry[] = [];\n\t\tlet entry: Entry | undefined = start;\n\t\twhile (entry !== undefined) {\n\t\t\tpath.push(entry);\n\t\t\tif (entry.parentId === null) break;\n\t\t\tentry = this.entries.get(entry.parentId);\n\t\t\tif (entry === undefined) throw new Error(\"Corrupt branch: missing parent\");\n\t\t}\n\t\tif (query.order === \"oldestFirst\") path.reverse();\n\n\t\tconst stopped: Entry[] = [];\n\t\tfor (const candidate of path) {\n\t\t\tstopped.push(candidate);\n\t\t\tif (candidate.id === query.stopAtId || candidate.type === query.stopAtType) break;\n\t\t}\n\t\tconst filtered = stopped\n\t\t\t.filter((candidate) => query.type === undefined || candidate.type === query.type)\n\t\t\t.filter((candidate) => query.customType === undefined || candidate.customType === query.customType)\n\t\t\t.filter(\n\t\t\t\t(candidate) =>\n\t\t\t\t\tquery.cursor === undefined ||\n\t\t\t\t\t(query.order === \"oldestFirst\" ? candidate.seq > query.cursor.seq : candidate.seq < query.cursor.seq),\n\t\t\t);\n\t\treturn query.limit === undefined ? filtered : filtered.slice(0, Math.max(0, query.limit));\n\t}\n\n\tscanBranchStructure(query: StorageBranchScan): EntryStructure[] {\n\t\treturn this.scanBranch(query).map((entry) => ({\n\t\t\tid: entry.id,\n\t\t\tparentId: entry.parentId,\n\t\t\tseq: entry.seq,\n\t\t\ttimestamp: entry.timestamp,\n\t\t\ttype: entry.type,\n\t\t\t...(entry.customType === undefined ? {} : { customType: entry.customType }),\n\t\t}));\n\t}\n\n\tscanEntries(query: EntryScan): Entry[] {\n\t\tconst limit = query.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.trunc(query.limit));\n\t\tconst entries: Entry[] = [];\n\t\tconst descending = query.order === \"desc\";\n\t\tlet index = descending ? this.entriesBySeq.length - 1 : 0;\n\t\twhile (index >= 0 && index < this.entriesBySeq.length && entries.length < limit) {\n\t\t\tconst entry = this.entriesBySeq[index]!;\n\t\t\tif (\n\t\t\t\t(query.type === undefined || entry.type === query.type) &&\n\t\t\t\t(query.customType === undefined || entry.customType === query.customType) &&\n\t\t\t\t(query.fromSeq === undefined || entry.seq >= query.fromSeq) &&\n\t\t\t\t(query.toSeq === undefined || entry.seq <= query.toSeq)\n\t\t\t) {\n\t\t\t\tentries.push(entry);\n\t\t\t}\n\t\t\tindex += descending ? -1 : 1;\n\t\t}\n\t\treturn entries;\n\t}\n\n\tscanUsage(query: UsageScan): UsageRow[] {\n\t\tconst rows = [...this.usage.values()]\n\t\t\t.filter((row) => query.fromSeq === undefined || row.seq >= query.fromSeq)\n\t\t\t.filter((row) => query.toSeq === undefined || row.seq <= query.toSeq)\n\t\t\t.sort((left, right) => (query.order === \"desc\" ? right.seq - left.seq : left.seq - right.seq));\n\t\treturn query.limit === undefined ? rows : rows.slice(0, Math.max(0, query.limit));\n\t}\n\n\tgetStats(): SessionStats {\n\t\treturn this.stats;\n\t}\n\n\tsnapshotEntriesAndValues(): { entries: Entry[]; scalarValues: StoredValue<unknown>[] } {\n\t\treturn {\n\t\t\tentries: [...this.entries.values()].sort((left, right) => left.seq - right.seq),\n\t\t\tscalarValues: [...this.scalarValues.values()],\n\t\t};\n\t}\n}\n"]}