@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.
- package/dist/blobs/adapters/database.d.ts +83 -0
- package/dist/blobs/adapters/database.d.ts.map +1 -0
- package/dist/blobs/adapters/database.js +180 -0
- package/dist/blobs/adapters/database.js.map +1 -0
- package/dist/blobs/adapters/s3.d.ts +82 -0
- package/dist/blobs/adapters/s3.d.ts.map +1 -0
- package/dist/blobs/adapters/s3.js +170 -0
- package/dist/blobs/adapters/s3.js.map +1 -0
- package/dist/blobs/index.d.ts +9 -0
- package/dist/blobs/index.d.ts.map +1 -0
- package/dist/blobs/index.js +9 -0
- package/dist/blobs/index.js.map +1 -0
- package/dist/blobs/manager.d.ts +195 -0
- package/dist/blobs/manager.d.ts.map +1 -0
- package/dist/blobs/manager.js +440 -0
- package/dist/blobs/manager.js.map +1 -0
- package/dist/blobs/migrate.d.ts +27 -0
- package/dist/blobs/migrate.d.ts.map +1 -0
- package/dist/blobs/migrate.js +119 -0
- package/dist/blobs/migrate.js.map +1 -0
- package/dist/blobs/types.d.ts +54 -0
- package/dist/blobs/types.d.ts.map +1 -0
- package/dist/blobs/types.js +5 -0
- package/dist/blobs/types.js.map +1 -0
- package/dist/clients.d.ts +14 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +7 -0
- package/dist/clients.js.map +1 -0
- package/dist/compaction.d.ts +27 -0
- package/dist/compaction.d.ts.map +1 -0
- package/dist/compaction.js +49 -0
- package/dist/compaction.js.map +1 -0
- package/dist/dialect/index.d.ts +5 -0
- package/dist/dialect/index.d.ts.map +1 -0
- package/dist/dialect/index.js +5 -0
- package/dist/dialect/index.js.map +1 -0
- package/dist/dialect/types.d.ts +170 -0
- package/dist/dialect/types.d.ts.map +1 -0
- package/dist/dialect/types.js +8 -0
- package/dist/dialect/types.js.map +1 -0
- package/dist/helpers/conflict.d.ts +52 -0
- package/dist/helpers/conflict.d.ts.map +1 -0
- package/dist/helpers/conflict.js +49 -0
- package/dist/helpers/conflict.js.map +1 -0
- package/dist/helpers/emitted-change.d.ts +56 -0
- package/dist/helpers/emitted-change.d.ts.map +1 -0
- package/dist/helpers/emitted-change.js +46 -0
- package/dist/helpers/emitted-change.js.map +1 -0
- package/dist/helpers/index.d.ts +10 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +10 -0
- package/dist/helpers/index.js.map +1 -0
- package/dist/helpers/paginate.d.ts +49 -0
- package/dist/helpers/paginate.d.ts.map +1 -0
- package/dist/helpers/paginate.js +54 -0
- package/dist/helpers/paginate.js.map +1 -0
- package/dist/helpers/scope-strings.d.ts +74 -0
- package/dist/helpers/scope-strings.d.ts.map +1 -0
- package/dist/helpers/scope-strings.js +82 -0
- package/dist/helpers/scope-strings.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +14 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +13 -0
- package/dist/migrate.js.map +1 -0
- package/dist/proxy/handler.d.ts +42 -0
- package/dist/proxy/handler.d.ts.map +1 -0
- package/dist/proxy/handler.js +99 -0
- package/dist/proxy/handler.js.map +1 -0
- package/dist/proxy/index.d.ts +9 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +14 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/mutation-detector.d.ts +31 -0
- package/dist/proxy/mutation-detector.d.ts.map +1 -0
- package/dist/proxy/mutation-detector.js +61 -0
- package/dist/proxy/mutation-detector.js.map +1 -0
- package/dist/proxy/oplog.d.ts +30 -0
- package/dist/proxy/oplog.d.ts.map +1 -0
- package/dist/proxy/oplog.js +110 -0
- package/dist/proxy/oplog.js.map +1 -0
- package/dist/proxy/registry.d.ts +35 -0
- package/dist/proxy/registry.d.ts.map +1 -0
- package/dist/proxy/registry.js +49 -0
- package/dist/proxy/registry.js.map +1 -0
- package/dist/proxy/types.d.ts +44 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +7 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/prune.d.ts +37 -0
- package/dist/prune.d.ts.map +1 -0
- package/dist/prune.js +112 -0
- package/dist/prune.js.map +1 -0
- package/dist/pull.d.ts +31 -0
- package/dist/pull.d.ts.map +1 -0
- package/dist/pull.js +414 -0
- package/dist/pull.js.map +1 -0
- package/dist/push.d.ts +33 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +329 -0
- package/dist/push.js.map +1 -0
- package/dist/realtime/in-memory.d.ts +13 -0
- package/dist/realtime/in-memory.d.ts.map +1 -0
- package/dist/realtime/in-memory.js +28 -0
- package/dist/realtime/in-memory.js.map +1 -0
- package/dist/realtime/index.d.ts +3 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +2 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/types.d.ts +50 -0
- package/dist/realtime/types.d.ts.map +1 -0
- package/dist/realtime/types.js +7 -0
- package/dist/realtime/types.js.map +1 -0
- package/dist/schema.d.ts +164 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +10 -0
- package/dist/schema.js.map +1 -0
- package/dist/shapes/create-handler.d.ts +119 -0
- package/dist/shapes/create-handler.d.ts.map +1 -0
- package/dist/shapes/create-handler.js +327 -0
- package/dist/shapes/create-handler.js.map +1 -0
- package/dist/shapes/index.d.ts +4 -0
- package/dist/shapes/index.d.ts.map +1 -0
- package/dist/shapes/index.js +4 -0
- package/dist/shapes/index.js.map +1 -0
- package/dist/shapes/registry.d.ts +20 -0
- package/dist/shapes/registry.d.ts.map +1 -0
- package/dist/shapes/registry.js +88 -0
- package/dist/shapes/registry.js.map +1 -0
- package/dist/shapes/types.d.ts +204 -0
- package/dist/shapes/types.d.ts.map +1 -0
- package/dist/shapes/types.js +2 -0
- package/dist/shapes/types.js.map +1 -0
- package/dist/snapshot-chunks/adapters/s3.d.ts +63 -0
- package/dist/snapshot-chunks/adapters/s3.d.ts.map +1 -0
- package/dist/snapshot-chunks/adapters/s3.js +50 -0
- package/dist/snapshot-chunks/adapters/s3.js.map +1 -0
- package/dist/snapshot-chunks/db-metadata.d.ts +33 -0
- package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -0
- package/dist/snapshot-chunks/db-metadata.js +169 -0
- package/dist/snapshot-chunks/db-metadata.js.map +1 -0
- package/dist/snapshot-chunks/index.d.ts +9 -0
- package/dist/snapshot-chunks/index.d.ts.map +1 -0
- package/dist/snapshot-chunks/index.js +9 -0
- package/dist/snapshot-chunks/index.js.map +1 -0
- package/dist/snapshot-chunks/types.d.ts +65 -0
- package/dist/snapshot-chunks/types.d.ts.map +1 -0
- package/dist/snapshot-chunks/types.js +8 -0
- package/dist/snapshot-chunks/types.js.map +1 -0
- package/dist/snapshot-chunks.d.ts +59 -0
- package/dist/snapshot-chunks.d.ts.map +1 -0
- package/dist/snapshot-chunks.js +202 -0
- package/dist/snapshot-chunks.js.map +1 -0
- package/dist/stats.d.ts +19 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +57 -0
- package/dist/stats.js.map +1 -0
- package/dist/subscriptions/index.d.ts +2 -0
- package/dist/subscriptions/index.d.ts.map +1 -0
- package/dist/subscriptions/index.js +2 -0
- package/dist/subscriptions/index.js.map +1 -0
- package/dist/subscriptions/resolve.d.ts +35 -0
- package/dist/subscriptions/resolve.d.ts.map +1 -0
- package/dist/subscriptions/resolve.js +134 -0
- package/dist/subscriptions/resolve.js.map +1 -0
- package/package.json +80 -0
- package/src/blobs/adapters/database.ts +290 -0
- package/src/blobs/adapters/s3.ts +271 -0
- package/src/blobs/index.ts +9 -0
- package/src/blobs/manager.ts +600 -0
- package/src/blobs/migrate.ts +150 -0
- package/src/blobs/types.ts +70 -0
- package/src/clients.ts +21 -0
- package/src/compaction.ts +77 -0
- package/src/dialect/index.ts +5 -0
- package/src/dialect/types.ts +222 -0
- package/src/helpers/conflict.ts +64 -0
- package/src/helpers/emitted-change.ts +69 -0
- package/src/helpers/index.ts +10 -0
- package/src/helpers/paginate.ts +82 -0
- package/src/helpers/scope-strings.ts +101 -0
- package/src/index.ts +28 -0
- package/src/migrate.ts +20 -0
- package/src/proxy/handler.ts +152 -0
- package/src/proxy/index.ts +18 -0
- package/src/proxy/mutation-detector.ts +83 -0
- package/src/proxy/oplog.ts +144 -0
- package/src/proxy/registry.ts +56 -0
- package/src/proxy/types.ts +46 -0
- package/src/prune.ts +200 -0
- package/src/pull.ts +551 -0
- package/src/push.ts +457 -0
- package/src/realtime/in-memory.ts +33 -0
- package/src/realtime/index.ts +5 -0
- package/src/realtime/types.ts +55 -0
- package/src/schema.ts +172 -0
- package/src/shapes/create-handler.ts +590 -0
- package/src/shapes/index.ts +3 -0
- package/src/shapes/registry.ts +109 -0
- package/src/shapes/types.ts +267 -0
- package/src/snapshot-chunks/adapters/s3.ts +68 -0
- package/src/snapshot-chunks/db-metadata.ts +238 -0
- package/src/snapshot-chunks/index.ts +9 -0
- package/src/snapshot-chunks/types.ts +79 -0
- package/src/snapshot-chunks.ts +301 -0
- package/src/stats.ts +104 -0
- package/src/subscriptions/index.ts +1 -0
- package/src/subscriptions/resolve.ts +185 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { ScopePattern, ScopeValues, StoredScopes, SyncOp, SyncOperation, SyncOperationResult } from '@syncular/core';
|
|
2
|
+
import type { ZodSchema, z } from 'zod';
|
|
3
|
+
import type { DbExecutor } from '../dialect/types';
|
|
4
|
+
import type { SyncCoreDb } from '../schema';
|
|
5
|
+
/**
|
|
6
|
+
* Emitted change to be stored in the oplog.
|
|
7
|
+
* Uses JSONB scopes instead of scope_keys array.
|
|
8
|
+
*/
|
|
9
|
+
export interface EmittedChange {
|
|
10
|
+
/** Table name */
|
|
11
|
+
table: string;
|
|
12
|
+
/** Row primary key */
|
|
13
|
+
row_id: string;
|
|
14
|
+
/** Operation type */
|
|
15
|
+
op: SyncOp;
|
|
16
|
+
/** Row data as JSON (null for deletes) */
|
|
17
|
+
row_json: unknown | null;
|
|
18
|
+
/** Row version for optimistic concurrency */
|
|
19
|
+
row_version: number | null;
|
|
20
|
+
/**
|
|
21
|
+
* Scope values for this change (stored as JSONB).
|
|
22
|
+
* Example: { user_id: 'U1', project_id: 'P1' }
|
|
23
|
+
*/
|
|
24
|
+
scopes: StoredScopes;
|
|
25
|
+
}
|
|
26
|
+
export interface ApplyOperationResult {
|
|
27
|
+
result: SyncOperationResult;
|
|
28
|
+
emittedChanges: EmittedChange[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Context for server operations.
|
|
32
|
+
*/
|
|
33
|
+
export interface ServerContext<DB extends SyncCoreDb = SyncCoreDb> {
|
|
34
|
+
/** Database connection (transaction in applyOperation) */
|
|
35
|
+
db: DbExecutor<DB>;
|
|
36
|
+
/** Actor ID (user ID from auth) */
|
|
37
|
+
actorId: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Context passed to snapshot method.
|
|
41
|
+
*/
|
|
42
|
+
export interface ServerSnapshotContext<DB extends SyncCoreDb = SyncCoreDb> extends ServerContext<DB> {
|
|
43
|
+
/** Database executor for the snapshot */
|
|
44
|
+
db: DbExecutor<DB>;
|
|
45
|
+
/** Effective scope values for this subscription */
|
|
46
|
+
scopeValues: ScopeValues;
|
|
47
|
+
/** Pagination cursor (row_id for keyset pagination) */
|
|
48
|
+
cursor: string | null;
|
|
49
|
+
/** Max rows to return */
|
|
50
|
+
limit: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Context passed to applyOperation method.
|
|
54
|
+
*/
|
|
55
|
+
export interface ServerApplyOperationContext<DB extends SyncCoreDb = SyncCoreDb> extends ServerContext<DB> {
|
|
56
|
+
/** Database executor for the operation */
|
|
57
|
+
trx: DbExecutor<DB>;
|
|
58
|
+
/** Client/device identifier */
|
|
59
|
+
clientId: string;
|
|
60
|
+
/** Unique commit identifier */
|
|
61
|
+
commitId: string;
|
|
62
|
+
/**
|
|
63
|
+
* Client's schema version when the commit was created.
|
|
64
|
+
* Use this to transform payloads from older client versions.
|
|
65
|
+
*/
|
|
66
|
+
schemaVersion: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Server-side scope configuration for advanced use cases.
|
|
70
|
+
* Use this when you need custom extraction, access control, or filtering.
|
|
71
|
+
*
|
|
72
|
+
* For simple cases, use the simplified scope array format:
|
|
73
|
+
* `scopes: ['user:{user_id}']`
|
|
74
|
+
*/
|
|
75
|
+
interface ServerScopeConfig {
|
|
76
|
+
/**
|
|
77
|
+
* Column name containing the scope value.
|
|
78
|
+
* For simple patterns like 'user:{user_id}' → column: 'user_id'
|
|
79
|
+
*/
|
|
80
|
+
column?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Custom extractor for complex patterns.
|
|
83
|
+
* Example: extract year/month from a date column
|
|
84
|
+
*/
|
|
85
|
+
extract?: (row: Record<string, unknown>) => Record<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* Optional access control per scope pattern.
|
|
88
|
+
* Return true if the actor can access this scope value.
|
|
89
|
+
*/
|
|
90
|
+
access?: (ctx: ServerContext, vars: Record<string, string>) => Promise<boolean>;
|
|
91
|
+
/**
|
|
92
|
+
* Optional filter builder for wildcard subscriptions.
|
|
93
|
+
* Called when the subscription uses wildcards for this pattern.
|
|
94
|
+
*/
|
|
95
|
+
toFilter?: (vars: Record<string, string | undefined>, query: unknown) => unknown;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Server shape options - configuration for a table's sync behavior.
|
|
99
|
+
*/
|
|
100
|
+
export interface ServerShapeOptions<DB extends SyncCoreDb = SyncCoreDb, Scopes extends Record<ScopePattern, Record<string, string>> = Record<ScopePattern, Record<string, string>>, TableName extends string = string, Params extends ZodSchema = ZodSchema> {
|
|
101
|
+
/**
|
|
102
|
+
* Scope patterns this shape uses.
|
|
103
|
+
* Array of pattern keys from SharedScopes.
|
|
104
|
+
*/
|
|
105
|
+
scopes: (keyof Scopes)[];
|
|
106
|
+
/**
|
|
107
|
+
* Scope definitions - how each pattern maps to row data.
|
|
108
|
+
* Defaults to using column name = variable name.
|
|
109
|
+
*/
|
|
110
|
+
scopeDefinitions?: Partial<Record<keyof Scopes, ServerScopeConfig>>;
|
|
111
|
+
/**
|
|
112
|
+
* Resolve allowed scope values for the current actor.
|
|
113
|
+
* Called once per request to determine what the actor can access.
|
|
114
|
+
*
|
|
115
|
+
* Returns scope values the actor is allowed to access.
|
|
116
|
+
* The server will intersect requested scopes with these.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* resolveScopes: async (ctx) => ({
|
|
120
|
+
* user_id: [ctx.user.id],
|
|
121
|
+
* project_id: ctx.user.projectIds,
|
|
122
|
+
* })
|
|
123
|
+
*/
|
|
124
|
+
resolveScopes: (ctx: ServerContext<DB>) => Promise<ScopeValues>;
|
|
125
|
+
/**
|
|
126
|
+
* Optional Zod schema for subscription parameters.
|
|
127
|
+
*/
|
|
128
|
+
params?: Params;
|
|
129
|
+
/**
|
|
130
|
+
* Primary key column (default: 'id')
|
|
131
|
+
*/
|
|
132
|
+
primaryKey?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Version column for optimistic concurrency (default: 'server_version')
|
|
135
|
+
*/
|
|
136
|
+
versionColumn?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Tables that must be bootstrapped before this one.
|
|
139
|
+
*/
|
|
140
|
+
dependsOn?: string[];
|
|
141
|
+
/**
|
|
142
|
+
* TTL for cached snapshot chunks (ms). Default: 24 hours.
|
|
143
|
+
*/
|
|
144
|
+
snapshotChunkTtlMs?: number;
|
|
145
|
+
/**
|
|
146
|
+
* Transform client payload → server row on writes.
|
|
147
|
+
*/
|
|
148
|
+
transformInbound?: (payload: Record<string, unknown>, ctx: ServerApplyOperationContext<DB>) => Partial<DB[TableName & keyof DB]>;
|
|
149
|
+
/**
|
|
150
|
+
* Transform server row → client payload on reads.
|
|
151
|
+
*/
|
|
152
|
+
transformOutbound?: (row: DB[TableName & keyof DB]) => Record<string, unknown>;
|
|
153
|
+
/**
|
|
154
|
+
* Custom snapshot implementation.
|
|
155
|
+
* Default uses keyset pagination ordered by primary key.
|
|
156
|
+
*/
|
|
157
|
+
snapshot?: (ctx: ServerSnapshotContext<DB>, params: Params extends ZodSchema ? z.infer<Params> : undefined) => Promise<{
|
|
158
|
+
rows: unknown[];
|
|
159
|
+
nextCursor: string | null;
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* Custom apply operation implementation.
|
|
163
|
+
*/
|
|
164
|
+
applyOperation?: (ctx: ServerApplyOperationContext<DB>, op: SyncOperation, opIndex: number) => Promise<ApplyOperationResult>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Server-side table handler for snapshots and mutations.
|
|
168
|
+
* This is the internal handler interface used by the sync engine.
|
|
169
|
+
*/
|
|
170
|
+
export interface ServerTableHandler<DB extends SyncCoreDb = SyncCoreDb> {
|
|
171
|
+
/** Table name */
|
|
172
|
+
table: string;
|
|
173
|
+
/** Scope patterns used by this shape */
|
|
174
|
+
scopePatterns: ScopePattern[];
|
|
175
|
+
/**
|
|
176
|
+
* Tables that must be bootstrapped before this one.
|
|
177
|
+
*/
|
|
178
|
+
dependsOn?: string[];
|
|
179
|
+
/**
|
|
180
|
+
* TTL for cached snapshot chunks (ms).
|
|
181
|
+
*/
|
|
182
|
+
snapshotChunkTtlMs?: number;
|
|
183
|
+
/**
|
|
184
|
+
* Resolve allowed scope values for the current actor.
|
|
185
|
+
*/
|
|
186
|
+
resolveScopes: (ctx: ServerContext<DB>) => Promise<ScopeValues>;
|
|
187
|
+
/**
|
|
188
|
+
* Extract stored scopes from a row.
|
|
189
|
+
*/
|
|
190
|
+
extractScopes: (row: Record<string, unknown>) => StoredScopes;
|
|
191
|
+
/**
|
|
192
|
+
* Build a bootstrap snapshot page.
|
|
193
|
+
*/
|
|
194
|
+
snapshot(ctx: ServerSnapshotContext<DB>, params: Record<string, unknown> | undefined): Promise<{
|
|
195
|
+
rows: unknown[];
|
|
196
|
+
nextCursor: string | null;
|
|
197
|
+
}>;
|
|
198
|
+
/**
|
|
199
|
+
* Apply a single operation.
|
|
200
|
+
*/
|
|
201
|
+
applyOperation(ctx: ServerApplyOperationContext<DB>, op: SyncOperation, opIndex: number): Promise<ApplyOperationResult>;
|
|
202
|
+
}
|
|
203
|
+
export {};
|
|
204
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shapes/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shapes/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,63 @@
|
|
|
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: "ndjson";
|
|
51
|
+
compression: "gzip";
|
|
52
|
+
}>;
|
|
53
|
+
readChunk: (chunkId: string) => Promise<Uint8Array<ArrayBufferLike> | null>;
|
|
54
|
+
findChunk: (pageKey: import("..").SnapshotChunkPageKey) => Promise<{
|
|
55
|
+
id: string;
|
|
56
|
+
byteLength: number;
|
|
57
|
+
sha256: string;
|
|
58
|
+
encoding: "ndjson";
|
|
59
|
+
compression: "gzip";
|
|
60
|
+
} | null>;
|
|
61
|
+
cleanupExpired: (beforeIso: string) => Promise<number>;
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=s3.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
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';
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server - Database-backed metadata store for snapshot chunks
|
|
3
|
+
*
|
|
4
|
+
* Stores chunk metadata in sync_snapshot_chunks_metadata table,
|
|
5
|
+
* body content in blob storage adapter.
|
|
6
|
+
*/
|
|
7
|
+
import type { BlobStorageAdapter, SyncSnapshotChunkRef } from '@syncular/core';
|
|
8
|
+
import type { Kysely } from 'kysely';
|
|
9
|
+
import type { SyncCoreDb } from '../schema';
|
|
10
|
+
import type { SnapshotChunkMetadata, SnapshotChunkPageKey } from './types';
|
|
11
|
+
export interface DbMetadataSnapshotChunkStorageOptions {
|
|
12
|
+
/** Database instance */
|
|
13
|
+
db: Kysely<SyncCoreDb>;
|
|
14
|
+
/** Blob storage adapter for body content */
|
|
15
|
+
blobAdapter: BlobStorageAdapter;
|
|
16
|
+
/** Optional prefix for chunk IDs */
|
|
17
|
+
chunkIdPrefix?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a snapshot chunk storage that uses:
|
|
21
|
+
* - Database for metadata (scope, commit seq, etc.)
|
|
22
|
+
* - Blob adapter for body content
|
|
23
|
+
*/
|
|
24
|
+
export declare function createDbMetadataChunkStorage(options: DbMetadataSnapshotChunkStorageOptions): {
|
|
25
|
+
name: string;
|
|
26
|
+
storeChunk: (metadata: Omit<SnapshotChunkMetadata, 'chunkId' | 'byteLength' | 'blobHash'> & {
|
|
27
|
+
body: Uint8Array;
|
|
28
|
+
}) => Promise<SyncSnapshotChunkRef>;
|
|
29
|
+
readChunk: (chunkId: string) => Promise<Uint8Array | null>;
|
|
30
|
+
findChunk: (pageKey: SnapshotChunkPageKey) => Promise<SyncSnapshotChunkRef | null>;
|
|
31
|
+
cleanupExpired: (beforeIso: string) => Promise<number>;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=db-metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-metadata.d.ts","sourceRoot":"","sources":["../../src/snapshot-chunks/db-metadata.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC/E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAE3E,MAAM,WAAW,qCAAqC;IACpD,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACvB,4CAA4C;IAC5C,WAAW,EAAE,kBAAkB,CAAC;IAChC,oCAAoC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,qCAAqC,GAC7C;IACD,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,CACV,QAAQ,EAAE,IAAI,CACZ,qBAAqB,EACrB,SAAS,GAAG,YAAY,GAAG,UAAU,CACtC,GAAG;QACF,IAAI,EAAE,UAAU,CAAC;KAClB,KACE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACnC,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3D,SAAS,EAAE,CACT,OAAO,EAAE,oBAAoB,KAC1B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAC1C,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACxD,CAiMA"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/server - Database-backed metadata store for snapshot chunks
|
|
3
|
+
*
|
|
4
|
+
* Stores chunk metadata in sync_snapshot_chunks_metadata table,
|
|
5
|
+
* body content in blob storage adapter.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
/**
|
|
9
|
+
* Create a snapshot chunk storage that uses:
|
|
10
|
+
* - Database for metadata (scope, commit seq, etc.)
|
|
11
|
+
* - Blob adapter for body content
|
|
12
|
+
*/
|
|
13
|
+
export function createDbMetadataChunkStorage(options) {
|
|
14
|
+
const { db, blobAdapter, chunkIdPrefix = 'chunk_' } = options;
|
|
15
|
+
// Generate deterministic blob hash from content
|
|
16
|
+
function computeBlobHash(body) {
|
|
17
|
+
return `sha256:${createHash('sha256').update(body).digest('hex')}`;
|
|
18
|
+
}
|
|
19
|
+
// Generate unique chunk ID
|
|
20
|
+
function generateChunkId() {
|
|
21
|
+
return `${chunkIdPrefix}${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
name: `db-metadata+${blobAdapter.name}`,
|
|
25
|
+
async storeChunk(metadata) {
|
|
26
|
+
const { body, ...metaWithoutBody } = metadata;
|
|
27
|
+
const blobHash = computeBlobHash(body);
|
|
28
|
+
const chunkId = generateChunkId();
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
// Check if blob already exists (content-addressed dedup)
|
|
31
|
+
const blobExists = await blobAdapter.exists(blobHash);
|
|
32
|
+
if (!blobExists) {
|
|
33
|
+
// Store body in blob adapter
|
|
34
|
+
if (blobAdapter.put) {
|
|
35
|
+
await blobAdapter.put(blobHash, body);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new Error(`Blob adapter ${blobAdapter.name} does not support direct put() for snapshot chunks`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Upsert metadata in database
|
|
42
|
+
await db
|
|
43
|
+
.insertInto('sync_snapshot_chunks')
|
|
44
|
+
.values({
|
|
45
|
+
chunk_id: chunkId,
|
|
46
|
+
partition_id: metaWithoutBody.partitionId,
|
|
47
|
+
scope_key: metaWithoutBody.scopeKey,
|
|
48
|
+
scope: metaWithoutBody.scope,
|
|
49
|
+
as_of_commit_seq: metaWithoutBody.asOfCommitSeq,
|
|
50
|
+
row_cursor: metaWithoutBody.rowCursor ?? '',
|
|
51
|
+
row_limit: metaWithoutBody.rowLimit,
|
|
52
|
+
encoding: metaWithoutBody.encoding,
|
|
53
|
+
compression: metaWithoutBody.compression,
|
|
54
|
+
sha256: metaWithoutBody.sha256,
|
|
55
|
+
byte_length: body.length,
|
|
56
|
+
blob_hash: blobHash,
|
|
57
|
+
expires_at: metaWithoutBody.expiresAt,
|
|
58
|
+
created_at: now,
|
|
59
|
+
})
|
|
60
|
+
.onConflict((oc) => oc
|
|
61
|
+
.columns([
|
|
62
|
+
'partition_id',
|
|
63
|
+
'scope_key',
|
|
64
|
+
'scope',
|
|
65
|
+
'as_of_commit_seq',
|
|
66
|
+
'row_cursor',
|
|
67
|
+
'row_limit',
|
|
68
|
+
'encoding',
|
|
69
|
+
'compression',
|
|
70
|
+
])
|
|
71
|
+
.doUpdateSet({
|
|
72
|
+
expires_at: metaWithoutBody.expiresAt,
|
|
73
|
+
blob_hash: blobHash,
|
|
74
|
+
sha256: metaWithoutBody.sha256,
|
|
75
|
+
byte_length: body.length,
|
|
76
|
+
row_cursor: metaWithoutBody.rowCursor ?? '',
|
|
77
|
+
}))
|
|
78
|
+
.execute();
|
|
79
|
+
return {
|
|
80
|
+
id: chunkId,
|
|
81
|
+
sha256: metaWithoutBody.sha256,
|
|
82
|
+
byteLength: body.length,
|
|
83
|
+
encoding: metaWithoutBody.encoding,
|
|
84
|
+
compression: metaWithoutBody.compression,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
async readChunk(chunkId) {
|
|
88
|
+
// Get metadata to find blob hash
|
|
89
|
+
const row = await db
|
|
90
|
+
.selectFrom('sync_snapshot_chunks')
|
|
91
|
+
.select(['blob_hash'])
|
|
92
|
+
.where('chunk_id', '=', chunkId)
|
|
93
|
+
.executeTakeFirst();
|
|
94
|
+
if (!row)
|
|
95
|
+
return null;
|
|
96
|
+
// Read from blob adapter
|
|
97
|
+
if (blobAdapter.get) {
|
|
98
|
+
return blobAdapter.get(row.blob_hash);
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Blob adapter ${blobAdapter.name} does not support direct get() for snapshot chunks`);
|
|
101
|
+
},
|
|
102
|
+
async findChunk(pageKey) {
|
|
103
|
+
const nowIso = new Date().toISOString();
|
|
104
|
+
const rowCursorKey = pageKey.rowCursor ?? '';
|
|
105
|
+
const row = await db
|
|
106
|
+
.selectFrom('sync_snapshot_chunks')
|
|
107
|
+
.select([
|
|
108
|
+
'chunk_id',
|
|
109
|
+
'sha256',
|
|
110
|
+
'byte_length',
|
|
111
|
+
'encoding',
|
|
112
|
+
'compression',
|
|
113
|
+
])
|
|
114
|
+
.where('partition_id', '=', pageKey.partitionId)
|
|
115
|
+
.where('scope_key', '=', pageKey.scopeKey)
|
|
116
|
+
.where('scope', '=', pageKey.scope)
|
|
117
|
+
.where('as_of_commit_seq', '=', pageKey.asOfCommitSeq)
|
|
118
|
+
.where('row_cursor', '=', rowCursorKey)
|
|
119
|
+
.where('row_limit', '=', pageKey.rowLimit)
|
|
120
|
+
.where('encoding', '=', pageKey.encoding)
|
|
121
|
+
.where('compression', '=', pageKey.compression)
|
|
122
|
+
.where('expires_at', '>', nowIso)
|
|
123
|
+
.executeTakeFirst();
|
|
124
|
+
if (!row)
|
|
125
|
+
return null;
|
|
126
|
+
if (row.encoding !== 'ndjson') {
|
|
127
|
+
throw new Error(`Unexpected snapshot chunk encoding: ${String(row.encoding)}`);
|
|
128
|
+
}
|
|
129
|
+
if (row.compression !== 'gzip') {
|
|
130
|
+
throw new Error(`Unexpected snapshot chunk compression: ${String(row.compression)}`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id: row.chunk_id,
|
|
134
|
+
sha256: row.sha256,
|
|
135
|
+
byteLength: Number(row.byte_length ?? 0),
|
|
136
|
+
encoding: row.encoding,
|
|
137
|
+
compression: row.compression,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
async cleanupExpired(beforeIso) {
|
|
141
|
+
// Find expired chunks
|
|
142
|
+
const expiredRows = await db
|
|
143
|
+
.selectFrom('sync_snapshot_chunks')
|
|
144
|
+
.select(['chunk_id', 'blob_hash'])
|
|
145
|
+
.where('expires_at', '<=', beforeIso)
|
|
146
|
+
.execute();
|
|
147
|
+
if (expiredRows.length === 0)
|
|
148
|
+
return 0;
|
|
149
|
+
// Delete from blob storage (best effort)
|
|
150
|
+
for (const row of expiredRows) {
|
|
151
|
+
try {
|
|
152
|
+
await blobAdapter.delete(row.blob_hash);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Ignore deletion errors - blob may be shared or already deleted
|
|
156
|
+
// Log for observability but don't fail the cleanup
|
|
157
|
+
console.warn(`Failed to delete blob ${row.blob_hash} for chunk ${row.chunk_id}, may be already deleted or shared`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Delete metadata from database
|
|
161
|
+
const result = await db
|
|
162
|
+
.deleteFrom('sync_snapshot_chunks')
|
|
163
|
+
.where('expires_at', '<=', beforeIso)
|
|
164
|
+
.executeTakeFirst();
|
|
165
|
+
return Number(result.numDeletedRows ?? 0);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=db-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-metadata.js","sourceRoot":"","sources":["../../src/snapshot-chunks/db-metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/snapshot-chunks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/snapshot-chunks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
import type { SyncSnapshotChunkRef } from '@syncular/core';
|
|
8
|
+
/**
|
|
9
|
+
* Page key for identifying a specific chunk
|
|
10
|
+
*/
|
|
11
|
+
export interface SnapshotChunkPageKey {
|
|
12
|
+
partitionId: string;
|
|
13
|
+
scopeKey: string;
|
|
14
|
+
scope: string;
|
|
15
|
+
asOfCommitSeq: number;
|
|
16
|
+
rowCursor: string | null;
|
|
17
|
+
rowLimit: number;
|
|
18
|
+
encoding: 'ndjson';
|
|
19
|
+
compression: 'gzip';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Metadata stored in the database for each chunk
|
|
23
|
+
*/
|
|
24
|
+
export interface SnapshotChunkMetadata {
|
|
25
|
+
chunkId: string;
|
|
26
|
+
partitionId: string;
|
|
27
|
+
scopeKey: string;
|
|
28
|
+
scope: string;
|
|
29
|
+
asOfCommitSeq: number;
|
|
30
|
+
rowCursor: string | null;
|
|
31
|
+
rowLimit: number;
|
|
32
|
+
encoding: 'ndjson';
|
|
33
|
+
compression: 'gzip';
|
|
34
|
+
sha256: string;
|
|
35
|
+
byteLength: number;
|
|
36
|
+
blobHash: string;
|
|
37
|
+
expiresAt: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Storage interface for snapshot chunks
|
|
41
|
+
*/
|
|
42
|
+
export interface SnapshotChunkStorage {
|
|
43
|
+
/** Storage adapter name */
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/**
|
|
46
|
+
* Store a chunk. Returns chunk reference.
|
|
47
|
+
* If chunk with same content already exists (by hash), returns existing reference.
|
|
48
|
+
*/
|
|
49
|
+
storeChunk(metadata: Omit<SnapshotChunkMetadata, 'chunkId' | 'byteLength' | 'blobHash'> & {
|
|
50
|
+
body: Uint8Array;
|
|
51
|
+
}): Promise<SyncSnapshotChunkRef>;
|
|
52
|
+
/**
|
|
53
|
+
* Read chunk body by chunk ID
|
|
54
|
+
*/
|
|
55
|
+
readChunk(chunkId: string): Promise<Uint8Array | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Find existing chunk by page key
|
|
58
|
+
*/
|
|
59
|
+
findChunk(pageKey: SnapshotChunkPageKey): Promise<SyncSnapshotChunkRef | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Delete expired chunks. Returns number deleted.
|
|
62
|
+
*/
|
|
63
|
+
cleanupExpired(beforeIso: string): Promise<number>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/snapshot-chunks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D;;GAEG;AACH,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;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,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,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,2BAA2B;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,UAAU,CACR,QAAQ,EAAE,IAAI,CACZ,qBAAqB,EACrB,SAAS,GAAG,YAAY,GAAG,UAAU,CACtC,GAAG;QACF,IAAI,EAAE,UAAU,CAAC;KAClB,GACA,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEjC;;OAEG;IACH,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAEvD;;OAEG;IACH,SAAS,CACP,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IAExC;;OAEG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACpD"}
|