@livestore/common 0.5.0-dev.0 → 0.5.0-dev.1

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.
Files changed (302) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ClientSessionLeaderThreadProxy.d.ts.map +1 -1
  3. package/dist/MaterializationJournal.d.ts +2 -2
  4. package/dist/MaterializationJournal.d.ts.map +1 -1
  5. package/dist/MaterializationJournal.js +7 -7
  6. package/dist/StateHead.d.ts +1 -1
  7. package/dist/StateHead.d.ts.map +1 -1
  8. package/dist/StateHead.js +4 -4
  9. package/dist/WorkerTransportError.d.ts.map +1 -1
  10. package/dist/__tests__/fixture.d.ts +7 -7
  11. package/dist/__tests__/fixture.js +2 -2
  12. package/dist/adapter-types.d.ts +4 -0
  13. package/dist/adapter-types.d.ts.map +1 -1
  14. package/dist/adapter-types.js +4 -4
  15. package/dist/bounded-collections.d.ts.map +1 -1
  16. package/dist/bounded-collections.js.map +1 -1
  17. package/dist/debug-info.d.ts.map +1 -1
  18. package/dist/debug-info.js +9 -8
  19. package/dist/debug-info.js.map +1 -1
  20. package/dist/defs.d.ts +4 -4
  21. package/dist/defs.js +2 -2
  22. package/dist/devtools/devtools-compatibility.test.js +1 -1
  23. package/dist/devtools/devtools-messages-client-session.d.ts +4 -4
  24. package/dist/devtools/devtools-messages-client-session.js +4 -4
  25. package/dist/devtools/devtools-messages-common.d.ts.map +1 -1
  26. package/dist/devtools/devtools-messages-common.js +1 -1
  27. package/dist/devtools/devtools-messages-leader.d.ts +9 -9
  28. package/dist/devtools/devtools-messages-leader.js +4 -4
  29. package/dist/devtools/devtools-messages.js +3 -3
  30. package/dist/devtools/devtools-sessioninfo.d.ts.map +1 -1
  31. package/dist/devtools/mod.d.ts.map +1 -1
  32. package/dist/devtools/mod.js +3 -3
  33. package/dist/errors.d.ts +4 -4
  34. package/dist/errors.d.ts.map +1 -1
  35. package/dist/errors.js +1 -1
  36. package/dist/errors.js.map +1 -1
  37. package/dist/index.js +22 -22
  38. package/dist/leader-thread/LeaderSyncProcessor.d.ts +27 -6
  39. package/dist/leader-thread/LeaderSyncProcessor.d.ts.map +1 -1
  40. package/dist/leader-thread/LeaderSyncProcessor.js +65 -55
  41. package/dist/leader-thread/LeaderSyncProcessor.js.map +1 -1
  42. package/dist/leader-thread/RejectedPushError.d.ts +9 -9
  43. package/dist/leader-thread/RejectedPushError.d.ts.map +1 -1
  44. package/dist/leader-thread/RejectedPushError.js +1 -1
  45. package/dist/leader-thread/connection.d.ts.map +1 -1
  46. package/dist/leader-thread/connection.js +2 -2
  47. package/dist/leader-thread/eventlog.d.ts +11 -1
  48. package/dist/leader-thread/eventlog.d.ts.map +1 -1
  49. package/dist/leader-thread/eventlog.js +13 -13
  50. package/dist/leader-thread/eventlog.js.map +1 -1
  51. package/dist/leader-thread/leader-worker-devtools.d.ts +1 -1
  52. package/dist/leader-thread/leader-worker-devtools.d.ts.map +1 -1
  53. package/dist/leader-thread/leader-worker-devtools.js +4 -4
  54. package/dist/leader-thread/leader-worker-devtools.js.map +1 -1
  55. package/dist/leader-thread/make-leader-thread-layer.d.ts +6 -0
  56. package/dist/leader-thread/make-leader-thread-layer.d.ts.map +1 -1
  57. package/dist/leader-thread/make-leader-thread-layer.js +43 -29
  58. package/dist/leader-thread/make-leader-thread-layer.js.map +1 -1
  59. package/dist/leader-thread/make-leader-thread-layer.test.js +2 -2
  60. package/dist/leader-thread/materialize-event.d.ts.map +1 -1
  61. package/dist/leader-thread/materialize-event.js +9 -9
  62. package/dist/leader-thread/mod.js +9 -9
  63. package/dist/leader-thread/recreate-db.d.ts +5 -1
  64. package/dist/leader-thread/recreate-db.d.ts.map +1 -1
  65. package/dist/leader-thread/recreate-db.js +21 -25
  66. package/dist/leader-thread/recreate-db.js.map +1 -1
  67. package/dist/leader-thread/shutdown-channel.js +1 -1
  68. package/dist/leader-thread/stream-events.d.ts.map +1 -1
  69. package/dist/leader-thread/stream-events.js +3 -5
  70. package/dist/leader-thread/stream-events.js.map +1 -1
  71. package/dist/leader-thread/types.d.ts +6 -4
  72. package/dist/leader-thread/types.d.ts.map +1 -1
  73. package/dist/leader-thread/types.js +5 -1
  74. package/dist/leader-thread/types.js.map +1 -1
  75. package/dist/make-client-session.d.ts +1 -1
  76. package/dist/make-client-session.d.ts.map +1 -1
  77. package/dist/make-client-session.js +2 -2
  78. package/dist/materializer-helper.d.ts.map +1 -1
  79. package/dist/materializer-helper.js +4 -4
  80. package/dist/materializer-helper.js.map +1 -1
  81. package/dist/otel.d.ts.map +1 -1
  82. package/dist/rematerialize-from-eventlog.d.ts +3 -1
  83. package/dist/rematerialize-from-eventlog.d.ts.map +1 -1
  84. package/dist/rematerialize-from-eventlog.js +16 -9
  85. package/dist/rematerialize-from-eventlog.js.map +1 -1
  86. package/dist/schema/EventDef/define.d.ts +2 -2
  87. package/dist/schema/EventDef/define.d.ts.map +1 -1
  88. package/dist/schema/EventDef/define.test.js +1 -1
  89. package/dist/schema/EventDef/deprecated.d.ts +1 -1
  90. package/dist/schema/EventDef/deprecated.d.ts.map +1 -1
  91. package/dist/schema/EventDef/deprecated.test.js +2 -2
  92. package/dist/schema/EventDef/facts.d.ts.map +1 -1
  93. package/dist/schema/EventDef/materializer.d.ts +2 -2
  94. package/dist/schema/EventDef/materializer.d.ts.map +1 -1
  95. package/dist/schema/EventDef/mod.js +5 -5
  96. package/dist/schema/EventSequenceNumber/client.d.ts +3 -8
  97. package/dist/schema/EventSequenceNumber/client.d.ts.map +1 -1
  98. package/dist/schema/EventSequenceNumber/client.js +12 -15
  99. package/dist/schema/EventSequenceNumber/client.js.map +1 -1
  100. package/dist/schema/EventSequenceNumber/mod.js +2 -2
  101. package/dist/schema/EventSequenceNumber.test.js +1 -1
  102. package/dist/schema/LiveStoreEvent/client.d.ts +8 -8
  103. package/dist/schema/LiveStoreEvent/client.d.ts.map +1 -1
  104. package/dist/schema/LiveStoreEvent/client.js +1 -1
  105. package/dist/schema/LiveStoreEvent/client.js.map +1 -1
  106. package/dist/schema/LiveStoreEvent/client.test.js +2 -2
  107. package/dist/schema/LiveStoreEvent/global.d.ts.map +1 -1
  108. package/dist/schema/LiveStoreEvent/global.js +1 -1
  109. package/dist/schema/LiveStoreEvent/input.d.ts +1 -1
  110. package/dist/schema/LiveStoreEvent/input.d.ts.map +1 -1
  111. package/dist/schema/LiveStoreEvent/mod.js +4 -4
  112. package/dist/schema/events.js +1 -1
  113. package/dist/schema/mod.js +11 -11
  114. package/dist/schema/schema.d.ts +1 -1
  115. package/dist/schema/schema.d.ts.map +1 -1
  116. package/dist/schema/schema.js +3 -3
  117. package/dist/schema/state/mod.js +2 -2
  118. package/dist/schema/state/sqlite/client-document-def.d.ts +1 -1
  119. package/dist/schema/state/sqlite/client-document-def.d.ts.map +1 -1
  120. package/dist/schema/state/sqlite/client-document-def.js +8 -8
  121. package/dist/schema/state/sqlite/client-document-def.js.map +1 -1
  122. package/dist/schema/state/sqlite/client-document-def.test.js +24 -10
  123. package/dist/schema/state/sqlite/client-document-def.test.js.map +1 -1
  124. package/dist/schema/state/sqlite/column-annotations.d.ts.map +1 -1
  125. package/dist/schema/state/sqlite/column-annotations.test.js +1 -1
  126. package/dist/schema/state/sqlite/column-def.d.ts.map +1 -1
  127. package/dist/schema/state/sqlite/column-def.js +17 -3
  128. package/dist/schema/state/sqlite/column-def.js.map +1 -1
  129. package/dist/schema/state/sqlite/column-def.test.js +60 -5
  130. package/dist/schema/state/sqlite/column-def.test.js.map +1 -1
  131. package/dist/schema/state/sqlite/column-spec.d.ts.map +1 -1
  132. package/dist/schema/state/sqlite/column-spec.js +1 -1
  133. package/dist/schema/state/sqlite/column-spec.test.js +2 -2
  134. package/dist/schema/state/sqlite/column-spec.test.js.map +1 -1
  135. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts +3 -0
  136. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.d.ts.map +1 -0
  137. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js +5 -0
  138. package/dist/schema/state/sqlite/db-schema/ast/fingerprint-digest.js.map +1 -0
  139. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts +5 -0
  140. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.d.ts.map +1 -0
  141. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js +183 -0
  142. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.js.map +1 -0
  143. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts +2 -0
  144. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.d.ts.map +1 -0
  145. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js +142 -0
  146. package/dist/schema/state/sqlite/db-schema/ast/fingerprint.test.js.map +1 -0
  147. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts +1 -6
  148. package/dist/schema/state/sqlite/db-schema/ast/sqlite.d.ts.map +1 -1
  149. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js +1 -82
  150. package/dist/schema/state/sqlite/db-schema/ast/sqlite.js.map +1 -1
  151. package/dist/schema/state/sqlite/db-schema/ast/validate.d.ts.map +1 -1
  152. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.d.ts.map +1 -1
  153. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js +2 -1
  154. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.js.map +1 -1
  155. package/dist/schema/state/sqlite/db-schema/dsl/field-defs.test.js +1 -1
  156. package/dist/schema/state/sqlite/db-schema/dsl/mod.d.ts.map +1 -1
  157. package/dist/schema/state/sqlite/db-schema/dsl/mod.js +1 -1
  158. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts +3 -0
  159. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.d.ts.map +1 -0
  160. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js +6 -0
  161. package/dist/schema/state/sqlite/db-schema/has-json-string-encoding.js.map +1 -0
  162. package/dist/schema/state/sqlite/db-schema/mod.js +2 -2
  163. package/dist/schema/state/sqlite/mod.d.ts.map +1 -1
  164. package/dist/schema/state/sqlite/mod.js +10 -9
  165. package/dist/schema/state/sqlite/mod.js.map +1 -1
  166. package/dist/schema/state/sqlite/persistence.d.ts.map +1 -1
  167. package/dist/schema/state/sqlite/query-builder/api.d.ts.map +1 -1
  168. package/dist/schema/state/sqlite/query-builder/astToSql.d.ts.map +1 -1
  169. package/dist/schema/state/sqlite/query-builder/astToSql.js +2 -7
  170. package/dist/schema/state/sqlite/query-builder/astToSql.js.map +1 -1
  171. package/dist/schema/state/sqlite/query-builder/impl.d.ts +1 -3
  172. package/dist/schema/state/sqlite/query-builder/impl.d.ts.map +1 -1
  173. package/dist/schema/state/sqlite/query-builder/impl.js +2 -2
  174. package/dist/schema/state/sqlite/query-builder/impl.js.map +1 -1
  175. package/dist/schema/state/sqlite/query-builder/impl.test.js +2 -2
  176. package/dist/schema/state/sqlite/query-builder/mod.js +2 -2
  177. package/dist/schema/state/sqlite/schema-helpers.d.ts.map +1 -1
  178. package/dist/schema/state/sqlite/schema-helpers.js +1 -1
  179. package/dist/schema/state/sqlite/schema-helpers.test.js +2 -2
  180. package/dist/schema/state/sqlite/system-tables/eventlog-tables.js +3 -3
  181. package/dist/schema/state/sqlite/system-tables/mod.js +2 -2
  182. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts +60 -17
  183. package/dist/schema/state/sqlite/system-tables/state-tables.d.ts.map +1 -1
  184. package/dist/schema/state/sqlite/system-tables/state-tables.js +14 -5
  185. package/dist/schema/state/sqlite/system-tables/state-tables.js.map +1 -1
  186. package/dist/schema/state/sqlite/table-def.d.ts +1 -1
  187. package/dist/schema/state/sqlite/table-def.d.ts.map +1 -1
  188. package/dist/schema/state/sqlite/table-def.js +24 -8
  189. package/dist/schema/state/sqlite/table-def.js.map +1 -1
  190. package/dist/schema/state/sqlite/table-def.test.js +54 -1
  191. package/dist/schema/state/sqlite/table-def.test.js.map +1 -1
  192. package/dist/schema/unknown-events.d.ts.map +1 -1
  193. package/dist/schema/unknown-events.js +1 -1
  194. package/dist/schema/unknown-events.js.map +1 -1
  195. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js +26 -4
  196. package/dist/schema-management/__tests__/migrations-autoincrement-quoting.test.js.map +1 -1
  197. package/dist/schema-management/common.d.ts.map +1 -1
  198. package/dist/schema-management/common.js +2 -2
  199. package/dist/schema-management/migrations.d.ts +4 -4
  200. package/dist/schema-management/migrations.d.ts.map +1 -1
  201. package/dist/schema-management/migrations.js +9 -9
  202. package/dist/schema-management/migrations.js.map +1 -1
  203. package/dist/schema-management/validate-schema.d.ts.map +1 -1
  204. package/dist/schema-management/validate-schema.js +1 -1
  205. package/dist/session-id-symbol.d.ts.map +1 -1
  206. package/dist/sql-queries/index.js +3 -3
  207. package/dist/sql-queries/sql-queries.d.ts.map +1 -1
  208. package/dist/sql-queries/sql-queries.js +3 -3
  209. package/dist/sql-queries/sql-queries.js.map +1 -1
  210. package/dist/sql-queries/sql-query-builder.d.ts.map +1 -1
  211. package/dist/sql-queries/sql-query-builder.js +1 -1
  212. package/dist/sql-queries/types.d.ts.map +1 -1
  213. package/dist/sqlite-db-helper.d.ts +2 -2
  214. package/dist/sqlite-db-helper.d.ts.map +1 -1
  215. package/dist/sqlite-db-helper.js +2 -2
  216. package/dist/sync/ClientSessionSyncProcessor.d.ts +3 -3
  217. package/dist/sync/ClientSessionSyncProcessor.d.ts.map +1 -1
  218. package/dist/sync/ClientSessionSyncProcessor.js +7 -7
  219. package/dist/sync/errors.js +1 -1
  220. package/dist/sync/index.js +6 -6
  221. package/dist/sync/mock-sync-backend.d.ts.map +1 -1
  222. package/dist/sync/mock-sync-backend.js +4 -4
  223. package/dist/sync/mock-sync-backend.test.js +2 -2
  224. package/dist/sync/next/compact-events.d.ts.map +1 -1
  225. package/dist/sync/next/compact-events.js +3 -3
  226. package/dist/sync/next/facts.d.ts.map +1 -1
  227. package/dist/sync/next/facts.js +1 -1
  228. package/dist/sync/next/history-dag-common.d.ts +1 -1
  229. package/dist/sync/next/history-dag-common.d.ts.map +1 -1
  230. package/dist/sync/next/history-dag-common.js +1 -1
  231. package/dist/sync/next/history-dag.d.ts.map +1 -1
  232. package/dist/sync/next/history-dag.js +3 -3
  233. package/dist/sync/next/mod.js +5 -5
  234. package/dist/sync/next/rebase-events.d.ts.map +1 -1
  235. package/dist/sync/next/rebase-events.js +2 -2
  236. package/dist/sync/next/test/compact-events.calculator.test.js +4 -4
  237. package/dist/sync/next/test/compact-events.test.js +4 -4
  238. package/dist/sync/next/test/event-fixtures.d.ts +1 -1
  239. package/dist/sync/next/test/event-fixtures.d.ts.map +1 -1
  240. package/dist/sync/next/test/event-fixtures.js +5 -5
  241. package/dist/sync/next/test/history-dag-common.test.js +2 -2
  242. package/dist/sync/next/test/mod.js +1 -1
  243. package/dist/sync/sync-backend-kv.js +1 -1
  244. package/dist/sync/sync-backend.d.ts.map +1 -1
  245. package/dist/sync/sync-backend.js +1 -1
  246. package/dist/sync/sync.js +2 -2
  247. package/dist/sync/syncstate.d.ts +3 -3
  248. package/dist/sync/syncstate.d.ts.map +1 -1
  249. package/dist/sync/syncstate.js +2 -2
  250. package/dist/sync/syncstate.js.map +1 -1
  251. package/dist/sync/syncstate.test.js +3 -3
  252. package/dist/sync/transport-chunking.d.ts.map +1 -1
  253. package/dist/sync/validate-push-payload.d.ts.map +1 -1
  254. package/dist/sync/validate-push-payload.js +2 -2
  255. package/dist/testing/event-factory.d.ts.map +1 -1
  256. package/dist/testing/event-factory.js +2 -2
  257. package/dist/testing/event-factory.js.map +1 -1
  258. package/dist/testing/mod.js +1 -1
  259. package/dist/util.d.ts.map +1 -1
  260. package/dist/util.js +1 -0
  261. package/dist/util.js.map +1 -1
  262. package/dist/version.d.ts.map +1 -1
  263. package/dist/version.js.map +1 -1
  264. package/package.json +40 -39
  265. package/src/adapter-types.ts +4 -0
  266. package/src/debug-info.ts +11 -8
  267. package/src/defs.ts +2 -2
  268. package/src/leader-thread/LeaderSyncProcessor.ts +121 -97
  269. package/src/leader-thread/eventlog.ts +13 -5
  270. package/src/leader-thread/leader-worker-devtools.ts +1 -1
  271. package/src/leader-thread/make-leader-thread-layer.ts +49 -23
  272. package/src/leader-thread/recreate-db.ts +26 -25
  273. package/src/leader-thread/stream-events.ts +1 -3
  274. package/src/leader-thread/types.ts +5 -0
  275. package/src/materializer-helper.ts +1 -1
  276. package/src/rematerialize-from-eventlog.ts +28 -13
  277. package/src/schema/EventSequenceNumber/client.ts +12 -15
  278. package/src/schema/schema.ts +1 -1
  279. package/src/schema/state/sqlite/client-document-def.test.ts +23 -7
  280. package/src/schema/state/sqlite/client-document-def.ts +2 -2
  281. package/src/schema/state/sqlite/column-def.test.ts +84 -3
  282. package/src/schema/state/sqlite/column-def.ts +16 -1
  283. package/src/schema/state/sqlite/db-schema/ast/fingerprint-digest.ts +6 -0
  284. package/src/schema/state/sqlite/db-schema/ast/fingerprint.test.ts +209 -0
  285. package/src/schema/state/sqlite/db-schema/ast/fingerprint.ts +225 -0
  286. package/src/schema/state/sqlite/db-schema/ast/sqlite.ts +1 -90
  287. package/src/schema/state/sqlite/db-schema/dsl/field-defs.ts +1 -0
  288. package/src/schema/state/sqlite/db-schema/has-json-string-encoding.ts +9 -0
  289. package/src/schema/state/sqlite/mod.ts +2 -1
  290. package/src/schema/state/sqlite/query-builder/astToSql.ts +1 -8
  291. package/src/schema/state/sqlite/system-tables/state-tables.ts +13 -2
  292. package/src/schema/state/sqlite/table-def.test.ts +66 -0
  293. package/src/schema/state/sqlite/table-def.ts +21 -5
  294. package/src/schema-management/__tests__/migrations-autoincrement-quoting.test.ts +33 -2
  295. package/src/schema-management/migrations.ts +7 -6
  296. package/src/sql-queries/sql-queries.ts +1 -1
  297. package/src/util.ts +1 -0
  298. package/dist/schema/state/sqlite/db-schema/hash.d.ts +0 -2
  299. package/dist/schema/state/sqlite/db-schema/hash.d.ts.map +0 -1
  300. package/dist/schema/state/sqlite/db-schema/hash.js +0 -15
  301. package/dist/schema/state/sqlite/db-schema/hash.js.map +0 -1
  302. package/src/schema/state/sqlite/db-schema/hash.ts +0 -13
@@ -0,0 +1,209 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { Schema } from '@livestore/utils/effect'
4
+
5
+ import { liveStoreStorageFormatVersion } from '../../../../../version.ts'
6
+ import { makeState } from '../../mod.ts'
7
+ import { REBUILD_META_TABLE } from '../../system-tables/state-tables.ts'
8
+ import { SqliteDsl } from '../mod.ts'
9
+ import { digestToFingerprint } from './fingerprint-digest.ts'
10
+ import { fingerprint } from './fingerprint.ts'
11
+
12
+ describe('SQLite storage fingerprints', () => {
13
+ test('pins the final fingerprint output', () => {
14
+ expect(fingerprint(makePhysicalTable().ast)).toBe('UnLwYzVBhwzPCK5q7TrPU3q2N0dTe8m_98bud8HsAs8')
15
+ expect(fingerprint(makeJsonTable('documents', representativeJsonSchema).ast)).toBe(
16
+ 'kUzaurzV2rcXYLljHOZ64TDR9c_RebqD7y5ZUM_nPBE',
17
+ )
18
+ expect(fingerprint(makeJsonTable('documents', normalizedCheckIdsJsonSchema).ast)).toBe(
19
+ 'I0XYuOUSYqWwG5Opxbc1u-TNNE3pFQCHzpBZ_ChYGaA',
20
+ )
21
+ expect(makeState({ tables: [], materializers: {} }).sqlite.hash).toBe('o8mmnRDnhXhkgDpBybM3FK7S-PkaLEj_pKcnbzgg3TU')
22
+ })
23
+
24
+ test('isolates pre-completion-marker state databases without changing the hash algorithm', () => {
25
+ const state = makeState({ tables: [], materializers: {} })
26
+ const legacyTables = [...state.sqlite.tables.values()]
27
+ .filter((table) => table.sqliteDef.name !== REBUILD_META_TABLE)
28
+ .map((table) => table.sqliteDef.ast)
29
+ const legacyHash = fingerprint({ _tag: 'dbSchema', tables: legacyTables })
30
+ expect(legacyHash).toBe('H5Uktp6Ffp84WDj_RWPnfDnaQEu_E61AMl7ieZ9Zn8k')
31
+ expect(state.sqlite.hash).not.toBe(legacyHash)
32
+ })
33
+
34
+ test('matches the standard SHA-256 vector', () => {
35
+ expect(digestToFingerprint(new TextEncoder().encode('abc'))).toBe('ungWv48Bz-pBQUDeXa4iI7ADYaOWF3qctBD_YfIAFa0')
36
+ })
37
+
38
+ test('keeps the full digest inside the Cloudflare VFS filename limit', () => {
39
+ const value = fingerprint(makePhysicalTable().ast)
40
+
41
+ expect(value).toHaveLength(43)
42
+ expect(`state${value}@${liveStoreStorageFormatVersion}.db`.length).toBeLessThanOrEqual(56)
43
+ })
44
+
45
+ test('is independent of table and index declaration order', () => {
46
+ const first = makeJsonTable('first', Schema.Struct({ value: Schema.String }))
47
+ const second = makeJsonTable('second', Schema.Struct({ value: Schema.Finite }))
48
+
49
+ expect(fingerprint({ _tag: 'dbSchema', tables: [first.ast, second.ast] })).toBe(
50
+ fingerprint({ _tag: 'dbSchema', tables: [second.ast, first.ast] }),
51
+ )
52
+
53
+ const table = SqliteDsl.table(
54
+ 'indexed',
55
+ {
56
+ id: SqliteDsl.text({ primaryKey: true }),
57
+ age: SqliteDsl.integer(),
58
+ },
59
+ [
60
+ { name: 'indexed_by_age', columns: ['age'], isUnique: false },
61
+ { name: 'indexed_by_id', columns: ['id'], isUnique: true },
62
+ ],
63
+ ).ast
64
+ expect(fingerprint({ ...table, indexes: table.indexes.toReversed() })).toBe(fingerprint(table))
65
+ })
66
+
67
+ test('ignores Effect metadata that does not affect accepted encoded JSON', () => {
68
+ const plain = makeJsonTable('documents', Schema.Struct({ value: Schema.String }))
69
+ const annotated = makeJsonTable(
70
+ 'documents',
71
+ Schema.Struct({ value: Schema.String }).annotate({ description: 'documentation only', title: 'Document' }),
72
+ )
73
+ const readonlyArray = makeJsonTable('documents', Schema.Array(Schema.String))
74
+ const mutableArray = makeJsonTable('documents', Schema.mutable(Schema.Array(Schema.String)))
75
+
76
+ expect(fingerprint(plain.ast)).toBe(fingerprint(annotated.ast))
77
+ expect(fingerprint(readonlyArray.ast)).toBe(fingerprint(mutableArray.ast))
78
+ })
79
+
80
+ test('changes when represented JSON checks change', () => {
81
+ const minimumTwo = makeJsonTable('documents', Schema.Struct({ value: Schema.String.check(Schema.isMinLength(2)) }))
82
+ const minimumThree = makeJsonTable(
83
+ 'documents',
84
+ Schema.Struct({ value: Schema.String.check(Schema.isMinLength(3)) }),
85
+ )
86
+
87
+ expect(fingerprint(minimumTwo.ast)).not.toBe(fingerprint(minimumThree.ast))
88
+ })
89
+
90
+ test('distinguishes codecs with the same encoded primitive', () => {
91
+ const date = makeJsonTable('documents', Schema.Struct({ value: Schema.DateFromString }))
92
+ const string = makeJsonTable('documents', Schema.Struct({ value: Schema.String }))
93
+
94
+ expect(fingerprint(date.ast)).not.toBe(fingerprint(string.ast))
95
+ })
96
+
97
+ test('handles recursive JSON schemas deterministically', () => {
98
+ const first = makeJsonTable('trees', makeTreeSchema())
99
+ const second = makeJsonTable('trees', makeTreeSchema())
100
+
101
+ expect(fingerprint(first.ast)).toBe(fingerprint(second.ast))
102
+ })
103
+
104
+ test('supports opaque Effect schemas without persistence annotations', () => {
105
+ const OpaqueString = Schema.declare<string>((value): value is string => typeof value === 'string')
106
+ const table = makeJsonTable('opaque_documents', Schema.Struct({ value: OpaqueString }))
107
+
108
+ expect(() => fingerprint(table.ast)).not.toThrow()
109
+ })
110
+
111
+ test('keeps application-owned representation payload fields', () => {
112
+ const first = Schema.declare<string>((value): value is string => typeof value === 'string', {
113
+ representation: { id: 'example/opaque', payload: { annotations: 'first', isMutable: true } },
114
+ })
115
+ const second = Schema.declare<string>((value): value is string => typeof value === 'string', {
116
+ representation: { id: 'example/opaque', payload: { annotations: 'second', isMutable: true } },
117
+ })
118
+
119
+ expect(fingerprint(makeJsonTable('documents', first).ast)).not.toBe(
120
+ fingerprint(makeJsonTable('documents', second).ast),
121
+ )
122
+ })
123
+
124
+ test('tracks JSON codecs passed through the generic text column API', () => {
125
+ const first = SqliteDsl.table('documents', {
126
+ value: SqliteDsl.text({ schema: Schema.fromJsonString(Schema.Struct({ value: Schema.String })) }),
127
+ })
128
+ const second = SqliteDsl.table('documents', {
129
+ value: SqliteDsl.text({ schema: Schema.fromJsonString(Schema.Struct({ value: Schema.Finite })) }),
130
+ })
131
+
132
+ expect(fingerprint(first.ast)).not.toBe(fingerprint(second.ast))
133
+ })
134
+
135
+ test('gives equivalent JSON DSL forms the same fingerprint', () => {
136
+ const valueSchema = Schema.Struct({ value: Schema.String })
137
+ const specialized = SqliteDsl.table('documents', {
138
+ value: SqliteDsl.json({ schema: valueSchema }),
139
+ })
140
+ const generic = SqliteDsl.table('documents', {
141
+ value: SqliteDsl.text({ schema: Schema.fromJsonString(valueSchema) }),
142
+ })
143
+
144
+ expect(fingerprint(specialized.ast)).toBe(fingerprint(generic.ast))
145
+ })
146
+
147
+ test('classifies physical defaults without hashing function source text', () => {
148
+ const none = SqliteDsl.text()
149
+ const firstThunk = SqliteDsl.text({ default: () => 'first' })
150
+ const secondThunk = SqliteDsl.text({ default: () => 'second' })
151
+ const literal = SqliteDsl.text({ default: 'first' })
152
+ const toTable = (definition: SqliteDsl.ColumnDefinition.Any) =>
153
+ SqliteDsl.table('defaults', { value: definition }).ast
154
+
155
+ expect(fingerprint(toTable(firstThunk))).toBe(fingerprint(toTable(secondThunk)))
156
+ expect(fingerprint(toTable(firstThunk))).not.toBe(fingerprint(toTable(none)))
157
+ expect(fingerprint(toTable(literal))).not.toBe(fingerprint(toTable(firstThunk)))
158
+ })
159
+ })
160
+
161
+ const makePhysicalTable = () =>
162
+ SqliteDsl.table(
163
+ 'users',
164
+ {
165
+ id: SqliteDsl.text({ primaryKey: true }),
166
+ age: SqliteDsl.integer({ nullable: true }),
167
+ },
168
+ [{ name: 'users_by_age', columns: ['age'], isUnique: false }],
169
+ )
170
+
171
+ const makeJsonTable = (name: string, schema: Schema.Codec<unknown, unknown>) =>
172
+ SqliteDsl.table(name, {
173
+ id: SqliteDsl.text({ primaryKey: true }),
174
+ value: SqliteDsl.json({ schema }),
175
+ })
176
+
177
+ const representativeJsonSchema = Schema.Struct({
178
+ title: Schema.String.check(Schema.isMinLength(1)),
179
+ tags: Schema.Array(Schema.String),
180
+ status: Schema.Union([Schema.Literal('draft'), Schema.Literal('published')]),
181
+ publishedAt: Schema.NullOr(Schema.DateFromString),
182
+ metadata: Schema.Record(Schema.String, Schema.Json),
183
+ })
184
+
185
+ /** Uses the JSON-compatible checks whose representation ids the fingerprint normalizes. */
186
+ const normalizedCheckIdsJsonSchema = Schema.Struct({
187
+ slug: Schema.String.check(
188
+ Schema.isBetweenLength(1, 64),
189
+ Schema.isStartingWith('a'),
190
+ Schema.isEndingWith('z'),
191
+ Schema.isIncluding('-'),
192
+ ),
193
+ tags: Schema.Array(Schema.String).check(Schema.isBetweenLength(0, 10)),
194
+ initial: Schema.Char,
195
+ labels: Schema.Record(Schema.String, Schema.String).check(Schema.isBetweenProperties(0, 5)),
196
+ })
197
+
198
+ interface TreeNode {
199
+ readonly value: string
200
+ readonly children: ReadonlyArray<TreeNode>
201
+ }
202
+
203
+ const makeTreeSchema = (): Schema.Codec<TreeNode> => {
204
+ const TreeNode: Schema.Codec<TreeNode> = Schema.Struct({
205
+ value: Schema.String,
206
+ children: Schema.Array(Schema.suspend(() => TreeNode)),
207
+ })
208
+ return TreeNode
209
+ }
@@ -0,0 +1,225 @@
1
+ import { casesHandled, textEncodeToArrayBuffer } from '@livestore/utils'
2
+ import { Schema, SchemaAST, SchemaRepresentation } from '@livestore/utils/effect'
3
+
4
+ import { isDefaultThunk, isSqlDefaultValue } from '../dsl/field-defs.ts'
5
+ import { hasJsonStringEncoding } from '../has-json-string-encoding.ts'
6
+ import { digestToFingerprint } from './fingerprint-digest.ts'
7
+ import type { Column, ColumnType, DbSchema, Index, Table } from './sqlite.ts'
8
+
9
+ type FingerprintInput = Table | DbSchema
10
+
11
+ export const fingerprint = (input: FingerprintInput): string => digestToFingerprint(toCanonicalBytes(input))
12
+
13
+ type CanonicalValue = Schema.Json
14
+
15
+ type SchemaDescriptorCache = WeakMap<Schema.Top, CanonicalValue>
16
+
17
+ const domain = 'livestore/state-storage-fingerprint'
18
+
19
+ const toCanonicalBytes = (input: FingerprintInput): Uint8Array<ArrayBuffer> =>
20
+ textEncodeToArrayBuffer(JSON.stringify(toRootDescriptor(input)))
21
+
22
+ const toRootDescriptor = (input: FingerprintInput): CanonicalValue => {
23
+ // Limit schema memoization to one fingerprint operation so descriptors remain collectible.
24
+ const cache: SchemaDescriptorCache = new WeakMap()
25
+ return [domain, toDescriptor(input, cache)]
26
+ }
27
+
28
+ const toDescriptor = (input: FingerprintInput, cache: SchemaDescriptorCache): CanonicalValue => {
29
+ switch (input._tag) {
30
+ case 'table':
31
+ return tableDescriptor(input, cache)
32
+ case 'dbSchema':
33
+ return dbSchemaDescriptor(input, cache)
34
+ default:
35
+ return casesHandled(input)
36
+ }
37
+ }
38
+
39
+ const columnDescriptor = (column: Column, cache: SchemaDescriptorCache): CanonicalValue => {
40
+ const {
41
+ _tag: _tagIgnored,
42
+ name,
43
+ type,
44
+ primaryKey,
45
+ nullable,
46
+ autoIncrement,
47
+ default: defaultValue,
48
+ schema,
49
+ ...unhandled
50
+ } = column
51
+ assertNoUnhandledKeys(unhandled)
52
+
53
+ return [
54
+ 'column',
55
+ name,
56
+ columnTypeDescriptor(type),
57
+ primaryKey,
58
+ nullable,
59
+ autoIncrement,
60
+ defaultDescriptor(defaultValue, schema),
61
+ hasJsonStringEncoding(schema.ast) === true ? schemaDescriptor(schema, cache) : null,
62
+ ]
63
+ }
64
+
65
+ const columnTypeDescriptor = (type: ColumnType.ColumnType): string => {
66
+ const { _tag, ...unhandled } = type
67
+ assertNoUnhandledKeys(unhandled)
68
+ switch (_tag) {
69
+ case 'text':
70
+ case 'null':
71
+ case 'real':
72
+ case 'integer':
73
+ case 'blob':
74
+ return _tag
75
+ default:
76
+ return casesHandled(_tag)
77
+ }
78
+ }
79
+
80
+ const indexDescriptor = (index: Index): CanonicalValue => {
81
+ const { _tag: _tagIgnored, name, unique, primaryKey, columns, ...unhandled } = index
82
+ assertNoUnhandledKeys(unhandled)
83
+ return ['index', name ?? null, unique ?? false, primaryKey ?? false, columns]
84
+ }
85
+
86
+ const tableDescriptor = (table: Table, cache: SchemaDescriptorCache): CanonicalValue => {
87
+ const { _tag: _tagIgnored, name, columns, indexes, ...unhandled } = table
88
+ assertNoUnhandledKeys(unhandled)
89
+ return [
90
+ 'table',
91
+ name,
92
+ columns.map((column) => columnDescriptor(column, cache)),
93
+ sortCanonicalValues(indexes.map(indexDescriptor)),
94
+ ]
95
+ }
96
+
97
+ const dbSchemaDescriptor = (dbSchema: DbSchema, cache: SchemaDescriptorCache): CanonicalValue => {
98
+ const { _tag: _tagIgnored, tables, ...unhandled } = dbSchema
99
+ assertNoUnhandledKeys(unhandled)
100
+ return ['database', sortByCanonicalKey(tables, (table) => table.name).map((table) => tableDescriptor(table, cache))]
101
+ }
102
+
103
+ const defaultDescriptor = (value: Column['default'], schema: Column['schema']): CanonicalValue => {
104
+ if (value._tag === 'None') return ['none']
105
+ if (isDefaultThunk(value.value) === true) return ['runtime-default']
106
+ if (value.value === null) return ['value', null]
107
+ if (isSqlDefaultValue(value.value) === true) return ['sql', value.value.sql]
108
+
109
+ return ['value', canonicalValue(Schema.encodeSync(schema)(value.value))]
110
+ }
111
+
112
+ const schemaDescriptor = (schema: Schema.Top, cache: SchemaDescriptorCache): CanonicalValue => {
113
+ const cached = cache.get(schema)
114
+ if (cached !== undefined) return cached
115
+
116
+ // Both ends matter: the encoded side describes stored JSON while the type side
117
+ // distinguishes codecs such as DateFromString from an unconstrained string.
118
+ const document = SchemaRepresentation.toRepresentations([schema.ast, SchemaAST.toType(schema.ast)], {
119
+ referencePolicy: () => undefined,
120
+ })
121
+ const descriptor: CanonicalValue = ['schema', schemaRepresentationDescriptor(document)]
122
+ cache.set(schema, descriptor)
123
+ return descriptor
124
+ }
125
+
126
+ const schemaRepresentationDescriptor = (value: unknown, parentKey?: string): CanonicalValue => {
127
+ // Representation payloads are application-owned JSON. Keys such as `annotations`
128
+ // inside them are data, not Effect metadata.
129
+ if (parentKey === 'payload') return canonicalValue(value)
130
+
131
+ if (Array.isArray(value) === true) return value.map((item) => schemaRepresentationDescriptor(item))
132
+
133
+ if (value !== null && typeof value === 'object' && value instanceof Uint8Array === false) {
134
+ const sourceEntries = Object.entries(value).map(([key, entryValue]): [string, unknown] =>
135
+ parentKey === 'representation' && key === 'id' && typeof entryValue === 'string'
136
+ ? [key, renamedEffectCheckIds.get(entryValue) ?? entryValue]
137
+ : [key, entryValue],
138
+ )
139
+ const isUnion = sourceEntries.some(([key, entryValue]) => key === '_tag' && entryValue === 'Union')
140
+ // Effect rc.113 removed this representation field because unions only support `anyOf`.
141
+ // Restore the old default so dependency upgrades preserve existing state fingerprints.
142
+ if (isUnion === true && sourceEntries.some(([key]) => key === 'mode') === false) {
143
+ sourceEntries.push(['mode', 'anyOf'])
144
+ }
145
+ const entries = sortByCanonicalKey(
146
+ sourceEntries.filter(([key]) => effectIgnoredFields.has(key) === false),
147
+ ([key]) => key,
148
+ ).map(([key, entryValue]) => [key, schemaRepresentationDescriptor(entryValue, key)] as const)
149
+ return Object.fromEntries(entries)
150
+ }
151
+
152
+ return canonicalValue(value)
153
+ }
154
+
155
+ const effectIgnoredFields = new Set(['annotations', 'isMutable'])
156
+
157
+ /** Maps renamed Effect check ids to the ids existing fingerprints were computed from, so the rename does not rebuild state. */
158
+ const renamedEffectCheckIds = new Map([
159
+ ['effect/schema/isBetweenLength', 'effect/schema/isLengthBetween'],
160
+ ['effect/schema/isStartingWith', 'effect/schema/isStartsWith'],
161
+ ['effect/schema/isEndingWith', 'effect/schema/isEndsWith'],
162
+ ['effect/schema/isIncluding', 'effect/schema/isIncludes'],
163
+ ['effect/schema/isBetweenSize', 'effect/schema/isSizeBetween'],
164
+ ['effect/schema/isBetweenProperties', 'effect/schema/isPropertiesLengthBetween'],
165
+ ])
166
+
167
+ const canonicalValue = (value: unknown): CanonicalValue => {
168
+ if (Array.isArray(value) === true) return value.map(canonicalValue)
169
+
170
+ if (value !== null && typeof value === 'object' && value instanceof Uint8Array === false) {
171
+ const entries = sortByCanonicalKey(Object.entries(value), ([key]) => key).map(
172
+ ([key, entryValue]) => [key, canonicalValue(entryValue)] as const,
173
+ )
174
+ return Object.fromEntries(entries)
175
+ }
176
+
177
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value
178
+
179
+ if (typeof value === 'number') {
180
+ if (Number.isNaN(value) === true) return specialValue('number', 'NaN')
181
+ if (value === Number.POSITIVE_INFINITY) return specialValue('number', 'Infinity')
182
+ if (value === Number.NEGATIVE_INFINITY) return specialValue('number', '-Infinity')
183
+ if (Object.is(value, -0) === true) return specialValue('number', '-0')
184
+ return value
185
+ }
186
+
187
+ if (typeof value === 'bigint') return specialValue('bigint', value.toString(10))
188
+ if (value === undefined) return specialValue('undefined')
189
+
190
+ if (typeof value === 'symbol') {
191
+ const symbolKey = Symbol.keyFor(value)
192
+ if (symbolKey === undefined) throw new Error('Local symbols cannot be represented in a persistent fingerprint')
193
+ return specialValue('symbol', symbolKey)
194
+ }
195
+
196
+ if (typeof value === 'function') throw new Error('Functions cannot be represented in a persistent fingerprint')
197
+ if (value instanceof Uint8Array) return specialValue('bytes', bytesToHex(value))
198
+
199
+ throw new Error('Unsupported canonical value')
200
+ }
201
+
202
+ const specialValue = (type: string, value?: string): CanonicalValue => ({
203
+ $livestore$type: value === undefined ? [type] : [type, value],
204
+ })
205
+
206
+ const bytesToHex = (bytes: Uint8Array): string => {
207
+ let output = ''
208
+ for (const byte of bytes) output += byte.toString(16).padStart(2, '0')
209
+ return output
210
+ }
211
+
212
+ /** Avoid repeatedly serializing deep descriptors from inside an O(n log n) sort comparator. */
213
+ const sortCanonicalValues = (values: ReadonlyArray<CanonicalValue>): ReadonlyArray<CanonicalValue> =>
214
+ values
215
+ .map((value) => ({ value, key: JSON.stringify(value) }))
216
+ .toSorted((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0))
217
+ .map(({ value }) => value)
218
+
219
+ const sortByCanonicalKey = <A>(values: ReadonlyArray<A>, key: (value: A) => CanonicalValue): ReadonlyArray<A> =>
220
+ values
221
+ .map((value) => ({ value, key: JSON.stringify(key(value)) }))
222
+ .toSorted((left, right) => (left.key < right.key ? -1 : left.key > right.key ? 1 : 0))
223
+ .map(({ value }) => value)
224
+
225
+ const assertNoUnhandledKeys = (_unhandled: Record<PropertyKey, never>): void => {}
@@ -1,7 +1,5 @@
1
1
  import { omitUndefineds } from '@livestore/utils'
2
- import { type Option, Schema, SchemaAST } from '@livestore/utils/effect'
3
-
4
- import { hashCode } from '../hash.ts'
2
+ import { type Option, Schema } from '@livestore/utils/effect'
5
3
 
6
4
  export namespace ColumnType {
7
5
  export type ColumnType = Text | Null | Real | Integer | Blob
@@ -83,92 +81,5 @@ export type DbSchema = {
83
81
 
84
82
  export const dbSchema = (tables: Table[]): DbSchema => ({ _tag: 'dbSchema', tables })
85
83
 
86
- /**
87
- * Helper to detect if a column is a JSON column (encoded as a JSON string)
88
- */
89
- const isJsonColumn = (column: Column): boolean => {
90
- if (column.type._tag !== 'text') return false
91
-
92
- return hasJsonStringEncoding(column.schema.ast)
93
- }
94
-
95
- /**
96
- * `SchemaTransformation.fromJsonString` is a factory since Effect rc.109, so encoding links can no
97
- * longer be matched by identity. `Schema.fromJsonString` annotates its encoded side as
98
- * `application/json`, which identifies the link regardless of the reviver/replacer/space options.
99
- */
100
- const hasJsonStringEncoding = (ast: SchemaAST.AST): boolean => {
101
- if (ast.encoding?.some((link) => link.to.annotations?.contentMediaType === 'application/json') === true) {
102
- return true
103
- }
104
- return SchemaAST.isUnion(ast) === true && ast.types.some(hasJsonStringEncoding)
105
- }
106
-
107
- /**
108
- * NOTE we're now including JSON schema information for JSON columns
109
- * to detect client document schema changes
110
- */
111
- export const hash = (obj: Table | Column | Index | ForeignKey | DbSchema): number =>
112
- hashCode(JSON.stringify(trimInfoForHasing(obj)))
113
-
114
- const trimInfoForHasing = (obj: Table | Column | Index | ForeignKey | DbSchema): Record<string, any> => {
115
- switch (obj._tag) {
116
- case 'table': {
117
- return {
118
- _tag: 'table',
119
- name: obj.name,
120
- columns: obj.columns.map((column) => trimInfoForHasing(column)),
121
- indexes: obj.indexes.map((index) => trimInfoForHasing(index)),
122
- }
123
- }
124
- case 'column': {
125
- const baseInfo: Record<string, any> = {
126
- _tag: 'column',
127
- name: obj.name,
128
- type: obj.type._tag,
129
- primaryKey: obj.primaryKey,
130
- nullable: obj.nullable,
131
- autoIncrement: obj.autoIncrement,
132
- default: obj.default,
133
- }
134
-
135
- // NEW: Include schema hash for JSON columns
136
- // This ensures that changes to the JSON schema are detected
137
- if (isJsonColumn(obj) === true && obj.schema !== undefined) {
138
- // Use Effect's Schema.hash for consistent hashing
139
- baseInfo.jsonSchemaHash = Schema.hash(obj.schema)
140
- }
141
-
142
- return baseInfo
143
- }
144
- case 'index': {
145
- return {
146
- _tag: 'index',
147
- columns: obj.columns,
148
- name: obj.name,
149
- unique: obj.unique,
150
- primaryKey: obj.primaryKey,
151
- }
152
- }
153
- case 'foreignKey': {
154
- return {
155
- _tag: 'foreignKey',
156
- references: obj.references,
157
- key: obj.key,
158
- columns: obj.columns,
159
- }
160
- }
161
- case 'dbSchema': {
162
- return {
163
- _tag: 'dbSchema',
164
- tables: obj.tables.map(trimInfoForHasing),
165
- }
166
- }
167
- default: {
168
- throw new Error(`Unreachable: ${String(obj)}`)
169
- }
170
- }
171
- }
172
-
173
84
  export const structSchemaForTable = (tableDef: Table) =>
174
85
  Schema.Struct(Object.fromEntries(tableDef.columns.map((column) => [column.name, column.schema])))
@@ -267,6 +267,7 @@ export const defaultSchemaForColumnType = <TColumnType extends FieldColumnType>(
267
267
  }
268
268
  case 'real': {
269
269
  // oxlint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- switch-based type narrowing for column type to schema mapping; each case is correct for its branch
270
+ // @effect-diagnostics-next-line schemaNumber:off -- SQLite REAL permits Infinity/NaN; SqliteReal is the documented domain carve-out
270
271
  return SqliteReal as Schema.Codec<T>
271
272
  }
272
273
  case 'blob': {
@@ -0,0 +1,9 @@
1
+ import { SchemaAST } from '@livestore/utils/effect'
2
+
3
+ const getContentMediaType = SchemaAST.resolveAt<string>('contentMediaType')
4
+
5
+ export const hasJsonStringEncoding = (ast: SchemaAST.AST): boolean => hasJsonMediaType(SchemaAST.toEncoded(ast))
6
+
7
+ const hasJsonMediaType = (ast: SchemaAST.AST): boolean =>
8
+ getContentMediaType(ast) === 'application/json' ||
9
+ (SchemaAST.isUnion(ast) === true && ast.types.some(hasJsonMediaType))
@@ -4,6 +4,7 @@ import type { MigrationOptions } from '../../../adapter-types.ts'
4
4
  import type { Materializer } from '../../EventDef/mod.ts'
5
5
  import type { InternalState } from '../../schema.ts'
6
6
  import { ClientDocumentTableDefSymbol, tableIsClientDocumentTable } from './client-document-def.ts'
7
+ import { fingerprint } from './db-schema/ast/fingerprint.ts'
7
8
  import { SqliteAst } from './db-schema/mod.ts'
8
9
  import { stateSystemTables } from './system-tables/state-tables.ts'
9
10
  import type { TableDef, TableDefBase } from './table-def.ts'
@@ -55,7 +56,7 @@ export const makeState = <TStateInput extends InputState>(inputSchema: TStateInp
55
56
  }
56
57
  }
57
58
 
58
- const hash = SqliteAst.hash({
59
+ const hash = fingerprint({
59
60
  _tag: 'dbSchema',
60
61
  tables: [...tables.values()].map((_) => _.sqliteDef.ast),
61
62
  })
@@ -4,6 +4,7 @@ import { Schema, SchemaAST, Struct } from '@livestore/utils/effect'
4
4
  import { SessionIdSymbol } from '../../../../session-id-symbol.ts'
5
5
  import type { SqlValue } from '../../../../util.ts'
6
6
  import type { State } from '../../../mod.ts'
7
+ import { hasJsonStringEncoding } from '../db-schema/has-json-string-encoding.ts'
7
8
  import type { QueryBuilderAst } from './api.ts'
8
9
 
9
10
  /**
@@ -47,14 +48,6 @@ const getJsonArrayElementSchema = (colSchema: Schema.Top): Schema.Top | undefine
47
48
  return undefined
48
49
  }
49
50
 
50
- /**
51
- * `SchemaTransformation.fromJsonString` is a factory since Effect rc.109, so encoding links can no
52
- * longer be matched by identity. `Schema.fromJsonString` annotates its encoded side as
53
- * `application/json`, which identifies the link regardless of the reviver/replacer/space options.
54
- */
55
- const hasJsonStringEncoding = (ast: SchemaAST.AST): boolean =>
56
- ast.encoding?.some((link) => link.to.annotations?.contentMediaType === 'application/json') === true
57
-
58
51
  /**
59
52
  * Encodes a JSON array element to the representation returned by SQLite's json_each().
60
53
  * Objects/arrays are stringified so they match json_each's TEXT representation.
@@ -6,7 +6,7 @@ import { table } from '../table-def.ts'
6
6
  * STATE DATABASE SYSTEM TABLES
7
7
  *
8
8
  * ⚠️ SAFE TO CHANGE: State tables are automatically rebuilt from eventlog when schema changes.
9
- * No need to bump `liveStoreStorageFormatVersion` (uses hash-based migration via SqliteAst.hash()).
9
+ * No need to bump `liveStoreStorageFormatVersion` because state tables use fingerprint-based migration.
10
10
  */
11
11
 
12
12
  export const SCHEMA_META_TABLE = '__livestore_schema'
@@ -18,7 +18,7 @@ export const schemaMetaTable = table({
18
18
  name: SCHEMA_META_TABLE,
19
19
  columns: {
20
20
  tableName: SqliteDsl.text({ primaryKey: true }),
21
- schemaHash: SqliteDsl.integer({ nullable: false }),
21
+ schemaHash: SqliteDsl.text({ nullable: false }),
22
22
  /** ISO date format */
23
23
  updatedAt: SqliteDsl.text({ nullable: false }),
24
24
  },
@@ -64,6 +64,16 @@ export const stateHeadMetaTable = table({
64
64
 
65
65
  export type StateHeadMetaRow = typeof stateHeadMetaTable.Type
66
66
 
67
+ export const REBUILD_META_TABLE = '__livestore_rebuild'
68
+
69
+ /** Only a completed replay and successful migration hooks may insert the marker row. */
70
+ export const rebuildMetaTable = table({
71
+ name: REBUILD_META_TABLE,
72
+ columns: {
73
+ id: SqliteDsl.integer({ primaryKey: true }),
74
+ },
75
+ })
76
+
67
77
  /**
68
78
  * Table which stores SQLite changeset blobs which is used for rolling back
69
79
  * read-model state during rebasing.
@@ -104,6 +114,7 @@ export const stateSystemTables = [
104
114
  schemaEventDefsMetaTable,
105
115
  stateHeadMetaTable,
106
116
  sessionChangesetMetaTable,
117
+ rebuildMetaTable,
107
118
  ] as const
108
119
 
109
120
  export const isStateSystemTable = (tableName: string) => stateSystemTables.some((_) => _.sqliteDef.name === tableName)