@syncular/server 0.0.1-111 → 0.0.1-114
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/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 +1 -0
- package/dist/blobs/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/pull.d.ts.map +1 -1
- package/dist/pull.js +40 -1
- package/dist/pull.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 +0 -1
- package/dist/snapshot-chunks/index.js.map +1 -1
- package/package.json +2 -2
- 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/index.ts +1 -0
- package/src/notify.test.ts +516 -0
- package/src/notify.ts +131 -0
- package/src/pull.ts +56 -2
- package/src/snapshot-chunks/index.ts +0 -1
- package/dist/snapshot-chunks/adapters/s3.d.ts +0 -74
- 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/src/pull.ts
CHANGED
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
startSyncSpan,
|
|
21
21
|
} from '@syncular/core';
|
|
22
22
|
import type { Kysely } from 'kysely';
|
|
23
|
-
import type { ServerSyncDialect } from './dialect/types';
|
|
23
|
+
import type { DbExecutor, ServerSyncDialect } from './dialect/types';
|
|
24
|
+
import { EXTERNAL_CLIENT_ID } from './notify';
|
|
24
25
|
import type { SyncCoreDb } from './schema';
|
|
25
26
|
import type { TableRegistry } from './shapes/registry';
|
|
26
27
|
import {
|
|
@@ -271,6 +272,33 @@ function recordPullMetrics(args: {
|
|
|
271
272
|
);
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Read synthetic commits created by notifyExternalDataChange() after a given cursor.
|
|
277
|
+
* Returns commit_seq and affected tables for each external change commit.
|
|
278
|
+
*/
|
|
279
|
+
async function readExternalDataChanges<DB extends SyncCoreDb>(
|
|
280
|
+
trx: DbExecutor<DB>,
|
|
281
|
+
dialect: ServerSyncDialect,
|
|
282
|
+
args: { partitionId: string; afterCursor: number }
|
|
283
|
+
): Promise<Array<{ commitSeq: number; tables: string[] }>> {
|
|
284
|
+
type SyncExecutor = Pick<Kysely<SyncCoreDb>, 'selectFrom'>;
|
|
285
|
+
const executor = trx as SyncExecutor;
|
|
286
|
+
|
|
287
|
+
const rows = await executor
|
|
288
|
+
.selectFrom('sync_commits')
|
|
289
|
+
.select(['commit_seq', 'affected_tables'])
|
|
290
|
+
.where('partition_id', '=', args.partitionId)
|
|
291
|
+
.where('client_id', '=', EXTERNAL_CLIENT_ID)
|
|
292
|
+
.where('commit_seq', '>', args.afterCursor)
|
|
293
|
+
.orderBy('commit_seq', 'asc')
|
|
294
|
+
.execute();
|
|
295
|
+
|
|
296
|
+
return rows.map((row) => ({
|
|
297
|
+
commitSeq: Number(row.commit_seq),
|
|
298
|
+
tables: dialect.dbToArray(row.affected_tables),
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
|
|
274
302
|
export async function pull<DB extends SyncCoreDb>(args: {
|
|
275
303
|
db: Kysely<DB>;
|
|
276
304
|
dialect: ServerSyncDialect;
|
|
@@ -342,6 +370,29 @@ export async function pull<DB extends SyncCoreDb>(args: {
|
|
|
342
370
|
const activeSubscriptions: { scopes: ScopeValues }[] = [];
|
|
343
371
|
const nextCursors: number[] = [];
|
|
344
372
|
|
|
373
|
+
// Detect external data changes (synthetic commits from notifyExternalDataChange)
|
|
374
|
+
// Compute minimum cursor across all active subscriptions to scope the query.
|
|
375
|
+
let minSubCursor = Number.MAX_SAFE_INTEGER;
|
|
376
|
+
for (const sub of resolved) {
|
|
377
|
+
if (
|
|
378
|
+
sub.status === 'revoked' ||
|
|
379
|
+
Object.keys(sub.scopes).length === 0
|
|
380
|
+
)
|
|
381
|
+
continue;
|
|
382
|
+
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
383
|
+
if (cursor >= 0 && cursor < minSubCursor) {
|
|
384
|
+
minSubCursor = cursor;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const externalDataChanges =
|
|
389
|
+
minSubCursor < Number.MAX_SAFE_INTEGER && minSubCursor >= 0
|
|
390
|
+
? await readExternalDataChanges(trx, dialect, {
|
|
391
|
+
partitionId,
|
|
392
|
+
afterCursor: minSubCursor,
|
|
393
|
+
})
|
|
394
|
+
: [];
|
|
395
|
+
|
|
345
396
|
for (const sub of resolved) {
|
|
346
397
|
const cursor = Math.max(-1, sub.cursor ?? -1);
|
|
347
398
|
// Validate shape exists (throws if not registered)
|
|
@@ -369,7 +420,10 @@ export async function pull<DB extends SyncCoreDb>(args: {
|
|
|
369
420
|
sub.bootstrapState != null ||
|
|
370
421
|
cursor < 0 ||
|
|
371
422
|
cursor > maxCommitSeq ||
|
|
372
|
-
(minCommitSeq > 0 && cursor < minCommitSeq - 1)
|
|
423
|
+
(minCommitSeq > 0 && cursor < minCommitSeq - 1) ||
|
|
424
|
+
externalDataChanges.some(
|
|
425
|
+
(c) => c.commitSeq > cursor && c.tables.includes(sub.shape)
|
|
426
|
+
);
|
|
373
427
|
|
|
374
428
|
if (needsBootstrap) {
|
|
375
429
|
const tables = args.shapes
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @syncular/server - S3-compatible snapshot chunk storage adapter
|
|
3
|
-
*
|
|
4
|
-
* Stores snapshot chunk bodies in S3/R2/MinIO with metadata in database.
|
|
5
|
-
*/
|
|
6
|
-
import type { BlobStorageAdapter } from '@syncular/core';
|
|
7
|
-
import type { Kysely } from 'kysely';
|
|
8
|
-
import type { SyncCoreDb } from '../../schema';
|
|
9
|
-
export interface S3SnapshotChunkStorageOptions {
|
|
10
|
-
/** Database instance for metadata */
|
|
11
|
-
db: Kysely<SyncCoreDb>;
|
|
12
|
-
/** S3 blob storage adapter */
|
|
13
|
-
s3Adapter: BlobStorageAdapter;
|
|
14
|
-
/** Optional key prefix for all chunks */
|
|
15
|
-
keyPrefix?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Create S3-compatible snapshot chunk storage.
|
|
19
|
-
*
|
|
20
|
-
* Stores chunk bodies in S3/R2/MinIO and metadata in the database.
|
|
21
|
-
* Supports presigned URLs for direct client downloads.
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* import { createS3BlobStorageAdapter } from '@syncular/server/blobs/adapters/s3';
|
|
26
|
-
* import { createS3SnapshotChunkStorage } from '@syncular/server/snapshot-chunks/adapters/s3';
|
|
27
|
-
*
|
|
28
|
-
* const s3Adapter = createS3BlobStorageAdapter({
|
|
29
|
-
* client: new S3Client({ region: 'us-east-1' }),
|
|
30
|
-
* bucket: 'my-snapshot-chunks',
|
|
31
|
-
* commands: { PutObjectCommand, GetObjectCommand, HeadObjectCommand, DeleteObjectCommand },
|
|
32
|
-
* getSignedUrl,
|
|
33
|
-
* });
|
|
34
|
-
*
|
|
35
|
-
* const chunkStorage = createS3SnapshotChunkStorage({
|
|
36
|
-
* db: kysely,
|
|
37
|
-
* s3Adapter,
|
|
38
|
-
* keyPrefix: 'snapshots/',
|
|
39
|
-
* });
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export declare function createS3SnapshotChunkStorage(options: S3SnapshotChunkStorageOptions): {
|
|
43
|
-
name: string;
|
|
44
|
-
storeChunk: (metadata: Omit<import("..").SnapshotChunkMetadata, "blobHash" | "byteLength" | "chunkId"> & {
|
|
45
|
-
body: Uint8Array<ArrayBufferLike>;
|
|
46
|
-
}) => Promise<{
|
|
47
|
-
id: string;
|
|
48
|
-
byteLength: number;
|
|
49
|
-
sha256: string;
|
|
50
|
-
encoding: "json-row-frame-v1";
|
|
51
|
-
compression: "gzip";
|
|
52
|
-
}>;
|
|
53
|
-
storeChunkStream: (metadata: Omit<import("..").SnapshotChunkMetadata, "blobHash" | "byteLength" | "chunkId"> & {
|
|
54
|
-
bodyStream: ReadableStream<Uint8Array<ArrayBufferLike>>;
|
|
55
|
-
byteLength?: number | undefined;
|
|
56
|
-
}) => Promise<{
|
|
57
|
-
id: string;
|
|
58
|
-
byteLength: number;
|
|
59
|
-
sha256: string;
|
|
60
|
-
encoding: "json-row-frame-v1";
|
|
61
|
-
compression: "gzip";
|
|
62
|
-
}>;
|
|
63
|
-
readChunk: (chunkId: string) => Promise<Uint8Array<ArrayBufferLike> | null>;
|
|
64
|
-
readChunkStream: (chunkId: string) => Promise<ReadableStream<Uint8Array<ArrayBufferLike>> | null>;
|
|
65
|
-
findChunk: (pageKey: import("..").SnapshotChunkPageKey) => Promise<{
|
|
66
|
-
id: string;
|
|
67
|
-
byteLength: number;
|
|
68
|
-
sha256: string;
|
|
69
|
-
encoding: "json-row-frame-v1";
|
|
70
|
-
compression: "gzip";
|
|
71
|
-
} | null>;
|
|
72
|
-
cleanupExpired: (beforeIso: string) => Promise<number>;
|
|
73
|
-
};
|
|
74
|
-
//# sourceMappingURL=s3.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../../../src/snapshot-chunks/adapters/s3.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,MAAM,WAAW,6BAA6B;IAC5C,qCAAqC;IACrC,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACvB,8BAA8B;IAC9B,SAAS,EAAE,kBAAkB,CAAC;IAC9B,yCAAyC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqBvC"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @syncular/server - S3-compatible snapshot chunk storage adapter
|
|
3
|
-
*
|
|
4
|
-
* Stores snapshot chunk bodies in S3/R2/MinIO with metadata in database.
|
|
5
|
-
*/
|
|
6
|
-
import { createDbMetadataChunkStorage } from '../db-metadata.js';
|
|
7
|
-
/**
|
|
8
|
-
* Create S3-compatible snapshot chunk storage.
|
|
9
|
-
*
|
|
10
|
-
* Stores chunk bodies in S3/R2/MinIO and metadata in the database.
|
|
11
|
-
* Supports presigned URLs for direct client downloads.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* import { createS3BlobStorageAdapter } from '@syncular/server/blobs/adapters/s3';
|
|
16
|
-
* import { createS3SnapshotChunkStorage } from '@syncular/server/snapshot-chunks/adapters/s3';
|
|
17
|
-
*
|
|
18
|
-
* const s3Adapter = createS3BlobStorageAdapter({
|
|
19
|
-
* client: new S3Client({ region: 'us-east-1' }),
|
|
20
|
-
* bucket: 'my-snapshot-chunks',
|
|
21
|
-
* commands: { PutObjectCommand, GetObjectCommand, HeadObjectCommand, DeleteObjectCommand },
|
|
22
|
-
* getSignedUrl,
|
|
23
|
-
* });
|
|
24
|
-
*
|
|
25
|
-
* const chunkStorage = createS3SnapshotChunkStorage({
|
|
26
|
-
* db: kysely,
|
|
27
|
-
* s3Adapter,
|
|
28
|
-
* keyPrefix: 'snapshots/',
|
|
29
|
-
* });
|
|
30
|
-
* ```
|
|
31
|
-
*/
|
|
32
|
-
export function createS3SnapshotChunkStorage(options) {
|
|
33
|
-
const { db, s3Adapter, keyPrefix } = options;
|
|
34
|
-
// Wrap the S3 adapter to use prefixed keys
|
|
35
|
-
const prefixedAdapter = keyPrefix
|
|
36
|
-
? {
|
|
37
|
-
...s3Adapter,
|
|
38
|
-
name: `${s3Adapter.name}+prefixed`,
|
|
39
|
-
// Keys are already handled by the S3 adapter, prefix is applied there
|
|
40
|
-
}
|
|
41
|
-
: s3Adapter;
|
|
42
|
-
// Use the database metadata storage with S3 for bodies
|
|
43
|
-
const storage = createDbMetadataChunkStorage({
|
|
44
|
-
db,
|
|
45
|
-
blobAdapter: prefixedAdapter,
|
|
46
|
-
chunkIdPrefix: keyPrefix ? `${keyPrefix.replace(/\/$/, '')}_` : 'chunk_',
|
|
47
|
-
});
|
|
48
|
-
return storage;
|
|
49
|
-
}
|
|
50
|
-
//# sourceMappingURL=s3.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"s3.js","sourceRoot":"","sources":["../../../src/snapshot-chunks/adapters/s3.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,4BAA4B,EAAE,MAAM,gBAAgB,CAAC;AAW9D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,4BAA4B,CAC1C,OAAsC,EACtC;IACA,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAE7C,2CAA2C;IAC3C,MAAM,eAAe,GAAuB,SAAS;QACnD,CAAC,CAAC;YACE,GAAG,SAAS;YACZ,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,WAAW;YAClC,sEAAsE;SACvE;QACH,CAAC,CAAC,SAAS,CAAC;IAEd,uDAAuD;IACvD,MAAM,OAAO,GAAG,4BAA4B,CAAC;QAC3C,EAAE;QACF,WAAW,EAAE,eAAe;QAC5B,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ;KACzE,CAAC,CAAC;IAEH,OAAO,OAAO,CAAC;AAAA,CAChB"}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @syncular/server - S3-compatible snapshot chunk storage adapter
|
|
3
|
-
*
|
|
4
|
-
* Stores snapshot chunk bodies in S3/R2/MinIO with metadata in database.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { BlobStorageAdapter } from '@syncular/core';
|
|
8
|
-
import type { Kysely } from 'kysely';
|
|
9
|
-
import type { SyncCoreDb } from '../../schema';
|
|
10
|
-
import { createDbMetadataChunkStorage } from '../db-metadata';
|
|
11
|
-
|
|
12
|
-
export interface S3SnapshotChunkStorageOptions {
|
|
13
|
-
/** Database instance for metadata */
|
|
14
|
-
db: Kysely<SyncCoreDb>;
|
|
15
|
-
/** S3 blob storage adapter */
|
|
16
|
-
s3Adapter: BlobStorageAdapter;
|
|
17
|
-
/** Optional key prefix for all chunks */
|
|
18
|
-
keyPrefix?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Create S3-compatible snapshot chunk storage.
|
|
23
|
-
*
|
|
24
|
-
* Stores chunk bodies in S3/R2/MinIO and metadata in the database.
|
|
25
|
-
* Supports presigned URLs for direct client downloads.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```typescript
|
|
29
|
-
* import { createS3BlobStorageAdapter } from '@syncular/server/blobs/adapters/s3';
|
|
30
|
-
* import { createS3SnapshotChunkStorage } from '@syncular/server/snapshot-chunks/adapters/s3';
|
|
31
|
-
*
|
|
32
|
-
* const s3Adapter = createS3BlobStorageAdapter({
|
|
33
|
-
* client: new S3Client({ region: 'us-east-1' }),
|
|
34
|
-
* bucket: 'my-snapshot-chunks',
|
|
35
|
-
* commands: { PutObjectCommand, GetObjectCommand, HeadObjectCommand, DeleteObjectCommand },
|
|
36
|
-
* getSignedUrl,
|
|
37
|
-
* });
|
|
38
|
-
*
|
|
39
|
-
* const chunkStorage = createS3SnapshotChunkStorage({
|
|
40
|
-
* db: kysely,
|
|
41
|
-
* s3Adapter,
|
|
42
|
-
* keyPrefix: 'snapshots/',
|
|
43
|
-
* });
|
|
44
|
-
* ```
|
|
45
|
-
*/
|
|
46
|
-
export function createS3SnapshotChunkStorage(
|
|
47
|
-
options: S3SnapshotChunkStorageOptions
|
|
48
|
-
) {
|
|
49
|
-
const { db, s3Adapter, keyPrefix } = options;
|
|
50
|
-
|
|
51
|
-
// Wrap the S3 adapter to use prefixed keys
|
|
52
|
-
const prefixedAdapter: BlobStorageAdapter = keyPrefix
|
|
53
|
-
? {
|
|
54
|
-
...s3Adapter,
|
|
55
|
-
name: `${s3Adapter.name}+prefixed`,
|
|
56
|
-
// Keys are already handled by the S3 adapter, prefix is applied there
|
|
57
|
-
}
|
|
58
|
-
: s3Adapter;
|
|
59
|
-
|
|
60
|
-
// Use the database metadata storage with S3 for bodies
|
|
61
|
-
const storage = createDbMetadataChunkStorage({
|
|
62
|
-
db,
|
|
63
|
-
blobAdapter: prefixedAdapter,
|
|
64
|
-
chunkIdPrefix: keyPrefix ? `${keyPrefix.replace(/\/$/, '')}_` : 'chunk_',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return storage;
|
|
68
|
-
}
|