@syncular/core 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +5 -19
  2. package/dist/blob-ref.d.ts +21 -0
  3. package/dist/blob-ref.js +97 -0
  4. package/dist/bytes.d.ts +45 -0
  5. package/dist/bytes.js +242 -0
  6. package/dist/canonical-json.d.ts +10 -0
  7. package/dist/canonical-json.js +18 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/crypto.d.ts +60 -0
  11. package/dist/crypto.js +251 -0
  12. package/dist/errors.d.ts +14 -0
  13. package/dist/errors.js +17 -0
  14. package/dist/frames.d.ts +24 -0
  15. package/dist/frames.js +42 -0
  16. package/dist/index.d.ts +18 -26
  17. package/dist/index.js +18 -43
  18. package/dist/message.d.ts +157 -0
  19. package/dist/message.js +786 -0
  20. package/dist/realtime.d.ts +92 -0
  21. package/dist/realtime.js +222 -0
  22. package/dist/render.d.ts +22 -0
  23. package/dist/render.js +273 -0
  24. package/dist/row-codec.d.ts +39 -0
  25. package/dist/row-codec.js +176 -0
  26. package/dist/segment.d.ts +18 -0
  27. package/dist/segment.js +105 -0
  28. package/dist/stream.d.ts +20 -0
  29. package/dist/stream.js +102 -0
  30. package/package.json +7 -59
  31. package/src/blob-ref.ts +135 -0
  32. package/src/bytes.ts +301 -0
  33. package/src/canonical-json.ts +20 -0
  34. package/src/constants.ts +7 -0
  35. package/src/crypto.ts +335 -0
  36. package/src/errors.ts +18 -0
  37. package/src/frames.ts +47 -0
  38. package/src/index.ts +18 -43
  39. package/src/message.ts +1024 -0
  40. package/src/realtime.ts +340 -0
  41. package/src/render.ts +325 -0
  42. package/src/row-codec.ts +265 -0
  43. package/src/segment.ts +150 -0
  44. package/src/stream.ts +128 -0
  45. package/dist/blobs.d.ts +0 -153
  46. package/dist/blobs.d.ts.map +0 -1
  47. package/dist/blobs.js +0 -47
  48. package/dist/blobs.js.map +0 -1
  49. package/dist/column-codecs.d.ts +0 -59
  50. package/dist/column-codecs.d.ts.map +0 -1
  51. package/dist/column-codecs.js +0 -159
  52. package/dist/column-codecs.js.map +0 -1
  53. package/dist/conflict.d.ts +0 -22
  54. package/dist/conflict.d.ts.map +0 -1
  55. package/dist/conflict.js +0 -81
  56. package/dist/conflict.js.map +0 -1
  57. package/dist/database.d.ts +0 -7
  58. package/dist/database.d.ts.map +0 -1
  59. package/dist/database.js +0 -14
  60. package/dist/database.js.map +0 -1
  61. package/dist/dialect.d.ts +0 -7
  62. package/dist/dialect.d.ts.map +0 -1
  63. package/dist/dialect.js +0 -2
  64. package/dist/dialect.js.map +0 -1
  65. package/dist/error-responses.d.ts +0 -393
  66. package/dist/error-responses.d.ts.map +0 -1
  67. package/dist/error-responses.js +0 -393
  68. package/dist/error-responses.js.map +0 -1
  69. package/dist/http/api-client.d.ts +0 -11
  70. package/dist/http/api-client.d.ts.map +0 -1
  71. package/dist/http/api-client.js +0 -30
  72. package/dist/http/api-client.js.map +0 -1
  73. package/dist/http/blob.d.ts +0 -6
  74. package/dist/http/blob.d.ts.map +0 -1
  75. package/dist/http/blob.js +0 -46
  76. package/dist/http/blob.js.map +0 -1
  77. package/dist/http/generated/api.d.ts +0 -5241
  78. package/dist/http/generated/api.d.ts.map +0 -1
  79. package/dist/http/generated/api.js +0 -6
  80. package/dist/http/generated/api.js.map +0 -1
  81. package/dist/http/index.d.ts +0 -18
  82. package/dist/http/index.d.ts.map +0 -1
  83. package/dist/http/index.js +0 -212
  84. package/dist/http/index.js.map +0 -1
  85. package/dist/http/shared.d.ts +0 -45
  86. package/dist/http/shared.d.ts.map +0 -1
  87. package/dist/http/shared.js +0 -122
  88. package/dist/http/shared.js.map +0 -1
  89. package/dist/http/transport-client.d.ts +0 -31
  90. package/dist/http/transport-client.d.ts.map +0 -1
  91. package/dist/http/transport-client.js +0 -193
  92. package/dist/http/transport-client.js.map +0 -1
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js.map +0 -1
  95. package/dist/kysely-column-codecs.d.ts +0 -31
  96. package/dist/kysely-column-codecs.d.ts.map +0 -1
  97. package/dist/kysely-column-codecs.js +0 -452
  98. package/dist/kysely-column-codecs.js.map +0 -1
  99. package/dist/logger.d.ts +0 -29
  100. package/dist/logger.d.ts.map +0 -1
  101. package/dist/logger.js +0 -26
  102. package/dist/logger.js.map +0 -1
  103. package/dist/proxy/index.d.ts +0 -54
  104. package/dist/proxy/index.d.ts.map +0 -1
  105. package/dist/proxy/index.js +0 -7
  106. package/dist/proxy/index.js.map +0 -1
  107. package/dist/schemas/blobs.d.ts +0 -82
  108. package/dist/schemas/blobs.d.ts.map +0 -1
  109. package/dist/schemas/blobs.js +0 -64
  110. package/dist/schemas/blobs.js.map +0 -1
  111. package/dist/schemas/common.d.ts +0 -100
  112. package/dist/schemas/common.d.ts.map +0 -1
  113. package/dist/schemas/common.js +0 -64
  114. package/dist/schemas/common.js.map +0 -1
  115. package/dist/schemas/index.d.ts +0 -7
  116. package/dist/schemas/index.d.ts.map +0 -1
  117. package/dist/schemas/index.js +0 -7
  118. package/dist/schemas/index.js.map +0 -1
  119. package/dist/schemas/sync.d.ts +0 -1065
  120. package/dist/schemas/sync.d.ts.map +0 -1
  121. package/dist/schemas/sync.js +0 -361
  122. package/dist/schemas/sync.js.map +0 -1
  123. package/dist/scopes/index.d.ts +0 -113
  124. package/dist/scopes/index.d.ts.map +0 -1
  125. package/dist/scopes/index.js +0 -103
  126. package/dist/scopes/index.js.map +0 -1
  127. package/dist/sentry.d.ts +0 -31
  128. package/dist/sentry.d.ts.map +0 -1
  129. package/dist/sentry.js +0 -147
  130. package/dist/sentry.js.map +0 -1
  131. package/dist/snapshot-chunks.d.ts +0 -110
  132. package/dist/snapshot-chunks.d.ts.map +0 -1
  133. package/dist/snapshot-chunks.js +0 -682
  134. package/dist/snapshot-chunks.js.map +0 -1
  135. package/dist/sync-packs.d.ts +0 -33
  136. package/dist/sync-packs.d.ts.map +0 -1
  137. package/dist/sync-packs.js +0 -821
  138. package/dist/sync-packs.js.map +0 -1
  139. package/dist/telemetry.d.ts +0 -114
  140. package/dist/telemetry.d.ts.map +0 -1
  141. package/dist/telemetry.js +0 -114
  142. package/dist/telemetry.js.map +0 -1
  143. package/dist/transforms.d.ts +0 -146
  144. package/dist/transforms.d.ts.map +0 -1
  145. package/dist/transforms.js +0 -155
  146. package/dist/transforms.js.map +0 -1
  147. package/dist/types.d.ts +0 -210
  148. package/dist/types.d.ts.map +0 -1
  149. package/dist/types.js +0 -20
  150. package/dist/types.js.map +0 -1
  151. package/dist/utils/bytes.d.ts +0 -4
  152. package/dist/utils/bytes.d.ts.map +0 -1
  153. package/dist/utils/bytes.js +0 -66
  154. package/dist/utils/bytes.js.map +0 -1
  155. package/dist/utils/compression.d.ts +0 -22
  156. package/dist/utils/compression.d.ts.map +0 -1
  157. package/dist/utils/compression.js +0 -129
  158. package/dist/utils/compression.js.map +0 -1
  159. package/dist/utils/crypto.d.ts +0 -13
  160. package/dist/utils/crypto.d.ts.map +0 -1
  161. package/dist/utils/crypto.js +0 -107
  162. package/dist/utils/crypto.js.map +0 -1
  163. package/dist/utils/id.d.ts +0 -3
  164. package/dist/utils/id.d.ts.map +0 -1
  165. package/dist/utils/id.js +0 -25
  166. package/dist/utils/id.js.map +0 -1
  167. package/dist/utils/index.d.ts +0 -10
  168. package/dist/utils/index.d.ts.map +0 -1
  169. package/dist/utils/index.js +0 -10
  170. package/dist/utils/index.js.map +0 -1
  171. package/dist/utils/internal-runtime.d.ts +0 -16
  172. package/dist/utils/internal-runtime.d.ts.map +0 -1
  173. package/dist/utils/internal-runtime.js +0 -14
  174. package/dist/utils/internal-runtime.js.map +0 -1
  175. package/dist/utils/object.d.ts +0 -2
  176. package/dist/utils/object.d.ts.map +0 -1
  177. package/dist/utils/object.js +0 -4
  178. package/dist/utils/object.js.map +0 -1
  179. package/dist/utils/realtime-change-index.d.ts +0 -9
  180. package/dist/utils/realtime-change-index.d.ts.map +0 -1
  181. package/dist/utils/realtime-change-index.js +0 -36
  182. package/dist/utils/realtime-change-index.js.map +0 -1
  183. package/dist/utils/realtime-connection-registry.d.ts +0 -40
  184. package/dist/utils/realtime-connection-registry.d.ts.map +0 -1
  185. package/dist/utils/realtime-connection-registry.js +0 -237
  186. package/dist/utils/realtime-connection-registry.js.map +0 -1
  187. package/dist/utils/table-registry.d.ts +0 -10
  188. package/dist/utils/table-registry.d.ts.map +0 -1
  189. package/dist/utils/table-registry.js +0 -54
  190. package/dist/utils/table-registry.js.map +0 -1
  191. package/dist/utils/url.d.ts +0 -3
  192. package/dist/utils/url.d.ts.map +0 -1
  193. package/dist/utils/url.js +0 -20
  194. package/dist/utils/url.js.map +0 -1
  195. package/src/blobs.ts +0 -219
  196. package/src/column-codecs.ts +0 -262
  197. package/src/conflict.ts +0 -92
  198. package/src/database.ts +0 -21
  199. package/src/dialect.ts +0 -8
  200. package/src/error-responses.ts +0 -423
  201. package/src/http/api-client.ts +0 -39
  202. package/src/http/blob.ts +0 -95
  203. package/src/http/generated/api.ts +0 -5241
  204. package/src/http/index.ts +0 -365
  205. package/src/http/shared.ts +0 -220
  206. package/src/http/transport-client.ts +0 -314
  207. package/src/kysely-column-codecs.ts +0 -585
  208. package/src/logger.ts +0 -38
  209. package/src/proxy/index.ts +0 -57
  210. package/src/schemas/blobs.ts +0 -110
  211. package/src/schemas/common.ts +0 -89
  212. package/src/schemas/index.ts +0 -7
  213. package/src/schemas/sync.ts +0 -517
  214. package/src/scopes/index.ts +0 -237
  215. package/src/sentry.ts +0 -200
  216. package/src/snapshot-chunks.ts +0 -964
  217. package/src/sync-packs.ts +0 -1096
  218. package/src/telemetry.ts +0 -243
  219. package/src/transforms.ts +0 -256
  220. package/src/types.ts +0 -267
  221. package/src/utils/bytes.ts +0 -75
  222. package/src/utils/compression.ts +0 -187
  223. package/src/utils/crypto.ts +0 -146
  224. package/src/utils/id.ts +0 -34
  225. package/src/utils/index.ts +0 -9
  226. package/src/utils/internal-runtime.ts +0 -39
  227. package/src/utils/object.ts +0 -3
  228. package/src/utils/realtime-change-index.ts +0 -44
  229. package/src/utils/realtime-connection-registry.ts +0 -286
  230. package/src/utils/table-registry.ts +0 -67
  231. package/src/utils/url.ts +0 -29
package/src/blobs.ts DELETED
@@ -1,219 +0,0 @@
1
- /**
2
- * @syncular/core - Blob types for media/binary handling
3
- *
4
- * Content-addressable blob storage with presigned URL support.
5
- * Protocol types (BlobRef, BlobMetadata, etc.) live in ./schemas/blobs.ts
6
- */
7
-
8
- import type { BlobRef } from './schemas/blobs';
9
-
10
- // ============================================================================
11
- // Client Transport Types
12
- // ============================================================================
13
-
14
- /**
15
- * Transport interface for client-server blob communication.
16
- * This is used by the client blob manager to communicate with the server.
17
- */
18
- export interface BlobTransport {
19
- /**
20
- * Initiate a blob upload.
21
- * Returns presigned URL info or indicates blob already exists (dedup).
22
- */
23
- initiateUpload(args: {
24
- hash: string;
25
- size: number;
26
- mimeType: string;
27
- }): Promise<{
28
- exists: boolean;
29
- uploadUrl?: string;
30
- uploadMethod?: 'PUT' | 'POST';
31
- uploadHeaders?: Record<string, string>;
32
- }>;
33
-
34
- /**
35
- * Complete a blob upload.
36
- * Call this after uploading to the presigned URL.
37
- */
38
- completeUpload(hash: string): Promise<{ ok: boolean; error?: string }>;
39
-
40
- /**
41
- * Get a presigned download URL.
42
- */
43
- getDownloadUrl(hash: string): Promise<{
44
- url: string;
45
- expiresAt: string;
46
- }>;
47
- }
48
-
49
- // ============================================================================
50
- // Storage Adapter Types (Server-side)
51
- // ============================================================================
52
-
53
- /**
54
- * Options for signing an upload URL.
55
- */
56
- export interface BlobSignUploadOptions {
57
- /** SHA-256 hash (for naming and checksum validation) */
58
- hash: string;
59
- /** Optional tenant/partition namespace for storage lookups */
60
- partitionId?: string;
61
- /** Content size in bytes */
62
- size: number;
63
- /** MIME type */
64
- mimeType: string;
65
- /** URL expiration in seconds */
66
- expiresIn: number;
67
- }
68
-
69
- /**
70
- * Result of signing an upload URL.
71
- */
72
- export interface BlobSignedUpload {
73
- /** The URL to upload to */
74
- url: string;
75
- /** HTTP method */
76
- method: 'PUT' | 'POST';
77
- /** Required headers */
78
- headers?: Record<string, string>;
79
- }
80
-
81
- /**
82
- * Options for signing a download URL.
83
- */
84
- export interface BlobSignDownloadOptions {
85
- /** SHA-256 hash */
86
- hash: string;
87
- /** Optional tenant/partition namespace for storage lookups */
88
- partitionId?: string;
89
- /** URL expiration in seconds */
90
- expiresIn: number;
91
- }
92
-
93
- export interface BlobStoragePartitionOptions {
94
- partitionId?: string;
95
- }
96
-
97
- /**
98
- * Adapter for blob storage backends (S3, R2, custom).
99
- * Implementations handle actual storage; the sync server orchestrates.
100
- */
101
- export interface BlobStorageAdapter {
102
- /** Adapter name for logging/debugging */
103
- readonly name: string;
104
-
105
- /**
106
- * Generate a presigned URL for uploading a blob.
107
- * The URL should enforce checksum validation if the backend supports it.
108
- */
109
- signUpload(options: BlobSignUploadOptions): Promise<BlobSignedUpload>;
110
-
111
- /**
112
- * Generate a presigned URL for downloading a blob.
113
- */
114
- signDownload(options: BlobSignDownloadOptions): Promise<string>;
115
-
116
- /**
117
- * Check if a blob exists in storage.
118
- */
119
- exists(hash: string, options?: BlobStoragePartitionOptions): Promise<boolean>;
120
-
121
- /**
122
- * Delete a blob (for garbage collection).
123
- */
124
- delete(hash: string, options?: BlobStoragePartitionOptions): Promise<void>;
125
-
126
- /**
127
- * Get blob metadata from storage (optional).
128
- * Used to verify uploads completed successfully.
129
- */
130
- getMetadata?(
131
- hash: string,
132
- options?: BlobStoragePartitionOptions
133
- ): Promise<{ size: number; mimeType?: string } | null>;
134
-
135
- /**
136
- * Store blob data directly (for adapters that support direct storage).
137
- * Used for snapshot chunks and other internal data.
138
- */
139
- put?(
140
- hash: string,
141
- data: Uint8Array,
142
- metadata?: Record<string, unknown>,
143
- options?: BlobStoragePartitionOptions
144
- ): Promise<void>;
145
-
146
- /**
147
- * Store blob data directly from a stream.
148
- * Preferred for large payloads to avoid full buffering in memory.
149
- */
150
- putStream?(
151
- hash: string,
152
- stream: ReadableStream<Uint8Array>,
153
- metadata?: Record<string, unknown>,
154
- options?: BlobStoragePartitionOptions
155
- ): Promise<void>;
156
-
157
- /**
158
- * Get blob data directly (for adapters that support direct retrieval).
159
- */
160
- get?(
161
- hash: string,
162
- options?: BlobStoragePartitionOptions
163
- ): Promise<Uint8Array | null>;
164
-
165
- /**
166
- * Get blob data directly as a stream (for adapters that support stream retrieval).
167
- */
168
- getStream?(
169
- hash: string,
170
- options?: BlobStoragePartitionOptions
171
- ): Promise<ReadableStream<Uint8Array> | null>;
172
- }
173
-
174
- // ============================================================================
175
- // Utility Functions
176
- // ============================================================================
177
-
178
- /**
179
- * Create a BlobRef from upload metadata.
180
- */
181
- export function createBlobRef(args: {
182
- hash: string;
183
- size: number;
184
- mimeType: string;
185
- encrypted?: boolean;
186
- keyId?: string;
187
- }): BlobRef {
188
- const ref: BlobRef = {
189
- hash: args.hash,
190
- size: args.size,
191
- mimeType: args.mimeType,
192
- };
193
- if (args.encrypted) {
194
- ref.encrypted = true;
195
- if (args.keyId) {
196
- ref.keyId = args.keyId;
197
- }
198
- }
199
- return ref;
200
- }
201
-
202
- /**
203
- * Parse a blob hash, validating format.
204
- * @returns The hex hash without prefix, or null if invalid.
205
- */
206
- export function parseBlobHash(hash: string): string | null {
207
- if (!hash.startsWith('sha256:')) return null;
208
- const hex = hash.slice(7);
209
- if (hex.length !== 64) return null;
210
- if (!/^[0-9a-f]+$/i.test(hex)) return null;
211
- return hex.toLowerCase();
212
- }
213
-
214
- /**
215
- * Create a blob hash string from hex.
216
- */
217
- export function createBlobHash(hexHash: string): string {
218
- return `sha256:${hexHash.toLowerCase()}`;
219
- }
@@ -1,262 +0,0 @@
1
- import type { SqlFamily } from './dialect';
2
-
3
- export type ColumnCodecDialect = SqlFamily;
4
-
5
- export interface ColumnCodecTypeImport {
6
- name: string;
7
- from: string;
8
- }
9
-
10
- export type ColumnCodecType =
11
- | string
12
- | { type: string; import?: ColumnCodecTypeImport };
13
-
14
- export interface ColumnCodec<App, Db> {
15
- ts: ColumnCodecType;
16
- toDb(value: App): Db;
17
- fromDb(value: Db): App;
18
- dialects?: Partial<
19
- Record<
20
- ColumnCodecDialect,
21
- {
22
- toDb?(value: App): Db;
23
- fromDb?(value: Db): App;
24
- }
25
- >
26
- >;
27
- }
28
-
29
- export type AnyColumnCodec = ColumnCodec<unknown, unknown>;
30
-
31
- export interface ColumnCodecColumn {
32
- table: string;
33
- column: string;
34
- sqlType?: string;
35
- nullable?: boolean;
36
- isPrimaryKey?: boolean;
37
- hasDefault?: boolean;
38
- dialect?: ColumnCodecDialect;
39
- }
40
-
41
- export type TableColumnCodecs = Record<string, AnyColumnCodec>;
42
-
43
- export type ColumnCodecSource = (
44
- column: ColumnCodecColumn
45
- ) => AnyColumnCodec | undefined;
46
-
47
- function hasCodecs(tableCodecs: TableColumnCodecs): boolean {
48
- return Object.keys(tableCodecs).length > 0;
49
- }
50
-
51
- function resolveCodecToDb(
52
- codec: AnyColumnCodec,
53
- dialect: ColumnCodecDialect
54
- ): (value: unknown) => unknown {
55
- return codec.dialects?.[dialect]?.toDb ?? codec.toDb;
56
- }
57
-
58
- function resolveCodecFromDb(
59
- codec: AnyColumnCodec,
60
- dialect: ColumnCodecDialect
61
- ): (value: unknown) => unknown {
62
- return codec.dialects?.[dialect]?.fromDb ?? codec.fromDb;
63
- }
64
-
65
- export function toTableColumnCodecs(
66
- table: string,
67
- codecSource: ColumnCodecSource | undefined,
68
- columns: Iterable<string>,
69
- options: {
70
- dialect?: ColumnCodecDialect;
71
- sqlTypes?: Record<string, string | undefined>;
72
- } = {}
73
- ): TableColumnCodecs {
74
- if (!codecSource) return {};
75
- const out: TableColumnCodecs = {};
76
-
77
- for (const column of columns) {
78
- if (column.length === 0) continue;
79
- const codec = codecSource({
80
- table,
81
- column,
82
- sqlType: options.sqlTypes?.[column],
83
- dialect: options.dialect,
84
- });
85
- if (codec) out[column] = codec;
86
- }
87
-
88
- return out;
89
- }
90
-
91
- export function createTableColumnCodecsResolver(
92
- codecSource: ColumnCodecSource | undefined,
93
- options: { dialect?: ColumnCodecDialect } = {}
94
- ): (table: string, row: Record<string, unknown>) => TableColumnCodecs {
95
- const tableCaches = new Map<string, Map<string, TableColumnCodecs>>();
96
-
97
- return (table, row) => {
98
- if (!codecSource) return {};
99
- const columns = Object.keys(row);
100
- if (columns.length === 0) return {};
101
-
102
- const cacheKey = columns.slice().sort().join('\u0000');
103
- let perTableCache = tableCaches.get(table);
104
- if (!perTableCache) {
105
- perTableCache = new Map<string, TableColumnCodecs>();
106
- tableCaches.set(table, perTableCache);
107
- }
108
-
109
- const cached = perTableCache.get(cacheKey);
110
- if (cached) return cached;
111
-
112
- const resolved = toTableColumnCodecs(table, codecSource, columns, {
113
- dialect: options.dialect,
114
- });
115
- perTableCache.set(cacheKey, resolved);
116
- return resolved;
117
- };
118
- }
119
-
120
- export function applyCodecToDbValue(
121
- codec: AnyColumnCodec,
122
- value: unknown,
123
- dialect: ColumnCodecDialect = 'sqlite'
124
- ): unknown {
125
- if (value === null || value === undefined) return value;
126
- const transform = resolveCodecToDb(codec, dialect);
127
- return transform(value);
128
- }
129
-
130
- export function applyCodecFromDbValue(
131
- codec: AnyColumnCodec,
132
- value: unknown,
133
- dialect: ColumnCodecDialect = 'sqlite'
134
- ): unknown {
135
- if (value === null || value === undefined) return value;
136
- const transform = resolveCodecFromDb(codec, dialect);
137
- return transform(value);
138
- }
139
-
140
- export function applyCodecsToDbRow(
141
- row: Record<string, unknown>,
142
- tableCodecs: TableColumnCodecs,
143
- dialect: ColumnCodecDialect = 'sqlite'
144
- ): Record<string, unknown> {
145
- if (!hasCodecs(tableCodecs)) return { ...row };
146
-
147
- const transformed: Record<string, unknown> = { ...row };
148
- for (const [column, codec] of Object.entries(tableCodecs)) {
149
- if (!(column in transformed)) continue;
150
- transformed[column] = applyCodecToDbValue(
151
- codec,
152
- transformed[column],
153
- dialect
154
- );
155
- }
156
- return transformed;
157
- }
158
-
159
- export function applyCodecsFromDbRow(
160
- row: Record<string, unknown>,
161
- tableCodecs: TableColumnCodecs,
162
- dialect: ColumnCodecDialect = 'sqlite'
163
- ): Record<string, unknown> {
164
- if (!hasCodecs(tableCodecs)) return { ...row };
165
-
166
- let transformed: Record<string, unknown> | null = null;
167
- for (const [column, codec] of Object.entries(tableCodecs)) {
168
- if (!(column in row)) continue;
169
- const value = row[column];
170
- if (value === null || value === undefined) continue;
171
- transformed ??= { ...row };
172
- transformed[column] = applyCodecFromDbValue(codec, value, dialect);
173
- }
174
- return transformed ?? row;
175
- }
176
-
177
- function parseBooleanValue(value: unknown): boolean {
178
- if (typeof value === 'boolean') return value;
179
- if (typeof value === 'number') return value !== 0;
180
- if (typeof value === 'string') {
181
- const normalized = value.trim().toLowerCase();
182
- if (normalized === 'true' || normalized === 't') return true;
183
- if (normalized === 'false' || normalized === 'f') return false;
184
- const asNumber = Number(normalized);
185
- if (Number.isFinite(asNumber)) return asNumber !== 0;
186
- return false;
187
- }
188
- return Boolean(value);
189
- }
190
-
191
- export function numberBoolean(): ColumnCodec<
192
- boolean,
193
- number | boolean | string
194
- > {
195
- return {
196
- ts: 'boolean',
197
- toDb: (value) => (value ? 1 : 0),
198
- fromDb: (value) => parseBooleanValue(value),
199
- dialects: {
200
- postgres: {
201
- toDb: (value) => value,
202
- fromDb: (value) => parseBooleanValue(value),
203
- },
204
- },
205
- };
206
- }
207
-
208
- export interface StringJsonCodecOptions<T> {
209
- ts?: ColumnCodecType;
210
- import?: ColumnCodecTypeImport;
211
- stringify?: (value: T) => string;
212
- parse?: (value: string) => T;
213
- }
214
-
215
- export function stringJson<T = unknown>(
216
- options: StringJsonCodecOptions<T> = {}
217
- ): ColumnCodec<T, string | T> {
218
- const stringify = options.stringify ?? ((value: T) => JSON.stringify(value));
219
- const parse = options.parse ?? ((value: string) => JSON.parse(value) as T);
220
-
221
- const ts: ColumnCodecType =
222
- options.ts ??
223
- (options.import
224
- ? { type: options.import.name, import: options.import }
225
- : 'unknown');
226
-
227
- return {
228
- ts,
229
- toDb: (value) => stringify(value),
230
- fromDb: (value) => {
231
- if (typeof value === 'string') {
232
- return parse(value);
233
- }
234
- return value as T;
235
- },
236
- };
237
- }
238
-
239
- export function timestampDate(): ColumnCodec<Date, string | Date> {
240
- return {
241
- ts: 'Date',
242
- toDb: (value) => value.toISOString(),
243
- fromDb: (value) =>
244
- value instanceof Date ? value : new Date(String(value)),
245
- };
246
- }
247
-
248
- export function dateString(): ColumnCodec<string, string | Date> {
249
- return {
250
- ts: 'string',
251
- toDb: (value) => value,
252
- fromDb: (value) =>
253
- value instanceof Date ? value.toISOString().slice(0, 10) : String(value),
254
- };
255
- }
256
-
257
- export const codecs = {
258
- numberBoolean,
259
- stringJson,
260
- timestampDate,
261
- dateString,
262
- };
package/src/conflict.ts DELETED
@@ -1,92 +0,0 @@
1
- /**
2
- * @syncular/core - Pure conflict detection and field-level merge utilities
3
- *
4
- * These are pure functions with no database dependencies.
5
- * Database-specific conflict detection (triggers, etc.) lives in @syncular/server.
6
- */
7
-
8
- import type { MergeResult } from './types';
9
-
10
- /**
11
- * Performs field-level merge between client changes and server state.
12
- *
13
- * Merge logic:
14
- * - If only client changed a field -> use client's value
15
- * - If only server changed a field -> keep server's value
16
- * - If both changed same field to different values -> true conflict
17
- *
18
- * @param baseRow - The row state when client started editing (from base_version)
19
- * @param serverRow - Current server row state
20
- * @param clientPayload - Client's intended changes
21
- * @returns MergeResult indicating if merge is possible and the result
22
- */
23
- export function performFieldLevelMerge(
24
- baseRow: Record<string, unknown> | null,
25
- serverRow: Record<string, unknown>,
26
- clientPayload: Record<string, unknown>
27
- ): MergeResult {
28
- // If no base row (new insert), client payload wins entirely
29
- if (!baseRow) {
30
- return { canMerge: true, mergedPayload: clientPayload };
31
- }
32
-
33
- const conflictingFields: string[] = [];
34
- const mergedPayload: Record<string, unknown> = { ...serverRow };
35
-
36
- // Check each field in the client payload
37
- for (const [field, clientValue] of Object.entries(clientPayload)) {
38
- const baseValue = baseRow[field];
39
- const serverValue = serverRow[field];
40
-
41
- const clientChanged = !deepEqual(baseValue, clientValue);
42
- const serverChanged = !deepEqual(baseValue, serverValue);
43
-
44
- if (clientChanged && serverChanged) {
45
- // Both changed the same field
46
- if (!deepEqual(clientValue, serverValue)) {
47
- // Changed to different values - true conflict
48
- conflictingFields.push(field);
49
- }
50
- // If they changed to the same value, no conflict - use either
51
- mergedPayload[field] = clientValue;
52
- } else if (clientChanged) {
53
- // Only client changed - use client's value
54
- mergedPayload[field] = clientValue;
55
- }
56
- // If only server changed or neither changed, keep server value (already in mergedPayload)
57
- }
58
-
59
- if (conflictingFields.length > 0) {
60
- return { canMerge: false, conflictingFields };
61
- }
62
-
63
- return { canMerge: true, mergedPayload };
64
- }
65
-
66
- /**
67
- * Deep equality check for values (handles primitives, arrays, objects)
68
- */
69
- function deepEqual(a: unknown, b: unknown): boolean {
70
- if (a === b) return true;
71
- if (a === null || b === null) return a === b;
72
- if (typeof a !== typeof b) return false;
73
-
74
- if (typeof a === 'object') {
75
- if (Array.isArray(a) && Array.isArray(b)) {
76
- if (a.length !== b.length) return false;
77
- return a.every((item, index) => deepEqual(item, b[index]));
78
- }
79
-
80
- if (Array.isArray(a) || Array.isArray(b)) return false;
81
-
82
- const aObj = a as Record<string, unknown>;
83
- const bObj = b as Record<string, unknown>;
84
- const aKeys = Object.keys(aObj);
85
- const bKeys = Object.keys(bObj);
86
-
87
- if (aKeys.length !== bKeys.length) return false;
88
- return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
89
- }
90
-
91
- return false;
92
- }
package/src/database.ts DELETED
@@ -1,21 +0,0 @@
1
- import { Kysely } from 'kysely';
2
- import type { ColumnCodecSource } from './column-codecs';
3
- import type { SqlFamily, SyncularDialectDescriptor } from './dialect';
4
- import { createColumnCodecsPlugin } from './kysely-column-codecs';
5
-
6
- export function createDatabase<T, F extends SqlFamily = SqlFamily>(
7
- options: SyncularDialectDescriptor<F> & {
8
- codecs?: ColumnCodecSource;
9
- }
10
- ) {
11
- const db = new Kysely<T>({
12
- dialect: options.dialect,
13
- });
14
- if (!options.codecs) return db;
15
- return db.withPlugin(
16
- createColumnCodecsPlugin({
17
- codecs: options.codecs,
18
- dialect: options.family,
19
- })
20
- );
21
- }
package/src/dialect.ts DELETED
@@ -1,8 +0,0 @@
1
- import type { Dialect } from 'kysely';
2
-
3
- export type SqlFamily = 'sqlite' | 'postgres';
4
-
5
- export interface SyncularDialectDescriptor<F extends SqlFamily = SqlFamily> {
6
- dialect: Dialect;
7
- family: F;
8
- }