@evolu/common 5.4.7 → 6.0.0

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 (242) hide show
  1. package/README.md +30 -34
  2. package/dist/src/Array.d.ts +17 -0
  3. package/dist/src/Array.d.ts.map +1 -0
  4. package/dist/src/Array.js +12 -0
  5. package/dist/src/Assert.d.ts +68 -0
  6. package/dist/src/Assert.d.ts.map +1 -0
  7. package/dist/src/Assert.js +77 -0
  8. package/dist/src/BigInt.d.ts +20 -0
  9. package/dist/src/BigInt.d.ts.map +1 -0
  10. package/dist/src/BigInt.js +18 -0
  11. package/dist/src/Buffer.d.ts +92 -0
  12. package/dist/src/Buffer.d.ts.map +1 -0
  13. package/dist/src/Buffer.js +62 -0
  14. package/dist/src/Callbacks.d.ts +20 -0
  15. package/dist/src/Callbacks.d.ts.map +1 -0
  16. package/dist/src/Callbacks.js +18 -0
  17. package/dist/src/Console.d.ts +78 -0
  18. package/dist/src/Console.d.ts.map +1 -0
  19. package/dist/src/Console.js +103 -0
  20. package/dist/src/Crypto.d.ts +72 -39
  21. package/dist/src/Crypto.d.ts.map +1 -1
  22. package/dist/src/Crypto.js +89 -54
  23. package/dist/src/Eq.d.ts +97 -0
  24. package/dist/src/Eq.d.ts.map +1 -0
  25. package/dist/src/Eq.js +167 -0
  26. package/dist/src/Error.d.ts +14 -10
  27. package/dist/src/Error.d.ts.map +1 -1
  28. package/dist/src/Error.js +43 -11
  29. package/dist/src/Evolu/Config.d.ts +69 -0
  30. package/dist/src/Evolu/Config.d.ts.map +1 -0
  31. package/dist/src/Evolu/Config.js +9 -0
  32. package/dist/src/Evolu/Db.d.ts +126 -0
  33. package/dist/src/Evolu/Db.d.ts.map +1 -0
  34. package/dist/src/Evolu/Db.js +774 -0
  35. package/dist/src/Evolu/Diff.d.ts +43 -0
  36. package/dist/src/Evolu/Diff.d.ts.map +1 -0
  37. package/dist/src/Evolu/Diff.js +95 -0
  38. package/dist/src/Evolu/Evolu.d.ts +334 -0
  39. package/dist/src/Evolu/Evolu.d.ts.map +1 -0
  40. package/dist/src/Evolu/Evolu.js +434 -0
  41. package/dist/src/Evolu/Internal.d.ts +26 -0
  42. package/dist/src/Evolu/Internal.d.ts.map +1 -0
  43. package/dist/src/Evolu/Internal.js +25 -0
  44. package/dist/src/Evolu/Kysely.d.ts +6 -0
  45. package/dist/src/Evolu/Kysely.d.ts.map +1 -0
  46. package/dist/src/Evolu/Kysely.js +21 -0
  47. package/dist/src/Evolu/Owner.d.ts +155 -0
  48. package/dist/src/Evolu/Owner.d.ts.map +1 -0
  49. package/dist/src/Evolu/Owner.js +126 -0
  50. package/dist/src/Evolu/Platform.d.ts +23 -0
  51. package/dist/src/Evolu/Platform.d.ts.map +1 -0
  52. package/dist/src/Evolu/Platform.js +1 -0
  53. package/dist/src/Evolu/Protocol.d.ts +401 -0
  54. package/dist/src/Evolu/Protocol.d.ts.map +1 -0
  55. package/dist/src/Evolu/Protocol.js +1151 -0
  56. package/dist/src/Evolu/Public.d.ts +18 -0
  57. package/dist/src/Evolu/Public.d.ts.map +1 -0
  58. package/dist/src/Evolu/Public.js +11 -0
  59. package/dist/src/Evolu/PublicKysely.d.ts +148 -0
  60. package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
  61. package/dist/src/Evolu/PublicKysely.js +185 -0
  62. package/dist/src/Evolu/Query.d.ts +63 -0
  63. package/dist/src/Evolu/Query.d.ts.map +1 -0
  64. package/dist/src/Evolu/Query.js +61 -0
  65. package/dist/src/Evolu/Relay.d.ts +13 -0
  66. package/dist/src/Evolu/Relay.d.ts.map +1 -0
  67. package/dist/src/Evolu/Relay.js +109 -0
  68. package/dist/src/Evolu/Schema.d.ts +201 -0
  69. package/dist/src/Evolu/Schema.d.ts.map +1 -0
  70. package/dist/src/Evolu/Schema.js +150 -0
  71. package/dist/src/Evolu/Storage.d.ts +49 -0
  72. package/dist/src/Evolu/Storage.d.ts.map +1 -0
  73. package/dist/src/Evolu/Storage.js +1111 -0
  74. package/dist/src/Evolu/Sync.d.ts +59 -0
  75. package/dist/src/Evolu/Sync.d.ts.map +1 -0
  76. package/dist/src/Evolu/Sync.js +29 -0
  77. package/dist/src/Evolu/Timestamp.d.ts +106 -0
  78. package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
  79. package/dist/src/Evolu/Timestamp.js +179 -0
  80. package/dist/src/Function.d.ts +54 -0
  81. package/dist/src/Function.d.ts.map +1 -0
  82. package/dist/src/Function.js +38 -0
  83. package/dist/src/ManyToManyMap.d.ts +26 -0
  84. package/dist/src/ManyToManyMap.d.ts.map +1 -0
  85. package/dist/src/ManyToManyMap.js +92 -0
  86. package/dist/src/NanoId.d.ts +27 -0
  87. package/dist/src/NanoId.d.ts.map +1 -0
  88. package/dist/src/NanoId.js +6 -0
  89. package/dist/src/Number.d.ts +42 -0
  90. package/dist/src/Number.d.ts.map +1 -0
  91. package/dist/src/Number.js +55 -0
  92. package/dist/src/Object.d.ts +35 -0
  93. package/dist/src/Object.d.ts.map +1 -0
  94. package/dist/src/Object.js +36 -0
  95. package/dist/src/Order.d.ts +90 -0
  96. package/dist/src/Order.d.ts.map +1 -0
  97. package/dist/src/Order.js +85 -0
  98. package/dist/src/Promise.d.ts +180 -0
  99. package/dist/src/Promise.d.ts.map +1 -0
  100. package/dist/src/Promise.js +176 -0
  101. package/dist/src/Random.d.ts +52 -0
  102. package/dist/src/Random.d.ts.map +1 -0
  103. package/dist/src/Random.js +29 -0
  104. package/dist/src/Ref.d.ts +40 -0
  105. package/dist/src/Ref.d.ts.map +1 -0
  106. package/dist/src/Ref.js +13 -0
  107. package/dist/src/Result.d.ts +421 -0
  108. package/dist/src/Result.d.ts.map +1 -0
  109. package/dist/src/Result.js +357 -0
  110. package/dist/src/Skiplist.d.ts +23 -0
  111. package/dist/src/Skiplist.d.ts.map +1 -0
  112. package/dist/src/Skiplist.js +58 -0
  113. package/dist/src/Sqlite.d.ts +116 -52
  114. package/dist/src/Sqlite.d.ts.map +1 -1
  115. package/dist/src/Sqlite.js +183 -67
  116. package/dist/src/Store.d.ts +45 -8
  117. package/dist/src/Store.d.ts.map +1 -1
  118. package/dist/src/Store.js +33 -17
  119. package/dist/src/String.d.ts +2 -0
  120. package/dist/src/String.d.ts.map +1 -0
  121. package/dist/src/String.js +14 -0
  122. package/dist/src/Time.d.ts +20 -0
  123. package/dist/src/Time.d.ts.map +1 -0
  124. package/dist/src/Time.js +25 -0
  125. package/dist/src/Type.d.ts +1937 -0
  126. package/dist/src/Type.d.ts.map +1 -0
  127. package/dist/src/Type.js +2002 -0
  128. package/dist/src/Types.d.ts +188 -0
  129. package/dist/src/Types.d.ts.map +1 -0
  130. package/dist/src/Types.js +6 -0
  131. package/dist/src/WebSocket.d.ts +112 -0
  132. package/dist/src/WebSocket.d.ts.map +1 -0
  133. package/dist/src/WebSocket.js +139 -0
  134. package/dist/src/Worker.d.ts +44 -0
  135. package/dist/src/Worker.d.ts.map +1 -0
  136. package/dist/src/Worker.js +66 -0
  137. package/dist/src/index.d.ts +24 -11
  138. package/dist/src/index.d.ts.map +1 -1
  139. package/dist/src/index.js +24 -11
  140. package/package.json +29 -38
  141. package/src/Array.ts +39 -0
  142. package/src/Assert.ts +116 -0
  143. package/src/BigInt.ts +29 -0
  144. package/src/Buffer.ts +175 -0
  145. package/src/Callbacks.ts +43 -0
  146. package/src/Console.ts +159 -0
  147. package/src/Crypto.ts +169 -115
  148. package/src/Eq.ts +204 -0
  149. package/src/Error.ts +57 -20
  150. package/src/Evolu/Config.ts +83 -0
  151. package/src/Evolu/Db.ts +1275 -0
  152. package/src/Evolu/Diff.ts +142 -0
  153. package/src/Evolu/Evolu.ts +947 -0
  154. package/src/Evolu/Internal.ts +26 -0
  155. package/src/Evolu/Kysely.ts +38 -0
  156. package/src/Evolu/Owner.ts +296 -0
  157. package/src/Evolu/Platform.ts +27 -0
  158. package/src/Evolu/Protocol.ts +1857 -0
  159. package/src/Evolu/Public.ts +43 -0
  160. package/src/Evolu/PublicKysely.ts +240 -0
  161. package/src/Evolu/Query.ts +167 -0
  162. package/src/Evolu/Relay.ts +142 -0
  163. package/src/Evolu/Schema.ts +417 -0
  164. package/src/Evolu/Storage.ts +1281 -0
  165. package/src/Evolu/Sync.ts +105 -0
  166. package/src/Evolu/Timestamp.ts +311 -0
  167. package/src/Function.ts +58 -0
  168. package/src/ManyToManyMap.ts +140 -0
  169. package/src/NanoId.ts +39 -0
  170. package/src/Number.ts +90 -0
  171. package/src/Object.ts +64 -0
  172. package/src/Order.ts +113 -0
  173. package/src/Promise.ts +295 -0
  174. package/src/Random.ts +68 -0
  175. package/src/Ref.ts +63 -0
  176. package/src/Result.ts +453 -0
  177. package/src/Skiplist.ts +102 -0
  178. package/src/Sqlite.ts +366 -153
  179. package/src/Store.ts +79 -36
  180. package/src/String.ts +10 -0
  181. package/src/Time.ts +36 -0
  182. package/src/Type.ts +3978 -0
  183. package/src/Types.ts +209 -0
  184. package/src/WebSocket.ts +273 -0
  185. package/src/Worker.ts +129 -0
  186. package/src/index.ts +24 -11
  187. package/dist/src/Config.d.ts +0 -56
  188. package/dist/src/Config.d.ts.map +0 -1
  189. package/dist/src/Config.js +0 -39
  190. package/dist/src/Crdt.d.ts +0 -89
  191. package/dist/src/Crdt.d.ts.map +0 -1
  192. package/dist/src/Crdt.js +0 -181
  193. package/dist/src/Db.d.ts +0 -107
  194. package/dist/src/Db.d.ts.map +0 -1
  195. package/dist/src/Db.js +0 -443
  196. package/dist/src/Diff.d.ts +0 -27
  197. package/dist/src/Diff.d.ts.map +0 -1
  198. package/dist/src/Diff.js +0 -84
  199. package/dist/src/Evolu.d.ts +0 -426
  200. package/dist/src/Evolu.d.ts.map +0 -1
  201. package/dist/src/Evolu.js +0 -333
  202. package/dist/src/Model.d.ts +0 -141
  203. package/dist/src/Model.d.ts.map +0 -1
  204. package/dist/src/Model.js +0 -125
  205. package/dist/src/Murmurhash.d.ts +0 -2
  206. package/dist/src/Murmurhash.d.ts.map +0 -1
  207. package/dist/src/Murmurhash.js +0 -60
  208. package/dist/src/Owner.d.ts +0 -33
  209. package/dist/src/Owner.d.ts.map +0 -1
  210. package/dist/src/Owner.js +0 -26
  211. package/dist/src/Platform.d.ts +0 -37
  212. package/dist/src/Platform.d.ts.map +0 -1
  213. package/dist/src/Platform.js +0 -11
  214. package/dist/src/Protobuf.d.ts +0 -81
  215. package/dist/src/Protobuf.d.ts.map +0 -1
  216. package/dist/src/Protobuf.js +0 -92
  217. package/dist/src/Public.d.ts +0 -13
  218. package/dist/src/Public.d.ts.map +0 -1
  219. package/dist/src/Public.js +0 -6
  220. package/dist/src/Socket.d.ts +0 -8
  221. package/dist/src/Socket.d.ts.map +0 -1
  222. package/dist/src/Socket.js +0 -51
  223. package/dist/src/Sql.d.ts +0 -12
  224. package/dist/src/Sql.d.ts.map +0 -1
  225. package/dist/src/Sql.js +0 -30
  226. package/dist/src/Sync.d.ts +0 -70
  227. package/dist/src/Sync.d.ts.map +0 -1
  228. package/dist/src/Sync.js +0 -127
  229. package/src/Config.ts +0 -119
  230. package/src/Crdt.ts +0 -361
  231. package/src/Db.ts +0 -955
  232. package/src/Diff.ts +0 -114
  233. package/src/Evolu.ts +0 -1016
  234. package/src/Model.ts +0 -233
  235. package/src/Murmurhash.ts +0 -70
  236. package/src/Owner.ts +0 -69
  237. package/src/Platform.ts +0 -47
  238. package/src/Protobuf.ts +0 -155
  239. package/src/Public.ts +0 -12
  240. package/src/Socket.ts +0 -83
  241. package/src/Sql.ts +0 -41
  242. package/src/Sync.ts +0 -315
@@ -0,0 +1,947 @@
1
+ import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
2
+ import { assertNonEmptyArray } from "../Assert.js";
3
+ import { createCallbacks } from "../Callbacks.js";
4
+ import { ConsoleDep } from "../Console.js";
5
+ import { SymmetricCryptoDecryptError } from "../Crypto.js";
6
+ import { TransferableError } from "../Error.js";
7
+ import { exhaustiveCheck } from "../Function.js";
8
+ import { NanoIdLibDep } from "../NanoId.js";
9
+ import { err, ok, Result } from "../Result.js";
10
+ import { isSqlMutation, SafeSql, SqliteError, SqliteQuery } from "../Sqlite.js";
11
+ import { createStore, StoreSubscribe } from "../Store.js";
12
+ import { TimeDep } from "../Time.js";
13
+ import {
14
+ createId,
15
+ Id,
16
+ InferErrors,
17
+ InferInput,
18
+ InferType,
19
+ Mnemonic,
20
+ ObjectType,
21
+ } from "../Type.js";
22
+ import { IntentionalNever } from "../Types.js";
23
+ import { Config, defaultConfig } from "./Config.js";
24
+ import { CreateDbWorkerDep } from "./Db.js";
25
+ import { applyPatches } from "./Diff.js";
26
+ import { kysely } from "./Kysely.js";
27
+ import { AppOwner } from "./Owner.js";
28
+ import { CreateAppStateDep, FlushSyncDep } from "./Platform.js";
29
+ import {
30
+ DbChange,
31
+ ProtocolError,
32
+ ProtocolUnsupportedVersionError,
33
+ } from "./Protocol.js";
34
+ import {
35
+ createSubscribedQueries,
36
+ emptyRows,
37
+ Queries,
38
+ QueriesToQueryRowsPromises,
39
+ Query,
40
+ QueryRows,
41
+ QueryRowsMap,
42
+ Row,
43
+ serializeQuery,
44
+ SubscribedQueries,
45
+ } from "./Query.js";
46
+ import {
47
+ assertValidEvoluSchema,
48
+ CreateQuery,
49
+ EvoluSchema,
50
+ insertable,
51
+ Mutation,
52
+ MutationKind,
53
+ MutationMapping,
54
+ MutationOptions,
55
+ updateable,
56
+ upsertable,
57
+ validEvoluSchemaToDbSchema,
58
+ ValidMutationSize,
59
+ ValidMutationSizeError,
60
+ } from "./Schema.js";
61
+ import { initialSyncState, SyncState } from "./Sync.js";
62
+ import { TimestampError } from "./Timestamp.js";
63
+
64
+ export interface Evolu<S extends EvoluSchema = EvoluSchema> {
65
+ /**
66
+ * Subscribe to {@link EvoluError} changes.
67
+ *
68
+ * ### Example
69
+ *
70
+ * ```ts
71
+ * const unsubscribe = evolu.subscribeError(() => {
72
+ * const error = evolu.getError();
73
+ * console.log(error);
74
+ * });
75
+ * ```
76
+ */
77
+ readonly subscribeError: StoreSubscribe;
78
+
79
+ /** Get {@link EvoluError}. */
80
+ readonly getError: () => EvoluError | null;
81
+
82
+ /**
83
+ * Create type-safe SQL {@link Query}.
84
+ *
85
+ * Evolu uses Kysely - the type-safe SQL query builder for TypeScript. See
86
+ * https://kysely.dev.
87
+ *
88
+ * All this function does is compile the Kysely query and serialize it into a
89
+ * unique string. Both operations are fast and cheap.
90
+ *
91
+ * For mutations, use {@link Evolu.insert} and {@link Evolu.update}.
92
+ *
93
+ * ### Example
94
+ *
95
+ * ```ts
96
+ * const allTodos = evolu.createQuery((db) =>
97
+ * db.selectFrom("todo").selectAll(),
98
+ * );
99
+ *
100
+ * const todoById = (id: TodoId) =>
101
+ * evolu.createQuery((db) =>
102
+ * db.selectFrom("todo").selectAll().where("id", "=", id),
103
+ * );
104
+ * ```
105
+ */
106
+ readonly createQuery: CreateQuery<S>;
107
+
108
+ /**
109
+ * Load {@link Query} and return a promise with {@link QueryRows}.
110
+ *
111
+ * A returned promise always resolves successfully because there is no reason
112
+ * why loading should fail. All data are local, and the query is typed. A
113
+ * serious unexpected Evolu error shall be handled with
114
+ * {@link Evolu.subscribeError}.
115
+ *
116
+ * Loading is batched, and returned promises are cached, so there is no need
117
+ * for an additional cache. Evolu's internal cache is invalidated on
118
+ * mutation.
119
+ *
120
+ * The returned promise is enriched with special status and value properties
121
+ * for the upcoming React `use` Hook, but other UI libraries can also leverage
122
+ * them. Speaking of React, there are two essential React Suspense-related
123
+ * patterns that every developer should be aware of—passing promises to
124
+ * children and caching over mutations.
125
+ *
126
+ * With promises passed to children, we can load a query as soon as possible,
127
+ * but we don't have to use the returned promise immediately. That's useful
128
+ * for prefetching, which is generally not necessary for local-first apps but
129
+ * can be if a query takes a long time to load.
130
+ *
131
+ * Caching over mutation is a pattern that every developer should know. As we
132
+ * said, Evolu caches promise until a mutation happens. A query loaded after
133
+ * that will return a new pending promise. That's okay for general usage but
134
+ * not for UI with React Suspense because a mutation would suspend rerendered
135
+ * queries on a page, and that's not a good UX.
136
+ *
137
+ * We call this pattern "caching over mutation" because it has no globally
138
+ * accepted name yet. React RFC for React Cache does not exist yet.
139
+ *
140
+ * For better UX, a query must be subscribed for updates. This way, instead of
141
+ * Suspense flashes, the user sees new data immediately because Evolu replaces
142
+ * cached promises with fresh, already resolved new ones.
143
+ *
144
+ * If you are curious why Evolu does not do that for all queries by default,
145
+ * the answer is simple: performance. Tracking changes is costly and
146
+ * meaningful only for visible (hence subscribed) queries anyway. To subscribe
147
+ * to a query, use {@link Evolu.subscribeQuery}.
148
+ *
149
+ * ### Example
150
+ *
151
+ * ```ts
152
+ * const allTodos = evolu.createQuery((db) =>
153
+ * db.selectFrom("todo").selectAll(),
154
+ * );
155
+ * evolu.loadQuery(allTodos).then(({ rows }) => {
156
+ * console.log(rows);
157
+ * });
158
+ * ```
159
+ */
160
+ readonly loadQuery: <R extends Row>(query: Query<R>) => Promise<QueryRows<R>>;
161
+
162
+ /**
163
+ * Load an array of {@link Query} queries and return an array of
164
+ * {@link QueryRows} promises. It's like `queries.map(loadQuery)` but with
165
+ * proper types for returned promises.
166
+ *
167
+ * ### Example
168
+ *
169
+ * ```ts
170
+ * evolu.loadQueries([allTodos, todoById(1)]);
171
+ * ```
172
+ */
173
+ readonly loadQueries: <R extends Row, Q extends Queries<R>>(
174
+ queries: [...Q],
175
+ ) => [...QueriesToQueryRowsPromises<Q>];
176
+
177
+ /**
178
+ * Subscribe to {@link Query} {@link QueryRows} changes.
179
+ *
180
+ * ### Example
181
+ *
182
+ * ```ts
183
+ * const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
184
+ * const rows = evolu.getQueryRows(allTodos);
185
+ * });
186
+ * ```
187
+ */
188
+ readonly subscribeQuery: (query: Query) => StoreSubscribe;
189
+
190
+ /**
191
+ * Get {@link QueryRows}.
192
+ *
193
+ * ### Example
194
+ *
195
+ * ```ts
196
+ * const unsubscribe = evolu.subscribeQuery(allTodos)(() => {
197
+ * const rows = evolu.getQueryRows(allTodos);
198
+ * });
199
+ * ```
200
+ */
201
+ readonly getQueryRows: <R extends Row>(query: Query<R>) => QueryRows<R>;
202
+
203
+ /**
204
+ * Subscribe to {@link AppOwner} changes.
205
+ *
206
+ * ### Example
207
+ *
208
+ * ```ts
209
+ * const unsubscribe = evolu.subscribeAppOwner(() => {
210
+ * const owner = evolu.getAppOwner();
211
+ * });
212
+ * ```
213
+ */
214
+ readonly subscribeAppOwner: StoreSubscribe;
215
+
216
+ /**
217
+ * Get {@link AppOwner}.
218
+ *
219
+ * ### Example
220
+ *
221
+ * ```ts
222
+ * const unsubscribe = evolu.subscribeAppOwner(() => {
223
+ * const owner = evolu.getAppOwner();
224
+ * });
225
+ * ```
226
+ */
227
+ readonly getAppOwner: () => AppOwner | null;
228
+
229
+ /**
230
+ * Subscribe to {@link SyncState} changes.
231
+ *
232
+ * ### Example
233
+ *
234
+ * ```ts
235
+ * const unsubscribe = evolu.subscribeSyncState(() => {
236
+ * const syncState = evolu.getSyncState();
237
+ * });
238
+ * ```
239
+ */
240
+ readonly subscribeSyncState: StoreSubscribe;
241
+
242
+ /**
243
+ * Get {@link SyncState}.
244
+ *
245
+ * ### Example
246
+ *
247
+ * ```ts
248
+ * const unsubscribe = evolu.subscribeSyncState(() => {
249
+ * const syncState = evolu.getSyncState();
250
+ * });
251
+ * ```
252
+ */
253
+ readonly getSyncState: () => SyncState;
254
+
255
+ /**
256
+ * Inserts a row.
257
+ *
258
+ * ### Example
259
+ *
260
+ * ```ts
261
+ * // TODO:
262
+ * ```
263
+ */
264
+ insert: Mutation<S, "insert">;
265
+
266
+ /**
267
+ * Updates a row.
268
+ *
269
+ * ### Example
270
+ *
271
+ * ```ts
272
+ * // TODO:
273
+ * ```
274
+ */
275
+ update: Mutation<S, "update">;
276
+
277
+ /**
278
+ * Upserts a row.
279
+ *
280
+ * ### Example
281
+ *
282
+ * ```ts
283
+ * // TODO:
284
+ * ```
285
+ */
286
+ upsert: Mutation<S, "upsert">;
287
+
288
+ /**
289
+ * Delete {@link AppOwner} and all their data from the current device. After
290
+ * the deletion, Evolu will purge the application state. For browsers, this
291
+ * will reload all tabs using Evolu. For native apps, it will restart the
292
+ * app.
293
+ *
294
+ * Reloading can be turned off via options if you want to provide a different
295
+ * UX.
296
+ */
297
+ readonly resetAppOwner: (options?: {
298
+ readonly reload?: boolean;
299
+ }) => Promise<void>;
300
+
301
+ /**
302
+ * Restore {@link AppOwner} with all their synced data. It uses
303
+ * {@link Evolu.resetAppOwner}, so be careful.
304
+ */
305
+ readonly restoreAppOwner: (
306
+ mnemonic: Mnemonic,
307
+ options?: {
308
+ readonly reload?: boolean;
309
+ },
310
+ ) => Promise<void>;
311
+
312
+ /**
313
+ * Reload the app in a platform-specific way. For browsers, this will reload
314
+ * all tabs using Evolu. For native apps, it will restart the app.
315
+ */
316
+ readonly reloadApp: () => void;
317
+
318
+ /** Export SQLite database file as Uint8Array. */
319
+ readonly exportDatabase: () => Promise<Uint8Array>;
320
+ }
321
+
322
+ /** Represent errors that can occur in Evolu. */
323
+ export type EvoluError =
324
+ | TimestampError
325
+ | ProtocolError
326
+ | TransferableError
327
+ | SymmetricCryptoDecryptError
328
+ | ProtocolUnsupportedVersionError
329
+ | SqliteError;
330
+
331
+ interface InternalEvoluInstance<S extends EvoluSchema = EvoluSchema>
332
+ extends Evolu<S> {
333
+ /**
334
+ * Ensure tables and columns defined in {@link EvoluSchema} exist in the
335
+ * database. This function is for hot reloading.
336
+ */
337
+ readonly ensureSchema: (schema: EvoluSchema) => void;
338
+ }
339
+
340
+ export type EvoluDeps = CreateDbWorkerDep &
341
+ TimeDep &
342
+ NanoIdLibDep &
343
+ Partial<FlushSyncDep> &
344
+ ConsoleDep &
345
+ CreateAppStateDep;
346
+
347
+ export interface EvoluConfigWithInitialData<S extends EvoluSchema = EvoluSchema>
348
+ extends Config {
349
+ /**
350
+ * Use this option to create initial data (fixtures).
351
+ *
352
+ * ### Example
353
+ *
354
+ * ```ts
355
+ * const evolu = createEvolu(evoluReactWebDeps)(Schema, {
356
+ * initialData: (evolu) => {
357
+ * const todoCategory = evolu.insert("todoCategory", {
358
+ * name: "Not Urgent",
359
+ * });
360
+ *
361
+ * // This is a developer error, which should be fixed immediately.
362
+ * assert(todoCategory.ok, "invalid initial data");
363
+ *
364
+ * evolu.insert("todo", {
365
+ * title: "Try React Suspense",
366
+ * categoryId: todoCategory.value.id,
367
+ * });
368
+ * },
369
+ * });
370
+ * ```
371
+ */
372
+ initialData?: (evolu: EvoluForInitialData<S>) => void;
373
+ }
374
+
375
+ export interface EvoluForInitialData<S extends EvoluSchema = EvoluSchema> {
376
+ insert: Mutation<S, "insert">;
377
+ }
378
+
379
+ // For hot reloading and Evolu multitenancy.
380
+ const evoluInstances = new Map<string, InternalEvoluInstance>();
381
+
382
+ let tabId: Id | null = null;
383
+
384
+ /**
385
+ * Creates an {@link Evolu} instance configured with the specified
386
+ * {@link EvoluSchema} and optional configuration.
387
+ *
388
+ * This function returns a configured Evolu instance, providing a typed
389
+ * interface for querying, mutating, and syncing your application's data. The
390
+ * returned instance includes:
391
+ *
392
+ * - Subscription methods for receiving updates on queries, the owner, errors, and
393
+ * sync state.
394
+ * - Methods for creating, updating, or deleting rows in a type-safe manner.
395
+ * - Methods for querying data using Evolu's typed SQL queries, leveraging Kysely
396
+ * under the hood.
397
+ * - Built-in support for local-first and offline-first data with automatic sync
398
+ * and merging.
399
+ * - Automatic schema evolution that updates the underlying database with new
400
+ * columns or tables.
401
+ * - Managing owner data with resetAppOwner and restoreAppOwner.
402
+ *
403
+ * ### Example
404
+ *
405
+ * ```ts
406
+ * const TodoId = id("Todo");
407
+ * type TodoId = InferType<typeof TodoId>;
408
+ *
409
+ * const TodoCategoryId = id("TodoCategory");
410
+ * type TodoCategoryId = InferType<typeof TodoCategoryId>;
411
+ *
412
+ * const NonEmptyString50 = maxLength(50, NonEmptyString);
413
+ * type NonEmptyString50 = InferType<typeof NonEmptyString50>;
414
+ *
415
+ * const Schema = {
416
+ * todo: {
417
+ * id: TodoId,
418
+ * title: NonEmptyString1000,
419
+ * isCompleted: nullOr(SqliteBoolean),
420
+ * categoryId: nullOr(TodoCategoryId),
421
+ * },
422
+ * todoCategory: {
423
+ * id: TodoCategoryId,
424
+ * name: NonEmptyString50,
425
+ * },
426
+ * };
427
+ *
428
+ * const evolu = createEvolu(evoluReactDeps)(Schema);
429
+ * ```
430
+ */
431
+ export const createEvolu =
432
+ (deps: EvoluDeps) =>
433
+ <S extends EvoluSchema>(
434
+ // TODO: Validate missing Id, unsupported types, used default types via TS types
435
+ // with type errors messages as we had it in the old Evolu.
436
+ schema: S,
437
+ partialConfig: Partial<EvoluConfigWithInitialData<S>> = {},
438
+ ): Evolu<S> => {
439
+ const config = { ...defaultConfig, ...partialConfig };
440
+
441
+ let evolu = evoluInstances.get(config.name);
442
+
443
+ if (evolu == null) {
444
+ evolu = createEvoluInstance(deps)(schema, config as IntentionalNever);
445
+ evoluInstances.set(config.name, evolu);
446
+ } else {
447
+ // Hot reloading. Note that indexes are intentionally omitted.
448
+ evolu.ensureSchema(schema);
449
+ }
450
+
451
+ return evolu as IntentionalNever;
452
+ };
453
+
454
+ const createEvoluInstance =
455
+ (deps: EvoluDeps) =>
456
+ (
457
+ schema: EvoluSchema,
458
+ evoluConfig: EvoluConfigWithInitialData,
459
+ ): InternalEvoluInstance => {
460
+ deps.console.enabled = evoluConfig.enableLogging ?? false;
461
+
462
+ deps.console.log("[evolu]", "createEvoluInstance");
463
+
464
+ const { initialData, indexes, ...config } = evoluConfig;
465
+
466
+ const errorStore = createStore<EvoluError | null>(null);
467
+ const rowsStore = createStore<QueryRowsMap>(new Map());
468
+ const ownerStore = createStore<AppOwner | null>(null);
469
+ const syncStore = createStore<SyncState>(initialSyncState);
470
+
471
+ const subscribedQueries = createSubscribedQueries(rowsStore);
472
+ const loadingPromises = createLoadingPromises(subscribedQueries);
473
+ const callbacks = createCallbacks(deps);
474
+
475
+ const appState = deps.createAppState(config);
476
+ const dbWorker = deps.createDbWorker(config.name);
477
+
478
+ const getTabId = () => {
479
+ tabId ??= createId(deps);
480
+ return tabId;
481
+ };
482
+
483
+ dbWorker.onMessage((message) => {
484
+ switch (message.type) {
485
+ case "onInit": {
486
+ ownerStore.set(message.owner);
487
+ break;
488
+ }
489
+
490
+ case "onError": {
491
+ errorStore.set(message.error);
492
+ break;
493
+ }
494
+
495
+ case "onChange": {
496
+ if (message.tabId !== getTabId()) return;
497
+
498
+ const state = rowsStore.get();
499
+ const nextState = new Map([
500
+ ...state,
501
+ ...message.patches.map(
502
+ ({ query, patches }): [Query, ReadonlyArray<Row>] => [
503
+ query,
504
+ applyPatches(patches, state.get(query) ?? emptyRows),
505
+ ],
506
+ ),
507
+ ]);
508
+
509
+ for (const { query } of message.patches) {
510
+ loadingPromises.resolve(query, nextState.get(query) ?? emptyRows);
511
+ }
512
+
513
+ if (deps.flushSync && message.onCompleteIds.length > 0) {
514
+ deps.flushSync(() => {
515
+ rowsStore.set(nextState);
516
+ });
517
+ } else {
518
+ rowsStore.set(nextState);
519
+ }
520
+
521
+ for (const id of message.onCompleteIds) {
522
+ callbacks.execute(id);
523
+ }
524
+ break;
525
+ }
526
+
527
+ case "onReceive": {
528
+ if (message.tabId && message.tabId === getTabId()) return;
529
+
530
+ loadingPromises.releaseUnsubscribed();
531
+ const queries = subscribedQueries.get();
532
+ if (isNonEmptyReadonlyArray(queries)) {
533
+ dbWorker.postMessage({ type: "query", tabId: getTabId(), queries });
534
+ }
535
+ break;
536
+ }
537
+
538
+ case "onReset": {
539
+ if (message.reload) {
540
+ appState.reset();
541
+ } else {
542
+ callbacks.execute(message.onCompleteId);
543
+ }
544
+ break;
545
+ }
546
+
547
+ case "onExport": {
548
+ callbacks.execute(message.onCompleteId, message.file);
549
+ break;
550
+ }
551
+
552
+ default:
553
+ exhaustiveCheck(message);
554
+ }
555
+ });
556
+
557
+ const dbSchema = validEvoluSchemaToDbSchema(
558
+ assertValidEvoluSchema(schema),
559
+ indexes,
560
+ );
561
+
562
+ const mutationTypesCache = new Map<
563
+ MutationKind,
564
+ Map<string, ValidMutationSize<any>>
565
+ >();
566
+
567
+ // Lazy create mutation Types like this: `insertable(Schema.todo)`
568
+ const getMutationType = (table: string, kind: MutationKind) => {
569
+ let types = mutationTypesCache.get(kind);
570
+ if (!types) {
571
+ types = new Map();
572
+ mutationTypesCache.set(kind, types);
573
+ }
574
+ let type = types.get(table);
575
+ if (!type) {
576
+ type = { insert: insertable, update: updateable, upsert: upsertable }[
577
+ kind
578
+ ](schema[table]);
579
+ types.set(table, type);
580
+ }
581
+ return type;
582
+ };
583
+
584
+ const initialDataDbChanges: Array<DbChange> = [];
585
+
586
+ /**
587
+ * Note that the initial data function is called even if it is unnecessary
588
+ * (initial data are already in the DB) because we don't want to wait for
589
+ * SQLite's response. Initial data should be small (because they are inlined
590
+ * in the code), so it's ok.
591
+ */
592
+ if (initialData)
593
+ initialData({
594
+ insert: (table, props) => {
595
+ const Type = getMutationType(table, "insert");
596
+ const id = createId(deps);
597
+
598
+ const result = Type.fromUnknown(props);
599
+
600
+ if (result.ok) {
601
+ initialDataDbChanges.push({
602
+ id,
603
+ table,
604
+ values: result.value,
605
+ } as unknown as DbChange);
606
+ return ok({ id });
607
+ }
608
+
609
+ return result;
610
+ },
611
+ });
612
+
613
+ dbWorker.postMessage({
614
+ type: "init",
615
+ config,
616
+ dbSchema,
617
+ initialData: initialDataDbChanges,
618
+ });
619
+
620
+ const loadQueryMicrotaskQueue: Array<Query> = [];
621
+
622
+ const mutateMicrotaskQueue: Array<
623
+ [DbChange | undefined, MutationOptions["onComplete"] | undefined]
624
+ > = [];
625
+
626
+ const createMutation =
627
+ <Kind extends MutationKind>(kind: Kind) =>
628
+ <TableName extends keyof typeof schema>(
629
+ table: TableName,
630
+ props: InferInput<
631
+ ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
632
+ >,
633
+ options?: MutationOptions,
634
+ ): Result<
635
+ { readonly id: InferType<(typeof schema)[TableName]["id"]> },
636
+ | ValidMutationSizeError
637
+ | InferErrors<
638
+ ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
639
+ >
640
+ > => {
641
+ const Type = getMutationType(table, kind);
642
+ const result = Type.fromUnknown(props);
643
+
644
+ const id =
645
+ kind === "insert"
646
+ ? createId(deps)
647
+ : (props as unknown as { id: Id }).id;
648
+
649
+ if (options?.onlyValidate !== true) {
650
+ if (!result.ok) {
651
+ // One error must invalidate the whole queue.
652
+ // We insert `undefined` to detect such a situation.
653
+ mutateMicrotaskQueue.push([undefined, undefined]);
654
+ } else {
655
+ // Remove `id` from values.
656
+ const { id: _id, ...values } = result.value;
657
+ // EvoluSchema Types ensure valid types.
658
+ const change = { table, id, values } as unknown as DbChange;
659
+ mutateMicrotaskQueue.push([change, options?.onComplete]);
660
+ }
661
+
662
+ if (mutateMicrotaskQueue.length === 1)
663
+ queueMicrotask(() => {
664
+ const changes = [];
665
+ const onCompletes = [];
666
+
667
+ for (const [change, onComplete] of mutateMicrotaskQueue) {
668
+ if (change) changes.push(change);
669
+ if (onComplete) onCompletes.push(onComplete);
670
+ }
671
+
672
+ const mutateMicrotaskQueueLength = mutateMicrotaskQueue.length;
673
+ mutateMicrotaskQueue.length = 0;
674
+
675
+ // Don't mutate anything if there was a validation error.
676
+ // All mutations within a queue are considered to be a transaction.
677
+ if (changes.length !== mutateMicrotaskQueueLength) {
678
+ return;
679
+ }
680
+
681
+ const onCompleteIds = onCompletes.map((onComplete) =>
682
+ callbacks.register(onComplete),
683
+ );
684
+
685
+ loadingPromises.releaseUnsubscribed();
686
+
687
+ if (isNonEmptyArray(changes))
688
+ dbWorker.postMessage({
689
+ type: "mutate",
690
+ tabId: getTabId(),
691
+ changes,
692
+ onCompleteIds,
693
+ subscribedQueries: subscribedQueries.get(),
694
+ });
695
+ });
696
+ }
697
+
698
+ if (result.ok) return ok({ id });
699
+
700
+ return err(
701
+ result.error as
702
+ | ValidMutationSizeError
703
+ | InferErrors<
704
+ ObjectType<MutationMapping<(typeof schema)[TableName], Kind>>
705
+ >,
706
+ );
707
+ };
708
+
709
+ const evolu: InternalEvoluInstance = {
710
+ subscribeError: errorStore.subscribe,
711
+ getError: errorStore.get,
712
+
713
+ createQuery: (queryCallback, options) => {
714
+ const compiledQuery = queryCallback(
715
+ kysely as IntentionalNever,
716
+ ).compile();
717
+
718
+ if (isSqlMutation(compiledQuery.sql))
719
+ throw new Error(
720
+ "SQL mutation (INSERT, UPDATE, DELETE, etc.) isn't allowed in the Evolu `createQuery` function. Kysely suggests it because there is no read-only Kysely yet, and removing such an API is not possible. For mutations, use Evolu Mutation API.",
721
+ );
722
+
723
+ return serializeQuery({
724
+ sql: compiledQuery.sql as SafeSql,
725
+ parameters: compiledQuery.parameters as NonNullable<
726
+ SqliteQuery["parameters"]
727
+ >,
728
+ ...(options && { options }),
729
+ });
730
+ },
731
+
732
+ loadQuery: <R extends Row>(query: Query<R>): Promise<QueryRows<R>> => {
733
+ const { promise, isNew } = loadingPromises.get(query);
734
+
735
+ if (isNew) {
736
+ loadQueryMicrotaskQueue.push(query);
737
+ if (loadQueryMicrotaskQueue.length === 1) {
738
+ queueMicrotask(() => {
739
+ // Dedupe
740
+ const queries = [...new Set(loadQueryMicrotaskQueue)];
741
+ loadQueryMicrotaskQueue.length = 0;
742
+ assertNonEmptyArray(queries);
743
+ dbWorker.postMessage({
744
+ type: "query",
745
+ tabId: getTabId(),
746
+ queries,
747
+ });
748
+ });
749
+ }
750
+ }
751
+
752
+ return promise;
753
+ },
754
+
755
+ loadQueries: <R extends Row, Q extends Queries<R>>(
756
+ queries: [...Q],
757
+ ): [...QueriesToQueryRowsPromises<Q>] =>
758
+ queries.map(evolu.loadQuery) as [...QueriesToQueryRowsPromises<Q>],
759
+
760
+ subscribeQuery: (query) => (listener) => {
761
+ // Call the listener only if the result has been changed.
762
+ let previousResult: unknown = null;
763
+ const unsubscribe = subscribedQueries.subscribe(query)(() => {
764
+ const result = evolu.getQueryRows(query);
765
+ if (previousResult === result) return;
766
+ previousResult = result;
767
+ listener();
768
+ });
769
+ return () => {
770
+ previousResult = null;
771
+ unsubscribe();
772
+ };
773
+ },
774
+
775
+ getQueryRows: <R extends Row>(query: Query<R>): QueryRows<R> =>
776
+ (rowsStore.get().get(query) ?? emptyRows) as QueryRows<R>,
777
+
778
+ subscribeAppOwner: ownerStore.subscribe,
779
+ getAppOwner: ownerStore.get,
780
+
781
+ subscribeSyncState: syncStore.subscribe,
782
+ getSyncState: syncStore.get,
783
+
784
+ insert: createMutation("insert"),
785
+ update: createMutation("update"),
786
+ upsert: createMutation("upsert"),
787
+
788
+ resetAppOwner: (options) => {
789
+ // Eslint bug, Promise<void> is correct by docs.
790
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
791
+ const { promise, resolve } = Promise.withResolvers<void>();
792
+ const onCompleteId = callbacks.register(() => {
793
+ resolve();
794
+ });
795
+ dbWorker.postMessage({
796
+ type: "reset",
797
+ onCompleteId,
798
+ reload: options?.reload ?? true,
799
+ });
800
+ return promise;
801
+ },
802
+
803
+ restoreAppOwner: (mnemonic, options) => {
804
+ // Eslint bug, Promise<void> is correct by docs.
805
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
806
+ const { promise, resolve } = Promise.withResolvers<void>();
807
+ const onCompleteId = callbacks.register(() => {
808
+ resolve();
809
+ });
810
+ dbWorker.postMessage({
811
+ type: "reset",
812
+ onCompleteId,
813
+ reload: options?.reload ?? true,
814
+ restore: { mnemonic, dbSchema },
815
+ });
816
+ return promise;
817
+ },
818
+
819
+ reloadApp: () => {
820
+ appState.reset();
821
+ },
822
+
823
+ ensureSchema: (schema) => {
824
+ mutationTypesCache.clear();
825
+ const validSchema = assertValidEvoluSchema(schema);
826
+ dbWorker.postMessage({
827
+ type: "ensureDbSchema",
828
+ dbSchema: validEvoluSchemaToDbSchema(validSchema),
829
+ });
830
+ },
831
+
832
+ exportDatabase: () => {
833
+ const { promise, resolve } = Promise.withResolvers<Uint8Array>();
834
+ const onCompleteId = callbacks.register((arg) => {
835
+ if (arg instanceof Uint8Array) resolve(arg);
836
+ });
837
+ dbWorker.postMessage({ type: "export", onCompleteId });
838
+ return promise;
839
+ },
840
+ };
841
+
842
+ return evolu;
843
+ };
844
+
845
+ export const createNamespaceName =
846
+ (config: Config) =>
847
+ (name: string): string =>
848
+ `evolu:${config.name}:${name}`;
849
+
850
+ interface LoadingPromises {
851
+ get: <R extends Row>(
852
+ query: Query<R>,
853
+ ) => {
854
+ readonly promise: Promise<QueryRows<R>>;
855
+ readonly isNew: boolean;
856
+ };
857
+
858
+ resolve: (query: Query, rows: ReadonlyArray<Row>) => void;
859
+
860
+ releaseUnsubscribed: () => void;
861
+ }
862
+
863
+ interface LoadingPromise {
864
+ /** Promise with props for the upcoming React use hook. */
865
+ promise: Promise<QueryRows> & {
866
+ status?: "pending" | "fulfilled" | "rejected";
867
+ value?: QueryRows;
868
+ reason?: unknown;
869
+ };
870
+ resolve: (rows: QueryRows) => void;
871
+ releaseOnResolve: boolean;
872
+ }
873
+
874
+ const createLoadingPromises = (
875
+ subscribedQueries: SubscribedQueries,
876
+ ): LoadingPromises => {
877
+ const loadingPromiseMap = new Map<Query, LoadingPromise>();
878
+
879
+ const loadingPromises: LoadingPromises = {
880
+ get: <R extends Row>(
881
+ query: Query<R>,
882
+ ): {
883
+ readonly promise: Promise<QueryRows<R>>;
884
+ readonly isNew: boolean;
885
+ } => {
886
+ let loadingPromise = loadingPromiseMap.get(query);
887
+ const isNew = !loadingPromise;
888
+ if (!loadingPromise) {
889
+ const { promise, resolve } = Promise.withResolvers<QueryRows>();
890
+ loadingPromise = { resolve, promise, releaseOnResolve: false };
891
+ loadingPromiseMap.set(query, loadingPromise);
892
+ }
893
+ return {
894
+ promise: loadingPromise.promise as Promise<QueryRows<R>>,
895
+ isNew,
896
+ };
897
+ },
898
+
899
+ resolve: (query, rows) => {
900
+ const loadingPromise = loadingPromiseMap.get(query);
901
+ if (!loadingPromise) return;
902
+
903
+ if (loadingPromise.promise.status !== "fulfilled") {
904
+ loadingPromise.resolve(rows);
905
+ } else {
906
+ // A promise can't be fulfilled 2x, so we need a new one.
907
+ loadingPromise.promise = Promise.resolve(rows);
908
+ }
909
+
910
+ /**
911
+ * "For example, a data framework can set the status and value fields on a
912
+ * promise preemptively, before passing to React, so that React can unwrap
913
+ * it without waiting a microtask."
914
+ * https://github.com/acdlite/rfcs/blob/first-class-promises/text/0000-first-class-support-for-promises.md
915
+ */
916
+ void Object.assign(loadingPromise.promise, {
917
+ status: "fulfilled",
918
+ value: rows,
919
+ });
920
+
921
+ if (loadingPromise.releaseOnResolve) {
922
+ loadingPromiseMap.delete(query);
923
+ }
924
+ },
925
+
926
+ /**
927
+ * We can't delete loading promises in `resolveLoadingPromises` because they
928
+ * must be cached, so repeated calls to `loadQuery` will always return the
929
+ * same promise until the data changes, and we also can't cache them forever
930
+ * because only subscribed queries are automatically updated (reactivity is
931
+ * expensive) hence this function must be called manually on any mutation.
932
+ */
933
+ releaseUnsubscribed: () => {
934
+ [...loadingPromiseMap.entries()]
935
+ .filter(([query]) => !subscribedQueries.has(query))
936
+ .forEach(([query, loadingPromise]) => {
937
+ if (loadingPromise.promise.status === "fulfilled") {
938
+ loadingPromiseMap.delete(query);
939
+ } else {
940
+ loadingPromise.releaseOnResolve = true;
941
+ }
942
+ });
943
+ },
944
+ };
945
+
946
+ return loadingPromises;
947
+ };