@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,8 @@
1
+ /**
2
+ * @syncular/server - Snapshot chunk storage types
3
+ *
4
+ * Separates chunk metadata (in database) from chunk body (in blob storage).
5
+ * Enables flexible storage backends (database, S3, R2, etc.)
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/snapshot-chunks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @syncular/server - Encoded snapshot chunk cache (server-side)
3
+ *
4
+ * Used for efficiently serving large bootstrap snapshots (e.g. catalogs)
5
+ * without embedding huge JSON payloads into pull responses.
6
+ */
7
+ import type { SyncSnapshotChunkRef } from '@syncular/core';
8
+ import { type Kysely } from 'kysely';
9
+ import type { SyncCoreDb } from './schema';
10
+ export interface SnapshotChunkPageKey {
11
+ partitionId: string;
12
+ scopeKey: string;
13
+ scope: string;
14
+ asOfCommitSeq: number;
15
+ rowCursor: string | null;
16
+ rowLimit: number;
17
+ encoding: 'ndjson';
18
+ compression: 'gzip';
19
+ }
20
+ export interface SnapshotChunkRow {
21
+ chunkId: string;
22
+ partitionId: string;
23
+ scopeKey: string;
24
+ scope: string;
25
+ asOfCommitSeq: number;
26
+ rowCursor: string;
27
+ rowLimit: number;
28
+ encoding: 'ndjson';
29
+ compression: 'gzip';
30
+ sha256: string;
31
+ byteLength: number;
32
+ body: Uint8Array;
33
+ expiresAt: string;
34
+ }
35
+ export declare function readSnapshotChunkRefByPageKey<DB extends SyncCoreDb>(db: Kysely<DB>, args: SnapshotChunkPageKey & {
36
+ nowIso?: string;
37
+ }): Promise<SyncSnapshotChunkRef | null>;
38
+ export declare function insertSnapshotChunk<DB extends SyncCoreDb>(db: Kysely<DB>, args: {
39
+ chunkId: string;
40
+ partitionId: string;
41
+ scopeKey: string;
42
+ scope: string;
43
+ asOfCommitSeq: number;
44
+ rowCursor: string | null;
45
+ rowLimit: number;
46
+ encoding: 'ndjson';
47
+ compression: 'gzip';
48
+ sha256: string;
49
+ body: Uint8Array;
50
+ expiresAt: string;
51
+ }): Promise<SyncSnapshotChunkRef>;
52
+ export declare function readSnapshotChunk<DB extends SyncCoreDb>(db: Kysely<DB>, chunkId: string, options?: {
53
+ /** External chunk storage for reading from S3/R2/etc */
54
+ chunkStorage?: {
55
+ readChunk(chunkId: string): Promise<Uint8Array | null>;
56
+ };
57
+ }): Promise<SnapshotChunkRow | null>;
58
+ export declare function deleteExpiredSnapshotChunks<DB extends SyncCoreDb>(db: Kysely<DB>, nowIso?: string): Promise<number>;
59
+ //# sourceMappingURL=snapshot-chunks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-chunks.d.ts","sourceRoot":"","sources":["../src/snapshot-chunks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAqBD,wBAAsB,6BAA6B,CAAC,EAAE,SAAS,UAAU,EACvE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,IAAI,EAAE,oBAAoB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/C,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA+CtC;AAED,wBAAsB,mBAAmB,CAAC,EAAE,SAAS,UAAU,EAC7D,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,IAAI,EAAE;IACJ,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,oBAAoB,CAAC,CAyE/B;AAED,wBAAsB,iBAAiB,CAAC,EAAE,SAAS,UAAU,EAC3D,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,wDAAwD;IACxD,YAAY,CAAC,EAAE;QAAE,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;KAAE,CAAC;CAC3E,GACA,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAiFlC;AAED,wBAAsB,2BAA2B,CAAC,EAAE,SAAS,UAAU,EACrE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,MAAM,SAA2B,GAChC,OAAO,CAAC,MAAM,CAAC,CAOjB"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @syncular/server - Encoded snapshot chunk cache (server-side)
3
+ *
4
+ * Used for efficiently serving large bootstrap snapshots (e.g. catalogs)
5
+ * without embedding huge JSON payloads into pull responses.
6
+ */
7
+ import { sql } from 'kysely';
8
+ function coerceChunkRow(value) {
9
+ // pg returns Buffer (subclass of Uint8Array); sqlite returns Uint8Array
10
+ if (value instanceof Uint8Array)
11
+ return value;
12
+ if (typeof Buffer !== 'undefined' && value instanceof Buffer)
13
+ return value;
14
+ if (value instanceof ArrayBuffer)
15
+ return new Uint8Array(value);
16
+ if (Array.isArray(value) && value.every((v) => typeof v === 'number')) {
17
+ return new Uint8Array(value);
18
+ }
19
+ throw new Error(`Unexpected snapshot chunk body type: ${Object.prototype.toString.call(value)}`);
20
+ }
21
+ function coerceIsoString(value) {
22
+ if (typeof value === 'string')
23
+ return value;
24
+ if (value instanceof Date)
25
+ return value.toISOString();
26
+ return String(value);
27
+ }
28
+ export async function readSnapshotChunkRefByPageKey(db, args) {
29
+ const nowIso = args.nowIso ?? new Date().toISOString();
30
+ const rowCursorKey = args.rowCursor ?? '';
31
+ const rowResult = await sql `
32
+ select chunk_id, sha256, byte_length, encoding, compression
33
+ from ${sql.table('sync_snapshot_chunks')}
34
+ where
35
+ partition_id = ${args.partitionId}
36
+ and scope_key = ${args.scopeKey}
37
+ and scope = ${args.scope}
38
+ and as_of_commit_seq = ${args.asOfCommitSeq}
39
+ and row_cursor = ${rowCursorKey}
40
+ and row_limit = ${args.rowLimit}
41
+ and encoding = ${args.encoding}
42
+ and compression = ${args.compression}
43
+ and expires_at > ${nowIso}
44
+ limit 1
45
+ `.execute(db);
46
+ const row = rowResult.rows[0];
47
+ if (!row)
48
+ return null;
49
+ if (row.encoding !== 'ndjson') {
50
+ throw new Error(`Unexpected snapshot chunk encoding: ${String(row.encoding)}`);
51
+ }
52
+ if (row.compression !== 'gzip') {
53
+ throw new Error(`Unexpected snapshot chunk compression: ${String(row.compression)}`);
54
+ }
55
+ return {
56
+ id: row.chunk_id,
57
+ sha256: row.sha256,
58
+ byteLength: Number(row.byte_length ?? 0),
59
+ encoding: row.encoding,
60
+ compression: row.compression,
61
+ };
62
+ }
63
+ export async function insertSnapshotChunk(db, args) {
64
+ const now = new Date().toISOString();
65
+ const rowCursorKey = args.rowCursor ?? '';
66
+ // Use content hash as blob_hash for legacy storage in DB
67
+ const blobHash = `sha256:${args.sha256}`;
68
+ await sql `
69
+ insert into ${sql.table('sync_snapshot_chunks')} (
70
+ chunk_id,
71
+ partition_id,
72
+ scope_key,
73
+ scope,
74
+ as_of_commit_seq,
75
+ row_cursor,
76
+ row_limit,
77
+ encoding,
78
+ compression,
79
+ sha256,
80
+ byte_length,
81
+ blob_hash,
82
+ body,
83
+ created_at,
84
+ expires_at
85
+ )
86
+ values (
87
+ ${args.chunkId},
88
+ ${args.partitionId},
89
+ ${args.scopeKey},
90
+ ${args.scope},
91
+ ${args.asOfCommitSeq},
92
+ ${rowCursorKey},
93
+ ${args.rowLimit},
94
+ ${args.encoding},
95
+ ${args.compression},
96
+ ${args.sha256},
97
+ ${args.body.length},
98
+ ${blobHash},
99
+ ${args.body},
100
+ ${now},
101
+ ${args.expiresAt}
102
+ )
103
+ on conflict (
104
+ partition_id,
105
+ scope_key,
106
+ scope,
107
+ as_of_commit_seq,
108
+ row_cursor,
109
+ row_limit,
110
+ encoding,
111
+ compression
112
+ )
113
+ do update set
114
+ expires_at = ${args.expiresAt},
115
+ blob_hash = ${blobHash}
116
+ `.execute(db);
117
+ const ref = await readSnapshotChunkRefByPageKey(db, {
118
+ partitionId: args.partitionId,
119
+ scopeKey: args.scopeKey,
120
+ scope: args.scope,
121
+ asOfCommitSeq: args.asOfCommitSeq,
122
+ rowCursor: args.rowCursor,
123
+ rowLimit: args.rowLimit,
124
+ encoding: args.encoding,
125
+ compression: args.compression,
126
+ });
127
+ if (!ref) {
128
+ throw new Error('Failed to read inserted snapshot chunk');
129
+ }
130
+ return ref;
131
+ }
132
+ export async function readSnapshotChunk(db, chunkId, options) {
133
+ const rowResult = await sql `
134
+ select
135
+ chunk_id,
136
+ partition_id,
137
+ scope_key,
138
+ scope,
139
+ as_of_commit_seq,
140
+ row_cursor,
141
+ row_limit,
142
+ encoding,
143
+ compression,
144
+ sha256,
145
+ byte_length,
146
+ blob_hash,
147
+ body,
148
+ expires_at
149
+ from ${sql.table('sync_snapshot_chunks')}
150
+ where chunk_id = ${chunkId}
151
+ limit 1
152
+ `.execute(db);
153
+ const row = rowResult.rows[0];
154
+ if (!row)
155
+ return null;
156
+ if (row.encoding !== 'ndjson') {
157
+ throw new Error(`Unexpected snapshot chunk encoding: ${String(row.encoding)}`);
158
+ }
159
+ if (row.compression !== 'gzip') {
160
+ throw new Error(`Unexpected snapshot chunk compression: ${String(row.compression)}`);
161
+ }
162
+ // Read body from external storage if available, otherwise use inline body
163
+ let body;
164
+ if (options?.chunkStorage) {
165
+ const externalBody = await options.chunkStorage.readChunk(chunkId);
166
+ if (externalBody) {
167
+ body = externalBody;
168
+ }
169
+ else if (row.body) {
170
+ body = coerceChunkRow(row.body);
171
+ }
172
+ else {
173
+ throw new Error(`Snapshot chunk body missing for chunk ${chunkId}`);
174
+ }
175
+ }
176
+ else {
177
+ body = coerceChunkRow(row.body);
178
+ }
179
+ return {
180
+ chunkId: row.chunk_id,
181
+ partitionId: row.partition_id,
182
+ scopeKey: row.scope_key,
183
+ scope: row.scope,
184
+ asOfCommitSeq: Number(row.as_of_commit_seq ?? 0),
185
+ rowCursor: row.row_cursor,
186
+ rowLimit: Number(row.row_limit ?? 0),
187
+ encoding: row.encoding,
188
+ compression: row.compression,
189
+ sha256: row.sha256,
190
+ byteLength: Number(row.byte_length ?? 0),
191
+ body,
192
+ expiresAt: coerceIsoString(row.expires_at),
193
+ };
194
+ }
195
+ export async function deleteExpiredSnapshotChunks(db, nowIso = new Date().toISOString()) {
196
+ const res = await sql `
197
+ delete from ${sql.table('sync_snapshot_chunks')}
198
+ where expires_at <= ${nowIso}
199
+ `.execute(db);
200
+ return Number(res.numAffectedRows ?? 0);
201
+ }
202
+ //# sourceMappingURL=snapshot-chunks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-chunks.js","sourceRoot":"","sources":["../src/snapshot-chunks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @syncular/server - Observability helpers
3
+ */
4
+ import type { Kysely } from 'kysely';
5
+ import type { SyncCoreDb } from './schema';
6
+ export interface SyncStats {
7
+ commitCount: number;
8
+ changeCount: number;
9
+ minCommitSeq: number;
10
+ maxCommitSeq: number;
11
+ clientCount: number;
12
+ activeClientCount: number;
13
+ minActiveClientCursor: number | null;
14
+ maxActiveClientCursor: number | null;
15
+ }
16
+ export declare function readSyncStats<DB extends SyncCoreDb>(db: Kysely<DB>, options?: {
17
+ activeWindowMs?: number;
18
+ }): Promise<SyncStats>;
19
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../src/stats.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,EAA+B,MAAM,QAAQ,CAAC;AAElE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAiB3C,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,wBAAsB,aAAa,CAAC,EAAE,SAAS,UAAU,EACvD,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,CAAA;CAAO,GACxC,OAAO,CAAC,SAAS,CAAC,CAkEpB"}
package/dist/stats.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @syncular/server - Observability helpers
3
+ */
4
+ import { sql } from 'kysely';
5
+ function coerceNumber(value) {
6
+ if (value === null || value === undefined)
7
+ return null;
8
+ if (typeof value === 'number')
9
+ return Number.isFinite(value) ? value : null;
10
+ if (typeof value === 'bigint')
11
+ return Number.isFinite(Number(value)) ? Number(value) : null;
12
+ if (typeof value === 'string') {
13
+ const n = Number(value);
14
+ return Number.isFinite(n) ? n : null;
15
+ }
16
+ return null;
17
+ }
18
+ export async function readSyncStats(db, options = {}) {
19
+ const syncDb = db;
20
+ const activeWindowMs = options.activeWindowMs ?? 14 * 24 * 60 * 60 * 1000;
21
+ const cutoffIso = new Date(Date.now() - activeWindowMs).toISOString();
22
+ const [commitRow, changeRow, clientRow, activeClientRow] = await Promise.all([
23
+ syncDb.selectFrom('sync_commits')
24
+ .select(({ fn }) => [
25
+ fn.countAll().as('commitCount'),
26
+ fn.min('commit_seq').as('minCommitSeq'),
27
+ fn.max('commit_seq').as('maxCommitSeq'),
28
+ ])
29
+ .executeTakeFirst(),
30
+ syncDb.selectFrom('sync_changes')
31
+ .select(({ fn }) => [fn.countAll().as('changeCount')])
32
+ .executeTakeFirst(),
33
+ syncDb.selectFrom('sync_client_cursors')
34
+ .select(({ fn }) => [fn.countAll().as('clientCount')])
35
+ .executeTakeFirst(),
36
+ syncDb.selectFrom('sync_client_cursors')
37
+ .where(sql `updated_at >= ${cutoffIso}`)
38
+ .where(sql `cursor >= ${0}`)
39
+ .select(({ fn }) => [
40
+ fn.countAll().as('activeClientCount'),
41
+ fn.min('cursor').as('minActiveClientCursor'),
42
+ fn.max('cursor').as('maxActiveClientCursor'),
43
+ ])
44
+ .executeTakeFirst(),
45
+ ]);
46
+ return {
47
+ commitCount: coerceNumber(commitRow?.commitCount) ?? 0,
48
+ changeCount: coerceNumber(changeRow?.changeCount) ?? 0,
49
+ minCommitSeq: coerceNumber(commitRow?.minCommitSeq) ?? 0,
50
+ maxCommitSeq: coerceNumber(commitRow?.maxCommitSeq) ?? 0,
51
+ clientCount: coerceNumber(clientRow?.clientCount) ?? 0,
52
+ activeClientCount: coerceNumber(activeClientRow?.activeClientCount) ?? 0,
53
+ minActiveClientCursor: coerceNumber(activeClientRow?.minActiveClientCursor),
54
+ maxActiveClientCursor: coerceNumber(activeClientRow?.maxActiveClientCursor),
55
+ };
56
+ }
57
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","sourceRoot":"","sources":["../src/stats.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './resolve';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/subscriptions/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './resolve';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/subscriptions/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC"}
@@ -0,0 +1,35 @@
1
+ import type { ScopeValues, SyncSubscriptionRequest } from '@syncular/core';
2
+ import type { Kysely } from 'kysely';
3
+ import type { SyncCoreDb } from '../schema';
4
+ import type { TableRegistry } from '../shapes/registry';
5
+ export declare class InvalidSubscriptionScopeError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * Resolved subscription with effective scopes.
10
+ */
11
+ export interface ResolvedSubscription {
12
+ id: string;
13
+ shape: string;
14
+ scopes: ScopeValues;
15
+ params: Record<string, unknown> | undefined;
16
+ cursor: number;
17
+ bootstrapState?: SyncSubscriptionRequest['bootstrapState'];
18
+ status: 'active' | 'revoked';
19
+ }
20
+ /**
21
+ * Resolve effective scopes for subscriptions.
22
+ *
23
+ * For each subscription:
24
+ * 1. Look up the shape by subscription.shape
25
+ * 2. Call shape.resolveScopes() to get allowed scopes for this actor
26
+ * 3. Intersect requested scopes with allowed scopes
27
+ * 4. Mark as revoked if no effective scopes
28
+ */
29
+ export declare function resolveEffectiveScopesForSubscriptions<DB extends SyncCoreDb>(args: {
30
+ db: Kysely<DB>;
31
+ actorId: string;
32
+ subscriptions: SyncSubscriptionRequest[];
33
+ shapes: TableRegistry<DB>;
34
+ }): Promise<ResolvedSubscription[]>;
35
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/subscriptions/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,qBAAa,6BAA8B,SAAQ,KAAK;IACtD,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,WAAW,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,uBAAuB,CAAC,gBAAgB,CAAC,CAAC;IAC3D,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC9B;AA8DD;;;;;;;;GAQG;AACH,wBAAsB,sCAAsC,CAC1D,EAAE,SAAS,UAAU,EACrB,IAAI,EAAE;IACN,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,uBAAuB,EAAE,CAAC;IACzC,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC,CAAC;CAC3B,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAmFlC"}
@@ -0,0 +1,134 @@
1
+ export class InvalidSubscriptionScopeError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'InvalidSubscriptionScopeError';
5
+ }
6
+ }
7
+ /**
8
+ * Intersect requested scopes with allowed scopes.
9
+ *
10
+ * For each key in requested:
11
+ * - If allowed has the same key, intersect the values
12
+ * - If allowed doesn't have the key, exclude it (no access)
13
+ *
14
+ * Returns only keys where there's intersection.
15
+ */
16
+ function intersectScopes(requested, allowed) {
17
+ const result = {};
18
+ for (const [key, reqValues] of Object.entries(requested)) {
19
+ const allowedValues = allowed[key];
20
+ if (allowedValues === undefined) {
21
+ // No access to this scope key
22
+ continue;
23
+ }
24
+ const reqArray = Array.isArray(reqValues) ? reqValues : [reqValues];
25
+ const allowedArray = Array.isArray(allowedValues)
26
+ ? allowedValues
27
+ : [allowedValues];
28
+ // Wildcard: allowed '*' means "allow any requested values for this key".
29
+ if (allowedArray.includes('*')) {
30
+ result[key] = reqValues;
31
+ continue;
32
+ }
33
+ const allowedSet = new Set(allowedArray);
34
+ // Intersect
35
+ const intersection = reqArray.filter((v) => allowedSet.has(v));
36
+ if (intersection.length > 0) {
37
+ // Keep as array if original was array, otherwise single value
38
+ result[key] =
39
+ intersection.length === 1 && !Array.isArray(reqValues)
40
+ ? intersection[0]
41
+ : intersection;
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+ /**
47
+ * Check if scopes are empty (no effective scope values).
48
+ */
49
+ function scopesEmpty(scopes) {
50
+ for (const value of Object.values(scopes)) {
51
+ const arr = Array.isArray(value) ? value : [value];
52
+ if (arr.length > 0)
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+ /**
58
+ * Resolve effective scopes for subscriptions.
59
+ *
60
+ * For each subscription:
61
+ * 1. Look up the shape by subscription.shape
62
+ * 2. Call shape.resolveScopes() to get allowed scopes for this actor
63
+ * 3. Intersect requested scopes with allowed scopes
64
+ * 4. Mark as revoked if no effective scopes
65
+ */
66
+ export async function resolveEffectiveScopesForSubscriptions(args) {
67
+ const out = [];
68
+ const seenIds = new Set();
69
+ for (const sub of args.subscriptions) {
70
+ if (!sub.id || typeof sub.id !== 'string') {
71
+ throw new InvalidSubscriptionScopeError('Subscription id is required');
72
+ }
73
+ if (seenIds.has(sub.id)) {
74
+ throw new InvalidSubscriptionScopeError(`Duplicate subscription id: ${sub.id}`);
75
+ }
76
+ seenIds.add(sub.id);
77
+ if (!sub.shape || typeof sub.shape !== 'string') {
78
+ throw new InvalidSubscriptionScopeError(`Subscription ${sub.id} requires a shape (table name)`);
79
+ }
80
+ const shape = args.shapes.get(sub.shape);
81
+ if (!shape) {
82
+ throw new InvalidSubscriptionScopeError(`Unknown shape: ${sub.shape} for subscription ${sub.id}`);
83
+ }
84
+ // Get allowed scopes from the shape
85
+ let allowed;
86
+ try {
87
+ allowed = await shape.resolveScopes({
88
+ db: args.db,
89
+ actorId: args.actorId,
90
+ });
91
+ }
92
+ catch (resolveErr) {
93
+ // Scope resolution failed - mark subscription as revoked
94
+ // rather than failing the entire pull
95
+ console.error(`[resolveScopes] Failed for shape ${sub.shape}, subscription ${sub.id}:`, resolveErr);
96
+ out.push({
97
+ id: sub.id,
98
+ shape: sub.shape,
99
+ scopes: {},
100
+ params: sub.params,
101
+ cursor: sub.cursor,
102
+ bootstrapState: sub.bootstrapState ?? null,
103
+ status: 'revoked',
104
+ });
105
+ continue;
106
+ }
107
+ // Intersect with requested scopes
108
+ const requested = sub.scopes ?? {};
109
+ const effective = intersectScopes(requested, allowed);
110
+ if (scopesEmpty(effective)) {
111
+ out.push({
112
+ id: sub.id,
113
+ shape: sub.shape,
114
+ scopes: {},
115
+ params: sub.params,
116
+ cursor: sub.cursor,
117
+ bootstrapState: sub.bootstrapState ?? null,
118
+ status: 'revoked',
119
+ });
120
+ continue;
121
+ }
122
+ out.push({
123
+ id: sub.id,
124
+ shape: sub.shape,
125
+ scopes: effective,
126
+ params: sub.params,
127
+ cursor: sub.cursor,
128
+ bootstrapState: sub.bootstrapState ?? null,
129
+ status: 'active',
130
+ });
131
+ }
132
+ return out;
133
+ }
134
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../src/subscriptions/resolve.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@syncular/server",
3
+ "version": "0.0.1-60",
4
+ "description": "Server-side sync engine with push/pull, pruning, and snapshot support",
5
+ "license": "MIT",
6
+ "author": "Benjamin Kniffler",
7
+ "homepage": "https://syncular.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/syncular/syncular.git",
11
+ "directory": "packages/server"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/syncular/syncular/issues"
15
+ },
16
+ "keywords": [
17
+ "sync",
18
+ "offline-first",
19
+ "realtime",
20
+ "database",
21
+ "typescript"
22
+ ],
23
+ "private": false,
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "type": "module",
28
+ "exports": {
29
+ ".": {
30
+ "bun": "./src/index.ts",
31
+ "import": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "./dialect/types": {
37
+ "bun": "./src/dialect/types.ts",
38
+ "import": {
39
+ "types": "./dist/dialect/types.d.ts",
40
+ "default": "./dist/dialect/types.js"
41
+ }
42
+ },
43
+ "./schema": {
44
+ "bun": "./src/schema.ts",
45
+ "import": {
46
+ "types": "./dist/schema.d.ts",
47
+ "default": "./dist/schema.js"
48
+ }
49
+ },
50
+ "./snapshot-chunks": {
51
+ "bun": "./src/snapshot-chunks/index.ts",
52
+ "import": {
53
+ "types": "./dist/snapshot-chunks/index.d.ts",
54
+ "default": "./dist/snapshot-chunks/index.js"
55
+ }
56
+ }
57
+ },
58
+ "scripts": {
59
+ "test": "bun test --pass-with-no-tests",
60
+ "tsgo": "tsgo --noEmit",
61
+ "build": "rm -rf dist && tsgo",
62
+ "release": "bun pm pack --destination . && npm publish ./*.tgz --tag latest && rm -f ./*.tgz"
63
+ },
64
+ "dependencies": {
65
+ "@syncular/core": "0.0.1"
66
+ },
67
+ "peerDependencies": {
68
+ "kysely": "^0.28.0",
69
+ "zod": "^4.0.0"
70
+ },
71
+ "devDependencies": {
72
+ "@syncular/config": "0.0.0",
73
+ "kysely": "*",
74
+ "zod": "*"
75
+ },
76
+ "files": [
77
+ "dist",
78
+ "src"
79
+ ]
80
+ }