@syncular/core 0.0.6-96 → 0.1.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 (159) hide show
  1. package/dist/blobs.d.ts +14 -7
  2. package/dist/blobs.d.ts.map +1 -1
  3. package/dist/blobs.js.map +1 -1
  4. package/dist/column-codecs.d.ts +3 -0
  5. package/dist/column-codecs.d.ts.map +1 -1
  6. package/dist/column-codecs.js +32 -4
  7. package/dist/column-codecs.js.map +1 -1
  8. package/dist/conflict.js.map +1 -1
  9. package/dist/database.js +1 -1
  10. package/dist/database.js.map +1 -1
  11. package/dist/error-responses.d.ts +393 -0
  12. package/dist/error-responses.d.ts.map +1 -0
  13. package/dist/error-responses.js +393 -0
  14. package/dist/error-responses.js.map +1 -0
  15. package/dist/http/api-client.d.ts +11 -0
  16. package/dist/http/api-client.d.ts.map +1 -0
  17. package/dist/http/api-client.js +30 -0
  18. package/dist/http/api-client.js.map +1 -0
  19. package/dist/http/blob.d.ts +6 -0
  20. package/dist/http/blob.d.ts.map +1 -0
  21. package/dist/http/blob.js +46 -0
  22. package/dist/http/blob.js.map +1 -0
  23. package/dist/http/generated/api.d.ts +5241 -0
  24. package/dist/http/generated/api.d.ts.map +1 -0
  25. package/dist/http/generated/api.js +6 -0
  26. package/dist/http/generated/api.js.map +1 -0
  27. package/dist/http/index.d.ts +18 -0
  28. package/dist/http/index.d.ts.map +1 -0
  29. package/dist/http/index.js +212 -0
  30. package/dist/http/index.js.map +1 -0
  31. package/dist/http/shared.d.ts +45 -0
  32. package/dist/http/shared.d.ts.map +1 -0
  33. package/dist/http/shared.js +122 -0
  34. package/dist/http/shared.js.map +1 -0
  35. package/dist/http/transport-client.d.ts +31 -0
  36. package/dist/http/transport-client.d.ts.map +1 -0
  37. package/dist/http/transport-client.js +193 -0
  38. package/dist/http/transport-client.js.map +1 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +19 -15
  42. package/dist/index.js.map +1 -1
  43. package/dist/kysely-column-codecs.d.ts +17 -1
  44. package/dist/kysely-column-codecs.d.ts.map +1 -1
  45. package/dist/kysely-column-codecs.js +56 -70
  46. package/dist/kysely-column-codecs.js.map +1 -1
  47. package/dist/logger.js +1 -1
  48. package/dist/logger.js.map +1 -1
  49. package/dist/schemas/blobs.d.ts +7 -1
  50. package/dist/schemas/blobs.d.ts.map +1 -1
  51. package/dist/schemas/blobs.js +1 -0
  52. package/dist/schemas/blobs.js.map +1 -1
  53. package/dist/schemas/common.d.ts +73 -1
  54. package/dist/schemas/common.d.ts.map +1 -1
  55. package/dist/schemas/common.js +38 -0
  56. package/dist/schemas/common.js.map +1 -1
  57. package/dist/schemas/index.js +3 -3
  58. package/dist/schemas/sync.d.ts +712 -38
  59. package/dist/schemas/sync.d.ts.map +1 -1
  60. package/dist/schemas/sync.js +209 -5
  61. package/dist/schemas/sync.js.map +1 -1
  62. package/dist/scopes/index.d.ts +5 -0
  63. package/dist/scopes/index.d.ts.map +1 -1
  64. package/dist/scopes/index.js +27 -0
  65. package/dist/scopes/index.js.map +1 -1
  66. package/dist/sentry.d.ts +31 -0
  67. package/dist/sentry.d.ts.map +1 -0
  68. package/dist/sentry.js +147 -0
  69. package/dist/sentry.js.map +1 -0
  70. package/dist/snapshot-chunks.d.ts +104 -20
  71. package/dist/snapshot-chunks.d.ts.map +1 -1
  72. package/dist/snapshot-chunks.js +665 -72
  73. package/dist/snapshot-chunks.js.map +1 -1
  74. package/dist/sync-packs.d.ts +33 -0
  75. package/dist/sync-packs.d.ts.map +1 -0
  76. package/dist/sync-packs.js +821 -0
  77. package/dist/sync-packs.js.map +1 -0
  78. package/dist/telemetry.d.ts.map +1 -1
  79. package/dist/telemetry.js +3 -2
  80. package/dist/telemetry.js.map +1 -1
  81. package/dist/transforms.js.map +1 -1
  82. package/dist/types.d.ts +48 -7
  83. package/dist/types.d.ts.map +1 -1
  84. package/dist/types.js.map +1 -1
  85. package/dist/utils/bytes.d.ts +4 -0
  86. package/dist/utils/bytes.d.ts.map +1 -0
  87. package/dist/utils/bytes.js +66 -0
  88. package/dist/utils/bytes.js.map +1 -0
  89. package/dist/utils/compression.d.ts +11 -3
  90. package/dist/utils/compression.d.ts.map +1 -1
  91. package/dist/utils/compression.js +102 -50
  92. package/dist/utils/compression.js.map +1 -1
  93. package/dist/utils/crypto.d.ts +7 -1
  94. package/dist/utils/crypto.d.ts.map +1 -1
  95. package/dist/utils/crypto.js +89 -7
  96. package/dist/utils/crypto.js.map +1 -1
  97. package/dist/utils/id.d.ts.map +1 -1
  98. package/dist/utils/id.js +4 -1
  99. package/dist/utils/id.js.map +1 -1
  100. package/dist/utils/index.d.ts +5 -0
  101. package/dist/utils/index.d.ts.map +1 -1
  102. package/dist/utils/index.js +9 -4
  103. package/dist/utils/index.js.map +1 -1
  104. package/dist/utils/internal-runtime.d.ts +16 -0
  105. package/dist/utils/internal-runtime.d.ts.map +1 -0
  106. package/dist/utils/internal-runtime.js +14 -0
  107. package/dist/utils/internal-runtime.js.map +1 -0
  108. package/dist/utils/object.js.map +1 -1
  109. package/dist/utils/realtime-change-index.d.ts +9 -0
  110. package/dist/utils/realtime-change-index.d.ts.map +1 -0
  111. package/dist/utils/realtime-change-index.js +36 -0
  112. package/dist/utils/realtime-change-index.js.map +1 -0
  113. package/dist/utils/realtime-connection-registry.d.ts +40 -0
  114. package/dist/utils/realtime-connection-registry.d.ts.map +1 -0
  115. package/dist/utils/realtime-connection-registry.js +237 -0
  116. package/dist/utils/realtime-connection-registry.js.map +1 -0
  117. package/dist/utils/table-registry.d.ts +10 -0
  118. package/dist/utils/table-registry.d.ts.map +1 -0
  119. package/dist/utils/table-registry.js +54 -0
  120. package/dist/utils/table-registry.js.map +1 -0
  121. package/dist/utils/url.d.ts +3 -0
  122. package/dist/utils/url.d.ts.map +1 -0
  123. package/dist/utils/url.js +20 -0
  124. package/dist/utils/url.js.map +1 -0
  125. package/package.json +52 -7
  126. package/src/blobs.ts +24 -7
  127. package/src/column-codecs.ts +36 -8
  128. package/src/error-responses.ts +423 -0
  129. package/src/http/api-client.ts +39 -0
  130. package/src/http/blob.ts +95 -0
  131. package/src/http/generated/api.ts +5241 -0
  132. package/src/http/index.ts +365 -0
  133. package/src/http/shared.ts +220 -0
  134. package/src/http/transport-client.ts +314 -0
  135. package/src/index.ts +4 -0
  136. package/src/kysely-column-codecs.ts +81 -86
  137. package/src/schemas/blobs.ts +10 -1
  138. package/src/schemas/common.ts +44 -0
  139. package/src/schemas/sync.ts +298 -7
  140. package/src/scopes/index.ts +42 -0
  141. package/src/sentry.ts +200 -0
  142. package/src/snapshot-chunks.ts +921 -69
  143. package/src/sync-packs.ts +1096 -0
  144. package/src/telemetry.ts +7 -2
  145. package/src/types.ts +63 -8
  146. package/src/utils/bytes.ts +75 -0
  147. package/src/utils/compression.ts +147 -51
  148. package/src/utils/crypto.ts +120 -7
  149. package/src/utils/id.ts +11 -1
  150. package/src/utils/index.ts +5 -0
  151. package/src/utils/internal-runtime.ts +39 -0
  152. package/src/utils/realtime-change-index.ts +44 -0
  153. package/src/utils/realtime-connection-registry.ts +286 -0
  154. package/src/utils/table-registry.ts +67 -0
  155. package/src/utils/url.ts +29 -0
  156. package/src/__tests__/compression.test.ts +0 -54
  157. package/src/__tests__/conflict.test.ts +0 -325
  158. package/src/__tests__/telemetry.test.ts +0 -170
  159. package/src/__tests__/utils.test.ts +0 -27
@@ -9,8 +9,11 @@
9
9
 
10
10
  import { z } from 'zod';
11
11
  import {
12
+ SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1,
13
+ SYNC_SCOPED_SNAPSHOT_ARTIFACT_MANIFEST_VERSION,
14
+ SYNC_SNAPSHOT_ARTIFACT_COMPRESSION_NONE,
12
15
  SYNC_SNAPSHOT_CHUNK_COMPRESSION,
13
- SYNC_SNAPSHOT_CHUNK_ENCODING,
16
+ SYNC_SNAPSHOT_CHUNK_ENCODINGS,
14
17
  } from '../snapshot-chunks';
15
18
 
16
19
  // ============================================================================
@@ -37,6 +40,95 @@ export const ScopeValuesSchema = z.record(
37
40
  z.union([z.string(), z.array(z.string())])
38
41
  );
39
42
 
43
+ // ============================================================================
44
+ // Auth Lease Schemas
45
+ // ============================================================================
46
+
47
+ export const SYNC_AUTH_LEASE_VERSION = 1;
48
+ export const SYNC_AUTH_LEASE_PROTOCOL_VERSION = 1;
49
+ export const SYNC_AUTH_LEASE_ALG_ES256 = 'ES256';
50
+ export const SYNC_AUTH_LEASE_TYP = 'syncular-auth-lease+jws';
51
+ export const SYNC_AUTH_LEASE_CODE_MISSING = 'sync.auth_lease_missing';
52
+ export const SYNC_AUTH_LEASE_CODE_INVALID = 'sync.auth_lease_invalid';
53
+ export const SYNC_AUTH_LEASE_CODE_EXPIRED = 'sync.auth_lease_expired';
54
+ export const SYNC_AUTH_LEASE_CODE_SCHEMA_MISMATCH =
55
+ 'sync.auth_lease_schema_mismatch';
56
+ export const SYNC_AUTH_LEASE_CODE_SCOPE_MISMATCH =
57
+ 'sync.auth_lease_scope_mismatch';
58
+ export const SYNC_AUTH_LEASE_CODE_SCOPE_REVOKED =
59
+ 'sync.auth_lease_scope_revoked';
60
+ export const SYNC_AUTH_LEASE_CODE_BUSINESS_REJECTED =
61
+ 'sync.auth_lease_business_rejected';
62
+
63
+ export const SyncAuthLeaseProtectedHeaderSchema = z.object({
64
+ alg: z.literal(SYNC_AUTH_LEASE_ALG_ES256),
65
+ kid: z.string().min(1),
66
+ typ: z.literal(SYNC_AUTH_LEASE_TYP),
67
+ });
68
+
69
+ export type SyncAuthLeaseProtectedHeader = z.infer<
70
+ typeof SyncAuthLeaseProtectedHeaderSchema
71
+ >;
72
+
73
+ export const SyncAuthLeaseCapabilitiesSchema = z.object({
74
+ allowBlobs: z.boolean(),
75
+ allowCrdt: z.boolean(),
76
+ allowEncryptedFields: z.boolean(),
77
+ });
78
+
79
+ export type SyncAuthLeaseCapabilities = z.infer<
80
+ typeof SyncAuthLeaseCapabilitiesSchema
81
+ >;
82
+
83
+ export const SyncAuthLeaseScopeSchema = z.object({
84
+ subscriptionId: z.string().min(1),
85
+ table: z.string().min(1),
86
+ values: ScopeValuesSchema,
87
+ operations: z.array(SyncOpSchema).min(1),
88
+ });
89
+
90
+ export type SyncAuthLeaseScope = z.infer<typeof SyncAuthLeaseScopeSchema>;
91
+
92
+ export const SyncAuthLeasePayloadSchema = z.object({
93
+ version: z.literal(SYNC_AUTH_LEASE_VERSION),
94
+ leaseId: z.string().min(1),
95
+ issuer: z.string().min(1),
96
+ audience: z.string().min(1),
97
+ actorId: z.string().min(1),
98
+ subject: z.record(z.string(), z.unknown()).default({}),
99
+ schemaVersion: z.number().int().min(1),
100
+ protocolVersion: z.literal(SYNC_AUTH_LEASE_PROTOCOL_VERSION),
101
+ issuedAtMs: z.number().int(),
102
+ notBeforeMs: z.number().int(),
103
+ expiresAtMs: z.number().int(),
104
+ maxClockSkewMs: z.number().int().min(0),
105
+ scopes: z.array(SyncAuthLeaseScopeSchema).min(1),
106
+ capabilities: SyncAuthLeaseCapabilitiesSchema,
107
+ });
108
+
109
+ export type SyncAuthLeasePayload = z.infer<typeof SyncAuthLeasePayloadSchema>;
110
+
111
+ export const SyncAuthLeaseIssueRequestSchema = z.object({
112
+ schemaVersion: z.number().int().min(1),
113
+ ttlMs: z.number().int().positive().optional(),
114
+ scopes: z.array(SyncAuthLeaseScopeSchema).min(1),
115
+ });
116
+
117
+ export type SyncAuthLeaseIssueRequest = z.infer<
118
+ typeof SyncAuthLeaseIssueRequestSchema
119
+ >;
120
+
121
+ export const SyncAuthLeaseIssueResponseSchema = z.object({
122
+ ok: z.literal(true),
123
+ token: z.string().min(1),
124
+ protectedHeader: SyncAuthLeaseProtectedHeaderSchema,
125
+ payload: SyncAuthLeasePayloadSchema,
126
+ });
127
+
128
+ export type SyncAuthLeaseIssueResponse = z.infer<
129
+ typeof SyncAuthLeaseIssueResponseSchema
130
+ >;
131
+
40
132
  // ============================================================================
41
133
  // Sync Operation Schema
42
134
  // ============================================================================
@@ -55,15 +147,34 @@ export type SyncOperation = z.infer<typeof SyncOperationSchema>;
55
147
  // Push Request/Response Schemas
56
148
  // ============================================================================
57
149
 
150
+ export const SyncAuthLeaseProvenanceSchema = z.object({
151
+ leaseId: z.string().min(1),
152
+ leaseExpiresAtMs: z.number().int(),
153
+ leaseStatusAtEnqueue: z.string().min(1),
154
+ leaseScopeSummaryJson: z.string().optional(),
155
+ leaseToken: z.string().min(1).optional(),
156
+ });
157
+
158
+ export type SyncAuthLeaseProvenance = z.infer<
159
+ typeof SyncAuthLeaseProvenanceSchema
160
+ >;
161
+
58
162
  export const SyncPushRequestSchema = z.object({
59
163
  clientId: z.string().min(1),
60
164
  clientCommitId: z.string().min(1),
61
165
  operations: z.array(SyncOperationSchema).min(1),
62
166
  schemaVersion: z.number().int().min(1),
167
+ authLease: SyncAuthLeaseProvenanceSchema.optional(),
63
168
  });
64
169
 
65
170
  export type SyncPushRequest = z.infer<typeof SyncPushRequestSchema>;
66
171
 
172
+ export const SyncPushCommitRequestSchema = SyncPushRequestSchema.omit({
173
+ clientId: true,
174
+ });
175
+
176
+ export type SyncPushCommitRequest = z.infer<typeof SyncPushCommitRequestSchema>;
177
+
67
178
  const SyncOperationResultAppliedSchema = z.object({
68
179
  opIndex: z.number().int(),
69
180
  status: z.literal('applied'),
@@ -73,6 +184,7 @@ const SyncOperationResultConflictSchema = z.object({
73
184
  opIndex: z.number().int(),
74
185
  status: z.literal('conflict'),
75
186
  message: z.string(),
187
+ code: z.string().optional(),
76
188
  server_version: z.number().int(),
77
189
  server_row: z.unknown(),
78
190
  });
@@ -102,6 +214,28 @@ export const SyncPushResponseSchema = z.object({
102
214
 
103
215
  export type SyncPushResponse = z.infer<typeof SyncPushResponseSchema>;
104
216
 
217
+ export const SyncPushBatchCommitResponseSchema = SyncPushResponseSchema.extend({
218
+ clientCommitId: z.string().min(1),
219
+ });
220
+
221
+ export type SyncPushBatchCommitResponse = z.infer<
222
+ typeof SyncPushBatchCommitResponseSchema
223
+ >;
224
+
225
+ export const SyncPushBatchRequestSchema = z.object({
226
+ clientId: z.string().min(1),
227
+ commits: z.array(SyncPushCommitRequestSchema).min(1),
228
+ });
229
+
230
+ export type SyncPushBatchRequest = z.infer<typeof SyncPushBatchRequestSchema>;
231
+
232
+ export const SyncPushBatchResponseSchema = z.object({
233
+ ok: z.literal(true),
234
+ commits: z.array(SyncPushBatchCommitResponseSchema),
235
+ });
236
+
237
+ export type SyncPushBatchResponse = z.infer<typeof SyncPushBatchResponseSchema>;
238
+
105
239
  // ============================================================================
106
240
  // Bootstrap State Schema
107
241
  // ============================================================================
@@ -119,6 +253,19 @@ export type SyncBootstrapState = z.infer<typeof SyncBootstrapStateSchema>;
119
253
  // Pull Request/Response Schemas
120
254
  // ============================================================================
121
255
 
256
+ export const SyncCrdtStateVectorHintSchema = z.object({
257
+ rowId: z.string().min(1),
258
+ field: z.string().min(1),
259
+ stateColumn: z.string().min(1),
260
+ stateVectorBase64: z.string().min(1),
261
+ syncMode: z.string().min(1),
262
+ updatedAt: z.number().int(),
263
+ });
264
+
265
+ export type SyncCrdtStateVectorHint = z.infer<
266
+ typeof SyncCrdtStateVectorHintSchema
267
+ >;
268
+
122
269
  export const SyncSubscriptionRequestSchema = z.object({
123
270
  id: z.string().min(1),
124
271
  table: z.string().min(1),
@@ -126,18 +273,46 @@ export const SyncSubscriptionRequestSchema = z.object({
126
273
  params: z.record(z.string(), z.unknown()).optional(),
127
274
  cursor: z.number().int(),
128
275
  bootstrapState: SyncBootstrapStateSchema.nullable().optional(),
276
+ verifiedRoot: z.string().optional(),
277
+ crdtStateVectors: z
278
+ .array(SyncCrdtStateVectorHintSchema)
279
+ .optional()
280
+ .default([]),
129
281
  });
130
282
 
131
283
  export type SyncSubscriptionRequest = z.infer<
132
284
  typeof SyncSubscriptionRequestSchema
133
285
  >;
134
286
 
287
+ export const SyncSnapshotChunkEncodingSchema = z.enum(
288
+ SYNC_SNAPSHOT_CHUNK_ENCODINGS
289
+ );
290
+ export const SyncScopedSnapshotArtifactKindSchema = z.literal(
291
+ SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1
292
+ );
293
+ export const SyncSnapshotArtifactCompressionSchema = z.union([
294
+ z.literal(SYNC_SNAPSHOT_ARTIFACT_COMPRESSION_NONE),
295
+ z.literal(SYNC_SNAPSHOT_CHUNK_COMPRESSION),
296
+ ]);
297
+
298
+ export const SyncSnapshotArtifactsRequestSchema = z.object({
299
+ artifactKinds: z.array(SyncScopedSnapshotArtifactKindSchema).min(1),
300
+ compressions: z.array(SyncSnapshotArtifactCompressionSchema).optional(),
301
+ featureSet: z.array(z.string()).optional(),
302
+ });
303
+
304
+ export type SyncSnapshotArtifactsRequest = z.infer<
305
+ typeof SyncSnapshotArtifactsRequestSchema
306
+ >;
307
+
135
308
  export const SyncPullRequestSchema = z.object({
136
309
  clientId: z.string().min(1),
310
+ schemaVersion: z.number().int().min(1),
137
311
  limitCommits: z.number().int().min(1),
138
312
  limitSnapshotRows: z.number().int().min(1).optional(),
139
313
  maxSnapshotPages: z.number().int().min(1).optional(),
140
314
  dedupeRows: z.boolean().optional(),
315
+ snapshotArtifacts: SyncSnapshotArtifactsRequestSchema.optional(),
141
316
  subscriptions: z.array(SyncSubscriptionRequestSchema),
142
317
  });
143
318
 
@@ -163,24 +338,137 @@ export const SyncCommitSchema = z.object({
163
338
 
164
339
  export type SyncCommit = z.infer<typeof SyncCommitSchema>;
165
340
 
341
+ export const SyncPullSubscriptionIntegritySchema = z.object({
342
+ partitionId: z.string(),
343
+ previousChainRoot: z.string(),
344
+ commitChainRoot: z.string(),
345
+ commitSeq: z.number().int(),
346
+ });
347
+
348
+ export type SyncPullSubscriptionIntegrity = z.infer<
349
+ typeof SyncPullSubscriptionIntegritySchema
350
+ >;
351
+
166
352
  export const SyncSnapshotChunkRefSchema = z.object({
167
353
  id: z.string(),
168
354
  byteLength: z.number().int(),
169
355
  sha256: z.string(),
170
- encoding: z.literal(SYNC_SNAPSHOT_CHUNK_ENCODING),
356
+ encoding: SyncSnapshotChunkEncodingSchema,
171
357
  compression: z.literal(SYNC_SNAPSHOT_CHUNK_COMPRESSION),
172
358
  });
173
359
 
174
360
  export type SyncSnapshotChunkRef = z.infer<typeof SyncSnapshotChunkRefSchema>;
175
361
 
176
- export const SyncSnapshotSchema = z.object({
362
+ export const SyncSnapshotManifestChunkSchema = z.object({
363
+ id: z.string(),
364
+ byteLength: z.number().int(),
365
+ sha256: z.string(),
366
+ encoding: SyncSnapshotChunkEncodingSchema,
367
+ compression: z.literal(SYNC_SNAPSHOT_CHUNK_COMPRESSION),
368
+ });
369
+
370
+ export const SyncSnapshotManifestSchema = z.object({
371
+ version: z.literal(1),
372
+ digest: z.string(),
373
+ table: z.string(),
374
+ asOfCommitSeq: z.number().int(),
375
+ scopeDigest: z.string(),
376
+ rowCursor: z.string().nullable(),
377
+ rowLimit: z.number().int().min(1),
378
+ nextRowCursor: z.string().nullable(),
379
+ isFirstPage: z.boolean(),
380
+ isLastPage: z.boolean(),
381
+ chunks: z.array(SyncSnapshotManifestChunkSchema),
382
+ });
383
+
384
+ export type SyncSnapshotManifest = z.infer<typeof SyncSnapshotManifestSchema>;
385
+
386
+ export const SyncScopedSnapshotArtifactManifestSchema = z.object({
387
+ version: z.literal(SYNC_SCOPED_SNAPSHOT_ARTIFACT_MANIFEST_VERSION),
388
+ artifactKind: z.literal(SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1),
389
+ digest: z.string(),
390
+ partitionId: z.string(),
391
+ subscriptionId: z.string(),
177
392
  table: z.string(),
178
- rows: z.array(z.unknown()),
179
- chunks: z.array(SyncSnapshotChunkRefSchema).optional(),
393
+ schemaVersion: z.string(),
394
+ asOfCommitSeq: z.number().int(),
395
+ scopeDigest: z.string(),
396
+ rowCursor: z.string().nullable(),
397
+ rowLimit: z.number().int().min(1),
398
+ rowCount: z.number().int().min(0),
399
+ nextRowCursor: z.string().nullable(),
180
400
  isFirstPage: z.boolean(),
181
401
  isLastPage: z.boolean(),
402
+ compression: z.union([
403
+ z.literal(SYNC_SNAPSHOT_ARTIFACT_COMPRESSION_NONE),
404
+ z.literal(SYNC_SNAPSHOT_CHUNK_COMPRESSION),
405
+ ]),
406
+ byteLength: z.number().int().min(0),
407
+ sha256: z.string(),
408
+ featureSet: z.array(z.string()),
409
+ });
410
+
411
+ export type SyncScopedSnapshotArtifactManifest = z.infer<
412
+ typeof SyncScopedSnapshotArtifactManifestSchema
413
+ >;
414
+
415
+ export const SyncSnapshotArtifactRefSchema = z.object({
416
+ id: z.string(),
417
+ byteLength: z.number().int().min(0),
418
+ sha256: z.string(),
419
+ manifestDigest: z.string(),
420
+ artifactKind: z.literal(SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1),
421
+ compression: z.union([
422
+ z.literal(SYNC_SNAPSHOT_ARTIFACT_COMPRESSION_NONE),
423
+ z.literal(SYNC_SNAPSHOT_CHUNK_COMPRESSION),
424
+ ]),
425
+ rowCount: z.number().int().min(0),
426
+ nextRowCursor: z.string().nullable(),
427
+ isFirstPage: z.boolean(),
428
+ isLastPage: z.boolean(),
429
+ manifest: SyncScopedSnapshotArtifactManifestSchema,
182
430
  });
183
431
 
432
+ export type SyncSnapshotArtifactRef = z.infer<
433
+ typeof SyncSnapshotArtifactRefSchema
434
+ >;
435
+
436
+ export const SyncSnapshotSchema = z
437
+ .object({
438
+ table: z.string(),
439
+ rows: z.array(z.unknown()),
440
+ chunks: z.array(SyncSnapshotChunkRefSchema).optional(),
441
+ manifest: SyncSnapshotManifestSchema.optional(),
442
+ artifacts: z.array(SyncSnapshotArtifactRefSchema).optional(),
443
+ isFirstPage: z.boolean(),
444
+ isLastPage: z.boolean(),
445
+ bootstrapStateAfter: SyncBootstrapStateSchema.nullable().optional(),
446
+ })
447
+ .superRefine((snapshot, ctx) => {
448
+ if (!snapshot.artifacts || snapshot.artifacts.length === 0) return;
449
+ if (snapshot.rows.length > 0) {
450
+ ctx.addIssue({
451
+ code: z.ZodIssueCode.custom,
452
+ path: ['rows'],
453
+ message: 'Snapshot artifacts cannot be mixed with inline rows',
454
+ });
455
+ }
456
+ if (snapshot.chunks && snapshot.chunks.length > 0) {
457
+ ctx.addIssue({
458
+ code: z.ZodIssueCode.custom,
459
+ path: ['chunks'],
460
+ message: 'Snapshot artifacts cannot be mixed with chunk refs',
461
+ });
462
+ }
463
+ if (snapshot.manifest) {
464
+ ctx.addIssue({
465
+ code: z.ZodIssueCode.custom,
466
+ path: ['manifest'],
467
+ message: 'Snapshot artifacts cannot be mixed with chunk manifests',
468
+ });
469
+ }
470
+ });
471
+
184
472
  export type SyncSnapshot = z.infer<typeof SyncSnapshotSchema>;
185
473
 
186
474
  export const SyncPullSubscriptionResponseSchema = z.object({
@@ -190,6 +478,7 @@ export const SyncPullSubscriptionResponseSchema = z.object({
190
478
  bootstrap: z.boolean(),
191
479
  bootstrapState: SyncBootstrapStateSchema.nullable().optional(),
192
480
  nextCursor: z.number().int(),
481
+ integrity: SyncPullSubscriptionIntegritySchema.optional(),
193
482
  commits: z.array(SyncCommitSchema),
194
483
  snapshots: z.array(SyncSnapshotSchema).optional(),
195
484
  });
@@ -211,7 +500,7 @@ export type SyncPullResponse = z.infer<typeof SyncPullResponseSchema>;
211
500
 
212
501
  export const SyncCombinedRequestSchema = z.object({
213
502
  clientId: z.string().min(1),
214
- push: SyncPushRequestSchema.omit({ clientId: true }).optional(),
503
+ push: SyncPushBatchRequestSchema.omit({ clientId: true }).optional(),
215
504
  pull: SyncPullRequestSchema.omit({ clientId: true }).optional(),
216
505
  });
217
506
 
@@ -219,7 +508,9 @@ export type SyncCombinedRequest = z.infer<typeof SyncCombinedRequestSchema>;
219
508
 
220
509
  export const SyncCombinedResponseSchema = z.object({
221
510
  ok: z.literal(true),
222
- push: SyncPushResponseSchema.optional(),
511
+ requiredSchemaVersion: z.number().int().min(1).optional(),
512
+ latestSchemaVersion: z.number().int().min(1).optional(),
513
+ push: SyncPushBatchResponseSchema.optional(),
223
514
  pull: SyncPullResponseSchema.optional(),
224
515
  });
225
516
 
@@ -179,6 +179,36 @@ export function normalizeScopes(
179
179
  return result;
180
180
  }
181
181
 
182
+ export function createSingleVariableScopeMetadata(
183
+ scopes: readonly ScopeDefinition[]
184
+ ): {
185
+ scopePatterns: ScopePattern[];
186
+ scopeColumnsByVariable: Record<string, string>;
187
+ } {
188
+ const scopeColumnMap = normalizeScopes(scopes);
189
+ const scopePatterns = Object.keys(scopeColumnMap) as ScopePattern[];
190
+ const scopeColumnsByVariable: Record<string, string> = {};
191
+
192
+ for (const [pattern, columnName] of Object.entries(scopeColumnMap)) {
193
+ const vars = extractScopeVars(pattern);
194
+ if (vars.length !== 1) {
195
+ throw new Error(
196
+ `Scope pattern "${pattern}" must contain exactly one placeholder (got ${vars.length}).`
197
+ );
198
+ }
199
+ const variableName = vars[0]!;
200
+ const existingColumn = scopeColumnsByVariable[variableName];
201
+ if (existingColumn && existingColumn !== columnName) {
202
+ throw new Error(
203
+ `Scope variable "${variableName}" is mapped to multiple columns: "${existingColumn}" and "${columnName}".`
204
+ );
205
+ }
206
+ scopeColumnsByVariable[variableName] = columnName;
207
+ }
208
+
209
+ return { scopePatterns, scopeColumnsByVariable };
210
+ }
211
+
182
212
  // ── Value operations (public) ────────────────────────────────────────
183
213
 
184
214
  /**
@@ -193,3 +223,15 @@ export function extractScopeVars(pattern: ScopePattern): string[] {
193
223
  if (!matches) return [];
194
224
  return matches.map((m) => m.slice(1, -1));
195
225
  }
226
+
227
+ export function collectScopeVars(
228
+ scopePatterns: readonly ScopePattern[]
229
+ ): Set<string> {
230
+ const keys = new Set<string>();
231
+ for (const pattern of scopePatterns) {
232
+ for (const key of extractScopeVars(pattern)) {
233
+ keys.add(key);
234
+ }
235
+ }
236
+ return keys;
237
+ }
package/src/sentry.ts ADDED
@@ -0,0 +1,200 @@
1
+ import type {
2
+ SyncMetricOptions,
3
+ SyncMetrics,
4
+ SyncSpan,
5
+ SyncSpanOptions,
6
+ SyncTelemetry,
7
+ SyncTelemetryAttributes,
8
+ SyncTelemetryAttributeValue,
9
+ SyncTelemetryEvent,
10
+ SyncTelemetryLevel,
11
+ SyncTracer,
12
+ } from '@syncular/core';
13
+
14
+ interface SentryLoggerAdapter {
15
+ trace?(message: string, attributes?: Record<string, unknown>): void;
16
+ debug?(message: string, attributes?: Record<string, unknown>): void;
17
+ info?(message: string, attributes?: Record<string, unknown>): void;
18
+ warn?(message: string, attributes?: Record<string, unknown>): void;
19
+ error?(message: string, attributes?: Record<string, unknown>): void;
20
+ fatal?(message: string, attributes?: Record<string, unknown>): void;
21
+ }
22
+
23
+ interface SentrySpanAdapter {
24
+ setAttribute?(name: string, value: SyncTelemetryAttributeValue): void;
25
+ setAttributes?(attributes: SyncTelemetryAttributes): void;
26
+ setStatus?(status: 'ok' | 'error' | string): void;
27
+ }
28
+
29
+ interface SentryMetricsAdapter {
30
+ count?(name: string, value: number, options?: SyncMetricOptions): void;
31
+ gauge?(name: string, value: number, options?: SyncMetricOptions): void;
32
+ distribution?(name: string, value: number, options?: SyncMetricOptions): void;
33
+ }
34
+
35
+ export interface SentryTelemetryAdapter {
36
+ logger?: SentryLoggerAdapter;
37
+ startSpan?<T>(
38
+ options: SyncSpanOptions,
39
+ callback: (span: SentrySpanAdapter) => T
40
+ ): T;
41
+ metrics?: SentryMetricsAdapter;
42
+ captureException?(error: unknown): void;
43
+ }
44
+
45
+ const noopSpan: SyncSpan = {
46
+ setAttribute() {},
47
+ setAttributes() {},
48
+ setStatus() {},
49
+ };
50
+
51
+ function resolveLogLevel(event: SyncTelemetryEvent): SyncTelemetryLevel {
52
+ if (event.level) return event.level;
53
+ return event.error ? 'error' : 'info';
54
+ }
55
+
56
+ function resolveLogMethod(
57
+ logger: SentryLoggerAdapter | undefined,
58
+ level: SyncTelemetryLevel
59
+ ): ((message: string, attributes?: Record<string, unknown>) => void) | null {
60
+ if (!logger) return null;
61
+ switch (level) {
62
+ case 'trace':
63
+ return logger.trace ?? logger.debug ?? logger.info ?? null;
64
+ case 'debug':
65
+ return logger.debug ?? logger.info ?? null;
66
+ case 'info':
67
+ return logger.info ?? null;
68
+ case 'warn':
69
+ return logger.warn ?? logger.info ?? null;
70
+ case 'error':
71
+ return logger.error ?? logger.warn ?? logger.info ?? null;
72
+ case 'fatal':
73
+ return logger.fatal ?? logger.error ?? logger.warn ?? logger.info ?? null;
74
+ default:
75
+ return logger.info ?? null;
76
+ }
77
+ }
78
+
79
+ function toSpan(span: SentrySpanAdapter): SyncSpan {
80
+ return {
81
+ setAttribute(name, value) {
82
+ span.setAttribute?.(name, value);
83
+ },
84
+ setAttributes(attributes) {
85
+ if (span.setAttributes) {
86
+ span.setAttributes(attributes);
87
+ return;
88
+ }
89
+
90
+ for (const [key, value] of Object.entries(attributes)) {
91
+ span.setAttribute?.(key, value);
92
+ }
93
+ },
94
+ setStatus(status) {
95
+ span.setStatus?.(status);
96
+ },
97
+ };
98
+ }
99
+
100
+ function toLogAttributeValue(
101
+ value: unknown
102
+ ): SyncTelemetryAttributeValue | undefined {
103
+ if (typeof value === 'string') return value;
104
+ if (typeof value === 'number') {
105
+ return Number.isFinite(value) ? value : undefined;
106
+ }
107
+ if (typeof value === 'boolean') return value;
108
+ if (typeof value === 'bigint') {
109
+ const asNumber = Number(value);
110
+ if (Number.isFinite(asNumber)) return asNumber;
111
+ return value.toString();
112
+ }
113
+ if (value instanceof Date) return value.toISOString();
114
+ if (value === null || value === undefined) return undefined;
115
+
116
+ if (Array.isArray(value) || typeof value === 'object') {
117
+ try {
118
+ return JSON.stringify(value);
119
+ } catch {
120
+ return String(value);
121
+ }
122
+ }
123
+
124
+ return String(value);
125
+ }
126
+
127
+ function sanitizeLogAttributes(
128
+ attributes: Record<string, unknown>
129
+ ): Record<string, SyncTelemetryAttributeValue> | null {
130
+ const sanitized: Record<string, SyncTelemetryAttributeValue> = {};
131
+ for (const [name, value] of Object.entries(attributes)) {
132
+ const normalized = toLogAttributeValue(value);
133
+ if (normalized !== undefined) {
134
+ sanitized[name] = normalized;
135
+ }
136
+ }
137
+
138
+ return Object.keys(sanitized).length > 0 ? sanitized : null;
139
+ }
140
+
141
+ function createTracer(adapter: SentryTelemetryAdapter): SyncTracer {
142
+ return {
143
+ startSpan<T>(options: SyncSpanOptions, callback: (span: SyncSpan) => T): T {
144
+ if (!adapter.startSpan) return callback(noopSpan);
145
+ return adapter.startSpan(options, (span) => callback(toSpan(span)));
146
+ },
147
+ };
148
+ }
149
+
150
+ function createMetrics(adapter: SentryTelemetryAdapter): SyncMetrics {
151
+ return {
152
+ count(name, value, options) {
153
+ adapter.metrics?.count?.(name, value ?? 1, options);
154
+ },
155
+ gauge(name, value, options) {
156
+ adapter.metrics?.gauge?.(name, value, options);
157
+ },
158
+ distribution(name, value, options) {
159
+ adapter.metrics?.distribution?.(name, value, options);
160
+ },
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Create a Syncular telemetry adapter backed by Sentry primitives.
166
+ */
167
+ export function createSentrySyncTelemetry(
168
+ adapter: SentryTelemetryAdapter
169
+ ): SyncTelemetry {
170
+ return {
171
+ log(event) {
172
+ const level = resolveLogLevel(event);
173
+ const { event: message, ...attributes } = event;
174
+ const logMethod = resolveLogMethod(adapter.logger, level);
175
+ if (!logMethod) return;
176
+ const sanitizedAttributes = sanitizeLogAttributes(attributes);
177
+ if (!sanitizedAttributes) {
178
+ logMethod(message);
179
+ return;
180
+ }
181
+ logMethod(message, sanitizedAttributes);
182
+ },
183
+ tracer: createTracer(adapter),
184
+ metrics: createMetrics(adapter),
185
+ captureException(error, context) {
186
+ adapter.captureException?.(error);
187
+ if (!context) return;
188
+ const logMethod =
189
+ resolveLogMethod(adapter.logger, 'error') ??
190
+ resolveLogMethod(adapter.logger, 'info');
191
+ if (!logMethod) return;
192
+ const sanitizedContext = sanitizeLogAttributes(context);
193
+ if (!sanitizedContext) {
194
+ logMethod('sync.exception.context');
195
+ return;
196
+ }
197
+ logMethod('sync.exception.context', sanitizedContext);
198
+ },
199
+ };
200
+ }