@syncular/server 0.0.1 → 0.0.2-126
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.
- package/README.md +25 -0
- package/dist/blobs/adapters/database.d.ts.map +1 -1
- package/dist/blobs/adapters/database.js +25 -3
- package/dist/blobs/adapters/database.js.map +1 -1
- package/dist/blobs/adapters/filesystem.d.ts +31 -0
- package/dist/blobs/adapters/filesystem.d.ts.map +1 -0
- package/dist/blobs/adapters/filesystem.js +140 -0
- package/dist/blobs/adapters/filesystem.js.map +1 -0
- package/dist/blobs/adapters/s3.d.ts +3 -2
- package/dist/blobs/adapters/s3.d.ts.map +1 -1
- package/dist/blobs/adapters/s3.js +49 -0
- package/dist/blobs/adapters/s3.js.map +1 -1
- package/dist/blobs/index.d.ts +1 -0
- package/dist/blobs/index.d.ts.map +1 -1
- package/dist/blobs/index.js +6 -5
- package/dist/blobs/index.js.map +1 -1
- package/dist/clients.d.ts +1 -0
- package/dist/clients.d.ts.map +1 -1
- package/dist/clients.js.map +1 -1
- package/dist/compaction.d.ts +1 -1
- package/dist/compaction.js +1 -1
- package/dist/dialect/base.d.ts +83 -0
- package/dist/dialect/base.d.ts.map +1 -0
- package/dist/dialect/base.js +144 -0
- package/dist/dialect/base.js.map +1 -0
- package/dist/dialect/helpers.d.ts +10 -0
- package/dist/dialect/helpers.d.ts.map +1 -0
- package/dist/dialect/helpers.js +59 -0
- package/dist/dialect/helpers.js.map +1 -0
- package/dist/dialect/index.d.ts +2 -0
- package/dist/dialect/index.d.ts.map +1 -1
- package/dist/dialect/index.js +3 -1
- package/dist/dialect/index.js.map +1 -1
- package/dist/dialect/types.d.ts +38 -46
- package/dist/dialect/types.d.ts.map +1 -1
- package/dist/{shapes → handlers}/create-handler.d.ts +18 -5
- package/dist/handlers/create-handler.d.ts.map +1 -0
- package/dist/{shapes → handlers}/create-handler.js +140 -43
- package/dist/handlers/create-handler.js.map +1 -0
- package/dist/handlers/index.d.ts.map +1 -0
- package/dist/handlers/index.js +4 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/registry.d.ts.map +1 -0
- package/dist/handlers/registry.js.map +1 -0
- package/dist/{shapes → handlers}/types.d.ts +7 -7
- package/dist/{shapes → handlers}/types.d.ts.map +1 -1
- package/dist/{shapes → handlers}/types.js.map +1 -1
- package/dist/helpers/conflict.d.ts +1 -1
- package/dist/helpers/conflict.d.ts.map +1 -1
- package/dist/helpers/emitted-change.d.ts +1 -1
- package/dist/helpers/emitted-change.d.ts.map +1 -1
- package/dist/helpers/index.js +4 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -16
- package/dist/index.js.map +1 -1
- package/dist/notify.d.ts +47 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +85 -0
- package/dist/notify.js.map +1 -0
- package/dist/proxy/handler.d.ts +1 -1
- package/dist/proxy/handler.d.ts.map +1 -1
- package/dist/proxy/handler.js +15 -11
- package/dist/proxy/handler.js.map +1 -1
- package/dist/proxy/index.d.ts +2 -2
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +3 -3
- package/dist/proxy/index.js.map +1 -1
- package/dist/proxy/mutation-detector.d.ts +4 -0
- package/dist/proxy/mutation-detector.d.ts.map +1 -1
- package/dist/proxy/mutation-detector.js +209 -24
- package/dist/proxy/mutation-detector.js.map +1 -1
- package/dist/proxy/oplog.d.ts +2 -1
- package/dist/proxy/oplog.d.ts.map +1 -1
- package/dist/proxy/oplog.js +15 -9
- package/dist/proxy/oplog.js.map +1 -1
- package/dist/proxy/registry.d.ts +0 -11
- package/dist/proxy/registry.d.ts.map +1 -1
- package/dist/proxy/registry.js +0 -24
- package/dist/proxy/registry.js.map +1 -1
- package/dist/proxy/types.d.ts +2 -0
- package/dist/proxy/types.d.ts.map +1 -1
- package/dist/pull.d.ts +4 -3
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +565 -314
- package/dist/pull.js.map +1 -1
- package/dist/push.d.ts +15 -3
- package/dist/push.d.ts.map +1 -1
- package/dist/push.js +359 -229
- package/dist/push.js.map +1 -1
- package/dist/realtime/index.js +1 -1
- package/dist/realtime/types.d.ts +2 -0
- package/dist/realtime/types.d.ts.map +1 -1
- package/dist/schema.d.ts +11 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +6 -1
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
- package/dist/snapshot-chunks/db-metadata.js +261 -92
- package/dist/snapshot-chunks/db-metadata.js.map +1 -1
- package/dist/snapshot-chunks/index.d.ts +0 -1
- package/dist/snapshot-chunks/index.d.ts.map +1 -1
- package/dist/snapshot-chunks/index.js +2 -3
- package/dist/snapshot-chunks/index.js.map +1 -1
- package/dist/snapshot-chunks/types.d.ts +20 -5
- package/dist/snapshot-chunks/types.d.ts.map +1 -1
- package/dist/snapshot-chunks.d.ts +12 -8
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +40 -12
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/subscriptions/index.js +1 -1
- package/dist/subscriptions/resolve.d.ts +6 -6
- package/dist/subscriptions/resolve.d.ts.map +1 -1
- package/dist/subscriptions/resolve.js +53 -14
- package/dist/subscriptions/resolve.js.map +1 -1
- package/package.json +28 -7
- package/src/blobs/adapters/database.test.ts +67 -0
- package/src/blobs/adapters/database.ts +34 -9
- package/src/blobs/adapters/filesystem.test.ts +132 -0
- package/src/blobs/adapters/filesystem.ts +189 -0
- package/src/blobs/adapters/s3.test.ts +522 -0
- package/src/blobs/adapters/s3.ts +55 -2
- package/src/blobs/index.ts +1 -0
- package/src/clients.ts +1 -0
- package/src/compaction.ts +1 -1
- package/src/dialect/base.ts +292 -0
- package/src/dialect/helpers.ts +61 -0
- package/src/dialect/index.ts +2 -0
- package/src/dialect/types.ts +50 -54
- package/src/{shapes → handlers}/create-handler.ts +219 -64
- package/src/{shapes → handlers}/types.ts +10 -7
- package/src/helpers/conflict.ts +1 -1
- package/src/helpers/emitted-change.ts +1 -1
- package/src/index.ts +2 -1
- package/src/notify.test.ts +516 -0
- package/src/notify.ts +131 -0
- package/src/proxy/handler.test.ts +120 -0
- package/src/proxy/handler.ts +18 -10
- package/src/proxy/index.ts +2 -1
- package/src/proxy/mutation-detector.test.ts +71 -0
- package/src/proxy/mutation-detector.ts +227 -29
- package/src/proxy/oplog.ts +19 -10
- package/src/proxy/registry.ts +0 -33
- package/src/proxy/types.ts +2 -0
- package/src/pull.ts +788 -405
- package/src/push.ts +507 -312
- package/src/realtime/types.ts +2 -0
- package/src/schema.ts +11 -1
- package/src/snapshot-chunks/db-metadata.test.ts +169 -0
- package/src/snapshot-chunks/db-metadata.ts +347 -105
- package/src/snapshot-chunks/index.ts +0 -1
- package/src/snapshot-chunks/types.ts +31 -5
- package/src/snapshot-chunks.ts +60 -21
- package/src/subscriptions/resolve.ts +73 -18
- package/dist/shapes/create-handler.d.ts.map +0 -1
- package/dist/shapes/create-handler.js.map +0 -1
- package/dist/shapes/index.d.ts.map +0 -1
- package/dist/shapes/index.js +0 -4
- package/dist/shapes/index.js.map +0 -1
- package/dist/shapes/registry.d.ts.map +0 -1
- package/dist/shapes/registry.js.map +0 -1
- package/dist/snapshot-chunks/adapters/s3.d.ts +0 -63
- package/dist/snapshot-chunks/adapters/s3.d.ts.map +0 -1
- package/dist/snapshot-chunks/adapters/s3.js +0 -50
- package/dist/snapshot-chunks/adapters/s3.js.map +0 -1
- package/src/snapshot-chunks/adapters/s3.ts +0 -68
- /package/dist/{shapes → handlers}/index.d.ts +0 -0
- /package/dist/{shapes → handlers}/registry.d.ts +0 -0
- /package/dist/{shapes → handlers}/registry.js +0 -0
- /package/dist/{shapes → handlers}/types.js +0 -0
- /package/src/{shapes → handlers}/index.ts +0 -0
- /package/src/{shapes → handlers}/registry.ts +0 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server - Base Server Sync Dialect
|
|
3
|
+
*
|
|
4
|
+
* Abstract base class that implements shared query methods for all
|
|
5
|
+
* database-specific sync dialect implementations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ScopeValues, StoredScopes } from '@syncular/core';
|
|
9
|
+
import type { Kysely, RawBuilder, Transaction } from 'kysely';
|
|
10
|
+
import { sql } from 'kysely';
|
|
11
|
+
import type { SyncChangeRow, SyncCommitRow, SyncCoreDb } from '../schema';
|
|
12
|
+
import { coerceIsoString, coerceNumber, parseScopes } from './helpers';
|
|
13
|
+
import type {
|
|
14
|
+
DbExecutor,
|
|
15
|
+
IncrementalPullRow,
|
|
16
|
+
IncrementalPullRowsArgs,
|
|
17
|
+
ServerSyncDialect,
|
|
18
|
+
} from './types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Abstract base class for server sync dialects.
|
|
22
|
+
*
|
|
23
|
+
* Implements methods that are identical across dialects (pure SQL with no
|
|
24
|
+
* dialect-specific syntax) and methods that differ only in trivial SQL
|
|
25
|
+
* fragments (IN vs ANY, jsonb casts). Dialect-specific fragments are
|
|
26
|
+
* provided via small abstract hook methods.
|
|
27
|
+
*
|
|
28
|
+
* Genuinely different methods (DDL, transaction control, scope filtering,
|
|
29
|
+
* compaction) remain abstract for each dialect to implement.
|
|
30
|
+
*/
|
|
31
|
+
export abstract class BaseServerSyncDialect implements ServerSyncDialect {
|
|
32
|
+
abstract readonly name: string;
|
|
33
|
+
abstract readonly supportsForUpdate: boolean;
|
|
34
|
+
abstract readonly supportsSavepoints: boolean;
|
|
35
|
+
|
|
36
|
+
// ===========================================================================
|
|
37
|
+
// Abstract SQL fragment hooks
|
|
38
|
+
// ===========================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build a SQL fragment for "column IN/= list of numbers".
|
|
42
|
+
* SQLite: `IN (1, 2, 3)` via sql.join
|
|
43
|
+
* Postgres: `= ANY(ARRAY[1,2,3]::bigint[])`
|
|
44
|
+
*/
|
|
45
|
+
protected abstract buildNumberListFilter(
|
|
46
|
+
values: number[]
|
|
47
|
+
): RawBuilder<unknown>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a SQL fragment for "column IN/= list of strings".
|
|
51
|
+
* SQLite: `IN ('a', 'b')` via sql.join
|
|
52
|
+
* Postgres: `= ANY(ARRAY['a','b']::text[])`
|
|
53
|
+
*/
|
|
54
|
+
protected abstract buildStringListFilter(
|
|
55
|
+
values: string[]
|
|
56
|
+
): RawBuilder<unknown>;
|
|
57
|
+
|
|
58
|
+
// ===========================================================================
|
|
59
|
+
// Abstract methods (genuinely different implementations)
|
|
60
|
+
// ===========================================================================
|
|
61
|
+
|
|
62
|
+
abstract ensureSyncSchema<DB extends SyncCoreDb>(
|
|
63
|
+
db: Kysely<DB>
|
|
64
|
+
): Promise<void>;
|
|
65
|
+
|
|
66
|
+
abstract ensureConsoleSchema?<DB extends SyncCoreDb>(
|
|
67
|
+
db: Kysely<DB>
|
|
68
|
+
): Promise<void>;
|
|
69
|
+
|
|
70
|
+
abstract executeInTransaction<DB extends SyncCoreDb, T>(
|
|
71
|
+
db: Kysely<DB>,
|
|
72
|
+
fn: (executor: DbExecutor<DB>) => Promise<T>
|
|
73
|
+
): Promise<T>;
|
|
74
|
+
|
|
75
|
+
abstract setRepeatableRead<DB extends SyncCoreDb>(
|
|
76
|
+
trx: DbExecutor<DB>
|
|
77
|
+
): Promise<void>;
|
|
78
|
+
|
|
79
|
+
abstract readChangesForCommits<DB extends SyncCoreDb>(
|
|
80
|
+
db: DbExecutor<DB>,
|
|
81
|
+
args: {
|
|
82
|
+
commitSeqs: number[];
|
|
83
|
+
table: string;
|
|
84
|
+
scopes: ScopeValues;
|
|
85
|
+
partitionId?: string;
|
|
86
|
+
}
|
|
87
|
+
): Promise<SyncChangeRow[]>;
|
|
88
|
+
|
|
89
|
+
protected abstract readIncrementalPullRowsBatch<DB extends SyncCoreDb>(
|
|
90
|
+
db: DbExecutor<DB>,
|
|
91
|
+
args: Omit<IncrementalPullRowsArgs, 'batchSize'>
|
|
92
|
+
): Promise<IncrementalPullRow[]>;
|
|
93
|
+
|
|
94
|
+
abstract compactChanges<DB extends SyncCoreDb>(
|
|
95
|
+
db: DbExecutor<DB>,
|
|
96
|
+
args: { fullHistoryHours: number }
|
|
97
|
+
): Promise<number>;
|
|
98
|
+
|
|
99
|
+
abstract scopesToDb(scopes: StoredScopes): unknown;
|
|
100
|
+
abstract dbToArray(value: unknown): string[];
|
|
101
|
+
abstract arrayToDb(values: string[]): unknown;
|
|
102
|
+
|
|
103
|
+
// ===========================================================================
|
|
104
|
+
// Concrete methods (identical SQL across dialects)
|
|
105
|
+
// ===========================================================================
|
|
106
|
+
|
|
107
|
+
async readMaxCommitSeq<DB extends SyncCoreDb>(
|
|
108
|
+
db: Kysely<DB> | Transaction<DB>,
|
|
109
|
+
options?: { partitionId?: string }
|
|
110
|
+
): Promise<number> {
|
|
111
|
+
const partitionId = options?.partitionId ?? 'default';
|
|
112
|
+
const res = await sql<{ max_seq: unknown }>`
|
|
113
|
+
SELECT max(commit_seq) as max_seq
|
|
114
|
+
FROM sync_commits
|
|
115
|
+
WHERE partition_id = ${partitionId}
|
|
116
|
+
`.execute(db);
|
|
117
|
+
|
|
118
|
+
return coerceNumber(res.rows[0]?.max_seq) ?? 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async readMinCommitSeq<DB extends SyncCoreDb>(
|
|
122
|
+
db: Kysely<DB> | Transaction<DB>,
|
|
123
|
+
options?: { partitionId?: string }
|
|
124
|
+
): Promise<number> {
|
|
125
|
+
const partitionId = options?.partitionId ?? 'default';
|
|
126
|
+
const res = await sql<{ min_seq: unknown }>`
|
|
127
|
+
SELECT min(commit_seq) as min_seq
|
|
128
|
+
FROM sync_commits
|
|
129
|
+
WHERE partition_id = ${partitionId}
|
|
130
|
+
`.execute(db);
|
|
131
|
+
|
|
132
|
+
return coerceNumber(res.rows[0]?.min_seq) ?? 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async readAffectedTablesFromChanges<DB extends SyncCoreDb>(
|
|
136
|
+
db: Kysely<DB> | Transaction<DB>,
|
|
137
|
+
commitSeq: number,
|
|
138
|
+
options?: { partitionId?: string }
|
|
139
|
+
): Promise<string[]> {
|
|
140
|
+
const partitionId = options?.partitionId ?? 'default';
|
|
141
|
+
const res = await sql<{ table: string }>`
|
|
142
|
+
SELECT DISTINCT "table"
|
|
143
|
+
FROM sync_changes
|
|
144
|
+
WHERE commit_seq = ${commitSeq}
|
|
145
|
+
AND partition_id = ${partitionId}
|
|
146
|
+
`.execute(db);
|
|
147
|
+
|
|
148
|
+
return res.rows
|
|
149
|
+
.map((r) => r.table)
|
|
150
|
+
.filter((t): t is string => typeof t === 'string' && t.length > 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
dbToScopes(value: unknown): StoredScopes {
|
|
154
|
+
return parseScopes(value);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ===========================================================================
|
|
158
|
+
// Concrete methods using hooks (trivial dialect diffs)
|
|
159
|
+
// ===========================================================================
|
|
160
|
+
|
|
161
|
+
async readCommitSeqsForPull<DB extends SyncCoreDb>(
|
|
162
|
+
db: Kysely<DB> | Transaction<DB>,
|
|
163
|
+
args: {
|
|
164
|
+
cursor: number;
|
|
165
|
+
limitCommits: number;
|
|
166
|
+
tables: string[];
|
|
167
|
+
partitionId?: string;
|
|
168
|
+
}
|
|
169
|
+
): Promise<number[]> {
|
|
170
|
+
const partitionId = args.partitionId ?? 'default';
|
|
171
|
+
if (args.tables.length === 0) return [];
|
|
172
|
+
|
|
173
|
+
const tablesFilter = this.buildStringListFilter(args.tables);
|
|
174
|
+
|
|
175
|
+
const res = await sql<{ commit_seq: unknown }>`
|
|
176
|
+
SELECT DISTINCT commit_seq
|
|
177
|
+
FROM sync_table_commits
|
|
178
|
+
WHERE partition_id = ${partitionId}
|
|
179
|
+
AND "table" ${tablesFilter}
|
|
180
|
+
AND commit_seq > ${args.cursor}
|
|
181
|
+
ORDER BY commit_seq ASC
|
|
182
|
+
LIMIT ${args.limitCommits}
|
|
183
|
+
`.execute(db);
|
|
184
|
+
|
|
185
|
+
return res.rows
|
|
186
|
+
.map((r) => coerceNumber(r.commit_seq))
|
|
187
|
+
.filter(
|
|
188
|
+
(n): n is number =>
|
|
189
|
+
typeof n === 'number' && Number.isFinite(n) && n > args.cursor
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async readCommits<DB extends SyncCoreDb>(
|
|
194
|
+
db: Kysely<DB> | Transaction<DB>,
|
|
195
|
+
commitSeqs: number[],
|
|
196
|
+
options?: { partitionId?: string }
|
|
197
|
+
): Promise<SyncCommitRow[]> {
|
|
198
|
+
const partitionId = options?.partitionId ?? 'default';
|
|
199
|
+
if (commitSeqs.length === 0) return [];
|
|
200
|
+
|
|
201
|
+
const seqsFilter = this.buildNumberListFilter(commitSeqs);
|
|
202
|
+
|
|
203
|
+
const res = await sql<{
|
|
204
|
+
commit_seq: unknown;
|
|
205
|
+
actor_id: string;
|
|
206
|
+
created_at: unknown;
|
|
207
|
+
result_json: unknown | null;
|
|
208
|
+
}>`
|
|
209
|
+
SELECT commit_seq, actor_id, created_at, result_json
|
|
210
|
+
FROM sync_commits
|
|
211
|
+
WHERE commit_seq ${seqsFilter}
|
|
212
|
+
AND partition_id = ${partitionId}
|
|
213
|
+
ORDER BY commit_seq ASC
|
|
214
|
+
`.execute(db);
|
|
215
|
+
|
|
216
|
+
return res.rows.map((row) => ({
|
|
217
|
+
commit_seq: coerceNumber(row.commit_seq) ?? 0,
|
|
218
|
+
actor_id: row.actor_id,
|
|
219
|
+
created_at: coerceIsoString(row.created_at),
|
|
220
|
+
result_json: row.result_json ?? null,
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async recordClientCursor<DB extends SyncCoreDb>(
|
|
225
|
+
db: Kysely<DB> | Transaction<DB>,
|
|
226
|
+
args: {
|
|
227
|
+
partitionId?: string;
|
|
228
|
+
clientId: string;
|
|
229
|
+
actorId: string;
|
|
230
|
+
cursor: number;
|
|
231
|
+
effectiveScopes: ScopeValues;
|
|
232
|
+
}
|
|
233
|
+
): Promise<void> {
|
|
234
|
+
const partitionId = args.partitionId ?? 'default';
|
|
235
|
+
const now = new Date().toISOString();
|
|
236
|
+
const scopesJson = JSON.stringify(args.effectiveScopes);
|
|
237
|
+
|
|
238
|
+
await sql`
|
|
239
|
+
INSERT INTO sync_client_cursors (partition_id, client_id, actor_id, cursor, effective_scopes, updated_at)
|
|
240
|
+
VALUES (${partitionId}, ${args.clientId}, ${args.actorId}, ${args.cursor}, ${scopesJson}, ${now})
|
|
241
|
+
ON CONFLICT(partition_id, client_id) DO UPDATE SET
|
|
242
|
+
actor_id = ${args.actorId},
|
|
243
|
+
cursor = ${args.cursor},
|
|
244
|
+
effective_scopes = ${scopesJson},
|
|
245
|
+
updated_at = ${now}
|
|
246
|
+
`.execute(db);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async *iterateIncrementalPullRows<DB extends SyncCoreDb>(
|
|
250
|
+
db: DbExecutor<DB>,
|
|
251
|
+
args: IncrementalPullRowsArgs
|
|
252
|
+
): AsyncGenerator<IncrementalPullRow> {
|
|
253
|
+
const limitCommits = Math.max(1, Math.min(500, args.limitCommits));
|
|
254
|
+
const batchSize = Math.max(
|
|
255
|
+
1,
|
|
256
|
+
Math.min(limitCommits, args.batchSize ?? 100, 500)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
let processedCommits = 0;
|
|
260
|
+
let cursor = args.cursor;
|
|
261
|
+
|
|
262
|
+
while (processedCommits < limitCommits) {
|
|
263
|
+
const remainingCommits = limitCommits - processedCommits;
|
|
264
|
+
const commitLimit = Math.min(batchSize, remainingCommits);
|
|
265
|
+
const rows = await this.readIncrementalPullRowsBatch(db, {
|
|
266
|
+
table: args.table,
|
|
267
|
+
scopes: args.scopes,
|
|
268
|
+
cursor,
|
|
269
|
+
limitCommits: commitLimit,
|
|
270
|
+
partitionId: args.partitionId,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (rows.length === 0) break;
|
|
274
|
+
|
|
275
|
+
let maxCommitSeq = cursor;
|
|
276
|
+
const commitSeqs = new Set<number>();
|
|
277
|
+
|
|
278
|
+
for (const row of rows) {
|
|
279
|
+
maxCommitSeq = Math.max(maxCommitSeq, row.commit_seq);
|
|
280
|
+
commitSeqs.add(row.commit_seq);
|
|
281
|
+
yield row;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (maxCommitSeq <= cursor) break;
|
|
285
|
+
|
|
286
|
+
processedCommits += commitSeqs.size;
|
|
287
|
+
cursor = maxCommitSeq;
|
|
288
|
+
|
|
289
|
+
if (commitSeqs.size < commitLimit) break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server - Shared dialect helpers
|
|
3
|
+
*
|
|
4
|
+
* Pure helper functions used by all server sync dialect implementations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StoredScopes } from '@syncular/core';
|
|
8
|
+
|
|
9
|
+
export function coerceNumber(value: unknown): number | null {
|
|
10
|
+
if (value === null || value === undefined) return null;
|
|
11
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
12
|
+
if (typeof value === 'bigint')
|
|
13
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
14
|
+
if (typeof value === 'string') {
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
return Number.isFinite(n) ? n : null;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function coerceIsoString(value: unknown): string {
|
|
22
|
+
if (typeof value === 'string') return value;
|
|
23
|
+
if (value instanceof Date) return value.toISOString();
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseScopes(value: unknown): StoredScopes {
|
|
28
|
+
if (value === null || value === undefined) return {};
|
|
29
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
30
|
+
const result: StoredScopes = {};
|
|
31
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
32
|
+
if (typeof v === 'string') {
|
|
33
|
+
result[k] = v;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'string') {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(value);
|
|
41
|
+
if (
|
|
42
|
+
typeof parsed === 'object' &&
|
|
43
|
+
parsed !== null &&
|
|
44
|
+
!Array.isArray(parsed)
|
|
45
|
+
) {
|
|
46
|
+
const result: StoredScopes = {};
|
|
47
|
+
for (const [k, v] of Object.entries(
|
|
48
|
+
parsed as Record<string, unknown>
|
|
49
|
+
)) {
|
|
50
|
+
if (typeof v === 'string') {
|
|
51
|
+
result[k] = v;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {};
|
|
61
|
+
}
|
package/src/dialect/index.ts
CHANGED
package/src/dialect/types.ts
CHANGED
|
@@ -22,6 +22,28 @@ export type DbExecutor<DB extends SyncCoreDb = SyncCoreDb> =
|
|
|
22
22
|
*/
|
|
23
23
|
export type ServerSyncDialectName = string;
|
|
24
24
|
|
|
25
|
+
export interface IncrementalPullRowsArgs {
|
|
26
|
+
table: string;
|
|
27
|
+
scopes: ScopeValues;
|
|
28
|
+
cursor: number;
|
|
29
|
+
limitCommits: number;
|
|
30
|
+
partitionId?: string;
|
|
31
|
+
batchSize?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IncrementalPullRow {
|
|
35
|
+
commit_seq: number;
|
|
36
|
+
actor_id: string;
|
|
37
|
+
created_at: string;
|
|
38
|
+
change_id: number;
|
|
39
|
+
table: string;
|
|
40
|
+
row_id: string;
|
|
41
|
+
op: SyncOp;
|
|
42
|
+
row_json: unknown | null;
|
|
43
|
+
row_version: number | null;
|
|
44
|
+
scopes: StoredScopes;
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
export interface ServerSyncDialect {
|
|
26
48
|
readonly name: ServerSyncDialectName;
|
|
27
49
|
|
|
@@ -41,10 +63,16 @@ export interface ServerSyncDialect {
|
|
|
41
63
|
setRepeatableRead<DB extends SyncCoreDb>(trx: DbExecutor<DB>): Promise<void>;
|
|
42
64
|
|
|
43
65
|
/** Read the maximum committed commit_seq (0 if none) */
|
|
44
|
-
readMaxCommitSeq<DB extends SyncCoreDb>(
|
|
66
|
+
readMaxCommitSeq<DB extends SyncCoreDb>(
|
|
67
|
+
db: DbExecutor<DB>,
|
|
68
|
+
options?: { partitionId?: string }
|
|
69
|
+
): Promise<number>;
|
|
45
70
|
|
|
46
71
|
/** Read the minimum committed commit_seq (0 if none) */
|
|
47
|
-
readMinCommitSeq<DB extends SyncCoreDb>(
|
|
72
|
+
readMinCommitSeq<DB extends SyncCoreDb>(
|
|
73
|
+
db: DbExecutor<DB>,
|
|
74
|
+
options?: { partitionId?: string }
|
|
75
|
+
): Promise<number>;
|
|
48
76
|
|
|
49
77
|
/**
|
|
50
78
|
* Read the next commit sequence numbers that have changes for the given tables.
|
|
@@ -52,13 +80,19 @@ export interface ServerSyncDialect {
|
|
|
52
80
|
*/
|
|
53
81
|
readCommitSeqsForPull<DB extends SyncCoreDb>(
|
|
54
82
|
db: DbExecutor<DB>,
|
|
55
|
-
args: {
|
|
83
|
+
args: {
|
|
84
|
+
cursor: number;
|
|
85
|
+
limitCommits: number;
|
|
86
|
+
tables: string[];
|
|
87
|
+
partitionId?: string;
|
|
88
|
+
}
|
|
56
89
|
): Promise<number[]>;
|
|
57
90
|
|
|
58
91
|
/** Read commit metadata for commit_seq values */
|
|
59
92
|
readCommits<DB extends SyncCoreDb>(
|
|
60
93
|
db: DbExecutor<DB>,
|
|
61
|
-
commitSeqs: number[]
|
|
94
|
+
commitSeqs: number[],
|
|
95
|
+
options?: { partitionId?: string }
|
|
62
96
|
): Promise<SyncCommitRow[]>;
|
|
63
97
|
|
|
64
98
|
/**
|
|
@@ -66,65 +100,25 @@ export interface ServerSyncDialect {
|
|
|
66
100
|
* Uses JSONB filtering for scope matching.
|
|
67
101
|
*/
|
|
68
102
|
readChangesForCommits<DB extends SyncCoreDb>(
|
|
69
|
-
db: DbExecutor<DB>,
|
|
70
|
-
args: { commitSeqs: number[]; table: string; scopes: ScopeValues }
|
|
71
|
-
): Promise<SyncChangeRow[]>;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Optimized incremental pull for a subscription.
|
|
75
|
-
*
|
|
76
|
-
* Returns change rows joined with commit metadata and filtered by
|
|
77
|
-
* the subscription's table and scope values.
|
|
78
|
-
*/
|
|
79
|
-
readIncrementalPullRows<DB extends SyncCoreDb>(
|
|
80
103
|
db: DbExecutor<DB>,
|
|
81
104
|
args: {
|
|
105
|
+
commitSeqs: number[];
|
|
82
106
|
table: string;
|
|
83
107
|
scopes: ScopeValues;
|
|
84
|
-
|
|
85
|
-
limitCommits: number;
|
|
108
|
+
partitionId?: string;
|
|
86
109
|
}
|
|
87
|
-
): Promise<
|
|
88
|
-
Array<{
|
|
89
|
-
commit_seq: number;
|
|
90
|
-
actor_id: string;
|
|
91
|
-
created_at: string;
|
|
92
|
-
change_id: number;
|
|
93
|
-
table: string;
|
|
94
|
-
row_id: string;
|
|
95
|
-
op: SyncOp;
|
|
96
|
-
row_json: unknown | null;
|
|
97
|
-
row_version: number | null;
|
|
98
|
-
scopes: StoredScopes;
|
|
99
|
-
}>
|
|
100
|
-
>;
|
|
110
|
+
): Promise<SyncChangeRow[]>;
|
|
101
111
|
|
|
102
112
|
/**
|
|
103
|
-
*
|
|
113
|
+
* Incremental pull iterator for a subscription.
|
|
104
114
|
*
|
|
105
|
-
* Yields
|
|
106
|
-
*
|
|
115
|
+
* Yields change rows joined with commit metadata and filtered by
|
|
116
|
+
* the subscription's table and scope values.
|
|
107
117
|
*/
|
|
108
|
-
|
|
118
|
+
iterateIncrementalPullRows<DB extends SyncCoreDb>(
|
|
109
119
|
db: DbExecutor<DB>,
|
|
110
|
-
args:
|
|
111
|
-
|
|
112
|
-
scopes: ScopeValues;
|
|
113
|
-
cursor: number;
|
|
114
|
-
limitCommits: number;
|
|
115
|
-
}
|
|
116
|
-
): AsyncGenerator<{
|
|
117
|
-
commit_seq: number;
|
|
118
|
-
actor_id: string;
|
|
119
|
-
created_at: string;
|
|
120
|
-
change_id: number;
|
|
121
|
-
table: string;
|
|
122
|
-
row_id: string;
|
|
123
|
-
op: SyncOp;
|
|
124
|
-
row_json: unknown | null;
|
|
125
|
-
row_version: number | null;
|
|
126
|
-
scopes: StoredScopes;
|
|
127
|
-
}>;
|
|
120
|
+
args: IncrementalPullRowsArgs
|
|
121
|
+
): AsyncGenerator<IncrementalPullRow>;
|
|
128
122
|
|
|
129
123
|
/**
|
|
130
124
|
* Optional compaction of the change log to reduce storage.
|
|
@@ -143,6 +137,7 @@ export interface ServerSyncDialect {
|
|
|
143
137
|
recordClientCursor<DB extends SyncCoreDb>(
|
|
144
138
|
db: DbExecutor<DB>,
|
|
145
139
|
args: {
|
|
140
|
+
partitionId?: string;
|
|
146
141
|
clientId: string;
|
|
147
142
|
actorId: string;
|
|
148
143
|
cursor: number;
|
|
@@ -182,7 +177,8 @@ export interface ServerSyncDialect {
|
|
|
182
177
|
*/
|
|
183
178
|
readAffectedTablesFromChanges<DB extends SyncCoreDb>(
|
|
184
179
|
db: DbExecutor<DB>,
|
|
185
|
-
commitSeq: number
|
|
180
|
+
commitSeq: number,
|
|
181
|
+
options?: { partitionId?: string }
|
|
186
182
|
): Promise<string[]>;
|
|
187
183
|
|
|
188
184
|
/**
|