index.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. /**
  2. * Opt-in SQLite persistence provider. Logical sessions remain unchanged;
  3. * the physical backend packs eligible chunk runs into schema-17 rows.
  4. * @module @deepseek-ai/dsh-session-persistence-sqlite
  5. */
  6. import { Context, Service } from '@deepseek-ai/cordis'
  7. import z from '@deepseek-ai/schemastery'
  8. import type {
  9. Session,
  10. SessionEvent,
  11. SessionHeader,
  12. SessionId,
  13. SessionPreparation,
  14. } from '@deepseek-ai/dsh-session'
  15. import {
  16. DEFAULT_PREPARED_SESSION_CACHE_SIZE,
  17. DEFAULT_WRITE_BATCH_MAX_DELAY_MS,
  18. MAX_WRITE_BATCH_DELAY_MS,
  19. type BorrowedSessionSource,
  20. PersistenceCoordinator,
  21. SessionPersistence,
  22. type SessionInspection,
  23. type SessionLocation,
  24. type SessionPersistenceSnapshot,
  25. } from '@deepseek-ai/dsh-session-persistence'
  26. import type { JournalMode } from './schema.ts'
  27. import { SqliteStore } from './store.ts'
  28. export { SCHEMA_VERSION } from './schema.ts'
  29. /** Default wait for another SQLite connection's write reservation. */
  30. export const DEFAULT_BUSY_TIMEOUT_MS = 5_000
  31. /** Largest busy timeout accepted by SQLite's signed millisecond interface. */
  32. export const MAX_BUSY_TIMEOUT_MS = 2_147_483_647
  33. /** Plugin configuration. */
  34. export interface Config {
  35. /** SQLite database path, or `:memory:` for an in-process database. */
  36. path: string
  37. /** Durable SQLite journal mode; defaults to `wal`. */
  38. journalMode?: JournalMode
  39. /** Maximum wait for another SQLite connection's lock; defaults to 5,000 ms. */
  40. busyTimeoutMs?: number
  41. /** Maximum cold Session preparations retained for history-to-resume reuse. */
  42. preparedSessionCacheSize?: number
  43. /** Fixed live-event coalescing window; not a backend completion deadline. */
  44. writeBatchMaxDelayMs?: number
  45. }
  46. /**
  47. * SQLite `SessionPersistence` provider with a schema-owned physical codec.
  48. */
  49. export class SqliteSessionPersistence extends SessionPersistence {
  50. override readonly supportsRawArtifacts = false
  51. override readonly name = 'session-persistence-sqlite'
  52. static inject = ['sessions']
  53. static Config: z<Config> = z.object({
  54. path: z.string().required(),
  55. journalMode: z.union(['wal', 'delete', 'truncate', 'persist'] as const).default('wal'),
  56. busyTimeoutMs: z.number().step(1).min(0).max(MAX_BUSY_TIMEOUT_MS).default(DEFAULT_BUSY_TIMEOUT_MS),
  57. preparedSessionCacheSize: z.number().step(1).min(1).default(DEFAULT_PREPARED_SESSION_CACHE_SIZE),
  58. writeBatchMaxDelayMs: z.number().step(1).min(1).max(MAX_WRITE_BATCH_DELAY_MS)
  59. .default(DEFAULT_WRITE_BATCH_MAX_DELAY_MS),
  60. })
  61. private readonly store: SqliteStore
  62. private readonly coordinator: PersistenceCoordinator<number>
  63. constructor(ctx: Context, public config: Config) {
  64. super(ctx)
  65. const preparedSessionCacheSize = config.preparedSessionCacheSize
  66. ?? DEFAULT_PREPARED_SESSION_CACHE_SIZE
  67. const writeBatchMaxDelayMs = config.writeBatchMaxDelayMs
  68. ?? DEFAULT_WRITE_BATCH_MAX_DELAY_MS
  69. this.store = new SqliteStore({
  70. path: config.path,
  71. journalMode: config.journalMode ?? 'wal',
  72. busyTimeoutMs: config.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS,
  73. })
  74. this.coordinator = new PersistenceCoordinator(this.ctx, this.store, {
  75. preparedSessionCacheSize,
  76. writeBatchMaxDelayMs,
  77. })
  78. }
  79. /** Reject self-contained path and ownership failures without loading Node SQLite. */
  80. protected async [Service.init](): Promise<void> {
  81. await this.store.validatePath()
  82. }
  83. /** SQLite has one database, not an independent per-session artifact. */
  84. locate(_meta: SessionHeader): SessionLocation | undefined {
  85. return undefined
  86. }
  87. create(meta: SessionHeader): Promise<void> {
  88. return this.coordinator.create(meta)
  89. }
  90. override ensureMaterialized(session: Session): Promise<void> {
  91. return this.coordinator.ensureMaterialized(session)
  92. }
  93. append(id: SessionId, events: readonly SessionEvent[]): Promise<void> {
  94. return this.coordinator.append(id, events)
  95. }
  96. override prepare(id: SessionId, signal?: AbortSignal): Promise<SessionPreparation> {
  97. return this.coordinator.prepare(id, signal)
  98. }
  99. load(id: SessionId): Promise<SessionInspection> {
  100. return this.coordinator.load(id)
  101. }
  102. inspect(id: SessionId, signal?: AbortSignal): Promise<SessionInspection> {
  103. return this.coordinator.inspect(id, signal)
  104. }
  105. override borrowSession(id: SessionId, signal?: AbortSignal): Promise<BorrowedSessionSource> {
  106. return this.coordinator.borrowSession(id, signal)
  107. }
  108. readFrom(
  109. id: SessionId,
  110. fromSeq: number,
  111. signal?: AbortSignal,
  112. ): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
  113. return this.coordinator.readFrom(id, fromSeq, signal)
  114. }
  115. list(signal?: AbortSignal): Promise<SessionHeader[]> {
  116. return this.store.list(signal)
  117. }
  118. listSnapshots(signal?: AbortSignal): Promise<SessionPersistenceSnapshot[]> {
  119. return this.store.listSnapshots(signal)
  120. }
  121. }
  122. export default SqliteSessionPersistence