@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,46 @@
1
+ /**
2
+ * @syncular/server - Proxy Types
3
+ *
4
+ * Types for database proxy with automatic oplog generation.
5
+ */
6
+
7
+ import type { StoredScopes } from '@syncular/core';
8
+
9
+ /**
10
+ * Proxy table handler for mutations.
11
+ *
12
+ * Defines how to compute scopes for rows affected by proxy operations.
13
+ */
14
+ export interface ProxyTableHandler {
15
+ /** Database table name */
16
+ table: string;
17
+ /** Primary key column name (default: 'id') */
18
+ primaryKey?: string;
19
+ /** Version column name (default: 'server_version') */
20
+ versionColumn?: string;
21
+ /**
22
+ * Compute scope values for a row.
23
+ *
24
+ * This determines which sync subscriptions will see changes to this row.
25
+ * Returns a JSONB-compatible object of scope key-value pairs.
26
+ *
27
+ * @example
28
+ * computeScopes: (row) => ({
29
+ * user_id: String(row.user_id),
30
+ * project_id: String(row.project_id),
31
+ * })
32
+ */
33
+ computeScopes(row: Record<string, unknown>): StoredScopes;
34
+ }
35
+
36
+ /**
37
+ * Context for executing a proxied query.
38
+ */
39
+ export interface ProxyQueryContext {
40
+ /** Actor ID for oplog tracking */
41
+ actorId: string;
42
+ /** Client ID for oplog tracking */
43
+ clientId: string;
44
+ /** Logical partition key (default: 'default') */
45
+ partitionId?: string;
46
+ }
package/src/prune.ts ADDED
@@ -0,0 +1,200 @@
1
+ /**
2
+ * @syncular/server - Pruning utilities
3
+ *
4
+ * Pruning strategy (initial):
5
+ * - Track per-client cursors in `sync_client_cursors`
6
+ * - Consider a client "active" if it has pulled within `activeWindowMs`
7
+ * - Compute watermark = min(cursor) across active clients (ignoring cursor < 0)
8
+ * - Delete commits with commit_seq <= watermark (cascade deletes changes)
9
+ *
10
+ * Clients behind pruned history will be forced to bootstrap.
11
+ */
12
+
13
+ import type {
14
+ DeleteQueryBuilder,
15
+ DeleteResult,
16
+ Kysely,
17
+ SelectQueryBuilder,
18
+ SqlBool,
19
+ } from 'kysely';
20
+ import { sql } from 'kysely';
21
+ import type { SyncCoreDb } from './schema';
22
+
23
+ type EmptySelection = Record<string, never>;
24
+
25
+ function coerceNumber(value: unknown): number | null {
26
+ if (value === null || value === undefined) return null;
27
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
28
+ if (typeof value === 'bigint')
29
+ return Number.isFinite(Number(value)) ? Number(value) : null;
30
+ if (typeof value === 'string') {
31
+ const n = Number(value);
32
+ return Number.isFinite(n) ? n : null;
33
+ }
34
+ return null;
35
+ }
36
+
37
+ export interface PruneOptions {
38
+ /** Clients with updated_at older than this are ignored for watermark. Default: 14 days. */
39
+ activeWindowMs?: number;
40
+ /**
41
+ * Time-based retention safety cap.
42
+ *
43
+ * The server prunes commits older than this age even if watermark pruning
44
+ * is stuck (e.g. a client never advances).
45
+ * Default: 30 days.
46
+ */
47
+ fallbackMaxAgeMs?: number;
48
+ /** Soft cap: keep at least this many newest commits even if watermark is high. Default: 1000. */
49
+ keepNewestCommits?: number;
50
+ }
51
+
52
+ export async function computePruneWatermarkCommitSeq<DB extends SyncCoreDb>(
53
+ db: Kysely<DB>,
54
+ options: PruneOptions = {}
55
+ ): Promise<number> {
56
+ type SyncDb = Pick<Kysely<SyncCoreDb>, 'selectFrom'>;
57
+ const syncDb = db as SyncDb;
58
+
59
+ const activeWindowMs = options.activeWindowMs ?? 14 * 24 * 60 * 60 * 1000;
60
+ const cutoffIso = new Date(Date.now() - activeWindowMs).toISOString();
61
+
62
+ const cursorsQ = syncDb.selectFrom(
63
+ 'sync_client_cursors'
64
+ ) as SelectQueryBuilder<SyncCoreDb, 'sync_client_cursors', EmptySelection>;
65
+
66
+ const row = await cursorsQ
67
+ .select(({ fn }) => fn.min('cursor').as('minCursor'))
68
+ .where(sql<SqlBool>`updated_at >= ${cutoffIso}`)
69
+ .where(sql<SqlBool>`cursor >= ${0}`)
70
+ .executeTakeFirst();
71
+
72
+ const minCursor = coerceNumber(row?.minCursor) ?? 0;
73
+
74
+ const fallbackMaxAgeMs = options.fallbackMaxAgeMs ?? 30 * 24 * 60 * 60 * 1000;
75
+ if (fallbackMaxAgeMs <= 0) return minCursor;
76
+
77
+ const ageCutoffIso = new Date(Date.now() - fallbackMaxAgeMs).toISOString();
78
+ const commitsQ = syncDb.selectFrom('sync_commits') as SelectQueryBuilder<
79
+ SyncCoreDb,
80
+ 'sync_commits',
81
+ EmptySelection
82
+ >;
83
+
84
+ const ageRow = await commitsQ
85
+ .select(({ fn }) => fn.max('commit_seq').as('maxSeq'))
86
+ .where(sql<SqlBool>`created_at < ${ageCutoffIso}`)
87
+ .executeTakeFirst();
88
+
89
+ const ageSeq = coerceNumber(ageRow?.maxSeq) ?? 0;
90
+ return Math.max(minCursor, ageSeq);
91
+ }
92
+
93
+ export async function pruneSync<DB extends SyncCoreDb>(
94
+ db: Kysely<DB>,
95
+ args: { watermarkCommitSeq: number; keepNewestCommits?: number }
96
+ ): Promise<number> {
97
+ if (args.watermarkCommitSeq <= 0) return 0;
98
+
99
+ type SyncDb = Pick<Kysely<SyncCoreDb>, 'deleteFrom' | 'selectFrom'>;
100
+ const syncDb = db as SyncDb;
101
+
102
+ const keepNewestCommits = args.keepNewestCommits ?? 1000;
103
+
104
+ // Don't delete the newest N commits (even if watermark is higher)
105
+ const commitsQ = syncDb.selectFrom('sync_commits') as SelectQueryBuilder<
106
+ SyncCoreDb,
107
+ 'sync_commits',
108
+ EmptySelection
109
+ >;
110
+
111
+ const maxRow = await commitsQ
112
+ .select(({ fn }) => fn.max('commit_seq').as('maxSeq'))
113
+ .executeTakeFirst();
114
+
115
+ const maxSeq = coerceNumber(maxRow?.maxSeq) ?? 0;
116
+ const minKept = Math.max(0, maxSeq - keepNewestCommits);
117
+ const pruneUpTo = Math.min(args.watermarkCommitSeq, minKept);
118
+
119
+ if (pruneUpTo <= 0) return 0;
120
+
121
+ // Delete dependent rows explicitly to be robust across dialects and older
122
+ // schemas that may not have FK cascade enabled.
123
+ await (
124
+ syncDb.deleteFrom('sync_table_commits') as DeleteQueryBuilder<
125
+ SyncCoreDb,
126
+ 'sync_table_commits',
127
+ DeleteResult
128
+ >
129
+ )
130
+ .where(sql<SqlBool>`commit_seq <= ${pruneUpTo}`)
131
+ .executeTakeFirst();
132
+
133
+ await (
134
+ syncDb.deleteFrom('sync_changes') as DeleteQueryBuilder<
135
+ SyncCoreDb,
136
+ 'sync_changes',
137
+ DeleteResult
138
+ >
139
+ )
140
+ .where(sql<SqlBool>`commit_seq <= ${pruneUpTo}`)
141
+ .executeTakeFirst();
142
+
143
+ const res = await (
144
+ syncDb.deleteFrom('sync_commits') as DeleteQueryBuilder<
145
+ SyncCoreDb,
146
+ 'sync_commits',
147
+ DeleteResult
148
+ >
149
+ )
150
+ .where(sql<SqlBool>`commit_seq <= ${pruneUpTo}`)
151
+ .executeTakeFirst();
152
+
153
+ return Number(res?.numDeletedRows ?? 0);
154
+ }
155
+
156
+ interface PruneState {
157
+ lastPruneAtMs: number;
158
+ pruneInFlight: Promise<number> | null;
159
+ }
160
+
161
+ const pruneStateByDb = new WeakMap<object, PruneState>();
162
+
163
+ function getPruneState(db: object): PruneState {
164
+ const existing = pruneStateByDb.get(db);
165
+ if (existing) return existing;
166
+
167
+ const created: PruneState = {
168
+ lastPruneAtMs: 0,
169
+ pruneInFlight: null,
170
+ };
171
+ pruneStateByDb.set(db, created);
172
+ return created;
173
+ }
174
+
175
+ export async function maybePruneSync<DB extends SyncCoreDb>(
176
+ db: Kysely<DB>,
177
+ args: { minIntervalMs: number; options?: PruneOptions }
178
+ ): Promise<number> {
179
+ const state = getPruneState(db);
180
+ const now = Date.now();
181
+ if (now - state.lastPruneAtMs < args.minIntervalMs) return 0;
182
+
183
+ if (state.pruneInFlight) return state.pruneInFlight;
184
+
185
+ state.pruneInFlight = (async () => {
186
+ try {
187
+ const watermark = await computePruneWatermarkCommitSeq(db, args.options);
188
+ const deleted = await pruneSync(db, {
189
+ watermarkCommitSeq: watermark,
190
+ keepNewestCommits: args.options?.keepNewestCommits,
191
+ });
192
+ state.lastPruneAtMs = Date.now();
193
+ return deleted;
194
+ } finally {
195
+ state.pruneInFlight = null;
196
+ }
197
+ })();
198
+
199
+ return state.pruneInFlight;
200
+ }