@syncular/server 0.1.2 → 0.2.0
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/README.md +773 -13
- package/dist/admin.d.ts +136 -0
- package/dist/admin.js +168 -0
- package/dist/blob-handlers.d.ts +69 -0
- package/dist/blob-handlers.js +245 -0
- package/dist/blob-store.d.ts +111 -0
- package/dist/blob-store.js +0 -0
- package/dist/content-encoding.d.ts +27 -0
- package/dist/content-encoding.js +69 -0
- package/dist/context.d.ts +151 -0
- package/dist/context.js +21 -0
- package/dist/crdt-merger.d.ts +28 -0
- package/dist/crdt-merger.js +11 -0
- package/dist/d1-storage.d.ts +50 -0
- package/dist/d1-storage.js +0 -0
- package/dist/errors.d.ts +35 -9
- package/dist/errors.js +220 -11
- package/dist/events-ring.d.ts +55 -0
- package/dist/events-ring.js +96 -0
- package/dist/events.d.ts +231 -0
- package/dist/events.js +22 -0
- package/dist/frame-bytes.d.ts +20 -0
- package/dist/frame-bytes.js +75 -0
- package/dist/handler.d.ts +9 -0
- package/dist/handler.js +490 -0
- package/dist/index.d.ts +42 -33
- package/dist/index.js +45 -27
- package/dist/lease-store.d.ts +50 -0
- package/dist/lease-store.js +0 -0
- package/dist/pg-executor-pglite.d.ts +27 -0
- package/dist/pg-executor-pglite.js +33 -0
- package/dist/pg-executor.d.ts +67 -0
- package/dist/pg-executor.js +56 -0
- package/dist/postgres-fanout.d.ts +83 -0
- package/dist/postgres-fanout.js +71 -0
- package/dist/postgres-storage.d.ts +69 -0
- package/dist/postgres-storage.js +766 -0
- package/dist/prune.d.ts +24 -39
- package/dist/prune.js +44 -146
- package/dist/pull.d.ts +42 -67
- package/dist/pull.js +387 -1187
- package/dist/push.d.ts +31 -66
- package/dist/push.js +406 -655
- package/dist/realtime.d.ts +197 -0
- package/dist/realtime.js +848 -0
- package/dist/relational-rows.d.ts +229 -0
- package/dist/relational-rows.js +514 -0
- package/dist/s3-blob-store.d.ts +142 -0
- package/dist/s3-blob-store.js +458 -0
- package/dist/s3-segment-store.d.ts +95 -0
- package/dist/s3-segment-store.js +380 -0
- package/dist/schema.d.ts +82 -286
- package/dist/schema.js +133 -9
- package/dist/scopes.d.ts +48 -0
- package/dist/scopes.js +98 -0
- package/dist/segment-download.d.ts +14 -0
- package/dist/segment-download.js +125 -0
- package/dist/segment-store.d.ts +81 -0
- package/dist/segment-store.js +63 -0
- package/dist/signed-url.d.ts +139 -0
- package/dist/signed-url.js +143 -0
- package/dist/sigv4.d.ts +97 -0
- package/dist/sigv4.js +161 -0
- package/dist/sqlite-blob-store.d.ts +22 -0
- package/dist/sqlite-blob-store.js +86 -0
- package/dist/sqlite-dialect.d.ts +101 -0
- package/dist/sqlite-dialect.js +250 -0
- package/dist/sqlite-image.d.ts +27 -0
- package/dist/sqlite-image.js +83 -0
- package/dist/sqlite-lease-store.d.ts +21 -0
- package/dist/sqlite-lease-store.js +94 -0
- package/dist/sqlite-segment-store.d.ts +25 -0
- package/dist/sqlite-segment-store.js +115 -0
- package/dist/sqlite-storage.d.ts +47 -0
- package/dist/sqlite-storage.js +477 -0
- package/dist/storage.d.ts +252 -0
- package/dist/storage.js +1 -0
- package/dist/validate.d.ts +83 -0
- package/dist/validate.js +44 -0
- package/package.json +17 -325
- package/src/admin.ts +312 -0
- package/src/blob-handlers.ts +340 -0
- package/src/blob-store.ts +0 -0
- package/src/content-encoding.ts +94 -0
- package/src/context.ts +176 -0
- package/src/crdt-merger.ts +33 -0
- package/src/d1-storage.ts +0 -0
- package/src/errors.ts +241 -19
- package/src/events-ring.ts +116 -0
- package/src/events.ts +291 -0
- package/src/frame-bytes.ts +94 -0
- package/src/handler.ts +678 -0
- package/src/index.ts +44 -27
- package/src/lease-store.ts +0 -0
- package/src/pg-executor-pglite.ts +63 -0
- package/src/pg-executor.ts +87 -0
- package/src/postgres-fanout.ts +143 -0
- package/src/postgres-storage.ts +1182 -0
- package/src/prune.ts +63 -289
- package/src/pull.ts +505 -1805
- package/src/push.ts +623 -1006
- package/src/realtime.ts +1082 -0
- package/src/relational-rows.ts +633 -0
- package/src/s3-blob-store.ts +616 -0
- package/src/s3-segment-store.ts +519 -0
- package/src/schema.ts +242 -281
- package/src/scopes.ts +127 -0
- package/src/segment-download.ts +169 -0
- package/src/segment-store.ts +154 -0
- package/src/signed-url.ts +308 -0
- package/src/sigv4.ts +267 -0
- package/src/sqlite-blob-store.ts +125 -0
- package/src/sqlite-dialect.ts +337 -0
- package/src/sqlite-image.ts +124 -0
- package/src/sqlite-lease-store.ts +144 -0
- package/src/sqlite-segment-store.ts +204 -0
- package/src/sqlite-storage.ts +792 -0
- package/src/storage.ts +316 -0
- package/src/validate.ts +123 -0
- package/dist/auth-leases.d.ts +0 -85
- package/dist/auth-leases.d.ts.map +0 -1
- package/dist/auth-leases.js +0 -368
- package/dist/auth-leases.js.map +0 -1
- package/dist/better-sqlite3.d.ts +0 -22
- package/dist/better-sqlite3.d.ts.map +0 -1
- package/dist/better-sqlite3.js +0 -16
- package/dist/better-sqlite3.js.map +0 -1
- package/dist/blobs/access.d.ts +0 -35
- package/dist/blobs/access.d.ts.map +0 -1
- package/dist/blobs/access.js +0 -149
- package/dist/blobs/access.js.map +0 -1
- package/dist/blobs/adapters/database.d.ts +0 -100
- package/dist/blobs/adapters/database.d.ts.map +0 -1
- package/dist/blobs/adapters/database.js +0 -247
- package/dist/blobs/adapters/database.js.map +0 -1
- package/dist/blobs/index.d.ts +0 -9
- package/dist/blobs/index.d.ts.map +0 -1
- package/dist/blobs/index.js +0 -9
- package/dist/blobs/index.js.map +0 -1
- package/dist/blobs/manager.d.ts +0 -220
- package/dist/blobs/manager.d.ts.map +0 -1
- package/dist/blobs/manager.js +0 -625
- package/dist/blobs/manager.js.map +0 -1
- package/dist/blobs/migrate.d.ts +0 -27
- package/dist/blobs/migrate.d.ts.map +0 -1
- package/dist/blobs/migrate.js +0 -127
- package/dist/blobs/migrate.js.map +0 -1
- package/dist/blobs/types.d.ts +0 -58
- package/dist/blobs/types.d.ts.map +0 -1
- package/dist/blobs/types.js +0 -5
- package/dist/blobs/types.js.map +0 -1
- package/dist/bun-sqlite.d.ts +0 -19
- package/dist/bun-sqlite.d.ts.map +0 -1
- package/dist/bun-sqlite.js +0 -19
- package/dist/bun-sqlite.js.map +0 -1
- package/dist/clients.d.ts +0 -15
- package/dist/clients.d.ts.map +0 -1
- package/dist/clients.js +0 -7
- package/dist/clients.js.map +0 -1
- package/dist/cloudflare/durable-object.d.ts +0 -93
- package/dist/cloudflare/durable-object.d.ts.map +0 -1
- package/dist/cloudflare/durable-object.js +0 -210
- package/dist/cloudflare/durable-object.js.map +0 -1
- package/dist/cloudflare/index.d.ts +0 -22
- package/dist/cloudflare/index.d.ts.map +0 -1
- package/dist/cloudflare/index.js +0 -22
- package/dist/cloudflare/index.js.map +0 -1
- package/dist/cloudflare/r2.d.ts +0 -180
- package/dist/cloudflare/r2.d.ts.map +0 -1
- package/dist/cloudflare/r2.js +0 -258
- package/dist/cloudflare/r2.js.map +0 -1
- package/dist/cloudflare/scope-cache.d.ts +0 -54
- package/dist/cloudflare/scope-cache.d.ts.map +0 -1
- package/dist/cloudflare/scope-cache.js +0 -223
- package/dist/cloudflare/scope-cache.js.map +0 -1
- package/dist/cloudflare/sentry.d.ts +0 -47
- package/dist/cloudflare/sentry.d.ts.map +0 -1
- package/dist/cloudflare/sentry.js +0 -163
- package/dist/cloudflare/sentry.js.map +0 -1
- package/dist/cloudflare/worker.d.ts +0 -46
- package/dist/cloudflare/worker.d.ts.map +0 -1
- package/dist/cloudflare/worker.js +0 -63
- package/dist/cloudflare/worker.js.map +0 -1
- package/dist/commit-integrity.d.ts +0 -38
- package/dist/commit-integrity.d.ts.map +0 -1
- package/dist/commit-integrity.js +0 -260
- package/dist/commit-integrity.js.map +0 -1
- package/dist/compaction.d.ts +0 -27
- package/dist/compaction.d.ts.map +0 -1
- package/dist/compaction.js +0 -49
- package/dist/compaction.js.map +0 -1
- package/dist/crdt-yjs/index.d.ts +0 -99
- package/dist/crdt-yjs/index.d.ts.map +0 -1
- package/dist/crdt-yjs/index.js +0 -629
- package/dist/crdt-yjs/index.js.map +0 -1
- package/dist/d1.d.ts +0 -13
- package/dist/d1.d.ts.map +0 -1
- package/dist/d1.js +0 -14
- package/dist/d1.js.map +0 -1
- package/dist/dialect/base.d.ts +0 -93
- package/dist/dialect/base.d.ts.map +0 -1
- package/dist/dialect/base.js +0 -181
- package/dist/dialect/base.js.map +0 -1
- package/dist/dialect/helpers.d.ts +0 -16
- package/dist/dialect/helpers.d.ts.map +0 -1
- package/dist/dialect/helpers.js +0 -83
- package/dist/dialect/helpers.js.map +0 -1
- package/dist/dialect/index.d.ts +0 -7
- package/dist/dialect/index.d.ts.map +0 -1
- package/dist/dialect/index.js +0 -7
- package/dist/dialect/index.js.map +0 -1
- package/dist/dialect/types.d.ts +0 -187
- package/dist/dialect/types.d.ts.map +0 -1
- package/dist/dialect/types.js +0 -8
- package/dist/dialect/types.js.map +0 -1
- package/dist/encrypted-crdt.d.ts +0 -67
- package/dist/encrypted-crdt.d.ts.map +0 -1
- package/dist/encrypted-crdt.js +0 -426
- package/dist/encrypted-crdt.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/filesystem/index.d.ts +0 -44
- package/dist/filesystem/index.d.ts.map +0 -1
- package/dist/filesystem/index.js +0 -164
- package/dist/filesystem/index.js.map +0 -1
- package/dist/handlers/collection.d.ts +0 -20
- package/dist/handlers/collection.d.ts.map +0 -1
- package/dist/handlers/collection.js +0 -42
- package/dist/handlers/collection.js.map +0 -1
- package/dist/handlers/create-handler.d.ts +0 -156
- package/dist/handlers/create-handler.d.ts.map +0 -1
- package/dist/handlers/create-handler.js +0 -626
- package/dist/handlers/create-handler.js.map +0 -1
- package/dist/handlers/index.d.ts +0 -4
- package/dist/handlers/index.d.ts.map +0 -1
- package/dist/handlers/index.js +0 -4
- package/dist/handlers/index.js.map +0 -1
- package/dist/handlers/types.d.ts +0 -295
- package/dist/handlers/types.d.ts.map +0 -1
- package/dist/handlers/types.js +0 -2
- package/dist/handlers/types.js.map +0 -1
- package/dist/helpers/conflict.d.ts +0 -52
- package/dist/helpers/conflict.d.ts.map +0 -1
- package/dist/helpers/conflict.js +0 -49
- package/dist/helpers/conflict.js.map +0 -1
- package/dist/helpers/emitted-change.d.ts +0 -56
- package/dist/helpers/emitted-change.d.ts.map +0 -1
- package/dist/helpers/emitted-change.js +0 -46
- package/dist/helpers/emitted-change.js.map +0 -1
- package/dist/helpers/index.d.ts +0 -12
- package/dist/helpers/index.d.ts.map +0 -1
- package/dist/helpers/index.js +0 -12
- package/dist/helpers/index.js.map +0 -1
- package/dist/helpers/paginate.d.ts +0 -49
- package/dist/helpers/paginate.d.ts.map +0 -1
- package/dist/helpers/paginate.js +0 -54
- package/dist/helpers/paginate.js.map +0 -1
- package/dist/helpers/scope-authorization.d.ts +0 -7
- package/dist/helpers/scope-authorization.d.ts.map +0 -1
- package/dist/helpers/scope-authorization.js +0 -19
- package/dist/helpers/scope-authorization.js.map +0 -1
- package/dist/helpers/scope-commit-index.d.ts +0 -12
- package/dist/helpers/scope-commit-index.d.ts.map +0 -1
- package/dist/helpers/scope-commit-index.js +0 -35
- package/dist/helpers/scope-commit-index.js.map +0 -1
- package/dist/helpers/scope-strings.d.ts +0 -74
- package/dist/helpers/scope-strings.d.ts.map +0 -1
- package/dist/helpers/scope-strings.js +0 -82
- package/dist/helpers/scope-strings.js.map +0 -1
- package/dist/hono/api-key-auth.d.ts +0 -49
- package/dist/hono/api-key-auth.d.ts.map +0 -1
- package/dist/hono/api-key-auth.js +0 -108
- package/dist/hono/api-key-auth.js.map +0 -1
- package/dist/hono/audit-redaction.d.ts +0 -20
- package/dist/hono/audit-redaction.d.ts.map +0 -1
- package/dist/hono/audit-redaction.js +0 -85
- package/dist/hono/audit-redaction.js.map +0 -1
- package/dist/hono/blobs.d.ts +0 -75
- package/dist/hono/blobs.d.ts.map +0 -1
- package/dist/hono/blobs.js +0 -586
- package/dist/hono/blobs.js.map +0 -1
- package/dist/hono/console/gateway.d.ts +0 -42
- package/dist/hono/console/gateway.d.ts.map +0 -1
- package/dist/hono/console/gateway.js +0 -2158
- package/dist/hono/console/gateway.js.map +0 -1
- package/dist/hono/console/live-auth.d.ts +0 -7
- package/dist/hono/console/live-auth.d.ts.map +0 -1
- package/dist/hono/console/live-auth.js +0 -39
- package/dist/hono/console/live-auth.js.map +0 -1
- package/dist/hono/console/route-descriptor.d.ts +0 -6
- package/dist/hono/console/route-descriptor.d.ts.map +0 -1
- package/dist/hono/console/route-descriptor.js +0 -16
- package/dist/hono/console/route-descriptor.js.map +0 -1
- package/dist/hono/console/routes/api-keys.d.ts +0 -8
- package/dist/hono/console/routes/api-keys.d.ts.map +0 -1
- package/dist/hono/console/routes/api-keys.js +0 -575
- package/dist/hono/console/routes/api-keys.js.map +0 -1
- package/dist/hono/console/routes/clients.d.ts +0 -8
- package/dist/hono/console/routes/clients.d.ts.map +0 -1
- package/dist/hono/console/routes/clients.js +0 -322
- package/dist/hono/console/routes/clients.js.map +0 -1
- package/dist/hono/console/routes/commits.d.ts +0 -8
- package/dist/hono/console/routes/commits.d.ts.map +0 -1
- package/dist/hono/console/routes/commits.js +0 -882
- package/dist/hono/console/routes/commits.js.map +0 -1
- package/dist/hono/console/routes/context.d.ts +0 -188
- package/dist/hono/console/routes/context.d.ts.map +0 -1
- package/dist/hono/console/routes/context.js +0 -633
- package/dist/hono/console/routes/context.js.map +0 -1
- package/dist/hono/console/routes/events.d.ts +0 -8
- package/dist/hono/console/routes/events.d.ts.map +0 -1
- package/dist/hono/console/routes/events.js +0 -501
- package/dist/hono/console/routes/events.js.map +0 -1
- package/dist/hono/console/routes/maintenance.d.ts +0 -8
- package/dist/hono/console/routes/maintenance.d.ts.map +0 -1
- package/dist/hono/console/routes/maintenance.js +0 -341
- package/dist/hono/console/routes/maintenance.js.map +0 -1
- package/dist/hono/console/routes/shared.d.ts +0 -174
- package/dist/hono/console/routes/shared.d.ts.map +0 -1
- package/dist/hono/console/routes/shared.js +0 -583
- package/dist/hono/console/routes/shared.js.map +0 -1
- package/dist/hono/console/routes/stats.d.ts +0 -8
- package/dist/hono/console/routes/stats.d.ts.map +0 -1
- package/dist/hono/console/routes/stats.js +0 -295
- package/dist/hono/console/routes/stats.js.map +0 -1
- package/dist/hono/console/routes/storage.d.ts +0 -8
- package/dist/hono/console/routes/storage.d.ts.map +0 -1
- package/dist/hono/console/routes/storage.js +0 -121
- package/dist/hono/console/routes/storage.js.map +0 -1
- package/dist/hono/console/routes.d.ts +0 -36
- package/dist/hono/console/routes.d.ts.map +0 -1
- package/dist/hono/console/routes.js +0 -112
- package/dist/hono/console/routes.js.map +0 -1
- package/dist/hono/console/schema-errors.d.ts +0 -2
- package/dist/hono/console/schema-errors.d.ts.map +0 -1
- package/dist/hono/console/schema-errors.js +0 -17
- package/dist/hono/console/schema-errors.js.map +0 -1
- package/dist/hono/console/schemas.d.ts +0 -1515
- package/dist/hono/console/schemas.d.ts.map +0 -1
- package/dist/hono/console/schemas.js +0 -661
- package/dist/hono/console/schemas.js.map +0 -1
- package/dist/hono/console/types.d.ts +0 -213
- package/dist/hono/console/types.d.ts.map +0 -1
- package/dist/hono/console/types.js +0 -2
- package/dist/hono/console/types.js.map +0 -1
- package/dist/hono/console/ui.d.ts +0 -38
- package/dist/hono/console/ui.d.ts.map +0 -1
- package/dist/hono/console/ui.js +0 -43
- package/dist/hono/console/ui.js.map +0 -1
- package/dist/hono/create-server.d.ts +0 -71
- package/dist/hono/create-server.d.ts.map +0 -1
- package/dist/hono/create-server.js +0 -121
- package/dist/hono/create-server.js.map +0 -1
- package/dist/hono/errors.d.ts +0 -13
- package/dist/hono/errors.d.ts.map +0 -1
- package/dist/hono/errors.js +0 -21
- package/dist/hono/errors.js.map +0 -1
- package/dist/hono/index.d.ts +0 -20
- package/dist/hono/index.d.ts.map +0 -1
- package/dist/hono/index.js +0 -31
- package/dist/hono/index.js.map +0 -1
- package/dist/hono/openapi.d.ts +0 -72
- package/dist/hono/openapi.d.ts.map +0 -1
- package/dist/hono/openapi.js +0 -99
- package/dist/hono/openapi.js.map +0 -1
- package/dist/hono/proxy/connection-manager.d.ts +0 -78
- package/dist/hono/proxy/connection-manager.d.ts.map +0 -1
- package/dist/hono/proxy/connection-manager.js +0 -251
- package/dist/hono/proxy/connection-manager.js.map +0 -1
- package/dist/hono/proxy/index.d.ts +0 -8
- package/dist/hono/proxy/index.d.ts.map +0 -1
- package/dist/hono/proxy/index.js +0 -8
- package/dist/hono/proxy/index.js.map +0 -1
- package/dist/hono/proxy/routes.d.ts +0 -86
- package/dist/hono/proxy/routes.d.ts.map +0 -1
- package/dist/hono/proxy/routes.js +0 -183
- package/dist/hono/proxy/routes.js.map +0 -1
- package/dist/hono/rate-limit.d.ts +0 -101
- package/dist/hono/rate-limit.d.ts.map +0 -1
- package/dist/hono/rate-limit.js +0 -184
- package/dist/hono/rate-limit.js.map +0 -1
- package/dist/hono/realtime-sync-packs.d.ts +0 -43
- package/dist/hono/realtime-sync-packs.d.ts.map +0 -1
- package/dist/hono/realtime-sync-packs.js +0 -219
- package/dist/hono/realtime-sync-packs.js.map +0 -1
- package/dist/hono/routes/audit.d.ts +0 -12
- package/dist/hono/routes/audit.d.ts.map +0 -1
- package/dist/hono/routes/audit.js +0 -385
- package/dist/hono/routes/audit.js.map +0 -1
- package/dist/hono/routes/auth-leases.d.ts +0 -8
- package/dist/hono/routes/auth-leases.d.ts.map +0 -1
- package/dist/hono/routes/auth-leases.js +0 -88
- package/dist/hono/routes/auth-leases.js.map +0 -1
- package/dist/hono/routes/combined.d.ts +0 -8
- package/dist/hono/routes/combined.d.ts.map +0 -1
- package/dist/hono/routes/combined.js +0 -392
- package/dist/hono/routes/combined.js.map +0 -1
- package/dist/hono/routes/context.d.ts +0 -314
- package/dist/hono/routes/context.d.ts.map +0 -1
- package/dist/hono/routes/context.js +0 -1147
- package/dist/hono/routes/context.js.map +0 -1
- package/dist/hono/routes/health.d.ts +0 -8
- package/dist/hono/routes/health.d.ts.map +0 -1
- package/dist/hono/routes/health.js +0 -16
- package/dist/hono/routes/health.js.map +0 -1
- package/dist/hono/routes/realtime.d.ts +0 -9
- package/dist/hono/routes/realtime.d.ts.map +0 -1
- package/dist/hono/routes/realtime.js +0 -626
- package/dist/hono/routes/realtime.js.map +0 -1
- package/dist/hono/routes/shared.d.ts +0 -689
- package/dist/hono/routes/shared.d.ts.map +0 -1
- package/dist/hono/routes/shared.js +0 -972
- package/dist/hono/routes/shared.js.map +0 -1
- package/dist/hono/routes/snapshots.d.ts +0 -10
- package/dist/hono/routes/snapshots.d.ts.map +0 -1
- package/dist/hono/routes/snapshots.js +0 -281
- package/dist/hono/routes/snapshots.js.map +0 -1
- package/dist/hono/routes.d.ts +0 -19
- package/dist/hono/routes.d.ts.map +0 -1
- package/dist/hono/routes.js +0 -37
- package/dist/hono/routes.js.map +0 -1
- package/dist/hono/validation.d.ts +0 -5
- package/dist/hono/validation.d.ts.map +0 -1
- package/dist/hono/validation.js +0 -47
- package/dist/hono/validation.js.map +0 -1
- package/dist/hono/websocket-origin.d.ts +0 -9
- package/dist/hono/websocket-origin.d.ts.map +0 -1
- package/dist/hono/websocket-origin.js +0 -96
- package/dist/hono/websocket-origin.js.map +0 -1
- package/dist/hono/ws.d.ts +0 -341
- package/dist/hono/ws.d.ts.map +0 -1
- package/dist/hono/ws.js +0 -711
- package/dist/hono/ws.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/libsql.d.ts +0 -29
- package/dist/libsql.d.ts.map +0 -1
- package/dist/libsql.js +0 -25
- package/dist/libsql.js.map +0 -1
- package/dist/migrate.d.ts +0 -14
- package/dist/migrate.d.ts.map +0 -1
- package/dist/migrate.js +0 -13
- package/dist/migrate.js.map +0 -1
- package/dist/neon.d.ts +0 -21
- package/dist/neon.d.ts.map +0 -1
- package/dist/neon.js +0 -22
- package/dist/neon.js.map +0 -1
- package/dist/notify.d.ts +0 -77
- package/dist/notify.d.ts.map +0 -1
- package/dist/notify.js +0 -233
- package/dist/notify.js.map +0 -1
- package/dist/pglite.d.ts +0 -37
- package/dist/pglite.d.ts.map +0 -1
- package/dist/pglite.js +0 -37
- package/dist/pglite.js.map +0 -1
- package/dist/plugins/index.d.ts +0 -2
- package/dist/plugins/index.d.ts.map +0 -1
- package/dist/plugins/index.js +0 -2
- package/dist/plugins/index.js.map +0 -1
- package/dist/plugins/types.d.ts +0 -73
- package/dist/plugins/types.d.ts.map +0 -1
- package/dist/plugins/types.js +0 -30
- package/dist/plugins/types.js.map +0 -1
- package/dist/postgres/index.d.ts +0 -68
- package/dist/postgres/index.d.ts.map +0 -1
- package/dist/postgres/index.js +0 -924
- package/dist/postgres/index.js.map +0 -1
- package/dist/proxy/collection.d.ts +0 -7
- package/dist/proxy/collection.d.ts.map +0 -1
- package/dist/proxy/collection.js +0 -6
- package/dist/proxy/collection.js.map +0 -1
- package/dist/proxy/handler.d.ts +0 -42
- package/dist/proxy/handler.d.ts.map +0 -1
- package/dist/proxy/handler.js +0 -102
- package/dist/proxy/handler.js.map +0 -1
- package/dist/proxy/index.d.ts +0 -9
- package/dist/proxy/index.d.ts.map +0 -1
- package/dist/proxy/index.js +0 -14
- package/dist/proxy/index.js.map +0 -1
- package/dist/proxy/mutation-detector.d.ts +0 -35
- package/dist/proxy/mutation-detector.d.ts.map +0 -1
- package/dist/proxy/mutation-detector.js +0 -246
- package/dist/proxy/mutation-detector.js.map +0 -1
- package/dist/proxy/oplog.d.ts +0 -30
- package/dist/proxy/oplog.d.ts.map +0 -1
- package/dist/proxy/oplog.js +0 -137
- package/dist/proxy/oplog.js.map +0 -1
- package/dist/proxy/types.d.ts +0 -44
- package/dist/proxy/types.d.ts.map +0 -1
- package/dist/proxy/types.js +0 -7
- package/dist/proxy/types.js.map +0 -1
- package/dist/prune.d.ts.map +0 -1
- package/dist/prune.js.map +0 -1
- package/dist/pull.d.ts.map +0 -1
- package/dist/pull.js.map +0 -1
- package/dist/push.d.ts.map +0 -1
- package/dist/push.js.map +0 -1
- package/dist/realtime/in-memory.d.ts +0 -13
- package/dist/realtime/in-memory.d.ts.map +0 -1
- package/dist/realtime/in-memory.js +0 -28
- package/dist/realtime/in-memory.js.map +0 -1
- package/dist/realtime/index.d.ts +0 -4
- package/dist/realtime/index.d.ts.map +0 -1
- package/dist/realtime/index.js +0 -3
- package/dist/realtime/index.js.map +0 -1
- package/dist/realtime/types.d.ts +0 -62
- package/dist/realtime/types.d.ts.map +0 -1
- package/dist/realtime/types.js +0 -19
- package/dist/realtime/types.js.map +0 -1
- package/dist/relay/client-role/forward-engine.d.ts +0 -63
- package/dist/relay/client-role/forward-engine.d.ts.map +0 -1
- package/dist/relay/client-role/forward-engine.js +0 -267
- package/dist/relay/client-role/forward-engine.js.map +0 -1
- package/dist/relay/client-role/index.d.ts +0 -9
- package/dist/relay/client-role/index.d.ts.map +0 -1
- package/dist/relay/client-role/index.js +0 -9
- package/dist/relay/client-role/index.js.map +0 -1
- package/dist/relay/client-role/pull-engine.d.ts +0 -72
- package/dist/relay/client-role/pull-engine.d.ts.map +0 -1
- package/dist/relay/client-role/pull-engine.js +0 -249
- package/dist/relay/client-role/pull-engine.js.map +0 -1
- package/dist/relay/client-role/sequence-mapper.d.ts +0 -65
- package/dist/relay/client-role/sequence-mapper.d.ts.map +0 -1
- package/dist/relay/client-role/sequence-mapper.js +0 -161
- package/dist/relay/client-role/sequence-mapper.js.map +0 -1
- package/dist/relay/evaluation/relay-paths.d.ts +0 -41
- package/dist/relay/evaluation/relay-paths.d.ts.map +0 -1
- package/dist/relay/evaluation/relay-paths.js +0 -504
- package/dist/relay/evaluation/relay-paths.js.map +0 -1
- package/dist/relay/evaluation/rust-boundary.d.ts +0 -47
- package/dist/relay/evaluation/rust-boundary.d.ts.map +0 -1
- package/dist/relay/evaluation/rust-boundary.js +0 -220
- package/dist/relay/evaluation/rust-boundary.js.map +0 -1
- package/dist/relay/index.d.ts +0 -37
- package/dist/relay/index.d.ts.map +0 -1
- package/dist/relay/index.js +0 -44
- package/dist/relay/index.js.map +0 -1
- package/dist/relay/migrate.d.ts +0 -18
- package/dist/relay/migrate.d.ts.map +0 -1
- package/dist/relay/migrate.js +0 -99
- package/dist/relay/migrate.js.map +0 -1
- package/dist/relay/mode-manager.d.ts +0 -60
- package/dist/relay/mode-manager.d.ts.map +0 -1
- package/dist/relay/mode-manager.js +0 -114
- package/dist/relay/mode-manager.js.map +0 -1
- package/dist/relay/realtime.d.ts +0 -90
- package/dist/relay/realtime.d.ts.map +0 -1
- package/dist/relay/realtime.js +0 -147
- package/dist/relay/realtime.js.map +0 -1
- package/dist/relay/relay.d.ts +0 -190
- package/dist/relay/relay.d.ts.map +0 -1
- package/dist/relay/relay.js +0 -320
- package/dist/relay/relay.js.map +0 -1
- package/dist/relay/schema.d.ts +0 -158
- package/dist/relay/schema.d.ts.map +0 -1
- package/dist/relay/schema.js +0 -7
- package/dist/relay/schema.js.map +0 -1
- package/dist/relay/server-role/index.d.ts +0 -56
- package/dist/relay/server-role/index.d.ts.map +0 -1
- package/dist/relay/server-role/index.js +0 -193
- package/dist/relay/server-role/index.js.map +0 -1
- package/dist/relay/server-role/pull.d.ts +0 -27
- package/dist/relay/server-role/pull.d.ts.map +0 -1
- package/dist/relay/server-role/pull.js +0 -24
- package/dist/relay/server-role/pull.js.map +0 -1
- package/dist/relay/server-role/push.d.ts +0 -29
- package/dist/relay/server-role/push.d.ts.map +0 -1
- package/dist/relay/server-role/push.js +0 -122
- package/dist/relay/server-role/push.js.map +0 -1
- package/dist/s3/index.d.ts +0 -83
- package/dist/s3/index.d.ts.map +0 -1
- package/dist/s3/index.js +0 -223
- package/dist/s3/index.js.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/service-worker/index.d.ts +0 -114
- package/dist/service-worker/index.d.ts.map +0 -1
- package/dist/service-worker/index.js +0 -487
- package/dist/service-worker/index.js.map +0 -1
- package/dist/snapshot-artifacts/sqlite-bun.d.ts +0 -15
- package/dist/snapshot-artifacts/sqlite-bun.d.ts.map +0 -1
- package/dist/snapshot-artifacts/sqlite-bun.js +0 -128
- package/dist/snapshot-artifacts/sqlite-bun.js.map +0 -1
- package/dist/snapshot-artifacts.d.ts +0 -206
- package/dist/snapshot-artifacts.d.ts.map +0 -1
- package/dist/snapshot-artifacts.js +0 -545
- package/dist/snapshot-artifacts.js.map +0 -1
- package/dist/snapshot-chunks/db-metadata.d.ts +0 -56
- package/dist/snapshot-chunks/db-metadata.d.ts.map +0 -1
- package/dist/snapshot-chunks/db-metadata.js +0 -360
- package/dist/snapshot-chunks/db-metadata.js.map +0 -1
- package/dist/snapshot-chunks/index.d.ts +0 -8
- package/dist/snapshot-chunks/index.d.ts.map +0 -1
- package/dist/snapshot-chunks/index.js +0 -8
- package/dist/snapshot-chunks/index.js.map +0 -1
- package/dist/snapshot-chunks/types.d.ts +0 -80
- package/dist/snapshot-chunks/types.d.ts.map +0 -1
- package/dist/snapshot-chunks/types.js +0 -8
- package/dist/snapshot-chunks/types.js.map +0 -1
- package/dist/snapshot-chunks.d.ts +0 -90
- package/dist/snapshot-chunks.d.ts.map +0 -1
- package/dist/snapshot-chunks.js +0 -304
- package/dist/snapshot-chunks.js.map +0 -1
- package/dist/sqlite/index.d.ts +0 -53
- package/dist/sqlite/index.d.ts.map +0 -1
- package/dist/sqlite/index.js +0 -795
- package/dist/sqlite/index.js.map +0 -1
- package/dist/sqlite3.d.ts +0 -22
- package/dist/sqlite3.d.ts.map +0 -1
- package/dist/sqlite3.js +0 -99
- package/dist/sqlite3.js.map +0 -1
- package/dist/stats.d.ts +0 -28
- package/dist/stats.d.ts.map +0 -1
- package/dist/stats.js +0 -93
- package/dist/stats.js.map +0 -1
- package/dist/subscriptions/cache.d.ts +0 -58
- package/dist/subscriptions/cache.d.ts.map +0 -1
- package/dist/subscriptions/cache.js +0 -250
- package/dist/subscriptions/cache.js.map +0 -1
- package/dist/subscriptions/index.d.ts +0 -3
- package/dist/subscriptions/index.d.ts.map +0 -1
- package/dist/subscriptions/index.js +0 -3
- package/dist/subscriptions/index.js.map +0 -1
- package/dist/subscriptions/resolve.d.ts +0 -40
- package/dist/subscriptions/resolve.d.ts.map +0 -1
- package/dist/subscriptions/resolve.js +0 -275
- package/dist/subscriptions/resolve.js.map +0 -1
- package/dist/sync.d.ts +0 -24
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -27
- package/dist/sync.js.map +0 -1
- package/src/auth-leases.ts +0 -649
- package/src/better-sqlite3.ts +0 -35
- package/src/blobs/access.ts +0 -244
- package/src/blobs/adapters/database.ts +0 -397
- package/src/blobs/index.ts +0 -9
- package/src/blobs/manager.ts +0 -901
- package/src/blobs/migrate.ts +0 -158
- package/src/blobs/types.ts +0 -74
- package/src/bun-sqlite-ambient.d.ts +0 -19
- package/src/bun-sqlite.ts +0 -27
- package/src/clients.ts +0 -22
- package/src/cloudflare/durable-object.ts +0 -289
- package/src/cloudflare/index.ts +0 -22
- package/src/cloudflare/r2.ts +0 -526
- package/src/cloudflare/scope-cache.ts +0 -341
- package/src/cloudflare/sentry.ts +0 -230
- package/src/cloudflare/worker.ts +0 -77
- package/src/commit-integrity.ts +0 -371
- package/src/compaction.ts +0 -77
- package/src/crdt-yjs/index.ts +0 -931
- package/src/d1.ts +0 -16
- package/src/dialect/base.ts +0 -360
- package/src/dialect/helpers.ts +0 -92
- package/src/dialect/index.ts +0 -7
- package/src/dialect/types.ts +0 -247
- package/src/encrypted-crdt.ts +0 -786
- package/src/filesystem/index.ts +0 -262
- package/src/handlers/collection.ts +0 -121
- package/src/handlers/create-handler.ts +0 -1134
- package/src/handlers/index.ts +0 -3
- package/src/handlers/types.ts +0 -403
- package/src/helpers/conflict.ts +0 -64
- package/src/helpers/emitted-change.ts +0 -69
- package/src/helpers/index.ts +0 -12
- package/src/helpers/paginate.ts +0 -82
- package/src/helpers/scope-authorization.ts +0 -27
- package/src/helpers/scope-commit-index.ts +0 -52
- package/src/helpers/scope-strings.ts +0 -101
- package/src/hono/api-key-auth.ts +0 -177
- package/src/hono/audit-redaction.ts +0 -135
- package/src/hono/blobs.ts +0 -851
- package/src/hono/console/gateway.ts +0 -3046
- package/src/hono/console/live-auth.ts +0 -46
- package/src/hono/console/route-descriptor.ts +0 -22
- package/src/hono/console/routes/api-keys.ts +0 -721
- package/src/hono/console/routes/clients.ts +0 -447
- package/src/hono/console/routes/commits.ts +0 -1137
- package/src/hono/console/routes/context.ts +0 -956
- package/src/hono/console/routes/events.ts +0 -669
- package/src/hono/console/routes/maintenance.ts +0 -461
- package/src/hono/console/routes/shared.ts +0 -816
- package/src/hono/console/routes/stats.ts +0 -392
- package/src/hono/console/routes/storage.ts +0 -159
- package/src/hono/console/routes.ts +0 -146
- package/src/hono/console/schema-errors.ts +0 -23
- package/src/hono/console/schemas.ts +0 -914
- package/src/hono/console/types.ts +0 -223
- package/src/hono/console/ui.ts +0 -100
- package/src/hono/create-server.ts +0 -230
- package/src/hono/errors.ts +0 -50
- package/src/hono/index.ts +0 -54
- package/src/hono/openapi.ts +0 -139
- package/src/hono/proxy/connection-manager.ts +0 -340
- package/src/hono/proxy/index.ts +0 -8
- package/src/hono/proxy/routes.ts +0 -272
- package/src/hono/rate-limit.ts +0 -319
- package/src/hono/realtime-sync-packs.ts +0 -354
- package/src/hono/routes/audit.ts +0 -499
- package/src/hono/routes/auth-leases.ts +0 -119
- package/src/hono/routes/combined.ts +0 -583
- package/src/hono/routes/context.ts +0 -1629
- package/src/hono/routes/health.ts +0 -26
- package/src/hono/routes/realtime.ts +0 -808
- package/src/hono/routes/shared.ts +0 -1626
- package/src/hono/routes/snapshots.ts +0 -345
- package/src/hono/routes.ts +0 -68
- package/src/hono/validation.ts +0 -81
- package/src/hono/websocket-origin.ts +0 -131
- package/src/hono/ws.ts +0 -1134
- package/src/libsql.ts +0 -51
- package/src/migrate.ts +0 -20
- package/src/neon.ts +0 -28
- package/src/notify.ts +0 -341
- package/src/pglite.ts +0 -68
- package/src/plugins/index.ts +0 -1
- package/src/plugins/types.ts +0 -144
- package/src/postgres/index.ts +0 -1291
- package/src/proxy/collection.ts +0 -17
- package/src/proxy/handler.ts +0 -159
- package/src/proxy/index.ts +0 -21
- package/src/proxy/mutation-detector.ts +0 -281
- package/src/proxy/oplog.ts +0 -181
- package/src/proxy/types.ts +0 -46
- package/src/realtime/in-memory.ts +0 -33
- package/src/realtime/index.ts +0 -7
- package/src/realtime/types.ts +0 -90
- package/src/relay/bun-types.d.ts +0 -50
- package/src/relay/client-role/forward-engine.ts +0 -355
- package/src/relay/client-role/index.ts +0 -9
- package/src/relay/client-role/pull-engine.ts +0 -329
- package/src/relay/client-role/sequence-mapper.ts +0 -201
- package/src/relay/evaluation/relay-paths.ts +0 -699
- package/src/relay/evaluation/rust-boundary.ts +0 -464
- package/src/relay/index.ts +0 -50
- package/src/relay/migrate.ts +0 -113
- package/src/relay/mode-manager.ts +0 -142
- package/src/relay/realtime.ts +0 -207
- package/src/relay/relay.ts +0 -431
- package/src/relay/schema.ts +0 -171
- package/src/relay/server-role/index.ts +0 -338
- package/src/relay/server-role/pull.ts +0 -43
- package/src/relay/server-role/push.ts +0 -164
- package/src/s3/index.ts +0 -346
- package/src/service-worker/index.ts +0 -773
- package/src/snapshot-artifacts/sqlite-bun.ts +0 -168
- package/src/snapshot-artifacts.ts +0 -896
- package/src/snapshot-chunks/db-metadata.ts +0 -537
- package/src/snapshot-chunks/index.ts +0 -8
- package/src/snapshot-chunks/types.ts +0 -105
- package/src/snapshot-chunks.ts +0 -453
- package/src/sqlite/index.ts +0 -1064
- package/src/sqlite3.ts +0 -137
- package/src/stats.ts +0 -180
- package/src/subscriptions/cache.ts +0 -376
- package/src/subscriptions/index.ts +0 -2
- package/src/subscriptions/resolve.ts +0 -357
- package/src/sync.ts +0 -111
package/src/push.ts
CHANGED
|
@@ -1,1076 +1,693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push apply (SPEC.md §6) with §3.4 write-path authorization.
|
|
3
|
+
*
|
|
4
|
+
* Security-critical rules implemented here:
|
|
5
|
+
* - authorization runs against the STORED row when it exists, never the
|
|
6
|
+
* pushed payload (§3.4 step 2);
|
|
7
|
+
* - declared scope columns are stripped from every update path (§3.4
|
|
8
|
+
* rule 5) — updates keep the stored row's scope column values;
|
|
9
|
+
* - a lost `baseVersion = 0` insert race re-authorizes the winner's row
|
|
10
|
+
* before disclosing it in a conflict record (§6.2).
|
|
11
|
+
*
|
|
12
|
+
* Per-commit atomicity (§6.4): one storage transaction per commit; the
|
|
13
|
+
* idempotency record persists in the same transaction as the writes.
|
|
14
|
+
*
|
|
15
|
+
* Optional per-table write validation (§6.7) runs after decode + the §3.4
|
|
16
|
+
* scope check, on the row that will persist (post scope-strip, post CRDT
|
|
17
|
+
* merge); a validator throw rejects the whole commit atomically with a
|
|
18
|
+
* host code.
|
|
19
|
+
*/
|
|
1
20
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
type
|
|
7
|
-
type
|
|
8
|
-
|
|
21
|
+
decodeRow,
|
|
22
|
+
encodeRow,
|
|
23
|
+
type PushCommitFrame,
|
|
24
|
+
type PushOperation,
|
|
25
|
+
type PushOperationResult,
|
|
26
|
+
type PushResultFrame,
|
|
27
|
+
parseBlobRef,
|
|
28
|
+
type RowValue,
|
|
9
29
|
} from '@syncular/core';
|
|
10
|
-
import type {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './
|
|
18
|
-
import type {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from './
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'selectFrom' | 'insertInto' | 'updateTable' | 'deleteFrom'
|
|
36
|
-
>;
|
|
37
|
-
|
|
38
|
-
export interface PushCommitResult {
|
|
39
|
-
response: SyncPushResponse;
|
|
40
|
-
/**
|
|
41
|
-
* Distinct tables affected by this commit.
|
|
42
|
-
* Empty for rejected commits and for commits that emit no changes.
|
|
43
|
-
*/
|
|
44
|
-
affectedTables: string[];
|
|
45
|
-
/**
|
|
46
|
-
* Scope keys derived from emitted changes (e.g. "org:abc", "team:xyz").
|
|
47
|
-
* Computed in-transaction so callers don't need an extra DB query.
|
|
48
|
-
* Empty for rejected/cached commits.
|
|
49
|
-
*/
|
|
50
|
-
scopeKeys: string[];
|
|
51
|
-
/**
|
|
52
|
-
* Changes emitted by this commit. Available for WS data delivery.
|
|
53
|
-
* Empty for rejected/cached commits.
|
|
54
|
-
*/
|
|
55
|
-
emittedChanges: SyncChange[];
|
|
56
|
-
/**
|
|
57
|
-
* Commit actor metadata for downstream notifications.
|
|
58
|
-
* Null when no commit row was persisted.
|
|
59
|
-
*/
|
|
60
|
-
commitActorId: string | null;
|
|
61
|
-
/**
|
|
62
|
-
* Commit timestamp metadata for downstream notifications.
|
|
63
|
-
* Null when no commit row was persisted.
|
|
64
|
-
*/
|
|
65
|
-
commitCreatedAt: string | null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface PushCommitValidationContext<
|
|
69
|
-
DB extends SyncCoreDb = SyncCoreDb,
|
|
70
|
-
Auth extends SyncServerAuth = SyncServerAuth,
|
|
71
|
-
> {
|
|
72
|
-
trx: DbExecutor<DB>;
|
|
73
|
-
dialect: ServerSyncDialect;
|
|
74
|
-
auth: Auth;
|
|
75
|
-
request: SyncPushRequest;
|
|
76
|
-
partitionId: string;
|
|
77
|
-
actorId: string;
|
|
78
|
-
commitSeq: number;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export type PushCommitValidator<
|
|
82
|
-
DB extends SyncCoreDb = SyncCoreDb,
|
|
83
|
-
Auth extends SyncServerAuth = SyncServerAuth,
|
|
84
|
-
> = (
|
|
85
|
-
ctx: PushCommitValidationContext<DB, Auth>
|
|
86
|
-
) =>
|
|
87
|
-
| Promise<SyncPushResponse['results'][number] | null>
|
|
88
|
-
| SyncPushResponse['results'][number]
|
|
89
|
-
| null;
|
|
90
|
-
|
|
91
|
-
class RejectCommitError extends Error {
|
|
92
|
-
constructor(public readonly response: SyncPushResponse) {
|
|
93
|
-
super('REJECT_COMMIT');
|
|
94
|
-
this.name = 'RejectCommitError';
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
99
|
-
return typeof value === 'object' && value !== null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function isSyncPushResponse(value: unknown): value is SyncPushResponse {
|
|
103
|
-
return (
|
|
104
|
-
isRecord(value) && value.ok === true && typeof value.status === 'string'
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function assertOperationIdentityUnchanged(
|
|
109
|
-
pluginName: string,
|
|
110
|
-
before: SyncPushRequest['operations'][number],
|
|
111
|
-
after: SyncPushRequest['operations'][number]
|
|
112
|
-
): void {
|
|
113
|
-
if (before.table !== after.table) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
`Server push plugin "${pluginName}" cannot change op.table (${before.table} -> ${after.table})`
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
if (before.row_id !== after.row_id) {
|
|
119
|
-
throw new Error(
|
|
120
|
-
`Server push plugin "${pluginName}" cannot change op.row_id (${before.row_id} -> ${after.row_id})`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
if (before.op !== after.op) {
|
|
124
|
-
throw new Error(
|
|
125
|
-
`Server push plugin "${pluginName}" cannot change op.op (${before.op} -> ${after.op})`
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function readCommitAffectedTables<DB extends SyncCoreDb>(
|
|
131
|
-
db: DbExecutor<DB>,
|
|
132
|
-
dialect: ServerSyncDialect,
|
|
133
|
-
commitSeq: number,
|
|
134
|
-
partitionId: string
|
|
135
|
-
): Promise<string[]> {
|
|
136
|
-
try {
|
|
137
|
-
const commitsQ = db.selectFrom('sync_commits') as SelectQueryBuilder<
|
|
138
|
-
DB,
|
|
139
|
-
'sync_commits',
|
|
140
|
-
EmptySelection
|
|
141
|
-
>;
|
|
142
|
-
|
|
143
|
-
const row = await commitsQ
|
|
144
|
-
.selectAll()
|
|
145
|
-
.where(sql<SqlBool>`commit_seq = ${commitSeq}`)
|
|
146
|
-
.where(sql<SqlBool>`partition_id = ${partitionId}`)
|
|
147
|
-
.executeTakeFirst();
|
|
148
|
-
|
|
149
|
-
const raw = row?.affected_tables;
|
|
150
|
-
return dialect.dbToArray(raw);
|
|
151
|
-
} catch {
|
|
152
|
-
// ignore and fall back to scanning changes (best-effort)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Fallback: read from changes using dialect-specific implementation
|
|
156
|
-
return dialect.readAffectedTablesFromChanges(db, commitSeq, { partitionId });
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function scopeKeysFromEmitted(
|
|
160
|
-
emitted: Array<{ scopes: Record<string, string> }>
|
|
30
|
+
import type { BlobStore } from './blob-store';
|
|
31
|
+
import type { SyncRequestContext } from './context';
|
|
32
|
+
import { clockOf } from './context';
|
|
33
|
+
import type { CrdtMergerRegistry } from './crdt-merger';
|
|
34
|
+
import { SyncError } from './errors';
|
|
35
|
+
import type { CompiledSchema, CompiledTable } from './schema';
|
|
36
|
+
import type { ResolvedScopes } from './scopes';
|
|
37
|
+
import { authorizeWrite, renderScopeValue, storedScopesForRow } from './scopes';
|
|
38
|
+
import type {
|
|
39
|
+
NewChange,
|
|
40
|
+
StorageTransaction,
|
|
41
|
+
StoredCommit,
|
|
42
|
+
StoredPushResult,
|
|
43
|
+
} from './storage';
|
|
44
|
+
import type { ValidateOpKind, ValidatorRegistry } from './validate';
|
|
45
|
+
import { toValidateRow, ValidationRejection } from './validate';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract the blobIds a decoded row references through its `blob_ref`
|
|
49
|
+
* columns (§5.9.4), skipping NULLs. Malformed BlobRefs already failed at
|
|
50
|
+
* row-codec decode (§5.9.1), so `parseBlobRef` here is total.
|
|
51
|
+
*/
|
|
52
|
+
function blobIdsInRow(
|
|
53
|
+
table: CompiledTable,
|
|
54
|
+
values: readonly RowValue[],
|
|
161
55
|
): string[] {
|
|
162
|
-
const
|
|
163
|
-
for (const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
56
|
+
const ids: string[] = [];
|
|
57
|
+
for (const index of table.blobRefColumnIndices) {
|
|
58
|
+
const value = values[index];
|
|
59
|
+
if (typeof value === 'string') ids.push(parseBlobRef(value).blobId);
|
|
167
60
|
}
|
|
168
|
-
return
|
|
61
|
+
return ids;
|
|
169
62
|
}
|
|
170
63
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
operationCount: number;
|
|
175
|
-
emittedChangeCount: number;
|
|
176
|
-
affectedTableCount: number;
|
|
177
|
-
}): void {
|
|
178
|
-
const {
|
|
179
|
-
status,
|
|
180
|
-
durationMs,
|
|
181
|
-
operationCount,
|
|
182
|
-
emittedChangeCount,
|
|
183
|
-
affectedTableCount,
|
|
184
|
-
} = args;
|
|
64
|
+
type OperationOutcome =
|
|
65
|
+
| { readonly kind: 'applied'; readonly change: NewChange | undefined }
|
|
66
|
+
| { readonly kind: 'terminate'; readonly record: PushOperationResult };
|
|
185
67
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
distributionSyncMetric('sync.server.push.duration_ms', durationMs, {
|
|
193
|
-
unit: 'millisecond',
|
|
194
|
-
attributes: { status },
|
|
195
|
-
});
|
|
196
|
-
distributionSyncMetric(
|
|
197
|
-
'sync.server.push.emitted_changes',
|
|
198
|
-
emittedChangeCount,
|
|
199
|
-
{
|
|
200
|
-
attributes: { status },
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
|
-
distributionSyncMetric(
|
|
204
|
-
'sync.server.push.affected_tables',
|
|
205
|
-
affectedTableCount,
|
|
206
|
-
{
|
|
207
|
-
attributes: { status },
|
|
208
|
-
}
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function createRejectedPushResult(
|
|
213
|
-
error: SyncPushResponse['results'][number]
|
|
214
|
-
): PushCommitResult {
|
|
68
|
+
function errorRecord(
|
|
69
|
+
opIndex: number,
|
|
70
|
+
code: string,
|
|
71
|
+
message: string,
|
|
72
|
+
retryable = false,
|
|
73
|
+
): OperationOutcome {
|
|
215
74
|
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
status: 'rejected',
|
|
219
|
-
results: [error],
|
|
220
|
-
},
|
|
221
|
-
affectedTables: [],
|
|
222
|
-
scopeKeys: [],
|
|
223
|
-
emittedChanges: [],
|
|
224
|
-
commitActorId: null,
|
|
225
|
-
commitCreatedAt: null,
|
|
75
|
+
kind: 'terminate',
|
|
76
|
+
record: { opIndex, status: 'error', code, message, retryable },
|
|
226
77
|
};
|
|
227
78
|
}
|
|
228
79
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
80
|
+
function conflictRecord(
|
|
81
|
+
opIndex: number,
|
|
82
|
+
serverVersion: number,
|
|
83
|
+
serverRow: Uint8Array,
|
|
84
|
+
): OperationOutcome {
|
|
233
85
|
return {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
86
|
+
kind: 'terminate',
|
|
87
|
+
record: {
|
|
88
|
+
opIndex,
|
|
89
|
+
status: 'conflict',
|
|
90
|
+
code: 'sync.version_conflict',
|
|
91
|
+
message: 'row version does not match baseVersion (§6.2)',
|
|
92
|
+
serverVersion,
|
|
93
|
+
serverRow,
|
|
94
|
+
},
|
|
238
95
|
};
|
|
239
96
|
}
|
|
240
97
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!request.clientId || !request.clientCommitId) {
|
|
245
|
-
return {
|
|
246
|
-
opIndex: 0,
|
|
247
|
-
status: 'error',
|
|
248
|
-
error: 'Invalid push request',
|
|
249
|
-
code: 'sync.invalid_request',
|
|
250
|
-
retriable: false,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const ops = request.operations ?? [];
|
|
255
|
-
if (!Array.isArray(ops) || ops.length === 0) {
|
|
256
|
-
return {
|
|
257
|
-
opIndex: 0,
|
|
258
|
-
status: 'error',
|
|
259
|
-
error: 'Empty commit',
|
|
260
|
-
code: 'sync.empty_commit',
|
|
261
|
-
retriable: false,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function shouldUseSavepoints<
|
|
269
|
-
DB extends SyncCoreDb,
|
|
270
|
-
Auth extends SyncServerAuth,
|
|
271
|
-
>(args: {
|
|
272
|
-
dialect: ServerSyncDialect;
|
|
273
|
-
handlers: ServerHandlerCollection<DB, Auth>;
|
|
274
|
-
operations: SyncPushRequest['operations'];
|
|
275
|
-
}): boolean {
|
|
276
|
-
if (!args.dialect.supportsSavepoints) {
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if ((args.operations?.length ?? 0) !== 1) {
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const singleOp = args.operations?.[0];
|
|
285
|
-
if (!singleOp) {
|
|
286
|
-
return true;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const singleOpHandler = args.handlers.byTable.get(singleOp.table);
|
|
290
|
-
if (!singleOpHandler) {
|
|
291
|
-
throw new Error(`Unknown table: ${singleOp.table}`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
return !singleOpHandler.canRejectSingleOperationWithoutSavepoint;
|
|
98
|
+
interface BlobApplyContext {
|
|
99
|
+
readonly store: BlobStore | undefined;
|
|
100
|
+
readonly partition: string;
|
|
295
101
|
}
|
|
296
102
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
103
|
+
/**
|
|
104
|
+
* §6.7: run the table's write-validation hook, if configured, on the row
|
|
105
|
+
* that WILL persist (post scope-strip, post CRDT-merge — the values the
|
|
106
|
+
* store receives). Returns a terminating outcome iff the validator rejects
|
|
107
|
+
* (its `ValidationRejection` code, or `sync.constraint_violation` for a
|
|
108
|
+
* non-`ValidationRejection` throw), else `undefined` (accept / no hook).
|
|
109
|
+
* A no-op for tables with no validator — the `undefined` short-circuit
|
|
110
|
+
* keeps the feature zero-cost when off.
|
|
111
|
+
*/
|
|
112
|
+
async function runValidator(
|
|
113
|
+
validators: ValidatorRegistry | undefined,
|
|
114
|
+
table: CompiledTable,
|
|
115
|
+
op: ValidateOpKind,
|
|
116
|
+
rowId: string,
|
|
117
|
+
values: readonly RowValue[] | undefined,
|
|
118
|
+
storedValues: readonly RowValue[] | undefined,
|
|
119
|
+
opIndex: number,
|
|
120
|
+
partition: string,
|
|
121
|
+
actorId: string,
|
|
122
|
+
): Promise<OperationOutcome | undefined> {
|
|
123
|
+
const validator = validators?.[table.name];
|
|
124
|
+
if (validator === undefined) return undefined;
|
|
125
|
+
try {
|
|
126
|
+
await validator(
|
|
127
|
+
{
|
|
128
|
+
op,
|
|
129
|
+
table: table.name,
|
|
130
|
+
rowId,
|
|
131
|
+
row:
|
|
132
|
+
values !== undefined
|
|
133
|
+
? toValidateRow(table.columns, values)
|
|
134
|
+
: undefined,
|
|
135
|
+
stored:
|
|
136
|
+
storedValues !== undefined
|
|
137
|
+
? toValidateRow(table.columns, storedValues)
|
|
138
|
+
: undefined,
|
|
139
|
+
},
|
|
140
|
+
{ actorId, partition },
|
|
141
|
+
);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (error instanceof ValidationRejection) {
|
|
144
|
+
return errorRecord(opIndex, error.code, error.message);
|
|
145
|
+
}
|
|
146
|
+
// §6.7: a non-ValidationRejection throw is still a rejection, mapped to
|
|
147
|
+
// the generic server-side constraint code (§10.2) — the validator's
|
|
148
|
+
// failure never crashes the request or leaks its message as a code.
|
|
149
|
+
return errorRecord(
|
|
150
|
+
opIndex,
|
|
151
|
+
'sync.constraint_violation',
|
|
152
|
+
`write validator for table ${JSON.stringify(table.name)} threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
);
|
|
341
154
|
}
|
|
155
|
+
return undefined;
|
|
342
156
|
}
|
|
343
157
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
)
|
|
381
|
-
|
|
158
|
+
/**
|
|
159
|
+
* §5.10.3: merge the row's `crdt` columns in place. For each crdt column,
|
|
160
|
+
* replace the incoming value with `merge(stored, incoming)` (§5.10.2) —
|
|
161
|
+
* never the raw pushed bytes. `storedValues` is undefined on insert (the
|
|
162
|
+
* stored value is `null` — the empty document). Returns `true` iff any crdt
|
|
163
|
+
* column value changed (so the caller re-encodes), or a terminating
|
|
164
|
+
* `sync.crdt_merge_failed` outcome if a merger is missing or throws.
|
|
165
|
+
*
|
|
166
|
+
* A NULL incoming crdt value is a semantic clear, not a merge — it passes
|
|
167
|
+
* through untouched (the app is nulling the column, the same as any other
|
|
168
|
+
* type). Merging only runs for a non-NULL incoming crdt value.
|
|
169
|
+
*/
|
|
170
|
+
async function mergeCrdtColumns(
|
|
171
|
+
table: CompiledTable,
|
|
172
|
+
values: RowValue[],
|
|
173
|
+
storedValues: readonly RowValue[] | undefined,
|
|
174
|
+
opIndex: number,
|
|
175
|
+
mergers: CrdtMergerRegistry | undefined,
|
|
176
|
+
): Promise<OperationOutcome | { readonly changed: boolean }> {
|
|
177
|
+
if (table.crdtColumns.length === 0) return { changed: false };
|
|
178
|
+
let changed = false;
|
|
179
|
+
for (const { index, crdtType } of table.crdtColumns) {
|
|
180
|
+
const incoming = values[index];
|
|
181
|
+
if (!(incoming instanceof Uint8Array)) continue; // NULL clear or absent
|
|
182
|
+
const merger = mergers?.[crdtType];
|
|
183
|
+
if (merger === undefined) {
|
|
184
|
+
return errorRecord(
|
|
185
|
+
opIndex,
|
|
186
|
+
'sync.crdt_merge_failed',
|
|
187
|
+
`no CRDT merger registered for crdtType ${JSON.stringify(crdtType)} (§5.10.2)`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const storedRaw = storedValues?.[index];
|
|
191
|
+
const stored = storedRaw instanceof Uint8Array ? storedRaw : null;
|
|
192
|
+
let merged: Uint8Array;
|
|
193
|
+
try {
|
|
194
|
+
merged = await merger(stored, incoming);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
return errorRecord(
|
|
197
|
+
opIndex,
|
|
198
|
+
'sync.crdt_merge_failed',
|
|
199
|
+
`CRDT merger for ${JSON.stringify(crdtType)} threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
values[index] = merged;
|
|
203
|
+
changed = true;
|
|
382
204
|
}
|
|
383
|
-
|
|
384
|
-
await finalizeCommitIntegrity({
|
|
385
|
-
db: args.trx,
|
|
386
|
-
dialect: args.dialect,
|
|
387
|
-
partitionId: args.partitionId,
|
|
388
|
-
commitSeq: args.commitSeq,
|
|
389
|
-
});
|
|
205
|
+
return { changed };
|
|
390
206
|
}
|
|
391
207
|
|
|
392
|
-
async function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (args.dialect.supportsForUpdate) {
|
|
412
|
-
query = query.forUpdate();
|
|
208
|
+
async function applyOperation(
|
|
209
|
+
tx: StorageTransaction,
|
|
210
|
+
schema: CompiledSchema,
|
|
211
|
+
resolved: ResolvedScopes,
|
|
212
|
+
op: PushOperation,
|
|
213
|
+
opIndex: number,
|
|
214
|
+
blobCtx: BlobApplyContext,
|
|
215
|
+
crdtMergers: CrdtMergerRegistry | undefined,
|
|
216
|
+
validators: ValidatorRegistry | undefined,
|
|
217
|
+
partition: string,
|
|
218
|
+
actorId: string,
|
|
219
|
+
): Promise<OperationOutcome> {
|
|
220
|
+
const table = schema.tables.get(op.table);
|
|
221
|
+
if (table === undefined) {
|
|
222
|
+
return errorRecord(
|
|
223
|
+
opIndex,
|
|
224
|
+
'sync.unknown_table',
|
|
225
|
+
`table ${JSON.stringify(op.table)} is not handled by this server`,
|
|
226
|
+
);
|
|
413
227
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
opIndex: 0,
|
|
421
|
-
status: 'error',
|
|
422
|
-
error: 'Idempotency cache miss',
|
|
423
|
-
code: 'sync.idempotency_cache_miss',
|
|
424
|
-
retriable: true,
|
|
425
|
-
});
|
|
228
|
+
if (!resolved.ok) {
|
|
229
|
+
return errorRecord(
|
|
230
|
+
opIndex,
|
|
231
|
+
'sync.forbidden',
|
|
232
|
+
'scope resolution failed (§3.4 step 4)',
|
|
233
|
+
);
|
|
426
234
|
}
|
|
235
|
+
const stored = await tx.getRow(op.table, op.rowId);
|
|
427
236
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
237
|
+
if (op.op === 'delete') {
|
|
238
|
+
if (stored === undefined) {
|
|
239
|
+
// Deleting an absent row is applied (idempotent, §6.2); no change.
|
|
240
|
+
return { kind: 'applied', change: undefined };
|
|
241
|
+
}
|
|
242
|
+
if (!authorizeWrite(table, stored.scopes, resolved)) {
|
|
243
|
+
return errorRecord(
|
|
244
|
+
opIndex,
|
|
245
|
+
'sync.forbidden',
|
|
246
|
+
'delete denied by scope authorization (§3.4)',
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const missing = missingScopeVariable(table, stored.scopes);
|
|
250
|
+
if (missing !== undefined) {
|
|
251
|
+
return errorRecord(
|
|
252
|
+
opIndex,
|
|
253
|
+
'sync.missing_scopes',
|
|
254
|
+
`stored row lacks scope variable ${JSON.stringify(missing)} (§3.1)`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
// §6.7: validate the delete against the stored row (row = undefined,
|
|
258
|
+
// stored = the row about to be removed). Only reached for an existing
|
|
259
|
+
// row — an absent-row delete is an idempotent no-op above.
|
|
260
|
+
const deleteReject = await runValidator(
|
|
261
|
+
validators,
|
|
262
|
+
table,
|
|
263
|
+
'delete',
|
|
264
|
+
op.rowId,
|
|
265
|
+
undefined,
|
|
266
|
+
decodeRow(table.columns, stored.payload),
|
|
267
|
+
opIndex,
|
|
268
|
+
partition,
|
|
269
|
+
actorId,
|
|
270
|
+
);
|
|
271
|
+
if (deleteReject !== undefined) return deleteReject;
|
|
272
|
+
await tx.deleteRow(op.table, op.rowId);
|
|
435
273
|
return {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
Number(existing.commit_seq),
|
|
444
|
-
args.partitionId
|
|
445
|
-
),
|
|
446
|
-
scopeKeys: [],
|
|
447
|
-
emittedChanges: [],
|
|
448
|
-
commitActorId:
|
|
449
|
-
typeof existing.actor_id === 'string' ? existing.actor_id : null,
|
|
450
|
-
commitCreatedAt:
|
|
451
|
-
typeof existing.created_at === 'string' ? existing.created_at : null,
|
|
274
|
+
kind: 'applied',
|
|
275
|
+
change: {
|
|
276
|
+
table: op.table,
|
|
277
|
+
rowId: op.rowId,
|
|
278
|
+
op: 'delete',
|
|
279
|
+
scopes: stored.scopes,
|
|
280
|
+
},
|
|
452
281
|
};
|
|
453
282
|
}
|
|
454
283
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
typeof existing.created_at === 'string' ? existing.created_at : null,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
async function insertPendingCommit(args: {
|
|
468
|
-
syncTrx: SyncMetadataTrx;
|
|
469
|
-
dialect: ServerSyncDialect;
|
|
470
|
-
commitRow: Insertable<SyncCoreDb['sync_commits']>;
|
|
471
|
-
}): Promise<number | null> {
|
|
472
|
-
if (args.dialect.supportsInsertReturning) {
|
|
473
|
-
const insertedCommit = await args.syncTrx
|
|
474
|
-
.insertInto('sync_commits')
|
|
475
|
-
.values(args.commitRow)
|
|
476
|
-
.onConflict((oc) =>
|
|
477
|
-
oc
|
|
478
|
-
.columns(['partition_id', 'client_id', 'client_commit_id'])
|
|
479
|
-
.doNothing()
|
|
480
|
-
)
|
|
481
|
-
.returning(['commit_seq'])
|
|
482
|
-
.executeTakeFirst();
|
|
483
|
-
|
|
484
|
-
if (!insertedCommit) {
|
|
485
|
-
return null;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return coerceNumber(insertedCommit.commit_seq) ?? 0;
|
|
284
|
+
// upsert — payload presence is enforced by the envelope codec (§6.1).
|
|
285
|
+
const payload = op.payload;
|
|
286
|
+
if (payload === undefined) {
|
|
287
|
+
return errorRecord(
|
|
288
|
+
opIndex,
|
|
289
|
+
'sync.invalid_request',
|
|
290
|
+
'upsert without payload',
|
|
291
|
+
);
|
|
489
292
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
.
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
293
|
+
let values: RowValue[];
|
|
294
|
+
try {
|
|
295
|
+
values = decodeRow(table.columns, payload);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return errorRecord(
|
|
298
|
+
opIndex,
|
|
299
|
+
'sync.invalid_request',
|
|
300
|
+
`row payload failed row-codec decode (§1.7): ${error instanceof Error ? error.message : String(error)}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const pkValue = renderScopeValue(values[table.primaryKeyIndex]);
|
|
304
|
+
if (pkValue !== op.rowId) {
|
|
305
|
+
return errorRecord(
|
|
306
|
+
opIndex,
|
|
307
|
+
'sync.invalid_request',
|
|
308
|
+
'payload primary key does not match rowId',
|
|
309
|
+
);
|
|
502
310
|
}
|
|
503
311
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
handlers: ServerHandlerCollection<DB, Auth>;
|
|
513
|
-
pushPlugins: readonly SyncServerPushPlugin<DB, Auth>[];
|
|
514
|
-
auth: Auth;
|
|
515
|
-
request: SyncPushRequest;
|
|
516
|
-
actorId: string;
|
|
517
|
-
commitId: string;
|
|
518
|
-
commitSeq: number;
|
|
519
|
-
}): Promise<{
|
|
520
|
-
results: SyncPushResponse['results'];
|
|
521
|
-
emittedChanges: PushCommitResult['emittedChanges'];
|
|
522
|
-
affectedTables: string[];
|
|
523
|
-
}> {
|
|
524
|
-
const ops = args.request.operations ?? [];
|
|
525
|
-
const allEmitted: PushCommitResult['emittedChanges'] = [];
|
|
526
|
-
const results: SyncPushResponse['results'] = [];
|
|
527
|
-
const affectedTablesSet = new Set<string>();
|
|
528
|
-
|
|
529
|
-
for (let i = 0; i < ops.length; ) {
|
|
530
|
-
const op = ops[i]!;
|
|
531
|
-
const handler = args.handlers.byTable.get(op.table);
|
|
532
|
-
if (!handler) {
|
|
533
|
-
throw new Error(`Unknown table: ${op.table}`);
|
|
312
|
+
if (stored !== undefined) {
|
|
313
|
+
// §3.4 step 2: authorize against the STORED row, never the payload.
|
|
314
|
+
if (!authorizeWrite(table, stored.scopes, resolved)) {
|
|
315
|
+
return errorRecord(
|
|
316
|
+
opIndex,
|
|
317
|
+
'sync.forbidden',
|
|
318
|
+
'write denied by scope authorization (§3.4)',
|
|
319
|
+
);
|
|
534
320
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
actorId: args.actorId,
|
|
540
|
-
auth: args.auth,
|
|
541
|
-
clientId: args.request.clientId,
|
|
542
|
-
commitId: args.commitId,
|
|
543
|
-
schemaVersion: args.request.schemaVersion,
|
|
544
|
-
authLease: args.request.authLease,
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
let transformedOp = op;
|
|
548
|
-
for (const plugin of args.pushPlugins) {
|
|
549
|
-
if (!plugin.beforeApplyOperation) continue;
|
|
550
|
-
const nextOp = await plugin.beforeApplyOperation({
|
|
551
|
-
ctx: operationCtx,
|
|
552
|
-
tableHandler: handler,
|
|
553
|
-
op: transformedOp,
|
|
554
|
-
opIndex: i,
|
|
555
|
-
});
|
|
556
|
-
assertOperationIdentityUnchanged(plugin.name, op, nextOp);
|
|
557
|
-
transformedOp = nextOp;
|
|
321
|
+
if (op.baseVersion === 0) {
|
|
322
|
+
// Lost insert race (§6.2); the stored row was authorized above,
|
|
323
|
+
// so disclosure of the winner is permitted.
|
|
324
|
+
return conflictRecord(opIndex, stored.serverVersion, stored.payload);
|
|
558
325
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
operationCtx,
|
|
576
|
-
batchInput
|
|
577
|
-
);
|
|
578
|
-
consumed = Math.max(1, appliedBatch.length);
|
|
326
|
+
if (
|
|
327
|
+
op.baseVersion !== undefined &&
|
|
328
|
+
op.baseVersion !== stored.serverVersion
|
|
329
|
+
) {
|
|
330
|
+
return conflictRecord(opIndex, stored.serverVersion, stored.payload);
|
|
331
|
+
}
|
|
332
|
+
// §3.4 rule 5: scope columns are immutable on update — keep the
|
|
333
|
+
// stored row's scope column values on both the baseVersion and
|
|
334
|
+
// last-write-wins paths.
|
|
335
|
+
const storedValues = decodeRow(table.columns, stored.payload);
|
|
336
|
+
let mutated = false;
|
|
337
|
+
for (const pattern of table.scopePatterns) {
|
|
338
|
+
const storedValue = storedValues[pattern.columnIndex] ?? null;
|
|
339
|
+
if (values[pattern.columnIndex] !== storedValue) {
|
|
340
|
+
values[pattern.columnIndex] = storedValue;
|
|
341
|
+
mutated = true;
|
|
579
342
|
}
|
|
580
343
|
}
|
|
344
|
+
// §5.10.3: crdt columns merge (stored ⊕ incoming) — never LWW, never
|
|
345
|
+
// baseVersion-conflict (they were excluded from the checks above).
|
|
346
|
+
const mergeOutcome = await mergeCrdtColumns(
|
|
347
|
+
table,
|
|
348
|
+
values,
|
|
349
|
+
storedValues,
|
|
350
|
+
opIndex,
|
|
351
|
+
crdtMergers,
|
|
352
|
+
);
|
|
353
|
+
if ('kind' in mergeOutcome) return mergeOutcome;
|
|
354
|
+
if (mergeOutcome.changed) mutated = true;
|
|
355
|
+
const newPayload = mutated ? encodeRow(table.columns, values) : payload;
|
|
356
|
+
const newVersion = stored.serverVersion + 1;
|
|
357
|
+
// §6.6 / §5.9.6: verify referenced blobs exist before writing.
|
|
358
|
+
const blobCheck = await checkAndRecordBlobs(
|
|
359
|
+
tx,
|
|
360
|
+
table,
|
|
361
|
+
op.rowId,
|
|
362
|
+
values,
|
|
363
|
+
opIndex,
|
|
364
|
+
blobCtx,
|
|
365
|
+
);
|
|
366
|
+
if (blobCheck !== undefined) return blobCheck;
|
|
367
|
+
// §6.7: validate the merged, scope-stripped row that will persist —
|
|
368
|
+
// for a crdt column the validator sees the MERGED value (§5.10.3), the
|
|
369
|
+
// state the store holds, not the raw pushed update.
|
|
370
|
+
const updateReject = await runValidator(
|
|
371
|
+
validators,
|
|
372
|
+
table,
|
|
373
|
+
'upsert',
|
|
374
|
+
op.rowId,
|
|
375
|
+
values,
|
|
376
|
+
storedValues,
|
|
377
|
+
opIndex,
|
|
378
|
+
partition,
|
|
379
|
+
actorId,
|
|
380
|
+
);
|
|
381
|
+
if (updateReject !== undefined) return updateReject;
|
|
382
|
+
const newRow = {
|
|
383
|
+
rowId: op.rowId,
|
|
384
|
+
serverVersion: newVersion,
|
|
385
|
+
scopes: stored.scopes,
|
|
386
|
+
payload: newPayload,
|
|
387
|
+
};
|
|
388
|
+
await tx.upsertRow(op.table, newRow);
|
|
389
|
+
return {
|
|
390
|
+
kind: 'applied',
|
|
391
|
+
change: {
|
|
392
|
+
table: op.table,
|
|
393
|
+
rowId: op.rowId,
|
|
394
|
+
op: 'upsert',
|
|
395
|
+
rowVersion: newVersion,
|
|
396
|
+
scopes: stored.scopes,
|
|
397
|
+
payload: newPayload,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
581
401
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
402
|
+
// Insert path: no stored row.
|
|
403
|
+
if (op.baseVersion !== undefined && op.baseVersion !== 0) {
|
|
404
|
+
// Authorize the payload first so absence is not disclosed to actors
|
|
405
|
+
// without the scope; then §6.2: baseVersion ≠ 0, row absent.
|
|
406
|
+
const extractedFirst = storedScopesForRow(table, values);
|
|
407
|
+
if (
|
|
408
|
+
'missing' in extractedFirst ||
|
|
409
|
+
!authorizeWrite(table, extractedFirst.scopes, resolved)
|
|
410
|
+
) {
|
|
411
|
+
return errorRecord(
|
|
412
|
+
opIndex,
|
|
413
|
+
'sync.forbidden',
|
|
414
|
+
'write denied by scope authorization (§3.4)',
|
|
587
415
|
);
|
|
588
|
-
|
|
589
|
-
for (const plugin of args.pushPlugins) {
|
|
590
|
-
if (!plugin.afterApplyOperation) continue;
|
|
591
|
-
appliedSingle = await plugin.afterApplyOperation({
|
|
592
|
-
ctx: operationCtx,
|
|
593
|
-
tableHandler: handler,
|
|
594
|
-
op: transformedOp,
|
|
595
|
-
opIndex: i,
|
|
596
|
-
applied: appliedSingle,
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
appliedBatch = [appliedSingle];
|
|
601
416
|
}
|
|
417
|
+
return errorRecord(
|
|
418
|
+
opIndex,
|
|
419
|
+
'sync.row_missing',
|
|
420
|
+
'upsert with baseVersion targets an absent row (§6.2)',
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const extracted = storedScopesForRow(table, values);
|
|
424
|
+
if ('missing' in extracted) {
|
|
425
|
+
// §3.4 step 2: a missing or empty scope column value ⇒ deny.
|
|
426
|
+
return errorRecord(
|
|
427
|
+
opIndex,
|
|
428
|
+
'sync.forbidden',
|
|
429
|
+
`insert missing scope column value for ${JSON.stringify(extracted.missing)} (§3.4)`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (!authorizeWrite(table, extracted.scopes, resolved)) {
|
|
433
|
+
return errorRecord(
|
|
434
|
+
opIndex,
|
|
435
|
+
'sync.forbidden',
|
|
436
|
+
'insert denied by scope authorization (§3.4)',
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
// §5.10.3: on insert a crdt column merges against the empty document
|
|
440
|
+
// (stored = null) — normalizes the initial state through the merger.
|
|
441
|
+
const insertMerge = await mergeCrdtColumns(
|
|
442
|
+
table,
|
|
443
|
+
values,
|
|
444
|
+
undefined,
|
|
445
|
+
opIndex,
|
|
446
|
+
crdtMergers,
|
|
447
|
+
);
|
|
448
|
+
if ('kind' in insertMerge) return insertMerge;
|
|
449
|
+
const insertPayload = insertMerge.changed
|
|
450
|
+
? encodeRow(table.columns, values)
|
|
451
|
+
: payload;
|
|
452
|
+
// §6.6 / §5.9.6: verify referenced blobs exist before writing.
|
|
453
|
+
const blobCheck = await checkAndRecordBlobs(
|
|
454
|
+
tx,
|
|
455
|
+
table,
|
|
456
|
+
op.rowId,
|
|
457
|
+
values,
|
|
458
|
+
opIndex,
|
|
459
|
+
blobCtx,
|
|
460
|
+
);
|
|
461
|
+
if (blobCheck !== undefined) return blobCheck;
|
|
462
|
+
// §6.7: validate the insert row (stored = undefined, so a validator can
|
|
463
|
+
// distinguish create from update); crdt columns are already merged
|
|
464
|
+
// against the empty document.
|
|
465
|
+
const insertReject = await runValidator(
|
|
466
|
+
validators,
|
|
467
|
+
table,
|
|
468
|
+
'upsert',
|
|
469
|
+
op.rowId,
|
|
470
|
+
values,
|
|
471
|
+
undefined,
|
|
472
|
+
opIndex,
|
|
473
|
+
partition,
|
|
474
|
+
actorId,
|
|
475
|
+
);
|
|
476
|
+
if (insertReject !== undefined) return insertReject;
|
|
477
|
+
const newRow = {
|
|
478
|
+
rowId: op.rowId,
|
|
479
|
+
serverVersion: 1,
|
|
480
|
+
scopes: extracted.scopes,
|
|
481
|
+
payload: insertPayload,
|
|
482
|
+
};
|
|
483
|
+
await tx.upsertRow(op.table, newRow);
|
|
484
|
+
return {
|
|
485
|
+
kind: 'applied',
|
|
486
|
+
change: {
|
|
487
|
+
table: op.table,
|
|
488
|
+
rowId: op.rowId,
|
|
489
|
+
op: 'upsert',
|
|
490
|
+
rowVersion: 1,
|
|
491
|
+
scopes: extracted.scopes,
|
|
492
|
+
payload: insertPayload,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
602
496
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
497
|
+
/**
|
|
498
|
+
* §5.9.6/§6.6: for a row's `blob_ref` columns, verify every referenced blob
|
|
499
|
+
* exists, then record the row's reference set in the index (§5.9.4). Returns
|
|
500
|
+
* a terminating `blob.not_found` outcome if any blob is absent (or the store
|
|
501
|
+
* is unconfigured while a ref exists), else `undefined` (proceed). No-op for
|
|
502
|
+
* tables with no `blob_ref` columns.
|
|
503
|
+
*/
|
|
504
|
+
async function checkAndRecordBlobs(
|
|
505
|
+
tx: StorageTransaction,
|
|
506
|
+
table: CompiledTable,
|
|
507
|
+
rowId: string,
|
|
508
|
+
values: readonly RowValue[],
|
|
509
|
+
opIndex: number,
|
|
510
|
+
blobCtx: BlobApplyContext,
|
|
511
|
+
): Promise<OperationOutcome | undefined> {
|
|
512
|
+
if (table.blobRefColumnIndices.length === 0) return undefined;
|
|
513
|
+
const blobIds = blobIdsInRow(table, values);
|
|
514
|
+
if (blobIds.length > 0) {
|
|
515
|
+
if (blobCtx.store === undefined) {
|
|
516
|
+
return errorRecord(
|
|
517
|
+
opIndex,
|
|
518
|
+
'blob.not_found',
|
|
519
|
+
'row references a blob but the server has no blob store (§5.9.6)',
|
|
606
520
|
);
|
|
607
521
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
522
|
+
for (const blobId of blobIds) {
|
|
523
|
+
if (!(await blobCtx.store.has(blobCtx.partition, blobId))) {
|
|
524
|
+
return errorRecord(
|
|
525
|
+
opIndex,
|
|
526
|
+
'blob.not_found',
|
|
527
|
+
`push references blob ${blobId} which has not been uploaded (§5.9.6)`,
|
|
614
528
|
);
|
|
615
529
|
}
|
|
616
|
-
|
|
617
|
-
for (const change of applied.emittedChanges ?? []) {
|
|
618
|
-
const scopes = change?.scopes;
|
|
619
|
-
if (!scopes || typeof scopes !== 'object') {
|
|
620
|
-
const error: SyncPushResponse['results'][number] = {
|
|
621
|
-
opIndex: applied.result.opIndex,
|
|
622
|
-
status: 'error',
|
|
623
|
-
error: 'Missing scopes',
|
|
624
|
-
code: 'sync.missing_scopes',
|
|
625
|
-
retriable: false,
|
|
626
|
-
};
|
|
627
|
-
results.push(error);
|
|
628
|
-
throw new RejectCommitError(
|
|
629
|
-
createRejectedPushResponse(error, args.commitSeq)
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
results.push(applied.result);
|
|
635
|
-
allEmitted.push(...applied.emittedChanges);
|
|
636
|
-
for (const change of applied.emittedChanges) {
|
|
637
|
-
affectedTablesSet.add(change.table);
|
|
638
|
-
}
|
|
639
530
|
}
|
|
531
|
+
}
|
|
532
|
+
// Update the reference index for this row (empty set clears it, §5.9.4).
|
|
533
|
+
if (tx.setBlobRefs !== undefined) {
|
|
534
|
+
await tx.setBlobRefs(table.name, rowId, blobIds);
|
|
535
|
+
}
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
640
538
|
|
|
641
|
-
|
|
539
|
+
function missingScopeVariable(
|
|
540
|
+
table: CompiledTable,
|
|
541
|
+
scopes: Record<string, string>,
|
|
542
|
+
): string | undefined {
|
|
543
|
+
for (const pattern of table.scopePatterns) {
|
|
544
|
+
const value = scopes[pattern.variable];
|
|
545
|
+
if (value === undefined || value.length === 0) return pattern.variable;
|
|
642
546
|
}
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
643
549
|
|
|
550
|
+
function resultFrame(
|
|
551
|
+
clientCommitId: string,
|
|
552
|
+
stored: StoredPushResult,
|
|
553
|
+
replay: boolean,
|
|
554
|
+
): PushResultFrame {
|
|
555
|
+
const status =
|
|
556
|
+
stored.status === 'applied' ? (replay ? 'cached' : 'applied') : 'rejected';
|
|
644
557
|
return {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
558
|
+
type: 'PUSH_RESULT',
|
|
559
|
+
clientCommitId,
|
|
560
|
+
status,
|
|
561
|
+
...(stored.status === 'applied' && stored.commitSeq !== undefined
|
|
562
|
+
? { commitSeq: stored.commitSeq }
|
|
563
|
+
: {}),
|
|
564
|
+
results: [...stored.results],
|
|
648
565
|
};
|
|
649
566
|
}
|
|
650
567
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
>(args: {
|
|
655
|
-
trx: DbExecutor<DB>;
|
|
656
|
-
dialect: ServerSyncDialect;
|
|
657
|
-
handlers: ServerHandlerCollection<DB, Auth>;
|
|
658
|
-
pushPlugins: readonly SyncServerPushPlugin<DB, Auth>[];
|
|
659
|
-
auth: Auth;
|
|
660
|
-
request: SyncPushRequest;
|
|
661
|
-
validateCommit?: PushCommitValidator<DB, Auth>;
|
|
662
|
-
}): Promise<PushCommitResult> {
|
|
663
|
-
const { trx, dialect, handlers, request, pushPlugins } = args;
|
|
664
|
-
const actorId = args.auth.actorId;
|
|
665
|
-
const partitionId = args.auth.partitionId ?? 'default';
|
|
666
|
-
const ops = request.operations ?? [];
|
|
667
|
-
const syncTrx = trx as SyncMetadataTrx;
|
|
668
|
-
|
|
669
|
-
if (!dialect.supportsSavepoints) {
|
|
670
|
-
await syncTrx
|
|
671
|
-
.deleteFrom('sync_commits')
|
|
672
|
-
.where('partition_id', '=', partitionId)
|
|
673
|
-
.where('client_id', '=', request.clientId)
|
|
674
|
-
.where('client_commit_id', '=', request.clientCommitId)
|
|
675
|
-
.where('result_json', 'is', null)
|
|
676
|
-
.execute();
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const commitCreatedAt = new Date().toISOString();
|
|
680
|
-
const commitRow: Insertable<SyncCoreDb['sync_commits']> = {
|
|
681
|
-
partition_id: partitionId,
|
|
682
|
-
actor_id: actorId,
|
|
683
|
-
client_id: request.clientId,
|
|
684
|
-
client_commit_id: request.clientCommitId,
|
|
685
|
-
created_at: commitCreatedAt,
|
|
686
|
-
meta: request.authLease
|
|
687
|
-
? toDialectJsonValue(dialect, {
|
|
688
|
-
authLease: request.authLease,
|
|
689
|
-
})
|
|
690
|
-
: null,
|
|
691
|
-
result_json: null,
|
|
692
|
-
};
|
|
693
|
-
let commitSeq =
|
|
694
|
-
(await insertPendingCommit({
|
|
695
|
-
syncTrx,
|
|
696
|
-
dialect,
|
|
697
|
-
commitRow,
|
|
698
|
-
})) ?? 0;
|
|
699
|
-
if (commitSeq === 0) {
|
|
700
|
-
return loadExistingCommitResult({
|
|
701
|
-
trx,
|
|
702
|
-
syncTrx,
|
|
703
|
-
dialect,
|
|
704
|
-
request,
|
|
705
|
-
partitionId,
|
|
706
|
-
});
|
|
707
|
-
}
|
|
568
|
+
export interface AppliedCommitEvent {
|
|
569
|
+
readonly commit: StoredCommit;
|
|
570
|
+
}
|
|
708
571
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
572
|
+
/**
|
|
573
|
+
* Process one `PUSH_COMMIT` frame: idempotency replay (§2.3), sequential
|
|
574
|
+
* atomic apply (§6.4), realtime notification for applied commits.
|
|
575
|
+
*/
|
|
576
|
+
export async function processPushCommit(
|
|
577
|
+
ctx: SyncRequestContext,
|
|
578
|
+
schema: CompiledSchema,
|
|
579
|
+
resolved: ResolvedScopes,
|
|
580
|
+
clientId: string,
|
|
581
|
+
frame: PushCommitFrame,
|
|
582
|
+
): Promise<PushResultFrame> {
|
|
583
|
+
const { storage, partition } = ctx;
|
|
584
|
+
let persisted: StoredPushResult | undefined;
|
|
585
|
+
try {
|
|
586
|
+
persisted = await storage.getPushResult(
|
|
587
|
+
partition,
|
|
588
|
+
clientId,
|
|
589
|
+
frame.clientCommitId,
|
|
590
|
+
);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
if (
|
|
593
|
+
error instanceof SyncError &&
|
|
594
|
+
error.code === 'sync.idempotency_cache_miss'
|
|
595
|
+
) {
|
|
596
|
+
// §6.3: answer the retryable cache-miss for this commit rather than
|
|
597
|
+
// re-applying. Not persisted — a retry may find a readable record.
|
|
598
|
+
return {
|
|
599
|
+
type: 'PUSH_RESULT',
|
|
600
|
+
clientCommitId: frame.clientCommitId,
|
|
601
|
+
status: 'rejected',
|
|
602
|
+
results: [
|
|
603
|
+
{
|
|
604
|
+
opIndex: 0,
|
|
605
|
+
status: 'error',
|
|
606
|
+
code: 'sync.idempotency_cache_miss',
|
|
607
|
+
message: error.message,
|
|
608
|
+
retryable: true,
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
throw error;
|
|
723
614
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const validationResult = await args.validateCommit?.({
|
|
727
|
-
trx,
|
|
728
|
-
dialect,
|
|
729
|
-
auth: args.auth,
|
|
730
|
-
request,
|
|
731
|
-
partitionId,
|
|
732
|
-
actorId,
|
|
733
|
-
commitSeq,
|
|
734
|
-
});
|
|
735
|
-
if (validationResult) {
|
|
736
|
-
const response = createRejectedPushResponse(validationResult, commitSeq);
|
|
737
|
-
await persistCommitOutcome({
|
|
738
|
-
trx,
|
|
739
|
-
dialect,
|
|
740
|
-
partitionId,
|
|
741
|
-
commitSeq,
|
|
742
|
-
response,
|
|
743
|
-
affectedTables: [],
|
|
744
|
-
emittedChangeCount: 0,
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
return {
|
|
748
|
-
response,
|
|
749
|
-
affectedTables: [],
|
|
750
|
-
scopeKeys: [],
|
|
751
|
-
emittedChanges: [],
|
|
752
|
-
commitActorId: actorId,
|
|
753
|
-
commitCreatedAt,
|
|
754
|
-
};
|
|
615
|
+
if (persisted !== undefined) {
|
|
616
|
+
return resultFrame(frame.clientCommitId, persisted, true);
|
|
755
617
|
}
|
|
756
618
|
|
|
757
|
-
const
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
});
|
|
763
|
-
let savepointCreated = false;
|
|
764
|
-
|
|
619
|
+
const createdAtMs = clockOf(ctx)();
|
|
620
|
+
const blobCtx: BlobApplyContext = { store: ctx.blobs, partition };
|
|
621
|
+
const crdtMergers = ctx.crdtMergers;
|
|
622
|
+
const validators = ctx.validators;
|
|
623
|
+
const tx = await storage.begin(partition);
|
|
765
624
|
try {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
};
|
|
788
|
-
await persistEmittedChanges({
|
|
789
|
-
trx,
|
|
790
|
-
dialect,
|
|
791
|
-
partitionId,
|
|
792
|
-
commitSeq,
|
|
793
|
-
emittedChanges: applied.emittedChanges,
|
|
794
|
-
});
|
|
795
|
-
await persistCommitOutcome({
|
|
796
|
-
trx,
|
|
797
|
-
dialect,
|
|
798
|
-
partitionId,
|
|
799
|
-
commitSeq,
|
|
800
|
-
response: appliedResponse,
|
|
801
|
-
affectedTables: applied.affectedTables,
|
|
802
|
-
emittedChangeCount: applied.emittedChanges.length,
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
if (useSavepoints) {
|
|
806
|
-
await sql.raw(`RELEASE SAVEPOINT ${savepointName}`).execute(trx);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return {
|
|
810
|
-
response: appliedResponse,
|
|
811
|
-
affectedTables: applied.affectedTables,
|
|
812
|
-
scopeKeys: scopeKeysFromEmitted(applied.emittedChanges),
|
|
813
|
-
emittedChanges: applied.emittedChanges,
|
|
814
|
-
commitActorId: actorId,
|
|
815
|
-
commitCreatedAt,
|
|
816
|
-
};
|
|
817
|
-
} catch (error) {
|
|
818
|
-
if (savepointCreated) {
|
|
819
|
-
try {
|
|
820
|
-
await sql.raw(`ROLLBACK TO SAVEPOINT ${savepointName}`).execute(trx);
|
|
821
|
-
await sql.raw(`RELEASE SAVEPOINT ${savepointName}`).execute(trx);
|
|
822
|
-
} catch (savepointError) {
|
|
823
|
-
console.error(
|
|
824
|
-
'[pushCommit] Savepoint rollback failed:',
|
|
825
|
-
savepointError
|
|
826
|
-
);
|
|
827
|
-
throw savepointError;
|
|
625
|
+
const results: PushOperationResult[] = [];
|
|
626
|
+
const changes: NewChange[] = [];
|
|
627
|
+
let terminated: PushOperationResult | undefined;
|
|
628
|
+
for (let opIndex = 0; opIndex < frame.operations.length; opIndex++) {
|
|
629
|
+
const op = frame.operations[opIndex];
|
|
630
|
+
if (op === undefined) continue;
|
|
631
|
+
const outcome = await applyOperation(
|
|
632
|
+
tx,
|
|
633
|
+
schema,
|
|
634
|
+
resolved,
|
|
635
|
+
op,
|
|
636
|
+
opIndex,
|
|
637
|
+
blobCtx,
|
|
638
|
+
crdtMergers,
|
|
639
|
+
validators,
|
|
640
|
+
partition,
|
|
641
|
+
ctx.actorId,
|
|
642
|
+
);
|
|
643
|
+
if (outcome.kind === 'terminate') {
|
|
644
|
+
terminated = outcome.record;
|
|
645
|
+
break;
|
|
828
646
|
}
|
|
647
|
+
results.push({ opIndex, status: 'applied' });
|
|
648
|
+
if (outcome.change !== undefined) changes.push(outcome.change);
|
|
829
649
|
}
|
|
830
650
|
|
|
831
|
-
if (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
partitionId,
|
|
839
|
-
commitSeq,
|
|
840
|
-
response: error.response,
|
|
841
|
-
affectedTables: [],
|
|
842
|
-
emittedChangeCount: 0,
|
|
843
|
-
});
|
|
844
|
-
|
|
845
|
-
return {
|
|
846
|
-
response: error.response,
|
|
847
|
-
affectedTables: [],
|
|
848
|
-
scopeKeys: [],
|
|
849
|
-
emittedChanges: [],
|
|
850
|
-
commitActorId: actorId,
|
|
851
|
-
commitCreatedAt,
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
export async function pushCommit<
|
|
857
|
-
DB extends SyncCoreDb,
|
|
858
|
-
Auth extends SyncServerAuth,
|
|
859
|
-
>(args: {
|
|
860
|
-
db: Kysely<DB>;
|
|
861
|
-
dialect: ServerSyncDialect;
|
|
862
|
-
handlers: ServerHandlerCollection<DB, Auth>;
|
|
863
|
-
plugins?: readonly SyncServerPushPlugin<DB, Auth>[];
|
|
864
|
-
auth: Auth;
|
|
865
|
-
request: SyncPushRequest;
|
|
866
|
-
validateCommit?: PushCommitValidator<DB, Auth>;
|
|
867
|
-
suppressTelemetry?: boolean;
|
|
868
|
-
}): Promise<PushCommitResult> {
|
|
869
|
-
const { db, dialect, handlers, request } = args;
|
|
870
|
-
const pushPlugins = sortServerPushPlugins(args.plugins);
|
|
871
|
-
const requestedOps = Array.isArray(request.operations)
|
|
872
|
-
? request.operations
|
|
873
|
-
: [];
|
|
874
|
-
const operationCount = requestedOps.length;
|
|
875
|
-
const startedAtMs = Date.now();
|
|
876
|
-
const suppressTelemetry = args.suppressTelemetry === true;
|
|
877
|
-
|
|
878
|
-
return startSyncSpan(
|
|
879
|
-
{
|
|
880
|
-
name: 'sync.server.push',
|
|
881
|
-
op: 'sync.push',
|
|
882
|
-
attributes: {
|
|
883
|
-
operation_count: operationCount,
|
|
884
|
-
},
|
|
885
|
-
},
|
|
886
|
-
async (span) => {
|
|
887
|
-
const finalizeResult = (result: PushCommitResult): PushCommitResult => {
|
|
888
|
-
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
|
889
|
-
const status = result.response.status;
|
|
890
|
-
|
|
891
|
-
span.setAttribute('status', status);
|
|
892
|
-
span.setAttribute('duration_ms', durationMs);
|
|
893
|
-
span.setAttribute('emitted_change_count', result.emittedChanges.length);
|
|
894
|
-
span.setAttribute('affected_table_count', result.affectedTables.length);
|
|
895
|
-
span.setStatus('ok');
|
|
896
|
-
|
|
897
|
-
if (!suppressTelemetry) {
|
|
898
|
-
recordPushMetrics({
|
|
899
|
-
status,
|
|
900
|
-
durationMs,
|
|
901
|
-
operationCount,
|
|
902
|
-
emittedChangeCount: result.emittedChanges.length,
|
|
903
|
-
affectedTableCount: result.affectedTables.length,
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return result;
|
|
651
|
+
if (terminated !== undefined) {
|
|
652
|
+
// §6.3 rejected: only the terminating operation's record; §6.4:
|
|
653
|
+
// every write of the commit rolls back.
|
|
654
|
+
await tx.rollback();
|
|
655
|
+
const stored: StoredPushResult = {
|
|
656
|
+
status: 'rejected',
|
|
657
|
+
results: [terminated],
|
|
908
658
|
};
|
|
909
|
-
|
|
659
|
+
const rejectionTx = await storage.begin(partition);
|
|
910
660
|
try {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
return finalizeResult(createRejectedPushResult(validationError));
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
return finalizeResult(
|
|
917
|
-
await dialect.executeInTransaction(db, async (trx) =>
|
|
918
|
-
executePushCommitInExecutor({
|
|
919
|
-
trx,
|
|
920
|
-
dialect,
|
|
921
|
-
handlers,
|
|
922
|
-
pushPlugins,
|
|
923
|
-
auth: args.auth,
|
|
924
|
-
request,
|
|
925
|
-
validateCommit: args.validateCommit,
|
|
926
|
-
})
|
|
927
|
-
)
|
|
928
|
-
);
|
|
661
|
+
await rejectionTx.putPushResult(clientId, frame.clientCommitId, stored);
|
|
662
|
+
await rejectionTx.commit();
|
|
929
663
|
} catch (error) {
|
|
930
|
-
|
|
931
|
-
span.setAttribute('status', 'error');
|
|
932
|
-
span.setAttribute('duration_ms', durationMs);
|
|
933
|
-
span.setStatus('error');
|
|
934
|
-
|
|
935
|
-
if (!suppressTelemetry) {
|
|
936
|
-
recordPushMetrics({
|
|
937
|
-
status: 'error',
|
|
938
|
-
durationMs,
|
|
939
|
-
operationCount,
|
|
940
|
-
emittedChangeCount: 0,
|
|
941
|
-
affectedTableCount: 0,
|
|
942
|
-
});
|
|
943
|
-
captureSyncException(error, {
|
|
944
|
-
event: 'sync.server.push',
|
|
945
|
-
operationCount,
|
|
946
|
-
});
|
|
947
|
-
}
|
|
664
|
+
await rejectionTx.rollback();
|
|
948
665
|
throw error;
|
|
949
666
|
}
|
|
667
|
+
return resultFrame(frame.clientCommitId, stored, false);
|
|
950
668
|
}
|
|
951
|
-
);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
export async function pushCommitBatch<
|
|
955
|
-
DB extends SyncCoreDb,
|
|
956
|
-
Auth extends SyncServerAuth,
|
|
957
|
-
>(args: {
|
|
958
|
-
db: Kysely<DB>;
|
|
959
|
-
dialect: ServerSyncDialect;
|
|
960
|
-
handlers: ServerHandlerCollection<DB, Auth>;
|
|
961
|
-
plugins?: readonly SyncServerPushPlugin<DB, Auth>[];
|
|
962
|
-
auth: Auth;
|
|
963
|
-
requests: SyncPushRequest[];
|
|
964
|
-
validateCommit?: PushCommitValidator<DB, Auth>;
|
|
965
|
-
suppressTelemetry?: boolean;
|
|
966
|
-
}): Promise<PushCommitResult[]> {
|
|
967
|
-
const { db, dialect, handlers, requests } = args;
|
|
968
|
-
const pushPlugins = sortServerPushPlugins(args.plugins);
|
|
969
|
-
const startedAtMs = Date.now();
|
|
970
|
-
const suppressTelemetry = args.suppressTelemetry === true;
|
|
971
|
-
const totalOperationCount = requests.reduce((count, request) => {
|
|
972
|
-
const operations = Array.isArray(request.operations)
|
|
973
|
-
? request.operations
|
|
974
|
-
: [];
|
|
975
|
-
return count + operations.length;
|
|
976
|
-
}, 0);
|
|
977
|
-
|
|
978
|
-
return startSyncSpan(
|
|
979
|
-
{
|
|
980
|
-
name: 'sync.server.push_batch',
|
|
981
|
-
op: 'sync.push.batch',
|
|
982
|
-
attributes: {
|
|
983
|
-
commit_count: requests.length,
|
|
984
|
-
operation_count: totalOperationCount,
|
|
985
|
-
},
|
|
986
|
-
},
|
|
987
|
-
async (span) => {
|
|
988
|
-
try {
|
|
989
|
-
const results = await dialect.executeInTransaction(db, async (trx) => {
|
|
990
|
-
const executed: PushCommitResult[] = [];
|
|
991
|
-
for (const request of requests) {
|
|
992
|
-
const validationError = validatePushRequest(request);
|
|
993
|
-
if (validationError) {
|
|
994
|
-
executed.push(createRejectedPushResult(validationError));
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
executed.push(
|
|
999
|
-
await executePushCommitInExecutor({
|
|
1000
|
-
trx,
|
|
1001
|
-
dialect,
|
|
1002
|
-
handlers,
|
|
1003
|
-
pushPlugins,
|
|
1004
|
-
auth: args.auth,
|
|
1005
|
-
request,
|
|
1006
|
-
validateCommit: args.validateCommit,
|
|
1007
|
-
})
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
return executed;
|
|
1011
|
-
});
|
|
1012
669
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
)
|
|
1031
|
-
? 'applied'
|
|
1032
|
-
: 'rejected';
|
|
1033
|
-
|
|
1034
|
-
span.setAttribute('status', status);
|
|
1035
|
-
span.setAttribute('duration_ms', durationMs);
|
|
1036
|
-
span.setAttribute('commit_count', results.length);
|
|
1037
|
-
span.setAttribute('emitted_change_count', emittedChangeCount);
|
|
1038
|
-
span.setAttribute('affected_table_count', affectedTableCount);
|
|
1039
|
-
span.setStatus('ok');
|
|
1040
|
-
|
|
1041
|
-
if (!suppressTelemetry) {
|
|
1042
|
-
recordPushMetrics({
|
|
1043
|
-
status,
|
|
1044
|
-
durationMs,
|
|
1045
|
-
operationCount: totalOperationCount,
|
|
1046
|
-
emittedChangeCount,
|
|
1047
|
-
affectedTableCount,
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
return results;
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
const durationMs = Math.max(0, Date.now() - startedAtMs);
|
|
1054
|
-
span.setAttribute('status', 'error');
|
|
1055
|
-
span.setAttribute('duration_ms', durationMs);
|
|
1056
|
-
span.setStatus('error');
|
|
1057
|
-
|
|
1058
|
-
if (!suppressTelemetry) {
|
|
1059
|
-
recordPushMetrics({
|
|
1060
|
-
status: 'error',
|
|
1061
|
-
durationMs,
|
|
1062
|
-
operationCount: totalOperationCount,
|
|
1063
|
-
emittedChangeCount: 0,
|
|
1064
|
-
affectedTableCount: 0,
|
|
1065
|
-
});
|
|
1066
|
-
captureSyncException(error, {
|
|
1067
|
-
event: 'sync.server.push_batch',
|
|
1068
|
-
commitCount: requests.length,
|
|
1069
|
-
operationCount: totalOperationCount,
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
throw error;
|
|
1073
|
-
}
|
|
670
|
+
const commitSeq = await tx.appendCommit({
|
|
671
|
+
clientId,
|
|
672
|
+
clientCommitId: frame.clientCommitId,
|
|
673
|
+
actorId: ctx.actorId,
|
|
674
|
+
createdAtMs,
|
|
675
|
+
changes,
|
|
676
|
+
});
|
|
677
|
+
const stored: StoredPushResult = { status: 'applied', commitSeq, results };
|
|
678
|
+
await tx.putPushResult(clientId, frame.clientCommitId, stored);
|
|
679
|
+
await tx.commit();
|
|
680
|
+
if (ctx.realtime !== undefined && changes.length > 0) {
|
|
681
|
+
await ctx.realtime.notifyCommit(partition, {
|
|
682
|
+
commitSeq,
|
|
683
|
+
createdAtMs,
|
|
684
|
+
actorId: ctx.actorId,
|
|
685
|
+
changes,
|
|
686
|
+
});
|
|
1074
687
|
}
|
|
1075
|
-
|
|
688
|
+
return resultFrame(frame.clientCommitId, stored, false);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
await tx.rollback();
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
1076
693
|
}
|