create-syncular-app 0.1.3 → 0.2.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 (64) hide show
  1. package/README.md +70 -27
  2. package/dist/cli.d.ts +11 -0
  3. package/dist/cli.js +131 -179
  4. package/dist/constants.d.ts +40 -0
  5. package/dist/constants.js +46 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.js +3 -0
  8. package/dist/scaffold.d.ts +44 -0
  9. package/dist/scaffold.js +106 -0
  10. package/package.json +22 -15
  11. package/src/cli.ts +174 -0
  12. package/src/constants.ts +53 -0
  13. package/src/index.ts +3 -0
  14. package/src/scaffold.ts +161 -0
  15. package/template/minimal/README.md +45 -0
  16. package/template/minimal/gitignore +5 -0
  17. package/template/minimal/migrations/0001_initial/up.sql +9 -0
  18. package/template/minimal/package.json +22 -0
  19. package/template/minimal/src/clients.ts +54 -0
  20. package/template/minimal/src/make-client.ts +26 -0
  21. package/template/minimal/src/server.ts +39 -0
  22. package/template/minimal/src/smoke.test.ts +70 -0
  23. package/template/minimal/src/syncular.generated.ts +66 -0
  24. package/template/minimal/syncular.ir.json +66 -0
  25. package/template/minimal/syncular.json +17 -0
  26. package/template/minimal/tsconfig.json +16 -0
  27. package/template/web/README.md +63 -0
  28. package/template/web/gitignore +5 -0
  29. package/template/web/migrations/0001_initial/up.sql +11 -0
  30. package/template/web/package.json +22 -0
  31. package/template/web/src/frontend/index.html +37 -0
  32. package/template/web/src/frontend/main.ts +191 -0
  33. package/template/web/src/frontend/worker.ts +9 -0
  34. package/template/web/src/server.ts +183 -0
  35. package/template/web/src/smoke.test.ts +91 -0
  36. package/template/web/src/syncular.generated.ts +74 -0
  37. package/template/web/syncular.ir.json +76 -0
  38. package/template/web/syncular.json +17 -0
  39. package/template/web/tsconfig.json +16 -0
  40. package/template/README.md +0 -122
  41. package/template/_gitignore +0 -5
  42. package/template/generated/kotlin/SyncularApp.kt +0 -1005
  43. package/template/generated/rust/diesel_tables.rs +0 -142
  44. package/template/generated/rust/migrations.rs +0 -32
  45. package/template/generated/rust/schema.rs +0 -15
  46. package/template/generated/rust/syncular.rs +0 -926
  47. package/template/generated/swift/SyncularApp.swift +0 -1191
  48. package/template/generated/syncular.codegen.json +0 -19
  49. package/template/index.html +0 -13
  50. package/template/migrations/0001_initial/down.sql +0 -1
  51. package/template/migrations/0001_initial/up.sql +0 -8
  52. package/template/package.json +0 -33
  53. package/template/scripts/dev.ts +0 -42
  54. package/template/src/app.tsx +0 -231
  55. package/template/src/client/syncular.ts +0 -61
  56. package/template/src/generated/syncular.generated.ts +0 -769
  57. package/template/src/generated/syncular.server.generated.ts +0 -512
  58. package/template/src/main.tsx +0 -5
  59. package/template/src/server/sync-server.ts +0 -129
  60. package/template/src/styles.css +0 -233
  61. package/template/syncular.app.ts +0 -23
  62. package/template/syncular.schema.json +0 -145
  63. package/template/tsconfig.json +0 -18
  64. package/template/vite.config.ts +0 -9
@@ -1,1005 +0,0 @@
1
- // @generated by `cargo run --manifest-path rust/Cargo.toml -p syncular-codegen --`
2
- // Source: migrations/*.sql and generated Syncular codegen handoff
3
-
4
- import kotlinx.serialization.json.Json
5
- import kotlinx.serialization.json.JsonElement
6
- import kotlinx.serialization.json.JsonNull
7
- import kotlinx.serialization.json.JsonObject
8
- import kotlinx.serialization.json.booleanOrNull
9
- import kotlinx.serialization.json.doubleOrNull
10
- import kotlinx.serialization.json.jsonArray
11
- import kotlinx.serialization.json.jsonObject
12
- import kotlinx.serialization.json.jsonPrimitive
13
- import kotlinx.serialization.json.longOrNull
14
-
15
- const val syncularNativeExpectedFfiAbiVersion: Int = 2
16
- const val syncularNativeExpectedCrateVersion: String = "0.1.0"
17
- const val syncularNativeGeneratedSchemaVersion: Int = 1
18
-
19
- const val syncularNativeGeneratedAppSchemaJson: String = "{\"clientSchemaSupport\":{\"current\":1,\"minSupported\":1,\"supported\":[1]},\"localBaseSchema\":{\"tableSetupSql\":[\"CREATE TABLE IF NOT EXISTS \\\"tasks\\\" (\\n \\\"id\\\" TEXT PRIMARY KEY,\\n \\\"title\\\" TEXT NOT NULL,\\n \\\"completed\\\" INTEGER NOT NULL DEFAULT 0,\\n \\\"user_id\\\" TEXT NOT NULL,\\n \\\"created_at\\\" INTEGER NOT NULL DEFAULT 0,\\n \\\"server_version\\\" INTEGER NOT NULL DEFAULT 0\\n) WITHOUT ROWID\"]},\"migrations\":[{\"name\":\"initial\",\"schemaVersion\":1,\"upSql\":\"CREATE TABLE IF NOT EXISTS tasks (\\n id TEXT PRIMARY KEY,\\n title TEXT NOT NULL,\\n completed INTEGER NOT NULL DEFAULT 0,\\n user_id TEXT NOT NULL,\\n created_at BIGINT NOT NULL DEFAULT 0,\\n server_version BIGINT NOT NULL DEFAULT 0\\n) WITHOUT ROWID;\",\"version\":\"0001\"}],\"schemaVersion\":1,\"tables\":[{\"blobColumns\":[],\"columns\":[{\"name\":\"id\",\"notnullRequired\":false,\"primaryKey\":true,\"typeFamily\":\"text\"},{\"name\":\"title\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"text\"},{\"name\":\"completed\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"integer\"},{\"name\":\"user_id\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"text\"},{\"name\":\"created_at\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"integer\"},{\"name\":\"server_version\",\"notnullRequired\":true,\"primaryKey\":false,\"typeFamily\":\"integer\"}],\"crdtYjsFields\":[],\"encryptedFields\":[],\"name\":\"tasks\",\"primaryKeyColumn\":\"id\",\"scopes\":[{\"column\":\"user_id\",\"name\":\"user_id\",\"required\":true,\"source\":\"actorId\"}],\"serverVersionColumn\":\"server_version\",\"softDeleteColumn\":null,\"subscriptionId\":\"sub-tasks\"}]}"
20
-
21
- data class SyncularNativeRuntimeManifest(
22
- val ffiAbiVersion: Int,
23
- val crateName: String,
24
- val crateVersion: String,
25
- val schemaVersion: Int,
26
- val storageBackend: String,
27
- val transportBackends: List<String> = emptyList(),
28
- val capabilities: List<String> = emptyList(),
29
- )
30
-
31
- fun assertSyncularNativeRuntimeManifest(manifest: SyncularNativeRuntimeManifest) {
32
- require(manifest.ffiAbiVersion == syncularNativeExpectedFfiAbiVersion) { "FFI ABI version ${manifest.ffiAbiVersion} does not match generated expectation $syncularNativeExpectedFfiAbiVersion" }
33
- require(manifest.crateVersion == syncularNativeExpectedCrateVersion) { "Rust crate version ${manifest.crateVersion} does not match generated expectation $syncularNativeExpectedCrateVersion" }
34
- require(manifest.storageBackend == "diesel-sqlite") { "Rust storage backend ${manifest.storageBackend} is not diesel-sqlite" }
35
- require(manifest.capabilities.contains("generated-json-local-operations")) { "Rust native runtime is missing generated-json-local-operations" }
36
- require(manifest.capabilities.contains("generated-json-mutations")) { "Rust native runtime is missing generated-json-mutations" }
37
- require(manifest.capabilities.contains("generated-json-leased-mutations")) { "Rust native runtime is missing generated-json-leased-mutations" }
38
- require(manifest.capabilities.contains("queued-json-leased-mutations")) { "Rust native runtime is missing queued-json-leased-mutations" }
39
- require(manifest.capabilities.contains("auth-lease-issue")) { "Rust native runtime is missing auth-lease-issue" }
40
- require(manifest.capabilities.contains("read-only-query-json")) { "Rust native runtime is missing read-only-query-json" }
41
- require(manifest.capabilities.contains("query-observer-events")) { "Rust native runtime is missing query-observer-events" }
42
- }
43
-
44
- enum class SyncularGeneratedOperationKind(val wireValue: String) {
45
- Upsert("upsert"),
46
- Delete("delete"),
47
- }
48
-
49
- data class SyncularGeneratedOperation(
50
- val table: String,
51
- val rowId: String,
52
- val op: SyncularGeneratedOperationKind,
53
- val payload: Map<String, Any?>?,
54
- val baseVersion: Long?,
55
- ) {
56
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
57
- "table" to table,
58
- "row_id" to rowId,
59
- "op" to op.wireValue,
60
- "payload" to payload,
61
- "base_version" to baseVersion,
62
- )
63
-
64
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
65
- }
66
-
67
- data class SyncularBlobRef(
68
- val hash: String,
69
- val size: Long,
70
- val mimeType: String,
71
- val encrypted: Boolean? = null,
72
- val keyId: String? = null,
73
- ) {
74
- fun toJsonValue(): Map<String, Any?> {
75
- val value = linkedMapOf<String, Any?>(
76
- "hash" to hash,
77
- "size" to size,
78
- "mimeType" to mimeType,
79
- )
80
- encrypted?.let { value["encrypted"] = it }
81
- keyId?.let { value["keyId"] = it }
82
- return value
83
- }
84
-
85
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
86
- }
87
-
88
- data class SyncularSubscriptionArgs(
89
- val actorId: String,
90
- val projectId: String? = null,
91
- val bootstrapPhases: Map<String, Long> = emptyMap(),
92
- )
93
-
94
- private fun syncularBootstrapPhase(args: SyncularSubscriptionArgs, table: String, subscriptionId: String): Long =
95
- args.bootstrapPhases[subscriptionId] ?: args.bootstrapPhases[table] ?: 0L
96
-
97
- data class SyncularSubscriptionSpec(
98
- val id: String,
99
- val table: String,
100
- val scopes: Map<String, Any?>,
101
- val params: Map<String, Any?> = emptyMap(),
102
- val bootstrapPhase: Long = 0,
103
- ) {
104
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
105
- "id" to id,
106
- "table" to table,
107
- "scopes" to scopes,
108
- "params" to params,
109
- "bootstrapPhase" to bootstrapPhase,
110
- )
111
-
112
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
113
- }
114
-
115
- data class SyncularAuthLeaseScope(
116
- val subscriptionId: String,
117
- val table: String,
118
- val values: Map<String, Any?>,
119
- val operations: List<String>,
120
- ) {
121
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
122
- "subscriptionId" to subscriptionId,
123
- "table" to table,
124
- "values" to values,
125
- "operations" to operations,
126
- )
127
-
128
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
129
- }
130
-
131
- data class SyncularAuthLeaseIssueRequest(
132
- val schemaVersion: Int = syncularNativeGeneratedSchemaVersion,
133
- val ttlMs: Long? = null,
134
- val scopes: List<SyncularAuthLeaseScope>,
135
- ) {
136
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
137
- "schemaVersion" to schemaVersion,
138
- "ttlMs" to ttlMs,
139
- "scopes" to scopes.map { it.toJsonValue() },
140
- )
141
-
142
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
143
- }
144
-
145
- data class SyncularAuthLeaseRecord(
146
- val leaseId: String,
147
- val kid: String,
148
- val actorId: String,
149
- val issuedAtMs: Long,
150
- val notBeforeMs: Long,
151
- val expiresAtMs: Long,
152
- val schemaVersion: Int,
153
- val payloadJson: String,
154
- val token: String,
155
- val status: String,
156
- val lastValidationError: String? = null,
157
- val createdAtMs: Long,
158
- val updatedAtMs: Long,
159
- )
160
-
161
- fun syncularDecodeAuthLeaseRecord(recordJson: String): SyncularAuthLeaseRecord {
162
- val value = syncularGeneratedJson.parseToJsonElement(recordJson).jsonObject
163
- return SyncularAuthLeaseRecord(
164
- leaseId = value.syncularRequiredString("leaseId"),
165
- kid = value.syncularRequiredString("kid"),
166
- actorId = value.syncularRequiredString("actorId"),
167
- issuedAtMs = value.syncularRequiredLong("issuedAtMs"),
168
- notBeforeMs = value.syncularRequiredLong("notBeforeMs"),
169
- expiresAtMs = value.syncularRequiredLong("expiresAtMs"),
170
- schemaVersion = value.syncularRequiredLong("schemaVersion").toInt(),
171
- payloadJson = value.syncularRequiredString("payloadJson"),
172
- token = value.syncularRequiredString("token"),
173
- status = value.syncularRequiredString("status"),
174
- lastValidationError = value.syncularOptionalString("lastValidationError"),
175
- createdAtMs = value.syncularRequiredLong("createdAtMs"),
176
- updatedAtMs = value.syncularRequiredLong("updatedAtMs"),
177
- )
178
- }
179
-
180
- fun syncularSubscriptionsJson(subscriptions: List<SyncularSubscriptionSpec>): String =
181
- syncularJsonValue(subscriptions.map { it.toJsonValue() })
182
-
183
- fun syncularDefaultSubscriptionsJson(actorId: String, projectId: String? = null, bootstrapPhases: Map<String, Long> = emptyMap()): String =
184
- syncularSubscriptionsJson(syncularDefaultSubscriptions(SyncularSubscriptionArgs(actorId = actorId, projectId = projectId, bootstrapPhases = bootstrapPhases)))
185
-
186
- fun syncularDefaultSubscriptions(args: SyncularSubscriptionArgs): List<SyncularSubscriptionSpec> = listOf(
187
- taskSubscription(args),
188
- )
189
-
190
- fun taskSubscription(args: SyncularSubscriptionArgs): SyncularSubscriptionSpec {
191
- val scopes = linkedMapOf<String, Any?>()
192
- scopes["user_id"] = args.actorId
193
- return SyncularSubscriptionSpec(id = "sub-tasks", table = "tasks", scopes = scopes, params = emptyMap<String, Any?>(), bootstrapPhase = syncularBootstrapPhase(args, "tasks", "sub-tasks"))
194
- }
195
-
196
- data class SyncularFieldEncryptionRule(
197
- val scope: String,
198
- val table: String?,
199
- val fields: List<String>,
200
- val rowIdField: String?,
201
- ) {
202
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
203
- "scope" to scope,
204
- "table" to table,
205
- "fields" to fields,
206
- "rowIdField" to rowIdField,
207
- ).filterValues { it != null }
208
- }
209
-
210
- val syncularGeneratedFieldEncryptionRules: List<SyncularFieldEncryptionRule> = listOf(
211
- )
212
-
213
- fun syncularGeneratedFieldEncryptionConfigJson(
214
- keys: Map<String, String>,
215
- encryptionKid: String? = null,
216
- decryptionErrorMode: String? = null,
217
- envelopePrefix: String? = null,
218
- ): String = syncularJsonValue(linkedMapOf(
219
- "rules" to syncularGeneratedFieldEncryptionRules.map { it.toJsonValue() },
220
- "keys" to keys,
221
- "encryptionKid" to encryptionKid,
222
- "decryptionErrorMode" to decryptionErrorMode,
223
- "envelopePrefix" to envelopePrefix,
224
- ).filterValues { it != null })
225
-
226
- data class SyncularReadonlyQuery(
227
- val sql: String,
228
- val params: List<Any?> = emptyList(),
229
- val tables: List<String> = emptyList(),
230
- ) {
231
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
232
- "sql" to sql,
233
- "params" to params,
234
- "tables" to tables,
235
- )
236
-
237
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
238
- }
239
-
240
- data class SyncularLiveQueryRegistration(
241
- val id: String,
242
- val tables: List<String>,
243
- val label: String? = null,
244
- ) {
245
- fun toJsonValue(): Map<String, Any?> = linkedMapOf(
246
- "id" to id,
247
- "tables" to tables,
248
- "label" to label,
249
- )
250
-
251
- fun toJsonString(): String = syncularJsonValue(toJsonValue())
252
- }
253
-
254
- data class SyncularChangedCrdtField(
255
- val field: String,
256
- val stateColumn: String,
257
- val containerKey: String,
258
- val rowIdField: String,
259
- val kind: String,
260
- val syncMode: String,
261
- )
262
-
263
- data class SyncularChangedRow(
264
- val table: String,
265
- val rowId: String? = null,
266
- val operation: String,
267
- val changedFields: List<String> = emptyList(),
268
- val crdtFields: List<String> = emptyList(),
269
- val crdtFieldChanges: List<SyncularChangedCrdtField> = emptyList(),
270
- val commitId: String? = null,
271
- val commitSeq: Long? = null,
272
- val subscriptionId: String? = null,
273
- val serverVersion: Long? = null,
274
- )
275
-
276
- data class SyncularBootstrapState(
277
- val asOfCommitSeq: Long = 0,
278
- val tables: List<String> = emptyList(),
279
- val tableIndex: Long = 0,
280
- val rowCursor: String? = null,
281
- )
282
-
283
- data class SyncularBootstrapSubscriptionStatus(
284
- val id: String,
285
- val table: String,
286
- val expected: Boolean,
287
- val ready: Boolean,
288
- val status: String? = null,
289
- val phase: String,
290
- val progressPercent: Long,
291
- val cursor: Long? = null,
292
- val bootstrapState: SyncularBootstrapState? = null,
293
- val bootstrapPhase: Long,
294
- )
295
-
296
- data class SyncularBootstrapPhaseStatus(
297
- val phase: Long,
298
- val expectedSubscriptionIds: List<String> = emptyList(),
299
- val readySubscriptionIds: List<String> = emptyList(),
300
- val pendingSubscriptionIds: List<String> = emptyList(),
301
- val isReady: Boolean,
302
- val progressPercent: Long,
303
- )
304
-
305
- data class SyncularBootstrapStatus(
306
- val channelPhase: String,
307
- val progressPercent: Long,
308
- val isBootstrapping: Boolean,
309
- val criticalReady: Boolean,
310
- val interactiveReady: Boolean,
311
- val complete: Boolean,
312
- val activePhase: Long? = null,
313
- val expectedSubscriptionIds: List<String> = emptyList(),
314
- val readySubscriptionIds: List<String> = emptyList(),
315
- val pendingSubscriptionIds: List<String> = emptyList(),
316
- val subscriptions: List<SyncularBootstrapSubscriptionStatus> = emptyList(),
317
- val phases: List<SyncularBootstrapPhaseStatus> = emptyList(),
318
- )
319
-
320
- data class SyncularNativeErrorInfo(
321
- val kind: String,
322
- val code: String,
323
- val category: String,
324
- val retryable: Boolean,
325
- val recommendedAction: String,
326
- val message: String,
327
- val debug: String? = null,
328
- )
329
-
330
- data class SyncularNativeLifecycleBootstrap(
331
- val complete: Boolean = false,
332
- val criticalReady: Boolean = false,
333
- val interactiveReady: Boolean = false,
334
- val isBootstrapping: Boolean = false,
335
- val progressPercent: Long = 0,
336
- )
337
-
338
- data class SyncularNativeLifecycleOutbox(
339
- val pending: Long = 0,
340
- )
341
-
342
- data class SyncularNativeLifecycleConflicts(
343
- val unresolved: Long = 0,
344
- )
345
-
346
- data class SyncularNativeLifecycleBlobUploads(
347
- val pending: Long = 0,
348
- val uploading: Long = 0,
349
- val failed: Long = 0,
350
- )
351
-
352
- data class SyncularNativeLifecycleState(
353
- val phase: String,
354
- val online: Boolean = false,
355
- val requiresAction: Boolean = false,
356
- val pendingRequests: Long = 0,
357
- val bootstrap: SyncularNativeLifecycleBootstrap? = null,
358
- val outbox: SyncularNativeLifecycleOutbox? = null,
359
- val conflicts: SyncularNativeLifecycleConflicts? = null,
360
- val blobUploads: SyncularNativeLifecycleBlobUploads? = null,
361
- )
362
-
363
- data class SyncularNativeEvent(
364
- val eventSeq: Long = 0,
365
- val kind: String,
366
- val error: SyncularNativeErrorInfo? = null,
367
- val tables: List<String> = emptyList(),
368
- val queries: List<String> = emptyList(),
369
- val changedRows: List<SyncularChangedRow> = emptyList(),
370
- val commandId: String? = null,
371
- val clientCommitId: String? = null,
372
- val durationMs: Long? = null,
373
- val droppedCount: Long? = null,
374
- val bootstrap: SyncularBootstrapStatus? = null,
375
- val lifecycle: SyncularNativeLifecycleState? = null,
376
- val resyncRequired: Boolean = false,
377
- ) {
378
- val eventStreamLost: Boolean get() = kind == "EventsOverflowed" || resyncRequired
379
- }
380
-
381
- fun syncularDecodeChangedCrdtField(field: JsonObject): SyncularChangedCrdtField = SyncularChangedCrdtField(
382
- field = field["field"]?.jsonPrimitive?.content ?: "",
383
- stateColumn = field["stateColumn"]?.jsonPrimitive?.content ?: "",
384
- containerKey = field["containerKey"]?.jsonPrimitive?.content ?: "",
385
- rowIdField = field["rowIdField"]?.jsonPrimitive?.content ?: "",
386
- kind = field["kind"]?.jsonPrimitive?.content ?: "",
387
- syncMode = field["syncMode"]?.jsonPrimitive?.content ?: "",
388
- )
389
-
390
- fun syncularDecodeChangedRow(row: JsonObject): SyncularChangedRow = SyncularChangedRow(
391
- table = row["table"]?.jsonPrimitive?.content ?: "",
392
- rowId = row["rowId"]?.jsonPrimitive?.content,
393
- operation = row["operation"]?.jsonPrimitive?.content ?: "",
394
- changedFields = row["changedFields"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
395
- crdtFields = row["crdtFields"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
396
- crdtFieldChanges = row["crdtFieldChanges"]?.jsonArray?.map { syncularDecodeChangedCrdtField(it.jsonObject) } ?: emptyList(),
397
- commitId = row["commitId"]?.jsonPrimitive?.content,
398
- commitSeq = row["commitSeq"]?.jsonPrimitive?.longOrNull,
399
- subscriptionId = row["subscriptionId"]?.jsonPrimitive?.content,
400
- serverVersion = row["serverVersion"]?.jsonPrimitive?.longOrNull,
401
- )
402
-
403
- private fun syncularOptionalString(value: JsonElement?): String? =
404
- if (value == null || value is JsonNull) null else value.jsonPrimitive.content
405
-
406
- private fun syncularJsonStringList(value: JsonElement?): List<String> =
407
- if (value == null || value is JsonNull) emptyList() else value.jsonArray.map { it.jsonPrimitive.content }
408
-
409
- private fun syncularDecodeBootstrapState(value: JsonElement?): SyncularBootstrapState? {
410
- if (value == null || value is JsonNull) return null
411
- val state = value.jsonObject
412
- return SyncularBootstrapState(
413
- asOfCommitSeq = state["asOfCommitSeq"]?.jsonPrimitive?.longOrNull ?: 0L,
414
- tables = syncularJsonStringList(state["tables"]),
415
- tableIndex = state["tableIndex"]?.jsonPrimitive?.longOrNull ?: 0L,
416
- rowCursor = syncularOptionalString(state["rowCursor"]),
417
- )
418
- }
419
-
420
- private fun syncularDecodeBootstrapSubscriptionStatus(value: JsonElement): SyncularBootstrapSubscriptionStatus {
421
- val subscription = value.jsonObject
422
- return SyncularBootstrapSubscriptionStatus(
423
- id = subscription["id"]?.jsonPrimitive?.content ?: "",
424
- table = subscription["table"]?.jsonPrimitive?.content ?: "",
425
- expected = subscription["expected"]?.jsonPrimitive?.booleanOrNull ?: false,
426
- ready = subscription["ready"]?.jsonPrimitive?.booleanOrNull ?: false,
427
- status = syncularOptionalString(subscription["status"]),
428
- phase = subscription["phase"]?.jsonPrimitive?.content ?: "pending",
429
- progressPercent = subscription["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
430
- cursor = subscription["cursor"]?.jsonPrimitive?.longOrNull,
431
- bootstrapState = syncularDecodeBootstrapState(subscription["bootstrapState"]),
432
- bootstrapPhase = subscription["bootstrapPhase"]?.jsonPrimitive?.longOrNull ?: 0L,
433
- )
434
- }
435
-
436
- private fun syncularDecodeBootstrapPhaseStatus(value: JsonElement): SyncularBootstrapPhaseStatus {
437
- val phase = value.jsonObject
438
- return SyncularBootstrapPhaseStatus(
439
- phase = phase["phase"]?.jsonPrimitive?.longOrNull ?: 0L,
440
- expectedSubscriptionIds = syncularJsonStringList(phase["expectedSubscriptionIds"]),
441
- readySubscriptionIds = syncularJsonStringList(phase["readySubscriptionIds"]),
442
- pendingSubscriptionIds = syncularJsonStringList(phase["pendingSubscriptionIds"]),
443
- isReady = phase["isReady"]?.jsonPrimitive?.booleanOrNull ?: false,
444
- progressPercent = phase["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
445
- )
446
- }
447
-
448
- private fun syncularDecodeBootstrapStatus(value: JsonElement?): SyncularBootstrapStatus? {
449
- if (value == null || value is JsonNull) return null
450
- val status = value.jsonObject
451
- return SyncularBootstrapStatus(
452
- channelPhase = status["channelPhase"]?.jsonPrimitive?.content ?: "idle",
453
- progressPercent = status["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
454
- isBootstrapping = status["isBootstrapping"]?.jsonPrimitive?.booleanOrNull ?: false,
455
- criticalReady = status["criticalReady"]?.jsonPrimitive?.booleanOrNull ?: false,
456
- interactiveReady = status["interactiveReady"]?.jsonPrimitive?.booleanOrNull ?: false,
457
- complete = status["complete"]?.jsonPrimitive?.booleanOrNull ?: false,
458
- activePhase = status["activePhase"]?.jsonPrimitive?.longOrNull,
459
- expectedSubscriptionIds = syncularJsonStringList(status["expectedSubscriptionIds"]),
460
- readySubscriptionIds = syncularJsonStringList(status["readySubscriptionIds"]),
461
- pendingSubscriptionIds = syncularJsonStringList(status["pendingSubscriptionIds"]),
462
- subscriptions = status["subscriptions"]?.jsonArray?.map { syncularDecodeBootstrapSubscriptionStatus(it) } ?: emptyList(),
463
- phases = status["phases"]?.jsonArray?.map { syncularDecodeBootstrapPhaseStatus(it) } ?: emptyList(),
464
- )
465
- }
466
-
467
- private fun syncularDecodeNativeErrorInfo(value: JsonElement?): SyncularNativeErrorInfo? {
468
- if (value == null || value is JsonNull) return null
469
- val error = value.jsonObject
470
- return SyncularNativeErrorInfo(
471
- kind = error["kind"]?.jsonPrimitive?.content ?: "",
472
- code = error["code"]?.jsonPrimitive?.content ?: "",
473
- category = error["category"]?.jsonPrimitive?.content ?: "",
474
- retryable = error["retryable"]?.jsonPrimitive?.booleanOrNull ?: false,
475
- recommendedAction = error["recommendedAction"]?.jsonPrimitive?.content ?: "",
476
- message = error["message"]?.jsonPrimitive?.content ?: "",
477
- debug = syncularOptionalString(error["debug"]),
478
- )
479
- }
480
-
481
- private fun syncularDecodeNativeLifecycleState(value: JsonElement?): SyncularNativeLifecycleState? {
482
- if (value == null || value is JsonNull) return null
483
- val state = value.jsonObject
484
- val bootstrap = state["bootstrap"]?.takeUnless { it is JsonNull }?.jsonObject
485
- val outbox = state["outbox"]?.takeUnless { it is JsonNull }?.jsonObject
486
- val conflicts = state["conflicts"]?.takeUnless { it is JsonNull }?.jsonObject
487
- val blobUploads = state["blobUploads"]?.takeUnless { it is JsonNull }?.jsonObject
488
- return SyncularNativeLifecycleState(
489
- phase = state["phase"]?.jsonPrimitive?.content ?: "offline",
490
- online = state["online"]?.jsonPrimitive?.booleanOrNull ?: false,
491
- requiresAction = state["requiresAction"]?.jsonPrimitive?.booleanOrNull ?: false,
492
- pendingRequests = state["pendingRequests"]?.jsonPrimitive?.longOrNull ?: 0L,
493
- bootstrap = bootstrap?.let { SyncularNativeLifecycleBootstrap(
494
- complete = it["complete"]?.jsonPrimitive?.booleanOrNull ?: false,
495
- criticalReady = it["criticalReady"]?.jsonPrimitive?.booleanOrNull ?: false,
496
- interactiveReady = it["interactiveReady"]?.jsonPrimitive?.booleanOrNull ?: false,
497
- isBootstrapping = it["isBootstrapping"]?.jsonPrimitive?.booleanOrNull ?: false,
498
- progressPercent = it["progressPercent"]?.jsonPrimitive?.longOrNull ?: 0L,
499
- ) },
500
- outbox = outbox?.let { SyncularNativeLifecycleOutbox(pending = it["pending"]?.jsonPrimitive?.longOrNull ?: 0L) },
501
- conflicts = conflicts?.let { SyncularNativeLifecycleConflicts(unresolved = it["unresolved"]?.jsonPrimitive?.longOrNull ?: 0L) },
502
- blobUploads = blobUploads?.let { SyncularNativeLifecycleBlobUploads(
503
- pending = it["pending"]?.jsonPrimitive?.longOrNull ?: 0L,
504
- uploading = it["uploading"]?.jsonPrimitive?.longOrNull ?: 0L,
505
- failed = it["failed"]?.jsonPrimitive?.longOrNull ?: 0L,
506
- ) },
507
- )
508
- }
509
-
510
- fun syncularDecodeNativeEvent(eventJson: String): SyncularNativeEvent {
511
- val event = Json.parseToJsonElement(eventJson).jsonObject
512
- return SyncularNativeEvent(
513
- eventSeq = event["event_seq"]?.jsonPrimitive?.longOrNull ?: 0L,
514
- kind = event["kind"]?.jsonPrimitive?.content ?: "",
515
- error = syncularDecodeNativeErrorInfo(event["error"]),
516
- tables = event["tables"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
517
- queries = event["queries"]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList(),
518
- changedRows = event["changedRows"]?.jsonArray?.map { syncularDecodeChangedRow(it.jsonObject) } ?: emptyList(),
519
- commandId = event["command_id"]?.jsonPrimitive?.content,
520
- clientCommitId = event["client_commit_id"]?.jsonPrimitive?.content,
521
- durationMs = event["duration_ms"]?.jsonPrimitive?.longOrNull,
522
- droppedCount = event["droppedCount"]?.jsonPrimitive?.longOrNull,
523
- bootstrap = syncularDecodeBootstrapStatus(event["bootstrap"]),
524
- lifecycle = syncularDecodeNativeLifecycleState(event["lifecycle"]),
525
- resyncRequired = event["resyncRequired"]?.jsonPrimitive?.booleanOrNull ?: (event["kind"]?.jsonPrimitive?.content == "EventsOverflowed"),
526
- )
527
- }
528
-
529
- fun syncularNativeEventRequiresFullRefresh(event: SyncularNativeEvent): Boolean =
530
- event.eventStreamLost
531
-
532
- interface SyncularNativeEventJsonSource {
533
- fun forEachEventJson(capacity: ULong = 256uL, handler: (String) -> Boolean)
534
- }
535
-
536
- fun SyncularNativeEventJsonSource.forEachNativeEvent(capacity: ULong = 256uL, handler: (SyncularNativeEvent) -> Boolean) {
537
- forEachEventJson(capacity) { eventJson -> handler(syncularDecodeNativeEvent(eventJson)) }
538
- }
539
-
540
- fun syncularDecodeNativeEvents(eventJson: Iterable<String>): List<SyncularNativeEvent> =
541
- eventJson.map(::syncularDecodeNativeEvent)
542
-
543
- data class TaskChangedFields(val raw: Set<String>) {
544
- constructor(fields: List<String>) : this(fields.toSet())
545
- val id: Boolean = raw.contains("id")
546
- val title: Boolean = raw.contains("title")
547
- val completed: Boolean = raw.contains("completed")
548
- val userId: Boolean = raw.contains("user_id")
549
- val createdAt: Boolean = raw.contains("created_at")
550
- val serverVersion: Boolean = raw.contains("server_version")
551
-
552
- fun contains(column: String): Boolean = when (column) {
553
- "id" -> id
554
- "title" -> title
555
- "completed" -> completed
556
- "user_id" -> userId
557
- "created_at" -> createdAt
558
- "server_version" -> serverVersion
559
- else -> false
560
- }
561
- }
562
-
563
- data class TaskChangedRow(
564
- val raw: SyncularChangedRow,
565
- val changed: TaskChangedFields = TaskChangedFields(raw.changedFields),
566
- val crdt: TaskChangedFields = TaskChangedFields(raw.crdtFields),
567
- ) {
568
- val rowId: String? get() = raw.rowId
569
- val operation: String get() = raw.operation
570
- val isInsert: Boolean get() = raw.operation == "insert"
571
- val isUpdate: Boolean get() = raw.operation == "update"
572
- val isDelete: Boolean get() = raw.operation == "delete"
573
-
574
- companion object {
575
- fun from(row: SyncularChangedRow): TaskChangedRow? =
576
- if (row.table == "tasks") TaskChangedRow(row) else null
577
- }
578
- }
579
-
580
- fun taskChangedRows(rows: List<SyncularChangedRow>): List<TaskChangedRow> =
581
- rows.mapNotNull { TaskChangedRow.from(it) }
582
-
583
- fun taskChangedRows(event: SyncularNativeEvent): List<TaskChangedRow> =
584
- taskChangedRows(event.changedRows)
585
-
586
- interface SyncularNativeJsonClient {
587
- fun applyMutationJson(mutationJson: String, localRowJson: String? = null): String
588
- fun applyLeasedMutationJson(mutationJson: String, localRowJson: String? = null): String
589
- fun enqueueMutationJson(mutationJson: String, localRowJson: String? = null): String
590
- fun enqueueLeasedMutationJson(mutationJson: String, localRowJson: String? = null): String
591
- fun issueAuthLeaseJson(requestJson: String): String
592
- fun queryJson(requestJson: String): String
593
- fun registerQueryJson(queryJson: String): String
594
- fun unregisterQuery(id: String): Boolean
595
- fun diagnosticSnapshotJson(): String
596
- }
597
-
598
- fun SyncularNativeJsonClient.apply(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
599
- applyMutationJson(operation.toJsonString(), localRowJson)
600
-
601
- fun SyncularNativeJsonClient.applyLeased(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
602
- applyLeasedMutationJson(operation.toJsonString(), localRowJson)
603
-
604
- fun SyncularNativeJsonClient.enqueue(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
605
- enqueueMutationJson(operation.toJsonString(), localRowJson)
606
-
607
- fun SyncularNativeJsonClient.enqueueLeased(operation: SyncularGeneratedOperation, localRowJson: String? = null): String =
608
- enqueueLeasedMutationJson(operation.toJsonString(), localRowJson)
609
-
610
- fun SyncularNativeJsonClient.issueAuthLease(request: SyncularAuthLeaseIssueRequest): SyncularAuthLeaseRecord =
611
- syncularDecodeAuthLeaseRecord(issueAuthLeaseJson(request.toJsonString()))
612
-
613
- fun SyncularNativeJsonClient.diagnosticSnapshot(): JsonObject =
614
- Json.parseToJsonElement(diagnosticSnapshotJson()).jsonObject
615
-
616
- fun SyncularNativeJsonClient.query(query: SyncularReadonlyQuery): List<JsonObject> =
617
- syncularGeneratedQueryRows(queryJson(query.toJsonString()))
618
-
619
- fun <Row> SyncularNativeJsonClient.query(query: SyncularReadonlyQuery, decode: (JsonObject) -> Row): List<Row> =
620
- query(query).map(decode)
621
-
622
- fun SyncularNativeJsonClient.registerLiveQuery(registration: SyncularLiveQueryRegistration): String =
623
- registerQueryJson(registration.toJsonString())
624
-
625
- class SyncularNativeLiveQuery<Row>(
626
- val id: String,
627
- val query: SyncularReadonlyQuery,
628
- private val decode: (JsonObject) -> Row,
629
- val label: String? = null,
630
- ) {
631
- var rows: List<Row> = emptyList()
632
- private set
633
-
634
- fun start(client: SyncularNativeJsonClient): List<Row> {
635
- client.registerLiveQuery(SyncularLiveQueryRegistration(id = id, tables = query.tables, label = label))
636
- return refresh(client)
637
- }
638
-
639
- fun refresh(client: SyncularNativeJsonClient): List<Row> {
640
- rows = client.query(query, decode)
641
- return rows
642
- }
643
-
644
- fun stop(client: SyncularNativeJsonClient): Boolean = client.unregisterQuery(id)
645
-
646
- fun matches(queryIds: Iterable<String>): Boolean = queryIds.any { it == id }
647
-
648
- fun refreshIfChanged(event: SyncularNativeEvent, client: SyncularNativeJsonClient): List<Row>? {
649
- if (syncularNativeEventRequiresFullRefresh(event)) return refresh(client)
650
- if (event.kind != "QueriesChanged" || !matches(event.queries)) return null
651
- return refresh(client)
652
- }
653
-
654
- fun refreshIfChanged(eventJson: String, client: SyncularNativeJsonClient): List<Row>? =
655
- refreshIfChanged(syncularDecodeNativeEvent(eventJson), client)
656
- }
657
-
658
- data class SyncularQueryPredicate(
659
- val sql: String,
660
- val params: List<Any?> = emptyList(),
661
- ) {
662
- infix fun and(other: SyncularQueryPredicate): SyncularQueryPredicate =
663
- SyncularQueryPredicate(sql = "(($sql) and (${other.sql}))", params = params + other.params)
664
-
665
- infix fun or(other: SyncularQueryPredicate): SyncularQueryPredicate =
666
- SyncularQueryPredicate(sql = "(($sql) or (${other.sql}))", params = params + other.params)
667
- }
668
-
669
- data class SyncularQueryOrder(
670
- val sql: String,
671
- )
672
-
673
- class SyncularQueryColumn<T>(
674
- val table: String,
675
- val name: String,
676
- ) {
677
- fun eq(value: T): SyncularQueryPredicate =
678
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} = ?", params = listOf(value))
679
-
680
- fun notEq(value: T): SyncularQueryPredicate =
681
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} != ?", params = listOf(value))
682
-
683
- fun gt(value: T): SyncularQueryPredicate =
684
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} > ?", params = listOf(value))
685
-
686
- fun gte(value: T): SyncularQueryPredicate =
687
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} >= ?", params = listOf(value))
688
-
689
- fun lt(value: T): SyncularQueryPredicate =
690
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} < ?", params = listOf(value))
691
-
692
- fun lte(value: T): SyncularQueryPredicate =
693
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} <= ?", params = listOf(value))
694
-
695
- fun isNull(): SyncularQueryPredicate =
696
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} is null")
697
-
698
- fun isNotNull(): SyncularQueryPredicate =
699
- SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} is not null")
700
-
701
- fun isIn(values: Iterable<T>): SyncularQueryPredicate {
702
- val list = values.toList()
703
- if (list.isEmpty()) return SyncularQueryPredicate(sql = "0 = 1")
704
- return SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} in (${list.joinToString(", ") { "?" }})", params = list)
705
- }
706
-
707
- fun notIn(values: Iterable<T>): SyncularQueryPredicate {
708
- val list = values.toList()
709
- if (list.isEmpty()) return SyncularQueryPredicate(sql = "1 = 1")
710
- return SyncularQueryPredicate(sql = "${syncularQuoteIdentifier(name)} not in (${list.joinToString(", ") { "?" }})", params = list)
711
- }
712
-
713
- fun asc(): SyncularQueryOrder = SyncularQueryOrder("${syncularQuoteIdentifier(name)} asc")
714
-
715
- fun desc(): SyncularQueryOrder = SyncularQueryOrder("${syncularQuoteIdentifier(name)} desc")
716
- }
717
-
718
- class SyncularQueryTable<Row>(
719
- val name: String,
720
- val columns: List<String>,
721
- val decode: (JsonObject) -> Row,
722
- ) {
723
- fun select(): SyncularSelectQuery<Row> = SyncularSelectQuery(table = this)
724
- }
725
-
726
- data class SyncularSelectQuery<Row>(
727
- val table: SyncularQueryTable<Row>,
728
- val predicates: List<SyncularQueryPredicate> = emptyList(),
729
- val orders: List<SyncularQueryOrder> = emptyList(),
730
- val limitValue: Int? = null,
731
- ) {
732
- fun filter(predicate: SyncularQueryPredicate): SyncularSelectQuery<Row> =
733
- copy(predicates = predicates + predicate)
734
-
735
- fun orderBy(order: SyncularQueryOrder): SyncularSelectQuery<Row> =
736
- copy(orders = orders + order)
737
-
738
- fun limit(value: Int): SyncularSelectQuery<Row> = copy(limitValue = value)
739
-
740
- fun readonlyQuery(): SyncularReadonlyQuery {
741
- val columnSql = table.columns.joinToString(", ") { syncularQuoteIdentifier(it) }
742
- val sql = buildString {
743
- append("select ")
744
- append(columnSql)
745
- append(" from ")
746
- append(syncularQuoteIdentifier(table.name))
747
- if (predicates.isNotEmpty()) {
748
- append(" where ")
749
- append(predicates.joinToString(" and ") { it.sql })
750
- }
751
- if (orders.isNotEmpty()) {
752
- append(" order by ")
753
- append(orders.joinToString(", ") { it.sql })
754
- }
755
- limitValue?.let { append(" limit $it") }
756
- }
757
- return SyncularReadonlyQuery(
758
- sql = sql,
759
- params = predicates.flatMap { it.params },
760
- tables = listOf(table.name),
761
- )
762
- }
763
-
764
- fun fetch(client: SyncularNativeJsonClient): List<Row> = client.query(readonlyQuery(), table.decode)
765
-
766
- fun liveQuery(id: String, label: String? = null): SyncularNativeLiveQuery<Row> =
767
- SyncularNativeLiveQuery(id = id, query = readonlyQuery(), decode = table.decode, label = label)
768
- }
769
-
770
- private fun syncularQuoteIdentifier(identifier: String): String =
771
- "\"" + identifier.replace("\"", "\"\"") + "\""
772
-
773
- data class TaskRow(
774
- val id: String,
775
- val title: String,
776
- val completed: Long,
777
- val userId: String,
778
- val createdAt: Long,
779
- val serverVersion: Long,
780
- )
781
-
782
- data class NewTask(
783
- val id: String,
784
- val title: String,
785
- val completed: Long? = null,
786
- val userId: String,
787
- val createdAt: Long? = null,
788
- )
789
-
790
- data class TaskPatch(
791
- val title: String? = null,
792
- val completed: Long? = null,
793
- val userId: String? = null,
794
- val createdAt: Long? = null,
795
- )
796
-
797
- object TaskQuery {
798
- val table = SyncularQueryTable(name = "tasks", columns = listOf("id", "title", "completed", "user_id", "created_at", "server_version"), decode = ::syncularDecodeTaskRow)
799
- val id = SyncularQueryColumn<String>(table = "tasks", name = "id")
800
- val title = SyncularQueryColumn<String>(table = "tasks", name = "title")
801
- val completed = SyncularQueryColumn<Long>(table = "tasks", name = "completed")
802
- val userId = SyncularQueryColumn<String>(table = "tasks", name = "user_id")
803
- val createdAt = SyncularQueryColumn<Long>(table = "tasks", name = "created_at")
804
- val serverVersion = SyncularQueryColumn<Long>(table = "tasks", name = "server_version")
805
- fun select(): SyncularSelectQuery<TaskRow> = table.select()
806
- }
807
-
808
- object SyncularAppOperations {
809
- fun newTask(input: NewTask, baseVersion: Long? = 0): SyncularGeneratedOperation {
810
- val payload = linkedMapOf<String, Any?>()
811
- payload["title"] = input.title
812
- payload["completed"] = input.completed ?: 0L
813
- payload["user_id"] = input.userId
814
- payload["created_at"] = input.createdAt ?: 0L
815
- return SyncularGeneratedOperation(
816
- table = "tasks",
817
- rowId = input.id,
818
- op = SyncularGeneratedOperationKind.Upsert,
819
- payload = payload,
820
- baseVersion = baseVersion,
821
- )
822
- }
823
-
824
- fun patchTask(rowId: String, patch: TaskPatch, baseVersion: Long? = null): SyncularGeneratedOperation {
825
- val payload = linkedMapOf<String, Any?>()
826
- patch.title?.let { payload["title"] = it }
827
- patch.completed?.let { payload["completed"] = it }
828
- patch.userId?.let { payload["user_id"] = it }
829
- patch.createdAt?.let { payload["created_at"] = it }
830
- return SyncularGeneratedOperation(
831
- table = "tasks",
832
- rowId = rowId,
833
- op = SyncularGeneratedOperationKind.Upsert,
834
- payload = payload,
835
- baseVersion = baseVersion,
836
- )
837
- }
838
-
839
- fun deleteTask(rowId: String, baseVersion: Long? = null): SyncularGeneratedOperation = SyncularGeneratedOperation(
840
- table = "tasks",
841
- rowId = rowId,
842
- op = SyncularGeneratedOperationKind.Delete,
843
- payload = null,
844
- baseVersion = baseVersion,
845
- )
846
-
847
- }
848
-
849
- class SyncularAppMutations internal constructor(
850
- private val client: SyncularNativeJsonClient,
851
- private val queued: Boolean = false,
852
- private val leased: Boolean = false,
853
- ) {
854
- val tasks: TaskMutations get() = TaskMutations(client, queued, leased)
855
- }
856
-
857
- val SyncularNativeJsonClient.mutations: SyncularAppMutations
858
- get() = SyncularAppMutations(this)
859
-
860
- val SyncularNativeJsonClient.queuedMutations: SyncularAppMutations
861
- get() = SyncularAppMutations(this, queued = true)
862
-
863
- val SyncularNativeJsonClient.leasedMutations: SyncularAppMutations
864
- get() = SyncularAppMutations(this, leased = true)
865
-
866
- val SyncularNativeJsonClient.queuedLeasedMutations: SyncularAppMutations
867
- get() = SyncularAppMutations(this, queued = true, leased = true)
868
-
869
- class TaskMutations internal constructor(
870
- private val client: SyncularNativeJsonClient,
871
- private val queued: Boolean,
872
- private val leased: Boolean,
873
- ) {
874
- fun insert(input: NewTask, baseVersion: Long? = 0, localRowJson: String? = null): String {
875
- val operation = SyncularAppOperations.newTask(input, baseVersion)
876
- return when {
877
- queued && leased -> client.enqueueLeased(operation, localRowJson)
878
- queued -> client.enqueue(operation, localRowJson)
879
- leased -> client.applyLeased(operation, localRowJson)
880
- else -> client.apply(operation, localRowJson)
881
- }
882
- }
883
-
884
- fun update(rowId: String, patch: TaskPatch, baseVersion: Long? = null, localRowJson: String? = null): String {
885
- val operation = SyncularAppOperations.patchTask(rowId, patch, baseVersion)
886
- return when {
887
- queued && leased -> client.enqueueLeased(operation, localRowJson)
888
- queued -> client.enqueue(operation, localRowJson)
889
- leased -> client.applyLeased(operation, localRowJson)
890
- else -> client.apply(operation, localRowJson)
891
- }
892
- }
893
-
894
- fun delete(rowId: String, baseVersion: Long? = null): String {
895
- val operation = SyncularAppOperations.deleteTask(rowId, baseVersion)
896
- return when {
897
- queued && leased -> client.enqueueLeased(operation)
898
- queued -> client.enqueue(operation)
899
- leased -> client.applyLeased(operation)
900
- else -> client.apply(operation)
901
- }
902
- }
903
- }
904
-
905
- private val syncularGeneratedJson = Json { ignoreUnknownKeys = true }
906
-
907
- private fun syncularGeneratedRows(json: String): List<JsonObject> =
908
- syncularGeneratedJson.parseToJsonElement(json).jsonArray.map { it.jsonObject }
909
-
910
- private fun syncularGeneratedQueryRows(json: String): List<JsonObject> =
911
- syncularGeneratedJson.parseToJsonElement(json).jsonObject["rows"]?.jsonArray?.map { it.jsonObject } ?: emptyList()
912
-
913
- private fun syncularDecodeTaskRows(json: String): List<TaskRow> =
914
- syncularGeneratedRows(json).map(::syncularDecodeTaskRow)
915
-
916
- private fun syncularDecodeTaskRow(row: JsonObject): TaskRow = TaskRow(
917
- id = row.syncularRequiredString("id"),
918
- title = row.syncularRequiredString("title"),
919
- completed = row.syncularRequiredLong("completed"),
920
- userId = row.syncularRequiredString("user_id"),
921
- createdAt = row.syncularRequiredLong("created_at"),
922
- serverVersion = row.syncularRequiredLong("server_version"),
923
- )
924
-
925
- private fun JsonObject.syncularRequiredString(name: String): String =
926
- syncularOptionalString(name) ?: error("missing string field $name")
927
-
928
- private fun JsonObject.syncularOptionalString(name: String): String? {
929
- val element = this[name] ?: return null
930
- if (element is JsonNull) return null
931
- return element.jsonPrimitive.content
932
- }
933
-
934
- private fun JsonObject.syncularRequiredBlobRef(name: String): SyncularBlobRef =
935
- syncularOptionalBlobRef(name) ?: error("missing blob ref field $name")
936
-
937
- private fun JsonObject.syncularOptionalBlobRef(name: String): SyncularBlobRef? {
938
- val element = this[name] ?: return null
939
- if (element is JsonNull) return null
940
- val objectValue = runCatching {
941
- syncularGeneratedJson.parseToJsonElement(element.jsonPrimitive.content).jsonObject
942
- }.getOrElse {
943
- element.jsonObject
944
- }
945
- return SyncularBlobRef(
946
- hash = objectValue.syncularRequiredString("hash"),
947
- size = objectValue.syncularRequiredLong("size"),
948
- mimeType = objectValue.syncularRequiredString("mimeType"),
949
- encrypted = objectValue.syncularOptionalBoolean("encrypted"),
950
- keyId = objectValue.syncularOptionalString("keyId"),
951
- )
952
- }
953
-
954
- private fun JsonObject.syncularRequiredLong(name: String): Long =
955
- syncularOptionalLong(name) ?: error("missing integer field $name")
956
-
957
- private fun JsonObject.syncularOptionalLong(name: String): Long? {
958
- val element = this[name] ?: return null
959
- if (element is JsonNull) return null
960
- return element.jsonPrimitive.longOrNull ?: error("field $name is not an integer")
961
- }
962
-
963
- private fun JsonObject.syncularRequiredDouble(name: String): Double =
964
- syncularOptionalDouble(name) ?: error("missing double field $name")
965
-
966
- private fun JsonObject.syncularOptionalDouble(name: String): Double? {
967
- val element = this[name] ?: return null
968
- if (element is JsonNull) return null
969
- return element.jsonPrimitive.doubleOrNull ?: error("field $name is not a double")
970
- }
971
-
972
- private fun JsonObject.syncularRequiredBoolean(name: String): Boolean =
973
- syncularOptionalBoolean(name) ?: error("missing boolean field $name")
974
-
975
- private fun JsonObject.syncularOptionalBoolean(name: String): Boolean? {
976
- val element = this[name] ?: return null
977
- if (element is JsonNull) return null
978
- return element.jsonPrimitive.booleanOrNull ?: error("field $name is not a boolean")
979
- }
980
-
981
- private fun syncularJsonValue(value: Any?): String = when (value) {
982
- null -> "null"
983
- is SyncularBlobRef -> syncularJsonString(value.toJsonString())
984
- is String -> syncularJsonString(value)
985
- is Number -> value.toString()
986
- is Boolean -> value.toString()
987
- is Map<*, *> -> value.entries.joinToString(prefix = "{", postfix = "}") { entry -> syncularJsonString(entry.key.toString()) + ":" + syncularJsonValue(entry.value) }
988
- is Iterable<*> -> value.joinToString(prefix = "[", postfix = "]") { syncularJsonValue(it) }
989
- else -> syncularJsonString(value.toString())
990
- }
991
-
992
- private fun syncularJsonString(value: String): String = buildString {
993
- append('\"')
994
- for (ch in value) {
995
- when (ch) {
996
- '\\' -> append("\\\\")
997
- '\"' -> append("\\\"")
998
- '\n' -> append("\\n")
999
- '\r' -> append("\\r")
1000
- '\t' -> append("\\t")
1001
- else -> append(ch)
1002
- }
1003
- }
1004
- append('\"')
1005
- }