@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.
Files changed (43) hide show
  1. package/dist/blobs/adapters/filesystem.d.ts +31 -0
  2. package/dist/blobs/adapters/filesystem.d.ts.map +1 -0
  3. package/dist/blobs/adapters/filesystem.js +140 -0
  4. package/dist/blobs/adapters/filesystem.js.map +1 -0
  5. package/dist/blobs/adapters/s3.d.ts +3 -2
  6. package/dist/blobs/adapters/s3.d.ts.map +1 -1
  7. package/dist/blobs/adapters/s3.js +49 -0
  8. package/dist/blobs/adapters/s3.js.map +1 -1
  9. package/dist/blobs/index.d.ts +1 -0
  10. package/dist/blobs/index.d.ts.map +1 -1
  11. package/dist/blobs/index.js +1 -0
  12. package/dist/blobs/index.js.map +1 -1
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/notify.d.ts +47 -0
  18. package/dist/notify.d.ts.map +1 -0
  19. package/dist/notify.js +85 -0
  20. package/dist/notify.js.map +1 -0
  21. package/dist/pull.d.ts.map +1 -1
  22. package/dist/pull.js +40 -1
  23. package/dist/pull.js.map +1 -1
  24. package/dist/snapshot-chunks/index.d.ts +0 -1
  25. package/dist/snapshot-chunks/index.d.ts.map +1 -1
  26. package/dist/snapshot-chunks/index.js +0 -1
  27. package/dist/snapshot-chunks/index.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/blobs/adapters/filesystem.test.ts +132 -0
  30. package/src/blobs/adapters/filesystem.ts +189 -0
  31. package/src/blobs/adapters/s3.test.ts +522 -0
  32. package/src/blobs/adapters/s3.ts +55 -2
  33. package/src/blobs/index.ts +1 -0
  34. package/src/index.ts +1 -0
  35. package/src/notify.test.ts +516 -0
  36. package/src/notify.ts +131 -0
  37. package/src/pull.ts +56 -2
  38. package/src/snapshot-chunks/index.ts +0 -1
  39. package/dist/snapshot-chunks/adapters/s3.d.ts +0 -74
  40. package/dist/snapshot-chunks/adapters/s3.d.ts.map +0 -1
  41. package/dist/snapshot-chunks/adapters/s3.js +0 -50
  42. package/dist/snapshot-chunks/adapters/s3.js.map +0 -1
  43. 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
@@ -4,6 +4,5 @@
4
4
  * Separates chunk metadata (database) from body content (blob storage).
5
5
  */
6
6
 
7
- export * from './adapters/s3';
8
7
  export * from './db-metadata';
9
8
  export * from './types';
@@ -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
- }