@syncular/server 0.0.1-60

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 (211) hide show
  1. package/dist/blobs/adapters/database.d.ts +83 -0
  2. package/dist/blobs/adapters/database.d.ts.map +1 -0
  3. package/dist/blobs/adapters/database.js +180 -0
  4. package/dist/blobs/adapters/database.js.map +1 -0
  5. package/dist/blobs/adapters/s3.d.ts +82 -0
  6. package/dist/blobs/adapters/s3.d.ts.map +1 -0
  7. package/dist/blobs/adapters/s3.js +170 -0
  8. package/dist/blobs/adapters/s3.js.map +1 -0
  9. package/dist/blobs/index.d.ts +9 -0
  10. package/dist/blobs/index.d.ts.map +1 -0
  11. package/dist/blobs/index.js +9 -0
  12. package/dist/blobs/index.js.map +1 -0
  13. package/dist/blobs/manager.d.ts +195 -0
  14. package/dist/blobs/manager.d.ts.map +1 -0
  15. package/dist/blobs/manager.js +440 -0
  16. package/dist/blobs/manager.js.map +1 -0
  17. package/dist/blobs/migrate.d.ts +27 -0
  18. package/dist/blobs/migrate.d.ts.map +1 -0
  19. package/dist/blobs/migrate.js +119 -0
  20. package/dist/blobs/migrate.js.map +1 -0
  21. package/dist/blobs/types.d.ts +54 -0
  22. package/dist/blobs/types.d.ts.map +1 -0
  23. package/dist/blobs/types.js +5 -0
  24. package/dist/blobs/types.js.map +1 -0
  25. package/dist/clients.d.ts +14 -0
  26. package/dist/clients.d.ts.map +1 -0
  27. package/dist/clients.js +7 -0
  28. package/dist/clients.js.map +1 -0
  29. package/dist/compaction.d.ts +27 -0
  30. package/dist/compaction.d.ts.map +1 -0
  31. package/dist/compaction.js +49 -0
  32. package/dist/compaction.js.map +1 -0
  33. package/dist/dialect/index.d.ts +5 -0
  34. package/dist/dialect/index.d.ts.map +1 -0
  35. package/dist/dialect/index.js +5 -0
  36. package/dist/dialect/index.js.map +1 -0
  37. package/dist/dialect/types.d.ts +170 -0
  38. package/dist/dialect/types.d.ts.map +1 -0
  39. package/dist/dialect/types.js +8 -0
  40. package/dist/dialect/types.js.map +1 -0
  41. package/dist/helpers/conflict.d.ts +52 -0
  42. package/dist/helpers/conflict.d.ts.map +1 -0
  43. package/dist/helpers/conflict.js +49 -0
  44. package/dist/helpers/conflict.js.map +1 -0
  45. package/dist/helpers/emitted-change.d.ts +56 -0
  46. package/dist/helpers/emitted-change.d.ts.map +1 -0
  47. package/dist/helpers/emitted-change.js +46 -0
  48. package/dist/helpers/emitted-change.js.map +1 -0
  49. package/dist/helpers/index.d.ts +10 -0
  50. package/dist/helpers/index.d.ts.map +1 -0
  51. package/dist/helpers/index.js +10 -0
  52. package/dist/helpers/index.js.map +1 -0
  53. package/dist/helpers/paginate.d.ts +49 -0
  54. package/dist/helpers/paginate.d.ts.map +1 -0
  55. package/dist/helpers/paginate.js +54 -0
  56. package/dist/helpers/paginate.js.map +1 -0
  57. package/dist/helpers/scope-strings.d.ts +74 -0
  58. package/dist/helpers/scope-strings.d.ts.map +1 -0
  59. package/dist/helpers/scope-strings.js +82 -0
  60. package/dist/helpers/scope-strings.js.map +1 -0
  61. package/dist/index.d.ts +28 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +27 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/migrate.d.ts +14 -0
  66. package/dist/migrate.d.ts.map +1 -0
  67. package/dist/migrate.js +13 -0
  68. package/dist/migrate.js.map +1 -0
  69. package/dist/proxy/handler.d.ts +42 -0
  70. package/dist/proxy/handler.d.ts.map +1 -0
  71. package/dist/proxy/handler.js +99 -0
  72. package/dist/proxy/handler.js.map +1 -0
  73. package/dist/proxy/index.d.ts +9 -0
  74. package/dist/proxy/index.d.ts.map +1 -0
  75. package/dist/proxy/index.js +14 -0
  76. package/dist/proxy/index.js.map +1 -0
  77. package/dist/proxy/mutation-detector.d.ts +31 -0
  78. package/dist/proxy/mutation-detector.d.ts.map +1 -0
  79. package/dist/proxy/mutation-detector.js +61 -0
  80. package/dist/proxy/mutation-detector.js.map +1 -0
  81. package/dist/proxy/oplog.d.ts +30 -0
  82. package/dist/proxy/oplog.d.ts.map +1 -0
  83. package/dist/proxy/oplog.js +110 -0
  84. package/dist/proxy/oplog.js.map +1 -0
  85. package/dist/proxy/registry.d.ts +35 -0
  86. package/dist/proxy/registry.d.ts.map +1 -0
  87. package/dist/proxy/registry.js +49 -0
  88. package/dist/proxy/registry.js.map +1 -0
  89. package/dist/proxy/types.d.ts +44 -0
  90. package/dist/proxy/types.d.ts.map +1 -0
  91. package/dist/proxy/types.js +7 -0
  92. package/dist/proxy/types.js.map +1 -0
  93. package/dist/prune.d.ts +37 -0
  94. package/dist/prune.d.ts.map +1 -0
  95. package/dist/prune.js +112 -0
  96. package/dist/prune.js.map +1 -0
  97. package/dist/pull.d.ts +31 -0
  98. package/dist/pull.d.ts.map +1 -0
  99. package/dist/pull.js +414 -0
  100. package/dist/pull.js.map +1 -0
  101. package/dist/push.d.ts +33 -0
  102. package/dist/push.d.ts.map +1 -0
  103. package/dist/push.js +329 -0
  104. package/dist/push.js.map +1 -0
  105. package/dist/realtime/in-memory.d.ts +13 -0
  106. package/dist/realtime/in-memory.d.ts.map +1 -0
  107. package/dist/realtime/in-memory.js +28 -0
  108. package/dist/realtime/in-memory.js.map +1 -0
  109. package/dist/realtime/index.d.ts +3 -0
  110. package/dist/realtime/index.d.ts.map +1 -0
  111. package/dist/realtime/index.js +2 -0
  112. package/dist/realtime/index.js.map +1 -0
  113. package/dist/realtime/types.d.ts +50 -0
  114. package/dist/realtime/types.d.ts.map +1 -0
  115. package/dist/realtime/types.js +7 -0
  116. package/dist/realtime/types.js.map +1 -0
  117. package/dist/schema.d.ts +164 -0
  118. package/dist/schema.d.ts.map +1 -0
  119. package/dist/schema.js +10 -0
  120. package/dist/schema.js.map +1 -0
  121. package/dist/shapes/create-handler.d.ts +119 -0
  122. package/dist/shapes/create-handler.d.ts.map +1 -0
  123. package/dist/shapes/create-handler.js +327 -0
  124. package/dist/shapes/create-handler.js.map +1 -0
  125. package/dist/shapes/index.d.ts +4 -0
  126. package/dist/shapes/index.d.ts.map +1 -0
  127. package/dist/shapes/index.js +4 -0
  128. package/dist/shapes/index.js.map +1 -0
  129. package/dist/shapes/registry.d.ts +20 -0
  130. package/dist/shapes/registry.d.ts.map +1 -0
  131. package/dist/shapes/registry.js +88 -0
  132. package/dist/shapes/registry.js.map +1 -0
  133. package/dist/shapes/types.d.ts +204 -0
  134. package/dist/shapes/types.d.ts.map +1 -0
  135. package/dist/shapes/types.js +2 -0
  136. package/dist/shapes/types.js.map +1 -0
  137. package/dist/snapshot-chunks/adapters/s3.d.ts +63 -0
  138. package/dist/snapshot-chunks/adapters/s3.d.ts.map +1 -0
  139. package/dist/snapshot-chunks/adapters/s3.js +50 -0
  140. package/dist/snapshot-chunks/adapters/s3.js.map +1 -0
  141. package/dist/snapshot-chunks/db-metadata.d.ts +33 -0
  142. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -0
  143. package/dist/snapshot-chunks/db-metadata.js +169 -0
  144. package/dist/snapshot-chunks/db-metadata.js.map +1 -0
  145. package/dist/snapshot-chunks/index.d.ts +9 -0
  146. package/dist/snapshot-chunks/index.d.ts.map +1 -0
  147. package/dist/snapshot-chunks/index.js +9 -0
  148. package/dist/snapshot-chunks/index.js.map +1 -0
  149. package/dist/snapshot-chunks/types.d.ts +65 -0
  150. package/dist/snapshot-chunks/types.d.ts.map +1 -0
  151. package/dist/snapshot-chunks/types.js +8 -0
  152. package/dist/snapshot-chunks/types.js.map +1 -0
  153. package/dist/snapshot-chunks.d.ts +59 -0
  154. package/dist/snapshot-chunks.d.ts.map +1 -0
  155. package/dist/snapshot-chunks.js +202 -0
  156. package/dist/snapshot-chunks.js.map +1 -0
  157. package/dist/stats.d.ts +19 -0
  158. package/dist/stats.d.ts.map +1 -0
  159. package/dist/stats.js +57 -0
  160. package/dist/stats.js.map +1 -0
  161. package/dist/subscriptions/index.d.ts +2 -0
  162. package/dist/subscriptions/index.d.ts.map +1 -0
  163. package/dist/subscriptions/index.js +2 -0
  164. package/dist/subscriptions/index.js.map +1 -0
  165. package/dist/subscriptions/resolve.d.ts +35 -0
  166. package/dist/subscriptions/resolve.d.ts.map +1 -0
  167. package/dist/subscriptions/resolve.js +134 -0
  168. package/dist/subscriptions/resolve.js.map +1 -0
  169. package/package.json +80 -0
  170. package/src/blobs/adapters/database.ts +290 -0
  171. package/src/blobs/adapters/s3.ts +271 -0
  172. package/src/blobs/index.ts +9 -0
  173. package/src/blobs/manager.ts +600 -0
  174. package/src/blobs/migrate.ts +150 -0
  175. package/src/blobs/types.ts +70 -0
  176. package/src/clients.ts +21 -0
  177. package/src/compaction.ts +77 -0
  178. package/src/dialect/index.ts +5 -0
  179. package/src/dialect/types.ts +222 -0
  180. package/src/helpers/conflict.ts +64 -0
  181. package/src/helpers/emitted-change.ts +69 -0
  182. package/src/helpers/index.ts +10 -0
  183. package/src/helpers/paginate.ts +82 -0
  184. package/src/helpers/scope-strings.ts +101 -0
  185. package/src/index.ts +28 -0
  186. package/src/migrate.ts +20 -0
  187. package/src/proxy/handler.ts +152 -0
  188. package/src/proxy/index.ts +18 -0
  189. package/src/proxy/mutation-detector.ts +83 -0
  190. package/src/proxy/oplog.ts +144 -0
  191. package/src/proxy/registry.ts +56 -0
  192. package/src/proxy/types.ts +46 -0
  193. package/src/prune.ts +200 -0
  194. package/src/pull.ts +551 -0
  195. package/src/push.ts +457 -0
  196. package/src/realtime/in-memory.ts +33 -0
  197. package/src/realtime/index.ts +5 -0
  198. package/src/realtime/types.ts +55 -0
  199. package/src/schema.ts +172 -0
  200. package/src/shapes/create-handler.ts +590 -0
  201. package/src/shapes/index.ts +3 -0
  202. package/src/shapes/registry.ts +109 -0
  203. package/src/shapes/types.ts +267 -0
  204. package/src/snapshot-chunks/adapters/s3.ts +68 -0
  205. package/src/snapshot-chunks/db-metadata.ts +238 -0
  206. package/src/snapshot-chunks/index.ts +9 -0
  207. package/src/snapshot-chunks/types.ts +79 -0
  208. package/src/snapshot-chunks.ts +301 -0
  209. package/src/stats.ts +104 -0
  210. package/src/subscriptions/index.ts +1 -0
  211. package/src/subscriptions/resolve.ts +185 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @syncular/server - Blob storage migrations
3
+ *
4
+ * These migrations are separate from core sync migrations because
5
+ * blob storage is optional and may use external storage (S3/R2).
6
+ */
7
+
8
+ import type { Kysely } from 'kysely';
9
+ import { sql } from 'kysely';
10
+ import type { SyncBlobDb, SyncBlobUploadsDb } from './types';
11
+
12
+ /**
13
+ * Ensures the blob uploads tracking schema exists.
14
+ * This table is required for the blob manager regardless of storage backend.
15
+ *
16
+ * For PostgreSQL.
17
+ */
18
+ async function ensureBlobUploadsSchemaPostgres<DB extends SyncBlobUploadsDb>(
19
+ db: Kysely<DB>
20
+ ): Promise<void> {
21
+ await db.schema
22
+ .createTable('sync_blob_uploads')
23
+ .ifNotExists()
24
+ .addColumn('hash', 'text', (col) => col.primaryKey())
25
+ .addColumn('size', 'bigint', (col) => col.notNull())
26
+ .addColumn('mime_type', 'text', (col) => col.notNull())
27
+ .addColumn('status', 'text', (col) => col.notNull())
28
+ .addColumn('actor_id', 'text', (col) => col.notNull())
29
+ .addColumn('created_at', 'timestamptz', (col) =>
30
+ col.notNull().defaultTo(sql`now()`)
31
+ )
32
+ .addColumn('expires_at', 'timestamptz', (col) => col.notNull())
33
+ .addColumn('completed_at', 'timestamptz')
34
+ .execute();
35
+
36
+ await db.schema
37
+ .createIndex('idx_sync_blob_uploads_status')
38
+ .ifNotExists()
39
+ .on('sync_blob_uploads')
40
+ .columns(['status'])
41
+ .execute();
42
+
43
+ await db.schema
44
+ .createIndex('idx_sync_blob_uploads_expires_at')
45
+ .ifNotExists()
46
+ .on('sync_blob_uploads')
47
+ .columns(['expires_at'])
48
+ .execute();
49
+ }
50
+
51
+ /**
52
+ * Ensures the blob uploads tracking schema exists.
53
+ * This table is required for the blob manager regardless of storage backend.
54
+ *
55
+ * For SQLite.
56
+ */
57
+ async function ensureBlobUploadsSchemasSqlite<DB extends SyncBlobUploadsDb>(
58
+ db: Kysely<DB>
59
+ ): Promise<void> {
60
+ await db.schema
61
+ .createTable('sync_blob_uploads')
62
+ .ifNotExists()
63
+ .addColumn('hash', 'text', (col) => col.primaryKey())
64
+ .addColumn('size', 'integer', (col) => col.notNull())
65
+ .addColumn('mime_type', 'text', (col) => col.notNull())
66
+ .addColumn('status', 'text', (col) => col.notNull())
67
+ .addColumn('actor_id', 'text', (col) => col.notNull())
68
+ .addColumn('created_at', 'text', (col) =>
69
+ col.notNull().defaultTo(sql`(datetime('now'))`)
70
+ )
71
+ .addColumn('expires_at', 'text', (col) => col.notNull())
72
+ .addColumn('completed_at', 'text')
73
+ .execute();
74
+
75
+ await db.schema
76
+ .createIndex('idx_sync_blob_uploads_status')
77
+ .ifNotExists()
78
+ .on('sync_blob_uploads')
79
+ .columns(['status'])
80
+ .execute();
81
+
82
+ await db.schema
83
+ .createIndex('idx_sync_blob_uploads_expires_at')
84
+ .ifNotExists()
85
+ .on('sync_blob_uploads')
86
+ .columns(['expires_at'])
87
+ .execute();
88
+ }
89
+
90
+ /**
91
+ * Ensures the blob storage schema exists (for database adapter).
92
+ * Only needed if using the database blob storage adapter.
93
+ *
94
+ * For PostgreSQL.
95
+ */
96
+ export async function ensureBlobStorageSchemaPostgres<DB extends SyncBlobDb>(
97
+ db: Kysely<DB>
98
+ ): Promise<void> {
99
+ // First ensure uploads table
100
+ await ensureBlobUploadsSchemaPostgres(db);
101
+
102
+ // Then create blobs table
103
+ await db.schema
104
+ .createTable('sync_blobs')
105
+ .ifNotExists()
106
+ .addColumn('hash', 'text', (col) => col.primaryKey())
107
+ .addColumn('size', 'bigint', (col) => col.notNull())
108
+ .addColumn('mime_type', 'text', (col) => col.notNull())
109
+ .addColumn('body', 'bytea', (col) => col.notNull())
110
+ .addColumn('created_at', 'timestamptz', (col) =>
111
+ col.notNull().defaultTo(sql`now()`)
112
+ )
113
+ .execute();
114
+ }
115
+
116
+ /**
117
+ * Ensures the blob storage schema exists (for database adapter).
118
+ * Only needed if using the database blob storage adapter.
119
+ *
120
+ * For SQLite.
121
+ */
122
+ export async function ensureBlobStorageSchemaSqlite<DB extends SyncBlobDb>(
123
+ db: Kysely<DB>
124
+ ): Promise<void> {
125
+ // First ensure uploads table
126
+ await ensureBlobUploadsSchemasSqlite(db);
127
+
128
+ // Then create blobs table
129
+ await db.schema
130
+ .createTable('sync_blobs')
131
+ .ifNotExists()
132
+ .addColumn('hash', 'text', (col) => col.primaryKey())
133
+ .addColumn('size', 'integer', (col) => col.notNull())
134
+ .addColumn('mime_type', 'text', (col) => col.notNull())
135
+ .addColumn('body', 'blob', (col) => col.notNull())
136
+ .addColumn('created_at', 'text', (col) =>
137
+ col.notNull().defaultTo(sql`(datetime('now'))`)
138
+ )
139
+ .execute();
140
+ }
141
+
142
+ /**
143
+ * Drops the blob schema from the database.
144
+ */
145
+ export async function dropBlobSchema<DB extends SyncBlobDb>(
146
+ db: Kysely<DB>
147
+ ): Promise<void> {
148
+ await db.schema.dropTable('sync_blobs').ifExists().execute();
149
+ await db.schema.dropTable('sync_blob_uploads').ifExists().execute();
150
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @syncular/server - Blob storage types
3
+ */
4
+
5
+ import type { Generated } from 'kysely';
6
+
7
+ // ============================================================================
8
+ // Blob Upload Tracking
9
+ // ============================================================================
10
+
11
+ /**
12
+ * Blob uploads tracking table.
13
+ * Tracks initiated uploads and their completion status.
14
+ */
15
+ export interface SyncBlobUploadsTable {
16
+ /** SHA-256 hash with prefix: "sha256:<hex>" */
17
+ hash: string;
18
+ /** Expected size in bytes */
19
+ size: number;
20
+ /** MIME type */
21
+ mime_type: string;
22
+ /** Upload status */
23
+ status: 'pending' | 'complete';
24
+ /** Actor who initiated the upload */
25
+ actor_id: string;
26
+ /** When the upload was initiated */
27
+ created_at: Generated<string>;
28
+ /** When the upload expires (for cleanup of incomplete uploads) */
29
+ expires_at: string;
30
+ /** When the upload was completed */
31
+ completed_at: string | null;
32
+ }
33
+
34
+ export interface SyncBlobUploadsDb {
35
+ sync_blob_uploads: SyncBlobUploadsTable;
36
+ }
37
+
38
+ // ============================================================================
39
+ // Blob Storage (Database Adapter)
40
+ // ============================================================================
41
+
42
+ /**
43
+ * Blob storage table (for database adapter).
44
+ * Stores blob content directly in the database.
45
+ */
46
+ export interface SyncBlobsTable {
47
+ /** SHA-256 hash with prefix: "sha256:<hex>" */
48
+ hash: string;
49
+ /** Size in bytes */
50
+ size: number;
51
+ /** MIME type */
52
+ mime_type: string;
53
+ /** Blob content */
54
+ body: Uint8Array;
55
+ /** When the blob was created */
56
+ created_at: Generated<string>;
57
+ }
58
+
59
+ export interface SyncBlobsDb {
60
+ sync_blobs: SyncBlobsTable;
61
+ }
62
+
63
+ // ============================================================================
64
+ // Combined DB Interface
65
+ // ============================================================================
66
+
67
+ /**
68
+ * Full database interface for blob storage.
69
+ */
70
+ export interface SyncBlobDb extends SyncBlobUploadsDb, SyncBlobsDb {}
package/src/clients.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @syncular/server - Client cursor tracking
3
+ */
4
+
5
+ import type { ScopeValues } from '@syncular/core';
6
+ import type { DbExecutor, ServerSyncDialect } from './dialect/types';
7
+ import type { SyncCoreDb } from './schema';
8
+
9
+ export async function recordClientCursor<DB extends SyncCoreDb>(
10
+ db: DbExecutor<DB>,
11
+ dialect: ServerSyncDialect,
12
+ args: {
13
+ partitionId?: string;
14
+ clientId: string;
15
+ actorId: string;
16
+ cursor: number;
17
+ effectiveScopes: ScopeValues;
18
+ }
19
+ ): Promise<void> {
20
+ await dialect.recordClientCursor(db, args);
21
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @syncular/server - Change-log compaction utilities
3
+ *
4
+ * Compaction reduces storage by deleting intermediate history while preserving
5
+ * the newest change per (shape, table_name, row_id, scope_key) for older data.
6
+ *
7
+ * Dialect-specific implementation lives in `ServerSyncDialect.compactChanges`.
8
+ */
9
+
10
+ import type { DbExecutor, ServerSyncDialect } from './dialect/types';
11
+ import type { SyncCoreDb } from './schema';
12
+
13
+ export interface CompactOptions {
14
+ /**
15
+ * Keep full (non-compacted) history for the most recent N hours.
16
+ * Older history may be compacted depending on dialect strategy.
17
+ */
18
+ fullHistoryHours: number;
19
+ }
20
+
21
+ export async function compactChanges<DB extends SyncCoreDb>(
22
+ db: DbExecutor<DB>,
23
+ args: { dialect: ServerSyncDialect; options: CompactOptions }
24
+ ): Promise<number> {
25
+ const fullHistoryHours = Math.max(0, args.options.fullHistoryHours);
26
+ if (fullHistoryHours <= 0) return 0;
27
+ return args.dialect.compactChanges(db, { fullHistoryHours });
28
+ }
29
+
30
+ interface CompactState {
31
+ lastCompactAtMs: number;
32
+ compactInFlight: Promise<number> | null;
33
+ }
34
+
35
+ const compactStateByDb = new WeakMap<object, CompactState>();
36
+
37
+ function getCompactState(db: object): CompactState {
38
+ const existing = compactStateByDb.get(db);
39
+ if (existing) return existing;
40
+
41
+ const created: CompactState = {
42
+ lastCompactAtMs: 0,
43
+ compactInFlight: null,
44
+ };
45
+ compactStateByDb.set(db, created);
46
+ return created;
47
+ }
48
+
49
+ export async function maybeCompactChanges<DB extends SyncCoreDb>(
50
+ db: DbExecutor<DB>,
51
+ args: {
52
+ dialect: ServerSyncDialect;
53
+ minIntervalMs: number;
54
+ options: CompactOptions;
55
+ }
56
+ ): Promise<number> {
57
+ const state = getCompactState(db);
58
+ const now = Date.now();
59
+ if (now - state.lastCompactAtMs < args.minIntervalMs) return 0;
60
+
61
+ if (state.compactInFlight) return state.compactInFlight;
62
+
63
+ state.compactInFlight = (async () => {
64
+ try {
65
+ const deleted = await compactChanges(db, {
66
+ dialect: args.dialect,
67
+ options: args.options,
68
+ });
69
+ state.lastCompactAtMs = Date.now();
70
+ return deleted;
71
+ } finally {
72
+ state.compactInFlight = null;
73
+ }
74
+ })();
75
+
76
+ return state.compactInFlight;
77
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @syncular/server - Dialect exports
3
+ */
4
+
5
+ export * from './types';
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @syncular/server - Server Sync Dialect Interface
3
+ *
4
+ * Abstracts database-specific operations for commit-log sync.
5
+ * Supports the new JSONB scopes model.
6
+ */
7
+
8
+ import type { ScopeValues, StoredScopes, SyncOp } from '@syncular/core';
9
+ import type { Kysely, Transaction } from 'kysely';
10
+ import type { SyncChangeRow, SyncCommitRow, SyncCoreDb } from '../schema';
11
+
12
+ /**
13
+ * Common database executor type that works with both Kysely and Transaction.
14
+ * Generic version allows for extended database types that include sync tables.
15
+ */
16
+ export type DbExecutor<DB extends SyncCoreDb = SyncCoreDb> =
17
+ | Kysely<DB>
18
+ | Transaction<DB>;
19
+
20
+ /**
21
+ * Supported dialect names.
22
+ */
23
+ export type ServerSyncDialectName = string;
24
+
25
+ export interface ServerSyncDialect {
26
+ readonly name: ServerSyncDialectName;
27
+
28
+ /** Create sync tables + indexes (idempotent) */
29
+ ensureSyncSchema<DB extends SyncCoreDb>(db: Kysely<DB>): Promise<void>;
30
+
31
+ /** Create console-specific tables (e.g., sync_request_events) - optional */
32
+ ensureConsoleSchema?<DB extends SyncCoreDb>(db: Kysely<DB>): Promise<void>;
33
+
34
+ /** Execute callback in a transaction (or directly if transactions not supported). */
35
+ executeInTransaction<DB extends SyncCoreDb, T>(
36
+ db: Kysely<DB>,
37
+ fn: (executor: DbExecutor<DB>) => Promise<T>
38
+ ): Promise<T>;
39
+
40
+ /** Set REPEATABLE READ (or closest equivalent) */
41
+ setRepeatableRead<DB extends SyncCoreDb>(trx: DbExecutor<DB>): Promise<void>;
42
+
43
+ /** Read the maximum committed commit_seq (0 if none) */
44
+ readMaxCommitSeq<DB extends SyncCoreDb>(
45
+ db: DbExecutor<DB>,
46
+ options?: { partitionId?: string }
47
+ ): Promise<number>;
48
+
49
+ /** Read the minimum committed commit_seq (0 if none) */
50
+ readMinCommitSeq<DB extends SyncCoreDb>(
51
+ db: DbExecutor<DB>,
52
+ options?: { partitionId?: string }
53
+ ): Promise<number>;
54
+
55
+ /**
56
+ * Read the next commit sequence numbers that have changes for the given tables.
57
+ * Must return commit_seq values in ascending order.
58
+ */
59
+ readCommitSeqsForPull<DB extends SyncCoreDb>(
60
+ db: DbExecutor<DB>,
61
+ args: {
62
+ cursor: number;
63
+ limitCommits: number;
64
+ tables: string[];
65
+ partitionId?: string;
66
+ }
67
+ ): Promise<number[]>;
68
+
69
+ /** Read commit metadata for commit_seq values */
70
+ readCommits<DB extends SyncCoreDb>(
71
+ db: DbExecutor<DB>,
72
+ commitSeqs: number[],
73
+ options?: { partitionId?: string }
74
+ ): Promise<SyncCommitRow[]>;
75
+
76
+ /**
77
+ * Read changes for commit_seq values, filtered by table and scopes.
78
+ * Uses JSONB filtering for scope matching.
79
+ */
80
+ readChangesForCommits<DB extends SyncCoreDb>(
81
+ db: DbExecutor<DB>,
82
+ args: {
83
+ commitSeqs: number[];
84
+ table: string;
85
+ scopes: ScopeValues;
86
+ partitionId?: string;
87
+ }
88
+ ): Promise<SyncChangeRow[]>;
89
+
90
+ /**
91
+ * Optimized incremental pull for a subscription.
92
+ *
93
+ * Returns change rows joined with commit metadata and filtered by
94
+ * the subscription's table and scope values.
95
+ */
96
+ readIncrementalPullRows<DB extends SyncCoreDb>(
97
+ db: DbExecutor<DB>,
98
+ args: {
99
+ table: string;
100
+ scopes: ScopeValues;
101
+ cursor: number;
102
+ limitCommits: number;
103
+ partitionId?: string;
104
+ }
105
+ ): Promise<
106
+ Array<{
107
+ commit_seq: number;
108
+ actor_id: string;
109
+ created_at: string;
110
+ change_id: number;
111
+ table: string;
112
+ row_id: string;
113
+ op: SyncOp;
114
+ row_json: unknown | null;
115
+ row_version: number | null;
116
+ scopes: StoredScopes;
117
+ }>
118
+ >;
119
+
120
+ /**
121
+ * Streaming incremental pull for large result sets.
122
+ *
123
+ * Yields changes one at a time instead of loading all into memory.
124
+ * Use this when expecting large numbers of changes.
125
+ */
126
+ streamIncrementalPullRows?<DB extends SyncCoreDb>(
127
+ db: DbExecutor<DB>,
128
+ args: {
129
+ table: string;
130
+ scopes: ScopeValues;
131
+ cursor: number;
132
+ limitCommits: number;
133
+ partitionId?: string;
134
+ }
135
+ ): AsyncGenerator<{
136
+ commit_seq: number;
137
+ actor_id: string;
138
+ created_at: string;
139
+ change_id: number;
140
+ table: string;
141
+ row_id: string;
142
+ op: SyncOp;
143
+ row_json: unknown | null;
144
+ row_version: number | null;
145
+ scopes: StoredScopes;
146
+ }>;
147
+
148
+ /**
149
+ * Optional compaction of the change log to reduce storage.
150
+ *
151
+ * Keeps full history for the most recent N hours.
152
+ * For older history, keeps only the newest change per (table, row_id, scopes).
153
+ */
154
+ compactChanges<DB extends SyncCoreDb>(
155
+ db: DbExecutor<DB>,
156
+ args: { fullHistoryHours: number }
157
+ ): Promise<number>;
158
+
159
+ /**
160
+ * Record/update a client cursor for tracking and pruning.
161
+ */
162
+ recordClientCursor<DB extends SyncCoreDb>(
163
+ db: DbExecutor<DB>,
164
+ args: {
165
+ partitionId?: string;
166
+ clientId: string;
167
+ actorId: string;
168
+ cursor: number;
169
+ effectiveScopes: ScopeValues;
170
+ }
171
+ ): Promise<void>;
172
+
173
+ /**
174
+ * Convert a StoredScopes object to database representation.
175
+ * For Postgres: returns as-is (native JSONB)
176
+ * For SQLite: returns JSON.stringify()
177
+ */
178
+ scopesToDb(scopes: StoredScopes): unknown;
179
+
180
+ /**
181
+ * Convert database scopes representation to StoredScopes.
182
+ */
183
+ dbToScopes(value: unknown): StoredScopes;
184
+
185
+ /**
186
+ * Whether the dialect supports SELECT ... FOR UPDATE row locking.
187
+ * Postgres: true
188
+ * SQLite: false (uses database-level locking)
189
+ */
190
+ readonly supportsForUpdate: boolean;
191
+
192
+ /**
193
+ * Whether the dialect supports SAVEPOINT / ROLLBACK TO SAVEPOINT.
194
+ * Postgres/SQLite: true
195
+ * D1 (Durable Object): false (blocks raw SAVEPOINT statements)
196
+ */
197
+ readonly supportsSavepoints: boolean;
198
+
199
+ /**
200
+ * Read distinct tables from sync_changes for a given commit.
201
+ * Used for realtime notifications.
202
+ */
203
+ readAffectedTablesFromChanges<DB extends SyncCoreDb>(
204
+ db: DbExecutor<DB>,
205
+ commitSeq: number,
206
+ options?: { partitionId?: string }
207
+ ): Promise<string[]>;
208
+
209
+ /**
210
+ * Convert database array representation to string[].
211
+ * For Postgres: returns as-is (native array)
212
+ * For SQLite: returns JSON.parse() or empty array if null
213
+ */
214
+ dbToArray(value: unknown): string[];
215
+
216
+ /**
217
+ * Convert string[] to database array representation.
218
+ * For Postgres: returns as-is (native array)
219
+ * For SQLite: returns JSON.stringify()
220
+ */
221
+ arrayToDb(values: string[]): unknown;
222
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @syncular/server - Conflict result builder
3
+ *
4
+ * Helper for building conflict results in server table handlers.
5
+ */
6
+
7
+ import type { ApplyOperationResult } from '../shapes/types';
8
+
9
+ export interface BuildConflictResultArgs {
10
+ /** Index of the operation in the batch */
11
+ opIndex: number;
12
+ /** Current server row data */
13
+ serverRow: unknown;
14
+ /** Current server version */
15
+ serverVersion: number;
16
+ /** Client's base version (what they thought they were updating) */
17
+ baseVersion: number | null;
18
+ }
19
+
20
+ /**
21
+ * Build a conflict result for applyOperation.
22
+ *
23
+ * Use this when the client's base version doesn't match the server's current version,
24
+ * indicating a concurrent modification conflict.
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const handler: ServerTableHandler = {
29
+ * table: 'tasks',
30
+ * async applyOperation(ctx, op, opIndex) {
31
+ * const existing = await ctx.db
32
+ * .selectFrom('tasks')
33
+ * .selectAll()
34
+ * .where('id', '=', op.row_id)
35
+ * .executeTakeFirst();
36
+ *
37
+ * // Check for version conflict
38
+ * if (existing && op.base_version !== null && existing.version !== op.base_version) {
39
+ * return {
40
+ * result: buildConflictResult({
41
+ * opIndex,
42
+ * serverRow: existing,
43
+ * serverVersion: existing.version,
44
+ * baseVersion: op.base_version,
45
+ * }),
46
+ * };
47
+ * }
48
+ *
49
+ * // ... apply the operation
50
+ * },
51
+ * };
52
+ * ```
53
+ */
54
+ export function buildConflictResult(
55
+ args: BuildConflictResultArgs
56
+ ): ApplyOperationResult['result'] {
57
+ return {
58
+ opIndex: args.opIndex,
59
+ status: 'conflict',
60
+ message: `Version conflict: server=${args.serverVersion}, base=${args.baseVersion}`,
61
+ server_version: args.serverVersion,
62
+ server_row: args.serverRow,
63
+ };
64
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @syncular/server - Emitted change builder
3
+ *
4
+ * Helper for creating emitted changes in server table handlers.
5
+ */
6
+
7
+ import type { StoredScopes } from '@syncular/core';
8
+ import type { EmittedChange } from '../shapes/types';
9
+
10
+ export interface CreateEmittedChangeArgs {
11
+ /** Table name */
12
+ table: string;
13
+ /** Row primary key */
14
+ rowId: string;
15
+ /** Operation type */
16
+ op: 'upsert' | 'delete';
17
+ /** Row data (null for delete) */
18
+ row: unknown | null;
19
+ /** Row version (null if not versioned) */
20
+ version: number | null;
21
+ /**
22
+ * Scope values for this change (stored as JSONB).
23
+ * Example: { user_id: 'U1', project_id: 'P1' }
24
+ */
25
+ scopes: StoredScopes;
26
+ }
27
+
28
+ /**
29
+ * Create an emitted change for broadcasting to subscribed clients.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const handler: ServerTableHandler = {
34
+ * table: 'tasks',
35
+ * async applyOperation(ctx, op, opIndex) {
36
+ * // ... apply the operation ...
37
+ *
38
+ * const newVersion = await getTaskVersion(ctx.db, op.row_id);
39
+ * const updatedRow = await getTask(ctx.db, op.row_id);
40
+ *
41
+ * return {
42
+ * result: { opIndex, status: 'applied', newVersion },
43
+ * emittedChanges: [
44
+ * createEmittedChange({
45
+ * table: 'tasks',
46
+ * rowId: op.row_id,
47
+ * op: 'upsert',
48
+ * row: updatedRow,
49
+ * version: newVersion,
50
+ * scopes: { user_id: updatedRow.user_id },
51
+ * }),
52
+ * ],
53
+ * };
54
+ * },
55
+ * };
56
+ * ```
57
+ */
58
+ export function createEmittedChange(
59
+ args: CreateEmittedChangeArgs
60
+ ): EmittedChange {
61
+ return {
62
+ table: args.table,
63
+ row_id: args.rowId,
64
+ op: args.op,
65
+ row_json: args.op === 'delete' ? null : args.row,
66
+ row_version: args.version,
67
+ scopes: args.scopes,
68
+ };
69
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @syncular/server - Helper utilities
3
+ *
4
+ * Convenience helpers for implementing server table handlers.
5
+ */
6
+
7
+ export * from './conflict';
8
+ export * from './emitted-change';
9
+ export * from './paginate';
10
+ export * from './scope-strings';