@syncular/server 0.0.6-159 → 0.0.6-167

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 (93) hide show
  1. package/dist/blobs/adapters/database.d.ts +26 -9
  2. package/dist/blobs/adapters/database.d.ts.map +1 -1
  3. package/dist/blobs/adapters/database.js +65 -21
  4. package/dist/blobs/adapters/database.js.map +1 -1
  5. package/dist/blobs/manager.d.ts +60 -3
  6. package/dist/blobs/manager.d.ts.map +1 -1
  7. package/dist/blobs/manager.js +227 -56
  8. package/dist/blobs/manager.js.map +1 -1
  9. package/dist/blobs/migrate.d.ts.map +1 -1
  10. package/dist/blobs/migrate.js +16 -8
  11. package/dist/blobs/migrate.js.map +1 -1
  12. package/dist/blobs/types.d.ts +4 -0
  13. package/dist/blobs/types.d.ts.map +1 -1
  14. package/dist/dialect/helpers.d.ts +3 -0
  15. package/dist/dialect/helpers.d.ts.map +1 -1
  16. package/dist/dialect/helpers.js +17 -0
  17. package/dist/dialect/helpers.js.map +1 -1
  18. package/dist/handlers/collection.d.ts +0 -2
  19. package/dist/handlers/collection.d.ts.map +1 -1
  20. package/dist/handlers/collection.js +5 -56
  21. package/dist/handlers/collection.js.map +1 -1
  22. package/dist/handlers/create-handler.d.ts +0 -4
  23. package/dist/handlers/create-handler.d.ts.map +1 -1
  24. package/dist/handlers/create-handler.js +6 -34
  25. package/dist/handlers/create-handler.js.map +1 -1
  26. package/dist/notify.d.ts.map +1 -1
  27. package/dist/notify.js +13 -37
  28. package/dist/notify.js.map +1 -1
  29. package/dist/proxy/collection.d.ts +0 -2
  30. package/dist/proxy/collection.d.ts.map +1 -1
  31. package/dist/proxy/collection.js +2 -17
  32. package/dist/proxy/collection.js.map +1 -1
  33. package/dist/proxy/handler.d.ts +1 -1
  34. package/dist/proxy/handler.d.ts.map +1 -1
  35. package/dist/proxy/handler.js +1 -2
  36. package/dist/proxy/handler.js.map +1 -1
  37. package/dist/proxy/index.d.ts +1 -1
  38. package/dist/proxy/index.d.ts.map +1 -1
  39. package/dist/proxy/index.js +1 -1
  40. package/dist/proxy/index.js.map +1 -1
  41. package/dist/proxy/oplog.d.ts.map +1 -1
  42. package/dist/proxy/oplog.js +1 -7
  43. package/dist/proxy/oplog.js.map +1 -1
  44. package/dist/prune.d.ts.map +1 -1
  45. package/dist/prune.js +1 -13
  46. package/dist/prune.js.map +1 -1
  47. package/dist/pull.d.ts.map +1 -1
  48. package/dist/pull.js +186 -54
  49. package/dist/pull.js.map +1 -1
  50. package/dist/push.d.ts +1 -1
  51. package/dist/push.d.ts.map +1 -1
  52. package/dist/push.js +9 -36
  53. package/dist/push.js.map +1 -1
  54. package/dist/snapshot-chunks/db-metadata.d.ts +18 -0
  55. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
  56. package/dist/snapshot-chunks/db-metadata.js +71 -23
  57. package/dist/snapshot-chunks/db-metadata.js.map +1 -1
  58. package/dist/snapshot-chunks.d.ts +5 -1
  59. package/dist/snapshot-chunks.d.ts.map +1 -1
  60. package/dist/snapshot-chunks.js +14 -1
  61. package/dist/snapshot-chunks.js.map +1 -1
  62. package/dist/stats.d.ts.map +1 -1
  63. package/dist/stats.js +1 -13
  64. package/dist/stats.js.map +1 -1
  65. package/dist/subscriptions/resolve.d.ts +1 -1
  66. package/dist/subscriptions/resolve.d.ts.map +1 -1
  67. package/dist/subscriptions/resolve.js +3 -16
  68. package/dist/subscriptions/resolve.js.map +1 -1
  69. package/dist/sync.d.ts.map +1 -1
  70. package/dist/sync.js +2 -4
  71. package/dist/sync.js.map +1 -1
  72. package/package.json +2 -2
  73. package/src/blobs/adapters/database.test.ts +7 -0
  74. package/src/blobs/adapters/database.ts +119 -39
  75. package/src/blobs/manager.ts +339 -53
  76. package/src/blobs/migrate.ts +16 -8
  77. package/src/blobs/types.ts +4 -0
  78. package/src/dialect/helpers.ts +19 -0
  79. package/src/handlers/collection.ts +17 -86
  80. package/src/handlers/create-handler.ts +9 -44
  81. package/src/notify.ts +15 -40
  82. package/src/proxy/collection.ts +5 -27
  83. package/src/proxy/handler.ts +2 -2
  84. package/src/proxy/index.ts +0 -2
  85. package/src/proxy/oplog.ts +1 -9
  86. package/src/prune.ts +1 -12
  87. package/src/pull.ts +280 -105
  88. package/src/push.ts +14 -43
  89. package/src/snapshot-chunks/db-metadata.ts +107 -27
  90. package/src/snapshot-chunks.ts +18 -0
  91. package/src/stats.ts +1 -12
  92. package/src/subscriptions/resolve.ts +4 -20
  93. package/src/sync.ts +6 -6
@@ -21,7 +21,8 @@ async function ensureBlobUploadsSchemaPostgres<DB extends SyncBlobUploadsDb>(
21
21
  await db.schema
22
22
  .createTable('sync_blob_uploads')
23
23
  .ifNotExists()
24
- .addColumn('hash', 'text', (col) => col.primaryKey())
24
+ .addColumn('partition_id', 'text', (col) => col.notNull())
25
+ .addColumn('hash', 'text', (col) => col.notNull())
25
26
  .addColumn('size', 'bigint', (col) => col.notNull())
26
27
  .addColumn('mime_type', 'text', (col) => col.notNull())
27
28
  .addColumn('status', 'text', (col) => col.notNull())
@@ -31,20 +32,21 @@ async function ensureBlobUploadsSchemaPostgres<DB extends SyncBlobUploadsDb>(
31
32
  )
32
33
  .addColumn('expires_at', 'timestamptz', (col) => col.notNull())
33
34
  .addColumn('completed_at', 'timestamptz')
35
+ .addPrimaryKeyConstraint('pk_sync_blob_uploads', ['partition_id', 'hash'])
34
36
  .execute();
35
37
 
36
38
  await db.schema
37
39
  .createIndex('idx_sync_blob_uploads_status')
38
40
  .ifNotExists()
39
41
  .on('sync_blob_uploads')
40
- .columns(['status'])
42
+ .columns(['partition_id', 'status'])
41
43
  .execute();
42
44
 
43
45
  await db.schema
44
46
  .createIndex('idx_sync_blob_uploads_expires_at')
45
47
  .ifNotExists()
46
48
  .on('sync_blob_uploads')
47
- .columns(['expires_at'])
49
+ .columns(['partition_id', 'expires_at'])
48
50
  .execute();
49
51
  }
50
52
 
@@ -60,7 +62,8 @@ async function ensureBlobUploadsSchemasSqlite<DB extends SyncBlobUploadsDb>(
60
62
  await db.schema
61
63
  .createTable('sync_blob_uploads')
62
64
  .ifNotExists()
63
- .addColumn('hash', 'text', (col) => col.primaryKey())
65
+ .addColumn('partition_id', 'text', (col) => col.notNull())
66
+ .addColumn('hash', 'text', (col) => col.notNull())
64
67
  .addColumn('size', 'integer', (col) => col.notNull())
65
68
  .addColumn('mime_type', 'text', (col) => col.notNull())
66
69
  .addColumn('status', 'text', (col) => col.notNull())
@@ -70,20 +73,21 @@ async function ensureBlobUploadsSchemasSqlite<DB extends SyncBlobUploadsDb>(
70
73
  )
71
74
  .addColumn('expires_at', 'text', (col) => col.notNull())
72
75
  .addColumn('completed_at', 'text')
76
+ .addPrimaryKeyConstraint('pk_sync_blob_uploads', ['partition_id', 'hash'])
73
77
  .execute();
74
78
 
75
79
  await db.schema
76
80
  .createIndex('idx_sync_blob_uploads_status')
77
81
  .ifNotExists()
78
82
  .on('sync_blob_uploads')
79
- .columns(['status'])
83
+ .columns(['partition_id', 'status'])
80
84
  .execute();
81
85
 
82
86
  await db.schema
83
87
  .createIndex('idx_sync_blob_uploads_expires_at')
84
88
  .ifNotExists()
85
89
  .on('sync_blob_uploads')
86
- .columns(['expires_at'])
90
+ .columns(['partition_id', 'expires_at'])
87
91
  .execute();
88
92
  }
89
93
 
@@ -103,13 +107,15 @@ export async function ensureBlobStorageSchemaPostgres<DB extends SyncBlobDb>(
103
107
  await db.schema
104
108
  .createTable('sync_blobs')
105
109
  .ifNotExists()
106
- .addColumn('hash', 'text', (col) => col.primaryKey())
110
+ .addColumn('partition_id', 'text', (col) => col.notNull())
111
+ .addColumn('hash', 'text', (col) => col.notNull())
107
112
  .addColumn('size', 'bigint', (col) => col.notNull())
108
113
  .addColumn('mime_type', 'text', (col) => col.notNull())
109
114
  .addColumn('body', 'bytea', (col) => col.notNull())
110
115
  .addColumn('created_at', 'timestamptz', (col) =>
111
116
  col.notNull().defaultTo(sql`now()`)
112
117
  )
118
+ .addPrimaryKeyConstraint('pk_sync_blobs', ['partition_id', 'hash'])
113
119
  .execute();
114
120
  }
115
121
 
@@ -129,13 +135,15 @@ export async function ensureBlobStorageSchemaSqlite<DB extends SyncBlobDb>(
129
135
  await db.schema
130
136
  .createTable('sync_blobs')
131
137
  .ifNotExists()
132
- .addColumn('hash', 'text', (col) => col.primaryKey())
138
+ .addColumn('partition_id', 'text', (col) => col.notNull())
139
+ .addColumn('hash', 'text', (col) => col.notNull())
133
140
  .addColumn('size', 'integer', (col) => col.notNull())
134
141
  .addColumn('mime_type', 'text', (col) => col.notNull())
135
142
  .addColumn('body', 'blob', (col) => col.notNull())
136
143
  .addColumn('created_at', 'text', (col) =>
137
144
  col.notNull().defaultTo(sql`(datetime('now'))`)
138
145
  )
146
+ .addPrimaryKeyConstraint('pk_sync_blobs', ['partition_id', 'hash'])
139
147
  .execute();
140
148
  }
141
149
 
@@ -13,6 +13,8 @@ import type { Generated } from 'kysely';
13
13
  * Tracks initiated uploads and their completion status.
14
14
  */
15
15
  export interface SyncBlobUploadsTable {
16
+ /** Partition/tenant namespace */
17
+ partition_id: string;
16
18
  /** SHA-256 hash with prefix: "sha256:<hex>" */
17
19
  hash: string;
18
20
  /** Expected size in bytes */
@@ -44,6 +46,8 @@ export interface SyncBlobUploadsDb {
44
46
  * Stores blob content directly in the database.
45
47
  */
46
48
  export interface SyncBlobsTable {
49
+ /** Partition/tenant namespace */
50
+ partition_id: string;
47
51
  /** SHA-256 hash with prefix: "sha256:<hex>" */
48
52
  hash: string;
49
53
  /** Size in bytes */
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { StoredScopes } from '@syncular/core';
8
+ import type { ServerSyncDialect } from './types';
8
9
 
9
10
  export function coerceNumber(value: unknown): number | null {
10
11
  if (value === null || value === undefined) return null;
@@ -24,6 +25,24 @@ export function coerceIsoString(value: unknown): string {
24
25
  return String(value);
25
26
  }
26
27
 
28
+ export function parseJsonValue(value: unknown): unknown {
29
+ if (typeof value !== 'string') return value;
30
+ try {
31
+ return JSON.parse(value);
32
+ } catch {
33
+ return value;
34
+ }
35
+ }
36
+
37
+ export function toDialectJsonValue(
38
+ dialect: Pick<ServerSyncDialect, 'family'>,
39
+ value: unknown
40
+ ): unknown {
41
+ if (value === null || value === undefined) return null;
42
+ if (dialect.family === 'sqlite') return JSON.stringify(value);
43
+ return value;
44
+ }
45
+
27
46
  export function parseScopes(value: unknown): StoredScopes {
28
47
  if (value === null || value === undefined) return {};
29
48
  if (typeof value === 'object' && !Array.isArray(value)) {
@@ -1,3 +1,8 @@
1
+ import {
2
+ assertKnownTableDependencies,
3
+ createTableLookup,
4
+ topologicallySortTablesByDependencies,
5
+ } from '@syncular/core';
1
6
  import type { SyncCoreDb } from '../schema';
2
7
  import type { ServerTableHandler, SyncServerAuth } from './types';
3
8
 
@@ -13,101 +18,27 @@ export function createServerHandlerCollection<
13
18
  DB extends SyncCoreDb = SyncCoreDb,
14
19
  Auth extends SyncServerAuth = SyncServerAuth,
15
20
  >(handlers: ServerTableHandler<DB, Auth>[]): ServerHandlerCollection<DB, Auth> {
16
- const byTable = new Map<string, ServerTableHandler<DB, Auth>>();
17
-
18
- for (const handler of handlers) {
19
- if (byTable.has(handler.table)) {
20
- throw new Error(`Table "${handler.table}" is already registered`);
21
- }
22
- byTable.set(handler.table, handler);
23
- }
24
-
25
- for (const handler of handlers) {
26
- for (const dep of handler.dependsOn ?? []) {
27
- if (!byTable.has(dep)) {
28
- throw new Error(
29
- `Table "${handler.table}" depends on unknown table "${dep}"`
30
- );
31
- }
32
- }
33
- }
21
+ const byTable = createTableLookup(
22
+ handlers,
23
+ (table) => `Table "${table}" is already registered`
24
+ );
25
+ assertKnownTableDependencies(
26
+ handlers,
27
+ byTable,
28
+ (table, dependency) =>
29
+ `Table "${table}" depends on unknown table "${dependency}"`
30
+ );
34
31
 
35
32
  return { handlers, byTable };
36
33
  }
37
34
 
38
- export function getServerHandler<
39
- DB extends SyncCoreDb = SyncCoreDb,
40
- Auth extends SyncServerAuth = SyncServerAuth,
41
- >(
42
- collection: ServerHandlerCollection<DB, Auth>,
43
- table: string
44
- ): ServerTableHandler<DB, Auth> | undefined {
45
- return collection.byTable.get(table);
46
- }
47
-
48
- export function getServerHandlerOrThrow<
49
- DB extends SyncCoreDb = SyncCoreDb,
50
- Auth extends SyncServerAuth = SyncServerAuth,
51
- >(
52
- collection: ServerHandlerCollection<DB, Auth>,
53
- table: string
54
- ): ServerTableHandler<DB, Auth> {
55
- const handler = collection.byTable.get(table);
56
- if (!handler) throw new Error(`Unknown table: ${table}`);
57
- return handler;
58
- }
59
-
60
- function topoSortTables<
61
- DB extends SyncCoreDb = SyncCoreDb,
62
- Auth extends SyncServerAuth = SyncServerAuth,
63
- >(
64
- collection: ServerHandlerCollection<DB, Auth>,
65
- targetTable?: string
66
- ): ServerTableHandler<DB, Auth>[] {
67
- const visited = new Set<string>();
68
- const visiting = new Set<string>();
69
- const sorted: ServerTableHandler<DB, Auth>[] = [];
70
-
71
- const visit = (table: string) => {
72
- if (visited.has(table)) return;
73
- if (visiting.has(table)) {
74
- throw new Error(
75
- `Circular dependency detected involving table "${table}"`
76
- );
77
- }
78
-
79
- const handler = collection.byTable.get(table);
80
- if (!handler) {
81
- throw new Error(`Unknown table: ${table}`);
82
- }
83
-
84
- visiting.add(table);
85
- for (const dep of handler.dependsOn ?? []) {
86
- visit(dep);
87
- }
88
- visiting.delete(table);
89
- visited.add(table);
90
- sorted.push(handler);
91
- };
92
-
93
- if (targetTable) {
94
- visit(targetTable);
95
- return sorted;
96
- }
97
-
98
- for (const table of collection.byTable.keys()) {
99
- visit(table);
100
- }
101
- return sorted;
102
- }
103
-
104
35
  export function getServerBootstrapOrder<
105
36
  DB extends SyncCoreDb = SyncCoreDb,
106
37
  Auth extends SyncServerAuth = SyncServerAuth,
107
38
  >(
108
39
  collection: ServerHandlerCollection<DB, Auth>
109
40
  ): ServerTableHandler<DB, Auth>[] {
110
- return topoSortTables(collection);
41
+ return topologicallySortTablesByDependencies(collection.byTable);
111
42
  }
112
43
 
113
44
  export function getServerBootstrapOrderFor<
@@ -117,5 +48,5 @@ export function getServerBootstrapOrderFor<
117
48
  collection: ServerHandlerCollection<DB, Auth>,
118
49
  table: string
119
50
  ): ServerTableHandler<DB, Auth>[] {
120
- return topoSortTables(collection, table);
51
+ return topologicallySortTablesByDependencies(collection.byTable, table);
121
52
  }
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  import type {
6
- ScopePattern,
7
6
  ScopeValues,
8
7
  ScopeValuesFromPatterns,
9
8
  ScopeDefinition as SimpleScopeDefinition,
@@ -15,9 +14,8 @@ import {
15
14
  applyCodecsToDbRow,
16
15
  type ColumnCodecDialect,
17
16
  type ColumnCodecSource,
18
- extractScopeVars,
19
- normalizeScopes,
20
- toTableColumnCodecs,
17
+ createSingleVariableScopeMetadata,
18
+ createTableColumnCodecsResolver,
21
19
  } from '@syncular/core';
22
20
  import type {
23
21
  DeleteQueryBuilder,
@@ -77,11 +75,6 @@ function isMissingColumnReferenceError(message: string): boolean {
77
75
  );
78
76
  }
79
77
 
80
- /**
81
- * Scope definition for a column - maps scope variable to column name.
82
- */
83
- export type ScopeColumnMap = Record<string, string>;
84
-
85
78
  /**
86
79
  * Options for creating a declarative server handler.
87
80
  */
@@ -274,44 +267,16 @@ export function createServerHandler<
274
267
  authorize,
275
268
  extractScopes: customExtractScopes,
276
269
  } = options;
277
- const codecCache = new Map<string, ReturnType<typeof toTableColumnCodecs>>();
270
+ const resolveRowCodecs = createTableColumnCodecsResolver(codecs, {
271
+ dialect: codecDialect,
272
+ });
278
273
  const primaryKeyColumn = primaryKey as keyof ServerDB[TableName] & string;
279
274
  const qualifiedVersionRef = `${table}.${versionColumn}`;
280
- const resolveTableCodecs = (row: Record<string, unknown>) => {
281
- if (!codecs) return {};
282
- const columns = Object.keys(row);
283
- if (columns.length === 0) return {};
284
- const cacheKey = columns.slice().sort().join('\u0000');
285
- const cached = codecCache.get(cacheKey);
286
- if (cached) return cached;
287
- const resolved = toTableColumnCodecs(table, codecs, columns, {
288
- dialect: codecDialect,
289
- });
290
- codecCache.set(cacheKey, resolved);
291
- return resolved;
292
- };
275
+ const resolveTableCodecs = (row: Record<string, unknown>) =>
276
+ resolveRowCodecs(table, row);
293
277
 
294
- // Normalize scopes to pattern map and extract patterns/columns
295
- const scopeColumnMap = normalizeScopes(scopeDefs);
296
- const scopePatterns = Object.keys(scopeColumnMap) as ScopePattern[];
297
- const scopeColumns: ScopeColumnMap = {};
298
-
299
- for (const [pattern, columnName] of Object.entries(scopeColumnMap)) {
300
- const vars = extractScopeVars(pattern);
301
- if (vars.length !== 1) {
302
- throw new Error(
303
- `Scope pattern "${pattern}" must contain exactly one placeholder (got ${vars.length}).`
304
- );
305
- }
306
- const varName = vars[0]!;
307
- const existing = scopeColumns[varName];
308
- if (existing && existing !== columnName) {
309
- throw new Error(
310
- `Scope variable "${varName}" is mapped to multiple columns: "${existing}" and "${columnName}".`
311
- );
312
- }
313
- scopeColumns[varName] = columnName;
314
- }
278
+ const { scopePatterns, scopeColumnsByVariable: scopeColumns } =
279
+ createSingleVariableScopeMetadata(scopeDefs);
315
280
 
316
281
  // Default extractScopes from scope columns
317
282
  const defaultExtractScopes = (row: Record<string, unknown>): StoredScopes => {
package/src/notify.ts CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { randomId } from '@syncular/core';
12
12
  import type { Insertable, Kysely } from 'kysely';
13
+ import { coerceNumber, toDialectJsonValue } from './dialect/helpers';
13
14
  import type { ServerSyncDialect } from './dialect/types';
14
15
  import type { SyncCoreDb } from './schema';
15
16
 
@@ -19,31 +20,6 @@ import type { SyncCoreDb } from './schema';
19
20
  */
20
21
  export const EXTERNAL_CLIENT_ID = '__external__';
21
22
 
22
- function toDialectJsonValue(
23
- dialect: ServerSyncDialect,
24
- value: unknown
25
- ): unknown {
26
- if (value === null || value === undefined) return null;
27
- if (dialect.family === 'sqlite') return JSON.stringify(value);
28
- return value;
29
- }
30
-
31
- function coerceNumber(value: unknown): number | null {
32
- if (value === null || value === undefined) return null;
33
- if (typeof value === 'number') {
34
- return Number.isFinite(value) ? value : null;
35
- }
36
- if (typeof value === 'bigint') {
37
- const coerced = Number(value);
38
- return Number.isFinite(coerced) ? coerced : null;
39
- }
40
- if (typeof value === 'string') {
41
- const coerced = Number(value);
42
- return Number.isFinite(coerced) ? coerced : null;
43
- }
44
- return null;
45
- }
46
-
47
23
  export interface NotifyExternalDataChangeArgs<DB extends SyncCoreDb> {
48
24
  db: Kysely<DB>;
49
25
  dialect: ServerSyncDialect;
@@ -80,8 +56,11 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
80
56
  const { db, dialect, tables } = args;
81
57
  const partitionId = args.partitionId ?? 'default';
82
58
  const actorId = args.actorId ?? EXTERNAL_CLIENT_ID;
59
+ const uniqueTables = Array.from(
60
+ new Set(tables.filter((table) => typeof table === 'string'))
61
+ );
83
62
 
84
- if (tables.length === 0) {
63
+ if (uniqueTables.length === 0) {
85
64
  throw new Error('notifyExternalDataChange: tables must not be empty');
86
65
  }
87
66
 
@@ -103,7 +82,7 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
103
82
  meta: null,
104
83
  result_json: toDialectJsonValue(dialect, { ok: true, status: 'applied' }),
105
84
  change_count: 0,
106
- affected_tables: dialect.arrayToDb(tables) as string[],
85
+ affected_tables: dialect.arrayToDb(uniqueTables) as string[],
107
86
  };
108
87
 
109
88
  let commitSeq = 0;
@@ -136,7 +115,7 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
136
115
 
137
116
  // 2. Insert sync_table_commits entries for each affected table
138
117
  const tableCommits: Array<Insertable<SyncCoreDb['sync_table_commits']>> =
139
- tables.map((table) => ({
118
+ uniqueTables.map((table) => ({
140
119
  partition_id: partitionId,
141
120
  table,
142
121
  commit_seq: commitSeq,
@@ -150,21 +129,17 @@ export async function notifyExternalDataChange<DB extends SyncCoreDb>(
150
129
  )
151
130
  .execute();
152
131
 
153
- // 3. Delete cached snapshot chunks for affected tables
154
- let deletedChunks = 0;
155
- for (const table of tables) {
156
- const result = await syncTrx
157
- .deleteFrom('sync_snapshot_chunks')
158
- .where('partition_id', '=', partitionId)
159
- .where('scope', '=', table)
160
- .executeTakeFirst();
161
-
162
- deletedChunks += Number(result?.numDeletedRows ?? 0);
163
- }
132
+ // 3. Delete cached snapshot chunks for affected tables.
133
+ const deletedResult = await syncTrx
134
+ .deleteFrom('sync_snapshot_chunks')
135
+ .where('partition_id', '=', partitionId)
136
+ .where('scope', 'in', uniqueTables)
137
+ .executeTakeFirst();
138
+ const deletedChunks = Number(deletedResult?.numDeletedRows ?? 0);
164
139
 
165
140
  return {
166
141
  commitSeq,
167
- tables,
142
+ tables: uniqueTables,
168
143
  deletedChunks,
169
144
  };
170
145
  });
@@ -1,3 +1,4 @@
1
+ import { createTableLookup } from '@syncular/core';
1
2
  import type { ProxyTableHandler } from './types';
2
3
 
3
4
  export interface ProxyHandlerCollection {
@@ -8,32 +9,9 @@ export interface ProxyHandlerCollection {
8
9
  export function createProxyHandlerCollection(
9
10
  handlers: ProxyTableHandler[]
10
11
  ): ProxyHandlerCollection {
11
- const byTable = new Map<string, ProxyTableHandler>();
12
- for (const handler of handlers) {
13
- if (byTable.has(handler.table)) {
14
- throw new Error(
15
- `Proxy table handler already registered: ${handler.table}`
16
- );
17
- }
18
- byTable.set(handler.table, handler);
19
- }
12
+ const byTable = createTableLookup(
13
+ handlers,
14
+ (table) => `Proxy table handler already registered: ${table}`
15
+ );
20
16
  return { handlers, byTable };
21
17
  }
22
-
23
- export function getProxyHandler(
24
- collection: ProxyHandlerCollection,
25
- tableName: string
26
- ): ProxyTableHandler | undefined {
27
- return collection.byTable.get(tableName);
28
- }
29
-
30
- export function getProxyHandlerOrThrow(
31
- collection: ProxyHandlerCollection,
32
- tableName: string
33
- ): ProxyTableHandler {
34
- const handler = collection.byTable.get(tableName);
35
- if (!handler) {
36
- throw new Error(`No proxy table handler for table: ${tableName}`);
37
- }
38
- return handler;
39
- }
@@ -8,7 +8,7 @@ import type { Kysely, RawBuilder } from 'kysely';
8
8
  import { sql } from 'kysely';
9
9
  import type { ServerSyncDialect } from '../dialect/types';
10
10
  import type { SyncCoreDb } from '../schema';
11
- import { getProxyHandler, type ProxyHandlerCollection } from './collection';
11
+ import type { ProxyHandlerCollection } from './collection';
12
12
  import {
13
13
  appendReturning,
14
14
  detectMutation,
@@ -111,7 +111,7 @@ export async function executeProxyQuery<DB extends SyncCoreDb>(
111
111
  }
112
112
 
113
113
  // Check if this table has a registered handler
114
- const handler = getProxyHandler(handlers, mutation.tableName);
114
+ const handler = handlers.byTable.get(mutation.tableName);
115
115
  if (!handler) {
116
116
  // No handler registered - execute without oplog
117
117
  // This allows proxy operations on non-synced tables
@@ -8,8 +8,6 @@
8
8
  // Collections
9
9
  export {
10
10
  createProxyHandlerCollection,
11
- getProxyHandler,
12
- getProxyHandlerOrThrow,
13
11
  type ProxyHandlerCollection,
14
12
  } from './collection';
15
13
  // Query execution
@@ -6,19 +6,11 @@
6
6
 
7
7
  import { randomId, type SyncOp } from '@syncular/core';
8
8
  import { type Kysely, sql } from 'kysely';
9
+ import { toDialectJsonValue } from '../dialect/helpers';
9
10
  import type { ServerSyncDialect } from '../dialect/types';
10
11
  import type { SyncCoreDb } from '../schema';
11
12
  import type { ProxyTableHandler } from './types';
12
13
 
13
- function toDialectJsonValue(
14
- dialect: ServerSyncDialect,
15
- value: unknown
16
- ): unknown {
17
- if (value === null || value === undefined) return null;
18
- if (dialect.family === 'sqlite') return JSON.stringify(value);
19
- return value;
20
- }
21
-
22
14
  /**
23
15
  * Create oplog entries for affected rows.
24
16
  *
package/src/prune.ts CHANGED
@@ -18,22 +18,11 @@ import type {
18
18
  SqlBool,
19
19
  } from 'kysely';
20
20
  import { sql } from 'kysely';
21
+ import { coerceNumber } from './dialect/helpers';
21
22
  import type { SyncCoreDb } from './schema';
22
23
 
23
24
  type EmptySelection = Record<string, never>;
24
25
 
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
26
  export interface PruneOptions {
38
27
  /** Clients with updated_at older than this are ignored for watermark. Default: 14 days. */
39
28
  activeWindowMs?: number;