backend.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. /**
  2. * Backend-facing vocabulary of the storage hub: a backend owns one medium
  3. * (a file-tree root, a database file) and exposes operation groups over it.
  4. * This module defines the normative contract text for backend implementers; the shared
  5. * conformance suite in `tests/contract.ts` checks every rule.
  6. * @module @deepseek-ai/dsh-storage/src/backend
  7. */
  8. /** Allowed format for unit and table names: safe as a file name and as a SQL identifier segment without escaping. */
  9. export const UNIT_NAME_RE = /^[a-z][a-z0-9_]*$/
  10. /**
  11. * One registered backend. A backend owns exactly one medium and shares its
  12. * lifecycle across all facets; facets are optional members — a backend that
  13. * cannot serve a data kind simply omits it, and resolution fails loud instead.
  14. */
  15. export interface StorageBackend {
  16. /** Key-value operations; absent when this backend cannot serve them. */
  17. readonly kv?: KvFacet
  18. /**
  19. * Drain in-flight writes across all open units and release the medium.
  20. * Idempotent; concurrent and repeated calls resolve once teardown finishes.
  21. * @returns resolution after the medium is released.
  22. */
  23. close(): Promise<void>
  24. }
  25. /** The key-value data shape: whole-unit snapshots plus per-record durable writes. */
  26. export interface KvFacet {
  27. /**
  28. * Open one unit, creating it when the medium holds no trace of it yet
  29. * (materialization may defer to the first write, but {@link KvUnit.loadAll}
  30. * must immediately serve the empty shape). A version already stamped on the
  31. * medium that differs from `descriptor.version` rejects with
  32. * `version-mismatch`; a medium that cannot be parsed as this unit rejects
  33. * with `malformed-medium`. Opening the same unit name twice without closing
  34. * is a caller bug and rejects.
  35. * @param descriptor - Static identity and shape of the unit to open.
  36. * @returns the opened unit.
  37. */
  38. open(descriptor: KvUnitDescriptor): Promise<KvUnit>
  39. }
  40. /** Static identity and shape of one KV unit, projected from its owner's spec. */
  41. export interface KvUnitDescriptor {
  42. /** Unit name; must match {@link UNIT_NAME_RE}. Also the file-name / SQL-identifier segment. */
  43. readonly name: string
  44. /** Unit format version; a non-negative integer stamped on the medium at first materialization. */
  45. readonly version: number
  46. /** Table names; each must match {@link UNIT_NAME_RE}. */
  47. readonly tables: readonly string[]
  48. /** Whether this unit carries the global singleton slot. */
  49. readonly hasGlobal: boolean
  50. /**
  51. * Medium layout. `single` (the default) keeps the whole unit in one
  52. * document; `per-record` keeps each record in its own document, so a unit
  53. * whose records are large or sparse never rewrites the rest on one write,
  54. * and an unaccepted version stamp discards only that record instead of
  55. * rejecting the whole unit. Backends that only serve one layout accept the
  56. * other's units as foreign documents.
  57. */
  58. readonly layout?: 'single' | 'per-record'
  59. /**
  60. * Older unit versions whose stored records are also readable under the
  61. * declaring owner's current record schemas (the owner vouches for that —
  62. * typically by declaring the fields old records lack as optional). Reads of
  63. * a `per-record` unit accept documents stamped with any listed version, and
  64. * the legacy whole-unit bootstrap accepts a legacy file stamped with one;
  65. * writes always stamp {@link version}. `single`-layout reads stay
  66. * exact-version.
  67. */
  68. readonly compatibleVersions?: readonly number[]
  69. }
  70. /**
  71. * One opened unit. Values are opaque JSON to this layer: no schema, no
  72. * events, no domain meaning. The unit does NOT serialize concurrent writes —
  73. * write ordering is the caller's responsibility (the domain layer runs one
  74. * write chain per unit); the unit only guarantees that each single call is
  75. * atomic on the medium and durable once resolved (a crash after resolution
  76. * followed by a re-open observes the write). Any call after {@link close}
  77. * rejects with `closed`.
  78. */
  79. export interface KvUnit {
  80. /**
  81. * Read the full current snapshot.
  82. * @returns every table's records keyed by table name, plus the global
  83. * singleton (`null` when never written or not declared).
  84. */
  85. loadAll(): Promise<{ tables: Record<string, Record<string, unknown>>; global: unknown }>
  86. /**
  87. * Upsert one record durably. Overwrite semantics: an existing key is replaced.
  88. * @param table - Declared table name.
  89. * @param key - Record key. In the `per-record` layout a key becomes a path
  90. * segment and must match `[a-zA-Z0-9_-]+` (an unsafe key rejects); in the
  91. * `single` layout keys stay opaque.
  92. * @param value - Opaque JSON-serializable record.
  93. * @returns resolution after durability.
  94. */
  95. putRecord(table: string, key: string, value: unknown): Promise<void>
  96. /**
  97. * Delete one record durably. Idempotent: a missing key is a no-op.
  98. * @param table - Declared table name.
  99. * @param key - Record key.
  100. * @returns resolution after durability.
  101. */
  102. deleteRecord(table: string, key: string): Promise<void>
  103. /**
  104. * Move one record's stored document out of the unit's readable set,
  105. * preserving its bytes for inspection instead of deleting them. Backends
  106. * whose medium has no per-record document to move (the `single` layout, a
  107. * row store) omit this member, and the caller falls back to its
  108. * reject-loud path. Absent after the move: a later {@link loadAll} reads
  109. * the key as missing and a later {@link putRecord} recreates it fresh.
  110. * @param table - Declared table name.
  111. * @param key - Record key.
  112. * @returns the medium location the document was moved to (diagnostics).
  113. */
  114. backupRecord?(table: string, key: string): Promise<string>
  115. /**
  116. * Write the global singleton durably. Only valid when the descriptor
  117. * declared `hasGlobal`.
  118. * @param value - Opaque JSON-serializable value.
  119. * @returns resolution after durability.
  120. */
  121. setGlobal(value: unknown): Promise<void>
  122. /**
  123. * Drain this unit's in-flight writes and release it. Idempotent.
  124. * @returns resolution after the unit is released.
  125. */
  126. close(): Promise<void>
  127. }