@abloatai/ablo 0.35.0 → 0.37.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/AGENTS.md +2 -2
- package/CHANGELOG.md +71 -1929
- package/NOTICE +2 -2
- package/README.md +23 -532
- package/assets/banner.png +0 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +2 -0
- package/dist/auth.js.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -0
- package/dist/coordination.d.ts +2 -0
- package/dist/coordination.d.ts.map +1 -0
- package/dist/coordination.js +2 -0
- package/dist/coordination.js.map +1 -0
- package/dist/index.d.ts +3 -112
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -161
- package/dist/index.js.map +1 -0
- package/dist/react.d.ts +4 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +3 -0
- package/dist/react.js.map +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -0
- package/dist/source-conformance.d.ts +2 -0
- package/dist/source-conformance.d.ts.map +1 -0
- package/dist/source-conformance.js +2 -0
- package/dist/source-conformance.js.map +1 -0
- package/dist/source-drizzle.d.ts +2 -0
- package/dist/source-drizzle.d.ts.map +1 -0
- package/dist/source-drizzle.js +2 -0
- package/dist/source-drizzle.js.map +1 -0
- package/dist/source-kysely.d.ts +2 -0
- package/dist/source-kysely.d.ts.map +1 -0
- package/dist/source-kysely.js +2 -0
- package/dist/source-kysely.js.map +1 -0
- package/dist/source-next.d.ts +2 -0
- package/dist/source-next.d.ts.map +1 -0
- package/dist/source-next.js +2 -0
- package/dist/source-next.js.map +1 -0
- package/dist/source.d.ts +2 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +2 -0
- package/dist/source.js.map +1 -0
- package/dist/wire.d.ts +2 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +2 -0
- package/dist/wire.js.map +1 -0
- package/docs/agents.md +2 -2
- package/docs/api-keys.md +13 -12
- package/docs/api.md +15 -53
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +9 -9
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +228 -86
- package/docs/data-sources.md +5 -5
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/agent-human.md +4 -4
- package/docs/examples/ai-sdk-tool.md +1 -1
- package/docs/examples/existing-python-backend.md +15 -4
- package/docs/examples/nextjs.md +27 -6
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +32 -8
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +24 -24
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +38 -16
- package/docs/internal/README.md +18 -0
- package/docs/internal/agent-fleet-coordination-design.md +171 -0
- package/docs/internal/agent-orchestration.md +58 -0
- package/docs/internal/commit-identifiers.md +91 -0
- package/docs/internal/concurrency-open-decisions.md +37 -0
- package/docs/internal/data-source-reverse-channel.md +150 -0
- package/docs/internal/per-field-conflict-detection.md +165 -0
- package/docs/internal/postgres-replication.md +64 -0
- package/docs/internal/serializable-schema.md +119 -0
- package/docs/internal/structure.md +32 -0
- package/docs/mcp.md +9 -9
- package/docs/migration.md +37 -18
- package/docs/projects.md +1 -1
- package/docs/quickstart.md +2 -2
- package/docs/react.md +24 -13
- package/docs/schema-contract.md +3 -3
- package/docs/sessions.md +91 -37
- package/docs/webhooks.md +9 -9
- package/examples/README.md +2 -2
- package/examples/data-source/README.md +1 -1
- package/examples/data-source/ablo-driver.ts +1 -1
- package/examples/data-source/customer-server.ts +1 -1
- package/examples/data-source/run.ts +1 -1
- package/examples/data-source/schema.ts +1 -1
- package/examples/quickstart.ts +2 -2
- package/llms.txt +12 -12
- package/package.json +64 -174
- package/dist/BaseSyncedStore.d.ts +0 -823
- package/dist/BaseSyncedStore.js +0 -1955
- package/dist/Database.d.ts +0 -335
- package/dist/Database.js +0 -1500
- package/dist/InstanceCache.d.ts +0 -233
- package/dist/InstanceCache.js +0 -1164
- package/dist/LazyReferenceCollection.d.ts +0 -177
- package/dist/LazyReferenceCollection.js +0 -461
- package/dist/Model.d.ts +0 -444
- package/dist/Model.js +0 -909
- package/dist/ModelRegistry.d.ts +0 -221
- package/dist/ModelRegistry.js +0 -537
- package/dist/NetworkMonitor.d.ts +0 -26
- package/dist/NetworkMonitor.js +0 -77
- package/dist/RuntimeContext.d.ts +0 -52
- package/dist/RuntimeContext.js +0 -80
- package/dist/SyncClient.d.ts +0 -551
- package/dist/SyncClient.js +0 -2199
- package/dist/adapters/alwaysOnline.d.ts +0 -14
- package/dist/adapters/alwaysOnline.js +0 -17
- package/dist/adapters/inMemoryStorage.d.ts +0 -31
- package/dist/adapters/inMemoryStorage.js +0 -110
- package/dist/ai-sdk/coordinatedTool.d.ts +0 -120
- package/dist/ai-sdk/coordinatedTool.js +0 -134
- package/dist/ai-sdk/coordinationContext.d.ts +0 -46
- package/dist/ai-sdk/coordinationContext.js +0 -106
- package/dist/ai-sdk/index.d.ts +0 -121
- package/dist/ai-sdk/index.js +0 -121
- package/dist/ai-sdk/wrap.d.ts +0 -65
- package/dist/ai-sdk/wrap.js +0 -39
- package/dist/auth/index.d.ts +0 -1
- package/dist/auth/index.js +0 -8
- package/dist/batching/index.d.ts +0 -55
- package/dist/batching/index.js +0 -147
- package/dist/cli.cjs +0 -288600
- package/dist/client/Ablo.d.ts +0 -231
- package/dist/client/Ablo.js +0 -149
- package/dist/client/abloClient.d.ts +0 -309
- package/dist/client/abloClient.js +0 -13
- package/dist/client/clientPrelude.d.ts +0 -52
- package/dist/client/clientPrelude.js +0 -60
- package/dist/client/consoleLogger.d.ts +0 -35
- package/dist/client/consoleLogger.js +0 -44
- package/dist/client/coreClient.d.ts +0 -60
- package/dist/client/coreClient.js +0 -118
- package/dist/client/createInternalComponents.d.ts +0 -46
- package/dist/client/createInternalComponents.js +0 -92
- package/dist/client/createModelProxy.d.ts +0 -228
- package/dist/client/createModelProxy.js +0 -818
- package/dist/client/humans.d.ts +0 -48
- package/dist/client/humans.js +0 -52
- package/dist/client/modelRegistration.d.ts +0 -10
- package/dist/client/modelRegistration.js +0 -312
- package/dist/client/options.d.ts +0 -461
- package/dist/client/options.js +0 -7
- package/dist/client/reactiveEngine.d.ts +0 -48
- package/dist/client/reactiveEngine.js +0 -910
- package/dist/client/resourceTypes.d.ts +0 -12
- package/dist/client/resourceTypes.js +0 -10
- package/dist/client/schemaConfig.d.ts +0 -44
- package/dist/client/schemaConfig.js +0 -185
- package/dist/client/validateAbloOptions.d.ts +0 -42
- package/dist/client/validateAbloOptions.js +0 -43
- package/dist/client/wsMutationExecutor.d.ts +0 -27
- package/dist/client/wsMutationExecutor.js +0 -72
- package/dist/context.d.ts +0 -29
- package/dist/context.js +0 -58
- package/dist/coordination/ClaimLog.d.ts +0 -26
- package/dist/coordination/ClaimLog.js +0 -32
- package/dist/coordination/index.d.ts +0 -1
- package/dist/coordination/index.js +0 -8
- package/dist/core/DatabaseManager.d.ts +0 -105
- package/dist/core/DatabaseManager.js +0 -387
- package/dist/core/QueryProcessor.d.ts +0 -75
- package/dist/core/QueryProcessor.js +0 -255
- package/dist/core/QueryView.d.ts +0 -79
- package/dist/core/QueryView.js +0 -218
- package/dist/core/StoreManager.d.ts +0 -112
- package/dist/core/StoreManager.js +0 -302
- package/dist/core/ViewRegistry.d.ts +0 -20
- package/dist/core/ViewRegistry.js +0 -55
- package/dist/core/index.d.ts +0 -33
- package/dist/core/index.js +0 -48
- package/dist/core/openIDBWithTimeout.d.ts +0 -65
- package/dist/core/openIDBWithTimeout.js +0 -153
- package/dist/core/queryUtils.d.ts +0 -45
- package/dist/core/queryUtils.js +0 -69
- package/dist/core/storeContract.d.ts +0 -145
- package/dist/core/storeContract.js +0 -12
- package/dist/docs/catalog.d.ts +0 -72
- package/dist/docs/catalog.js +0 -227
- package/dist/docs/index.d.ts +0 -10
- package/dist/docs/index.js +0 -10
- package/dist/environment.d.ts +0 -1
- package/dist/environment.js +0 -8
- package/dist/interfaces/index.d.ts +0 -311
- package/dist/interfaces/index.js +0 -9
- package/dist/keys/index.d.ts +0 -1
- package/dist/keys/index.js +0 -8
- package/dist/mutators/RecordingMutation.d.ts +0 -36
- package/dist/mutators/RecordingMutation.js +0 -182
- package/dist/mutators/Transaction.d.ts +0 -40
- package/dist/mutators/Transaction.js +0 -58
- package/dist/mutators/UndoManager.d.ts +0 -258
- package/dist/mutators/UndoManager.js +0 -658
- package/dist/mutators/defineMutators.d.ts +0 -60
- package/dist/mutators/defineMutators.js +0 -18
- package/dist/mutators/inverseOp.d.ts +0 -126
- package/dist/mutators/inverseOp.js +0 -71
- package/dist/mutators/mutateActions.d.ts +0 -45
- package/dist/mutators/mutateActions.js +0 -105
- package/dist/mutators/readerActions.d.ts +0 -33
- package/dist/mutators/readerActions.js +0 -57
- package/dist/mutators/undoApply.d.ts +0 -51
- package/dist/mutators/undoApply.js +0 -117
- package/dist/policy/index.d.ts +0 -21
- package/dist/policy/index.js +0 -20
- package/dist/query/client.d.ts +0 -61
- package/dist/query/client.js +0 -137
- package/dist/query/types.d.ts +0 -85
- package/dist/query/types.js +0 -16
- package/dist/react/AbloProvider.d.ts +0 -230
- package/dist/react/AbloProvider.js +0 -455
- package/dist/react/ClientSideSuspense.d.ts +0 -36
- package/dist/react/ClientSideSuspense.js +0 -17
- package/dist/react/DefaultFallback.d.ts +0 -24
- package/dist/react/DefaultFallback.js +0 -43
- package/dist/react/context.d.ts +0 -55
- package/dist/react/context.js +0 -29
- package/dist/react/index.d.ts +0 -61
- package/dist/react/index.js +0 -66
- package/dist/react/internalContext.d.ts +0 -33
- package/dist/react/internalContext.js +0 -3
- package/dist/react/useAblo.d.ts +0 -75
- package/dist/react/useAblo.js +0 -102
- package/dist/react/useCurrentUserId.d.ts +0 -22
- package/dist/react/useCurrentUserId.js +0 -34
- package/dist/react/useErrorListener.d.ts +0 -20
- package/dist/react/useErrorListener.js +0 -38
- package/dist/react/useMutationFailureListener.d.ts +0 -26
- package/dist/react/useMutationFailureListener.js +0 -38
- package/dist/react/useMutators.d.ts +0 -56
- package/dist/react/useMutators.js +0 -84
- package/dist/react/useReactive.d.ts +0 -35
- package/dist/react/useReactive.js +0 -123
- package/dist/react/useSyncStatus.d.ts +0 -59
- package/dist/react/useSyncStatus.js +0 -76
- package/dist/react/useUndoScope.d.ts +0 -34
- package/dist/react/useUndoScope.js +0 -81
- package/dist/schema/coordination.d.ts +0 -112
- package/dist/schema/coordination.js +0 -129
- package/dist/schema/ddl.d.ts +0 -97
- package/dist/schema/ddl.js +0 -491
- package/dist/schema/ddlLock.d.ts +0 -35
- package/dist/schema/ddlLock.js +0 -46
- package/dist/schema/diff.d.ts +0 -225
- package/dist/schema/diff.js +0 -289
- package/dist/schema/generate.d.ts +0 -19
- package/dist/schema/generate.js +0 -86
- package/dist/schema/index.d.ts +0 -41
- package/dist/schema/index.js +0 -76
- package/dist/schema/queries.d.ts +0 -201
- package/dist/schema/queries.js +0 -144
- package/dist/schema/select.d.ts +0 -40
- package/dist/schema/select.js +0 -87
- package/dist/schema/serialize.d.ts +0 -115
- package/dist/schema/serialize.js +0 -262
- package/dist/schema/sugar.d.ts +0 -109
- package/dist/schema/sugar.js +0 -83
- package/dist/schema/syncDeltaRow.d.ts +0 -6
- package/dist/schema/syncDeltaRow.js +0 -6
- package/dist/server/adapter.d.ts +0 -173
- package/dist/server/adapter.js +0 -18
- package/dist/server/commit.d.ts +0 -107
- package/dist/server/commit.js +0 -1
- package/dist/server/index.d.ts +0 -14
- package/dist/server/index.js +0 -2
- package/dist/server/readConfig.d.ts +0 -80
- package/dist/server/readConfig.js +0 -8
- package/dist/server/storageMode.d.ts +0 -23
- package/dist/server/storageMode.js +0 -17
- package/dist/source/adapter.d.ts +0 -81
- package/dist/source/adapter.js +0 -22
- package/dist/source/adapters/drizzle.d.ts +0 -48
- package/dist/source/adapters/drizzle.js +0 -219
- package/dist/source/adapters/kysely.d.ts +0 -42
- package/dist/source/adapters/kysely.js +0 -205
- package/dist/source/adapters/kyselyMutationCore.d.ts +0 -76
- package/dist/source/adapters/kyselyMutationCore.js +0 -125
- package/dist/source/adapters/memory.d.ts +0 -13
- package/dist/source/adapters/memory.js +0 -130
- package/dist/source/adapters/prisma.d.ts +0 -63
- package/dist/source/adapters/prisma.js +0 -202
- package/dist/source/conformance.d.ts +0 -37
- package/dist/source/conformance.js +0 -215
- package/dist/source/connector.d.ts +0 -95
- package/dist/source/connector.js +0 -266
- package/dist/source/connectorProtocol.d.ts +0 -154
- package/dist/source/connectorProtocol.js +0 -163
- package/dist/source/contract.d.ts +0 -195
- package/dist/source/contract.js +0 -164
- package/dist/source/factory.d.ts +0 -92
- package/dist/source/factory.js +0 -286
- package/dist/source/footprint.d.ts +0 -111
- package/dist/source/footprint.js +0 -0
- package/dist/source/idempotency.d.ts +0 -61
- package/dist/source/idempotency.js +0 -144
- package/dist/source/index.d.ts +0 -23
- package/dist/source/index.js +0 -30
- package/dist/source/migrations.d.ts +0 -21
- package/dist/source/migrations.js +0 -103
- package/dist/source/next.d.ts +0 -32
- package/dist/source/next.js +0 -25
- package/dist/source/pushQueue.d.ts +0 -134
- package/dist/source/pushQueue.js +0 -256
- package/dist/source/signing.d.ts +0 -92
- package/dist/source/signing.js +0 -162
- package/dist/source/types.d.ts +0 -401
- package/dist/source/types.js +0 -59
- package/dist/stores/ObjectStore.d.ts +0 -115
- package/dist/stores/ObjectStore.js +0 -393
- package/dist/stores/ObjectStoreContract.d.ts +0 -38
- package/dist/stores/ObjectStoreContract.js +0 -1
- package/dist/stores/SyncActionStore.d.ts +0 -97
- package/dist/stores/SyncActionStore.js +0 -504
- package/dist/stores/syncAction.d.ts +0 -26
- package/dist/stores/syncAction.js +0 -16
- package/dist/surface.d.ts +0 -36
- package/dist/surface.js +0 -75
- package/dist/sync/BootstrapFetcher.d.ts +0 -280
- package/dist/sync/BootstrapFetcher.js +0 -962
- package/dist/sync/ConnectionManager.d.ts +0 -8
- package/dist/sync/ConnectionManager.js +0 -8
- package/dist/sync/OnDemandLoader.d.ts +0 -228
- package/dist/sync/OnDemandLoader.js +0 -742
- package/dist/sync/SubscriptionManager.d.ts +0 -159
- package/dist/sync/SubscriptionManager.js +0 -243
- package/dist/sync/SyncWebSocket.d.ts +0 -173
- package/dist/sync/SyncWebSocket.js +0 -438
- package/dist/sync/awaitClaimGrant.d.ts +0 -6
- package/dist/sync/awaitClaimGrant.js +0 -6
- package/dist/sync/bootstrapApply.d.ts +0 -70
- package/dist/sync/bootstrapApply.js +0 -73
- package/dist/sync/commitFrames.d.ts +0 -8
- package/dist/sync/commitFrames.js +0 -8
- package/dist/sync/contextPorts.d.ts +0 -18
- package/dist/sync/contextPorts.js +0 -31
- package/dist/sync/createClaimStream.d.ts +0 -7
- package/dist/sync/createClaimStream.js +0 -7
- package/dist/sync/createPresenceStream.d.ts +0 -69
- package/dist/sync/createPresenceStream.js +0 -200
- package/dist/sync/createSnapshot.d.ts +0 -29
- package/dist/sync/createSnapshot.js +0 -118
- package/dist/sync/credentialLifecycle.d.ts +0 -7
- package/dist/sync/credentialLifecycle.js +0 -7
- package/dist/sync/deltaPipeline.d.ts +0 -113
- package/dist/sync/deltaPipeline.js +0 -261
- package/dist/sync/groupChange.d.ts +0 -113
- package/dist/sync/groupChange.js +0 -242
- package/dist/sync/participants.d.ts +0 -115
- package/dist/sync/participants.js +0 -344
- package/dist/sync/persistedPrefix.d.ts +0 -12
- package/dist/sync/persistedPrefix.js +0 -22
- package/dist/sync/schemaDrift.d.ts +0 -55
- package/dist/sync/schemaDrift.js +0 -53
- package/dist/sync/schemas.d.ts +0 -70
- package/dist/sync/schemas.js +0 -94
- package/dist/sync/syncCursor.d.ts +0 -40
- package/dist/sync/syncCursor.js +0 -55
- package/dist/sync/syncPlan.d.ts +0 -54
- package/dist/sync/syncPlan.js +0 -50
- package/dist/sync/wsFrameHandlers.d.ts +0 -8
- package/dist/sync/wsFrameHandlers.js +0 -8
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/dist/transaction/ablo.d.ts +0 -88
- package/dist/transaction/ablo.js +0 -33
- package/dist/transaction/auth/apiKey.d.ts +0 -152
- package/dist/transaction/auth/apiKey.js +0 -419
- package/dist/transaction/auth/bootstrapScope.d.ts +0 -15
- package/dist/transaction/auth/bootstrapScope.js +0 -1
- package/dist/transaction/auth/capability.d.ts +0 -177
- package/dist/transaction/auth/capability.js +0 -199
- package/dist/transaction/auth/credentialEndpoint.d.ts +0 -61
- package/dist/transaction/auth/credentialEndpoint.js +0 -86
- package/dist/transaction/auth/credentialPolicy.d.ts +0 -148
- package/dist/transaction/auth/credentialPolicy.js +0 -125
- package/dist/transaction/auth/credentialSource.d.ts +0 -30
- package/dist/transaction/auth/credentialSource.js +0 -55
- package/dist/transaction/auth/hostedEndpoints.d.ts +0 -21
- package/dist/transaction/auth/hostedEndpoints.js +0 -21
- package/dist/transaction/auth/identity.d.ts +0 -55
- package/dist/transaction/auth/identity.js +0 -210
- package/dist/transaction/auth/index.d.ts +0 -162
- package/dist/transaction/auth/index.js +0 -304
- package/dist/transaction/auth/schemas.d.ts +0 -59
- package/dist/transaction/auth/schemas.js +0 -85
- package/dist/transaction/auth/sessionMint.d.ts +0 -28
- package/dist/transaction/auth/sessionMint.js +0 -85
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +0 -49
- package/dist/transaction/coordination/awaitClaimGrant.js +0 -112
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +0 -50
- package/dist/transaction/coordination/claimHeartbeatLoop.js +0 -88
- package/dist/transaction/coordination/claimMeta.d.ts +0 -49
- package/dist/transaction/coordination/claimMeta.js +0 -52
- package/dist/transaction/coordination/createClaimStream.d.ts +0 -64
- package/dist/transaction/coordination/createClaimStream.js +0 -475
- package/dist/transaction/coordination/events.d.ts +0 -74
- package/dist/transaction/coordination/events.js +0 -7
- package/dist/transaction/coordination/index.d.ts +0 -19
- package/dist/transaction/coordination/index.js +0 -44
- package/dist/transaction/coordination/locator.d.ts +0 -83
- package/dist/transaction/coordination/locator.js +0 -82
- package/dist/transaction/coordination/schema.d.ts +0 -1473
- package/dist/transaction/coordination/schema.js +0 -1013
- package/dist/transaction/coordination/targetConflict.d.ts +0 -2
- package/dist/transaction/coordination/targetConflict.js +0 -103
- package/dist/transaction/coordination/trace.d.ts +0 -78
- package/dist/transaction/coordination/trace.js +0 -138
- package/dist/transaction/durableWrites.d.ts +0 -62
- package/dist/transaction/durableWrites.js +0 -71
- package/dist/transaction/environment.d.ts +0 -105
- package/dist/transaction/environment.js +0 -108
- package/dist/transaction/errorCodes.d.ts +0 -403
- package/dist/transaction/errorCodes.js +0 -480
- package/dist/transaction/errors.d.ts +0 -428
- package/dist/transaction/errors.js +0 -686
- package/dist/transaction/index.d.ts +0 -20
- package/dist/transaction/index.js +0 -20
- package/dist/transaction/keys/index.d.ts +0 -87
- package/dist/transaction/keys/index.js +0 -207
- package/dist/transaction/log/syncDeltaRow.d.ts +0 -158
- package/dist/transaction/log/syncDeltaRow.js +0 -95
- package/dist/transaction/logPosition.d.ts +0 -97
- package/dist/transaction/logPosition.js +0 -125
- package/dist/transaction/logger.d.ts +0 -16
- package/dist/transaction/logger.js +0 -7
- package/dist/transaction/observability.d.ts +0 -53
- package/dist/transaction/observability.js +0 -19
- package/dist/transaction/persistence.d.ts +0 -12
- package/dist/transaction/persistence.js +0 -11
- package/dist/transaction/plugin.d.ts +0 -192
- package/dist/transaction/plugin.js +0 -87
- package/dist/transaction/policy/types.d.ts +0 -217
- package/dist/transaction/policy/types.js +0 -126
- package/dist/transaction/resources/functionalUpdate.d.ts +0 -79
- package/dist/transaction/resources/functionalUpdate.js +0 -87
- package/dist/transaction/resources/httpResources.d.ts +0 -266
- package/dist/transaction/resources/httpResources.js +0 -7
- package/dist/transaction/resources/modelOperations.d.ts +0 -319
- package/dist/transaction/resources/modelOperations.js +0 -12
- package/dist/transaction/resources/mutationOptions.d.ts +0 -66
- package/dist/transaction/resources/mutationOptions.js +0 -9
- package/dist/transaction/resources/where.d.ts +0 -85
- package/dist/transaction/resources/where.js +0 -70
- package/dist/transaction/resources/writeOptionsSchema.d.ts +0 -47
- package/dist/transaction/resources/writeOptionsSchema.js +0 -73
- package/dist/transaction/schema/field.d.ts +0 -126
- package/dist/transaction/schema/field.js +0 -265
- package/dist/transaction/schema/loadStrategy.d.ts +0 -45
- package/dist/transaction/schema/loadStrategy.js +0 -46
- package/dist/transaction/schema/model.d.ts +0 -379
- package/dist/transaction/schema/model.js +0 -123
- package/dist/transaction/schema/openapi.d.ts +0 -57
- package/dist/transaction/schema/openapi.js +0 -340
- package/dist/transaction/schema/relation.d.ts +0 -199
- package/dist/transaction/schema/relation.js +0 -104
- package/dist/transaction/schema/residency.d.ts +0 -29
- package/dist/transaction/schema/residency.js +0 -25
- package/dist/transaction/schema/roles.d.ts +0 -249
- package/dist/transaction/schema/roles.js +0 -230
- package/dist/transaction/schema/schema.d.ts +0 -324
- package/dist/transaction/schema/schema.js +0 -305
- package/dist/transaction/schema/tenancy.d.ts +0 -139
- package/dist/transaction/schema/tenancy.js +0 -190
- package/dist/transaction/transactionLayer.d.ts +0 -82
- package/dist/transaction/transactionLayer.js +0 -24
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +0 -143
- package/dist/transaction/transactions/settlement/commitEnvelope.js +0 -161
- package/dist/transaction/transactions/settlement/httpCommitEnvelope.d.ts +0 -53
- package/dist/transaction/transactions/settlement/httpCommitEnvelope.js +0 -207
- package/dist/transaction/transactions/settlement/idempotencyKey.d.ts +0 -10
- package/dist/transaction/transactions/settlement/idempotencyKey.js +0 -9
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +0 -112
- package/dist/transaction/transactions/settlement/pendingWrite.js +0 -20
- package/dist/transaction/transport/commitFrames.d.ts +0 -90
- package/dist/transaction/transport/commitFrames.js +0 -134
- package/dist/transaction/transport/connectionManager.d.ts +0 -215
- package/dist/transaction/transport/connectionManager.js +0 -673
- package/dist/transaction/transport/credentialLifecycle.d.ts +0 -177
- package/dist/transaction/transport/credentialLifecycle.js +0 -324
- package/dist/transaction/transport/heartbeat.d.ts +0 -65
- package/dist/transaction/transport/heartbeat.js +0 -93
- package/dist/transaction/transport/httpClient.d.ts +0 -123
- package/dist/transaction/transport/httpClient.js +0 -145
- package/dist/transaction/transport/httpOptions.d.ts +0 -33
- package/dist/transaction/transport/httpOptions.js +0 -12
- package/dist/transaction/transport/httpTransport.d.ts +0 -8
- package/dist/transaction/transport/httpTransport.js +0 -1276
- package/dist/transaction/transport/networkProbe.d.ts +0 -84
- package/dist/transaction/transport/networkProbe.js +0 -207
- package/dist/transaction/transport/wsFrameHandlers.d.ts +0 -128
- package/dist/transaction/transport/wsFrameHandlers.js +0 -429
- package/dist/transaction/transport/wsTransport.d.ts +0 -576
- package/dist/transaction/transport/wsTransport.js +0 -1017
- package/dist/transaction/types/assertExact.d.ts +0 -17
- package/dist/transaction/types/assertExact.js +0 -1
- package/dist/transaction/types/global.d.ts +0 -107
- package/dist/transaction/types/global.js +0 -40
- package/dist/transaction/types/index.d.ts +0 -205
- package/dist/transaction/types/index.js +0 -56
- package/dist/transaction/types/modelData.d.ts +0 -10
- package/dist/transaction/types/modelData.js +0 -9
- package/dist/transaction/types/participant.d.ts +0 -20
- package/dist/transaction/types/participant.js +0 -10
- package/dist/transaction/types/streams.d.ts +0 -540
- package/dist/transaction/types/streams.js +0 -11
- package/dist/transaction/utils/asyncIterator.d.ts +0 -34
- package/dist/transaction/utils/asyncIterator.js +0 -135
- package/dist/transaction/utils/duration.d.ts +0 -25
- package/dist/transaction/utils/duration.js +0 -45
- package/dist/transaction/utils/json.d.ts +0 -57
- package/dist/transaction/utils/json.js +0 -276
- package/dist/transaction/wire/accountResponses.d.ts +0 -351
- package/dist/transaction/wire/accountResponses.js +0 -255
- package/dist/transaction/wire/auth.d.ts +0 -49
- package/dist/transaction/wire/auth.js +0 -57
- package/dist/transaction/wire/bootstrapReason.d.ts +0 -9
- package/dist/transaction/wire/bootstrapReason.js +0 -8
- package/dist/transaction/wire/claimEvent.d.ts +0 -76
- package/dist/transaction/wire/claimEvent.js +0 -73
- package/dist/transaction/wire/claims.d.ts +0 -463
- package/dist/transaction/wire/claims.js +0 -229
- package/dist/transaction/wire/commit.d.ts +0 -603
- package/dist/transaction/wire/commit.js +0 -321
- package/dist/transaction/wire/delta.d.ts +0 -250
- package/dist/transaction/wire/delta.js +0 -147
- package/dist/transaction/wire/errorEnvelope.d.ts +0 -72
- package/dist/transaction/wire/errorEnvelope.js +0 -123
- package/dist/transaction/wire/feedCursor.d.ts +0 -60
- package/dist/transaction/wire/feedCursor.js +0 -82
- package/dist/transaction/wire/feedEvent.d.ts +0 -177
- package/dist/transaction/wire/feedEvent.js +0 -39
- package/dist/transaction/wire/frames.d.ts +0 -194
- package/dist/transaction/wire/frames.js +0 -50
- package/dist/transaction/wire/inboundFrames.d.ts +0 -552
- package/dist/transaction/wire/inboundFrames.js +0 -116
- package/dist/transaction/wire/index.d.ts +0 -50
- package/dist/transaction/wire/index.js +0 -74
- package/dist/transaction/wire/listEnvelope.d.ts +0 -37
- package/dist/transaction/wire/listEnvelope.js +0 -42
- package/dist/transaction/wire/modelResponses.d.ts +0 -85
- package/dist/transaction/wire/modelResponses.js +0 -43
- package/dist/transaction/wire/protocol.d.ts +0 -38
- package/dist/transaction/wire/protocol.js +0 -38
- package/dist/transaction/wire/protocolVersion.d.ts +0 -73
- package/dist/transaction/wire/protocolVersion.js +0 -83
- package/dist/transactions/mutations/MutationQueue.d.ts +0 -655
- package/dist/transactions/mutations/MutationQueue.js +0 -2797
- package/dist/transactions/mutations/MutationStore.d.ts +0 -20
- package/dist/transactions/mutations/MutationStore.js +0 -53
- package/dist/transactions/mutations/UnconfirmedWrites.d.ts +0 -82
- package/dist/transactions/mutations/UnconfirmedWrites.js +0 -104
- package/dist/transactions/mutations/coalesceRules.d.ts +0 -58
- package/dist/transactions/mutations/coalesceRules.js +0 -140
- package/dist/transactions/mutations/commitLatency.d.ts +0 -52
- package/dist/transactions/mutations/commitLatency.js +0 -130
- package/dist/transactions/mutations/commitOutboxStore.d.ts +0 -28
- package/dist/transactions/mutations/commitOutboxStore.js +0 -26
- package/dist/transactions/mutations/commitPayload.d.ts +0 -164
- package/dist/transactions/mutations/commitPayload.js +0 -152
- package/dist/transactions/mutations/deltaConfirmation.d.ts +0 -59
- package/dist/transactions/mutations/deltaConfirmation.js +0 -233
- package/dist/transactions/mutations/durableWriteStore.d.ts +0 -14
- package/dist/transactions/mutations/durableWriteStore.js +0 -12
- package/dist/transactions/mutations/optimisticApply.d.ts +0 -49
- package/dist/transactions/mutations/optimisticApply.js +0 -65
- package/dist/transactions/mutations/replayValidation.d.ts +0 -186
- package/dist/transactions/mutations/replayValidation.js +0 -163
- package/dist/utils/mobxSetup.d.ts +0 -53
- package/dist/utils/mobxSetup.js +0 -330
- package/dist/webhooks/events.d.ts +0 -43
- package/dist/webhooks/events.js +0 -42
- package/dist/webhooks/index.d.ts +0 -8
- package/dist/webhooks/index.js +0 -8
- package/dist/wire/index.d.ts +0 -1
- package/dist/wire/index.js +0 -8
- package/docs/interaction-model.md +0 -99
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Commit identifiers: the two axes
|
|
2
|
+
|
|
3
|
+
Maintainer reference. A commit carries several ids, and they are easy to
|
|
4
|
+
conflate because they travel together and are all "some number attached to a
|
|
5
|
+
write." They are not interchangeable. Each answers a different question, and
|
|
6
|
+
they split cleanly along **one line**: does this id help decide whether the
|
|
7
|
+
write *wins*, or does it only help *identify* the write after the fact?
|
|
8
|
+
|
|
9
|
+
Keeping the two axes separate is what lets each id be reasoned about — and
|
|
10
|
+
audited — on its own. This doc is the single place they sit side by side.
|
|
11
|
+
|
|
12
|
+
## The line
|
|
13
|
+
|
|
14
|
+
| axis | the question it answers | when it acts | if it's absent |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| **Conflict resolution** | *should this write land, given what else happened?* | at the commit chokepoint, before the row is written | the write is unguarded (last-writer-wins) |
|
|
17
|
+
| **Correlation / audit** | *which write is this, and have I seen it before?* | on receipt (dedup) and after the fact (attribution) | the write still lands; you just can't dedup or trace it as precisely |
|
|
18
|
+
|
|
19
|
+
A conflict-resolution id can **reject** a commit. A correlation id never does —
|
|
20
|
+
at most it makes a retried commit a no-op (idempotency). Never reach for one to
|
|
21
|
+
do the other's job: a correlation id can't fence a stale write, and a fence
|
|
22
|
+
can't dedup a retry.
|
|
23
|
+
|
|
24
|
+
## Axis 1: conflict resolution (does this write win?)
|
|
25
|
+
|
|
26
|
+
Evaluated inside `executeCommit`'s transaction, atomic with the delta write.
|
|
27
|
+
Three independent fences, each catching what the others can't; the full
|
|
28
|
+
narrative is [ADR 0009 §6](../../../../docs/decisions/0009-claim-durability-two-reclaim-clocks.md)
|
|
29
|
+
and the [coordination reference](../coordination.md).
|
|
30
|
+
|
|
31
|
+
| id | wire field | persisted to | what it asserts | rejects when |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
| **read basis** | per-op `readAt` | `sync_deltas.read_at_sync_id` | "the state I reasoned **from**": a version watermark | the row moved since `readAt` (version-CAS), under `onStale: 'reject'` |
|
|
34
|
+
| **fencing token** | per-op `fenceToken` | `sync_deltas.fence_token` **and** `claim_fence_watermark.fence_token` | "the lease generation I was authorized **at**": a monotonic per-entity high-water | the token is below the entity's persisted high-water: a lapsed holder writing after its successor already claimed, wrote, and released |
|
|
35
|
+
| **claim / lease** | `claimId`, `heldBy` on the `WireClaim` | the coordination store (Redis), not `sync_deltas` | "I hold this row right now": live mutual exclusion | a non-holder writes a row another participant holds |
|
|
36
|
+
|
|
37
|
+
`onStale` (`notify` / `reject` / `overwrite`) is **not** an id — it's the
|
|
38
|
+
disposition that decides what a stale `readAt` *does*. It rides with the read
|
|
39
|
+
basis but is policy, not evidence, so it isn't persisted.
|
|
40
|
+
|
|
41
|
+
Why the token is a distinct id from `readAt`, and not just reused `sync_id`:
|
|
42
|
+
`readAt` advances on every **write** and asserts *from what data*; the token
|
|
43
|
+
advances on every **grant** and asserts *at what lease generation*. Their events
|
|
44
|
+
differ, so one can't stand in for the other — a lapsed holder that skips
|
|
45
|
+
version-CAS (no `readAt`, a blind write) is invisible to the read basis but
|
|
46
|
+
still carries a stale token. That is precisely fence (c) closing what (a) can't.
|
|
47
|
+
The reasoning in full lives in
|
|
48
|
+
[the fencing-token scope doc](../../../../docs/plans/claim-fencing-token-option-b-scope.md).
|
|
49
|
+
|
|
50
|
+
## Axis 2: correlation / audit (which write is this?)
|
|
51
|
+
|
|
52
|
+
Never decides a conflict. These are how a write is recognized — as a duplicate,
|
|
53
|
+
as your own echo, or as one row in a signed history.
|
|
54
|
+
|
|
55
|
+
| id | wire field | persisted to | purpose |
|
|
56
|
+
|---|---|---|---|
|
|
57
|
+
| **idempotency key** | batch `clientTxId` (public alias `idempotencyKey`) | dedup ledger keyed by it | a retried batch commits **once**: the second attempt is recognized and folded to a no-op, not re-applied |
|
|
58
|
+
| **per-op transaction id** | per-op `transactionId` | `sync_deltas.transaction_id` | echo detection: the broadcast delta arrives at the originating client carrying the **same** id its queue marked pending, so it reconciles its optimistic write instead of double-applying |
|
|
59
|
+
| **sync id** | assigned server-side (`next_sync_id`) | `sync_deltas.id` | the monotonic total order: the serialization order every reader tails and every `readAt` names. It is *assigned*, never client-supplied |
|
|
60
|
+
| **attribution** | actor / capability / delegation on the frame | `sync_deltas` actor columns + the signed audit chain | who acted, on whose behalf, under which key: the [audit log](../audit.md)'s who/when |
|
|
61
|
+
|
|
62
|
+
The batch key and the per-op id are deliberately separate: a multi-row commit is
|
|
63
|
+
**one** idempotent unit (one `clientTxId`) made of **many** individually-echoable
|
|
64
|
+
ops (each its own `transactionId`). Collapsing them would make echo detection
|
|
65
|
+
batch-coarse and break optimistic reconciliation for multi-op commits.
|
|
66
|
+
|
|
67
|
+
## The evidence tuple on a `sync_deltas` row
|
|
68
|
+
|
|
69
|
+
Both axes leave their mark on the delta, which is what makes a delta a complete,
|
|
70
|
+
self-describing audit record — you can reconstruct the full justification of a
|
|
71
|
+
write from the row alone, never from a live lease that has since vanished:
|
|
72
|
+
|
|
73
|
+
- **who:** `actor_id` / `capability_id` (+ the signed chain)
|
|
74
|
+
- **what:** `data` / `previous_data`
|
|
75
|
+
- **when:** `id` (`sync_id`) / `created_at`
|
|
76
|
+
- **from what known state:** `read_at_sync_id` (the read basis)
|
|
77
|
+
- **at what lease generation:** `fence_token` (the token the commit fenced)
|
|
78
|
+
- **as which client operation:** `transaction_id` (echo identity)
|
|
79
|
+
|
|
80
|
+
`read_at_sync_id` and `fence_token` are companions: the first records the data
|
|
81
|
+
version the write reasoned against, the second the lease generation it was
|
|
82
|
+
authorized at. Both are `NULL` when the write carried none (an unclaimed write, a
|
|
83
|
+
human `user` committer exempt under Law 7, or a legacy row) — **never fabricated
|
|
84
|
+
server-side**. The evidence derives only from what the write actually presented,
|
|
85
|
+
so the audit row can't drift from what the fence enforced.
|
|
86
|
+
|
|
87
|
+
## One-line test for "which id is this?"
|
|
88
|
+
|
|
89
|
+
> If removing it could turn an accepted commit into a rejected one, it's
|
|
90
|
+
> **Axis 1**. If removing it only costs you dedup, echo reconciliation, or
|
|
91
|
+
> traceability — while the write still lands — it's **Axis 2**.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Concurrency Convention: Open Decisions
|
|
2
|
+
|
|
3
|
+
Maintainer notes for [`concurrency-convention.md`](../concurrency-convention.md).
|
|
4
|
+
These are decisions the team has deliberately not made yet. They change public
|
|
5
|
+
behaviour, so they are tracked here rather than in the public contract, where an
|
|
6
|
+
unmade decision reads as an unsettled guarantee.
|
|
7
|
+
|
|
8
|
+
## Default disposition for agents
|
|
9
|
+
|
|
10
|
+
Should an agent-participant guarded write default to `notify` (philosophy
|
|
11
|
+
aligned: surface, do not overwrite) instead of `reject` (back-compat)?
|
|
12
|
+
|
|
13
|
+
The trade-off is alignment against a behaviour change for existing agent
|
|
14
|
+
callers. Today a guarded write with `readAt` but no `onStale` defaults to
|
|
15
|
+
`reject` for every participant kind.
|
|
16
|
+
|
|
17
|
+
## Batch premises through the policy seam
|
|
18
|
+
|
|
19
|
+
Should premise conflicts also pass through `ConflictPolicy`, or stay on the
|
|
20
|
+
direct `onStale` mapping?
|
|
21
|
+
|
|
22
|
+
Routing them through the seam requires a group-aware conflict shape, because a
|
|
23
|
+
batch premise can name a sync group rather than a row. Custom `ConflictPolicy`
|
|
24
|
+
functions currently see write-target conflicts only (`stale_context` /
|
|
25
|
+
`claim_held`); batch-premise conflicts resolve directly through each entry's
|
|
26
|
+
`onStale`.
|
|
27
|
+
|
|
28
|
+
## The serializability floor
|
|
29
|
+
|
|
30
|
+
A batch premise is a sound check, not a full precedence-graph guarantee: it
|
|
31
|
+
catches only what the caller declared. A caller that declares nothing gets no
|
|
32
|
+
check at all, because write-target checking needs a `readAt` to check against,
|
|
33
|
+
and a plain write is last-writer-wins.
|
|
34
|
+
|
|
35
|
+
The floor is therefore zero, and closing that gap is the subject of ADR 0018.
|
|
36
|
+
The public page states the resulting behaviour as a limit; the framing of it as
|
|
37
|
+
a gap to be closed belongs here.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Data Source Reverse Channel (local-dev parity)
|
|
2
|
+
|
|
3
|
+
Maintainer scoping doc. Closes the one real day-one DX gap in Data Source
|
|
4
|
+
mode: the `commit`/`load`/`list` legs are inbound webhooks (Ablo → your
|
|
5
|
+
endpoint), so on `localhost` they need a tunnel (ngrok/cloudflared). This
|
|
6
|
+
scopes a built-in reverse channel so Data Source works on localhost the way
|
|
7
|
+
managed mode already does — the Stripe-CLI `stripe listen` pattern.
|
|
8
|
+
|
|
9
|
+
## The gap, precisely
|
|
10
|
+
|
|
11
|
+
Data Source has two directions today:
|
|
12
|
+
|
|
13
|
+
| Leg | Direction | Transport(s) today | localhost-friendly? |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `events` (external writes → Ablo) | customer → Ablo | **poll** (`events` handler, Ablo calls you) **+ push** (`createPushQueue` → `POST /api/source/events`, you call Ablo) | ✅ yes, via push |
|
|
16
|
+
| `commit` / `load` / `list` | Ablo → customer | **inbound webhook only** (`dataSource()` route) | ❌ no: needs public URL |
|
|
17
|
+
|
|
18
|
+
The asymmetry is the whole bug. `events` already ships an outbound transport
|
|
19
|
+
(`src/source/pushQueue.ts`), so external writes reach Ablo from localhost
|
|
20
|
+
without a tunnel. The `commit`/`load`/`list` leg never got one, so Ablo Cloud
|
|
21
|
+
has no way to reach a `localhost:3000` dev server.
|
|
22
|
+
|
|
23
|
+
Inbound is exactly what localhost cannot receive. Managed mode has no inbound
|
|
24
|
+
leg (the browser/SDK opens the only connection, outbound to Ablo), which is why
|
|
25
|
+
managed mode "just works" locally and Data Source doesn't.
|
|
26
|
+
|
|
27
|
+
## Prior art
|
|
28
|
+
|
|
29
|
+
- **Stripe CLI `stripe listen`:** the canonical fix. The CLI opens an
|
|
30
|
+
*outbound* WebSocket to Stripe; Stripe drains webhook events down it and the
|
|
31
|
+
CLI forwards them to `localhost`. No public URL, no tunnel. We want the same
|
|
32
|
+
for the `commit`/`load`/`list` leg.
|
|
33
|
+
- **Our own `createPushQueue`:** already proves the outbound-from-customer
|
|
34
|
+
pattern for the `events` leg. The reverse channel is the symmetric primitive
|
|
35
|
+
for the other direction.
|
|
36
|
+
|
|
37
|
+
## Design
|
|
38
|
+
|
|
39
|
+
A customer-run **source connector** dials out to Ablo Cloud and serves
|
|
40
|
+
`commit`/`load`/`list` over the open connection, instead of Ablo making inbound
|
|
41
|
+
HTTP calls.
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
LOCAL DEV (no public URL)
|
|
45
|
+
|
|
46
|
+
ablo client ──ws──▶ Ablo Cloud ──┐
|
|
47
|
+
│ (no inbound HTTP to localhost)
|
|
48
|
+
customer connector ──ws (dial-out)──▶ Ablo Cloud
|
|
49
|
+
│ drains pending commit/load/list for this source
|
|
50
|
+
▼
|
|
51
|
+
dataSource(options) ← UNCHANGED handler, fed a synthesized Request
|
|
52
|
+
▼
|
|
53
|
+
local Postgres
|
|
54
|
+
│ signed response posted back up the same ws
|
|
55
|
+
└──────────────────────────────────────▶ Ablo Cloud ──ws──▶ ablo client
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Why it's small
|
|
59
|
+
|
|
60
|
+
`dataSource(options)` is already `(request: Request) => Promise<Response>` in
|
|
61
|
+
`src/source/factory.ts`. The connector does not reimplement any handler logic —
|
|
62
|
+
it:
|
|
63
|
+
|
|
64
|
+
1. Opens a WS to a new Ablo Cloud endpoint (e.g. `/v1/source/listen`),
|
|
65
|
+
authenticating with the project API key.
|
|
66
|
+
2. Registers which source/org it serves. Ablo Cloud routes that source's
|
|
67
|
+
`commit`/`load`/`list` requests to this socket instead of the configured
|
|
68
|
+
webhook URL (when a live connector is attached).
|
|
69
|
+
3. For each drained request frame: synthesize a `Request` with the same signed
|
|
70
|
+
headers Ablo would have sent, call the customer's existing `dataSource`
|
|
71
|
+
handler, and post the `Response` back up the socket.
|
|
72
|
+
|
|
73
|
+
Customer-side surface is one wrapper around the handler they already wrote:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// dev only — same handler object as the deployed route
|
|
77
|
+
import { dataSource, createSourceConnector } from '@abloatai/ablo';
|
|
78
|
+
import { sourceOptions } from './ablo.source'; // shared with route.ts
|
|
79
|
+
|
|
80
|
+
const connector = createSourceConnector({
|
|
81
|
+
apiKey: process.env.ABLO_API_KEY!, // sk_test_*
|
|
82
|
+
handler: dataSource(sourceOptions), // the unchanged (Request)=>Response
|
|
83
|
+
});
|
|
84
|
+
await connector.run(abortSignal);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`route.ts` (deployed) and the connector (local) share the same
|
|
88
|
+
`sourceOptions` — zero handler drift.
|
|
89
|
+
|
|
90
|
+
### Server side (sync-server)
|
|
91
|
+
|
|
92
|
+
- New WS endpoint `/v1/source/listen`. Auth: project API key → resolves the
|
|
93
|
+
source. Reject if the key isn't `sk_test_*` unless the source explicitly
|
|
94
|
+
opts into reverse-channel for production (see "Production" below).
|
|
95
|
+
- Per-source request queue. When a `commit`/`load`/`list` needs the customer
|
|
96
|
+
and a connector is attached, enqueue + drain down the socket instead of
|
|
97
|
+
POSTing the webhook URL. Reuse the same signed-envelope shape so the
|
|
98
|
+
customer handler verifies identically (`verifyAbloSourceRequest` unchanged).
|
|
99
|
+
- Fallback: no connector attached → existing inbound webhook path. The
|
|
100
|
+
reverse channel is purely additive; nothing changes for deployed apps.
|
|
101
|
+
|
|
102
|
+
### Signature / security
|
|
103
|
+
|
|
104
|
+
- The drained frames carry the **same** Standard Webhooks signature
|
|
105
|
+
(`webhook-id`/`webhook-timestamp`/`webhook-signature`) computed with the
|
|
106
|
+
project key, so the connector verifies them through the existing
|
|
107
|
+
`verifyAbloSourceRequest` with no special-casing. The transport changes; the
|
|
108
|
+
trust model does not.
|
|
109
|
+
- Gate to `sk_test_*` by default. The DB still stays canonical in the
|
|
110
|
+
customer's process; nothing here gives Ablo the `DATABASE_URL`.
|
|
111
|
+
|
|
112
|
+
## Test-mode interplay
|
|
113
|
+
|
|
114
|
+
`SourceRequestContext.mode` (`src/source/types.ts`) already distinguishes
|
|
115
|
+
`test`/`live`. The reverse channel is the natural home for `mode: 'test'`
|
|
116
|
+
traffic: a local connector attached with an `sk_test_*` key receives the
|
|
117
|
+
source's test commits, runs them against the customer's test DB, and the SDK
|
|
118
|
+
sees confirmed rows + fan-out exactly as in production. This is the missing
|
|
119
|
+
piece that makes `sk_test_*` a complete local loop rather than just a data
|
|
120
|
+
namespace.
|
|
121
|
+
|
|
122
|
+
## Production stance
|
|
123
|
+
|
|
124
|
+
Keep the inbound webhook as the default deployed transport — it's lower
|
|
125
|
+
latency (no long-lived socket to babysit) and stateless. The reverse channel
|
|
126
|
+
is primarily the **dev** affordance. A secondary, opt-in use is a
|
|
127
|
+
"no-public-URL deploy" mode for customers who cannot expose an inbound
|
|
128
|
+
endpoint at all (locked-down VPCs); that's a follow-on, not the initial scope.
|
|
129
|
+
|
|
130
|
+
## Scope boundary (what this is NOT)
|
|
131
|
+
|
|
132
|
+
- Not a generic tunnel — it forwards only signed Ablo source frames for one
|
|
133
|
+
source, not arbitrary traffic.
|
|
134
|
+
- Not a change to the handler contract — `dataSource` is untouched; the
|
|
135
|
+
connector wraps its handler.
|
|
136
|
+
- Not a managed-mode change — managed mode has no inbound leg and is unaffected.
|
|
137
|
+
|
|
138
|
+
## Touch list (when built)
|
|
139
|
+
|
|
140
|
+
- `packages/transaction/src/source/connector.ts` — `createSourceConnector`
|
|
141
|
+
(dial-out WS client; synthesize Request → existing handler → post Response).
|
|
142
|
+
- `packages/transaction` export surface — expose `createSourceConnector` next to
|
|
143
|
+
`createPushQueue`.
|
|
144
|
+
- `apps/sync-server` — `/v1/source/listen` WS endpoint + per-source request
|
|
145
|
+
queue + "drain to connector if attached, else webhook" branch in the source
|
|
146
|
+
dispatch path.
|
|
147
|
+
- `docs/data-sources.md` — document the local-dev loop (the current docs only
|
|
148
|
+
describe the public-HTTPS webhook).
|
|
149
|
+
- Tests: connector round-trip (drained commit → handler → response), signature
|
|
150
|
+
parity with the webhook path, fallback-to-webhook when no connector attached.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Per-Field Conflict Detection (Track A)
|
|
2
|
+
|
|
3
|
+
Maintainer decision doc. Scopes the move from entity-level to field-level stale
|
|
4
|
+
detection in `executeCommit`. Library-free; restores Linear parity for the
|
|
5
|
+
disjoint-field case while keeping the agent-specific `readAt` reject.
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`executeCommit` Step 0 (`apps/sync-server/src/mutators/commit.ts`) detects stale
|
|
10
|
+
writes at **entity granularity**:
|
|
11
|
+
|
|
12
|
+
```sql
|
|
13
|
+
SELECT MAX(id) FROM sync_deltas WHERE model_name = ? AND model_id = ?
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
If `observed > op.readAt`, the op conflicts. This means two writers touching
|
|
17
|
+
**different fields** of the same row collide falsely: agent A sets
|
|
18
|
+
`report.status`, human B sets `report.reviewer`, B carried a `readAt` from before A's
|
|
19
|
+
write → B is rejected with `AbloStaleContextError`, even though the edits never
|
|
20
|
+
overlapped.
|
|
21
|
+
|
|
22
|
+
This is an over-rejection. It is also stricter than the system we
|
|
23
|
+
reverse-engineered from (Linear), which never had this problem.
|
|
24
|
+
|
|
25
|
+
## Why Linear says this is right
|
|
26
|
+
|
|
27
|
+
The model is reverse-engineered from Linear's sync engine. Linear's design
|
|
28
|
+
confirms every load-bearing choice here:
|
|
29
|
+
|
|
30
|
+
- **Transactions are property-level.** Linear's `UpdateTransaction` records
|
|
31
|
+
"the name of the changed property and its previous value" — it carries only
|
|
32
|
+
the changed properties, not a whole-object snapshot. Our `changed_fields`
|
|
33
|
+
column re-derives exactly that.
|
|
34
|
+
- **Resolution is last-writer-wins, per property, by total order:** `syncId`
|
|
35
|
+
(our `sync_id_seq`) is the total order. A partial transaction applies only its
|
|
36
|
+
properties, so two clients editing **different** properties both win — LWW
|
|
37
|
+
only bites on the **same** property.
|
|
38
|
+
- **CRDT is used only for issue descriptions.** Linear keeps LWW-per-property
|
|
39
|
+
for structured fields and reserves a CRDT for the one rich-text body. That is
|
|
40
|
+
the same Track A / Track B line we draw: this doc is Track A; rich-text bodies
|
|
41
|
+
(TipTap `content_json`) are out of scope and belong to a separate CRDT track.
|
|
42
|
+
|
|
43
|
+
So Track A is not a new feature — it **restores Linear parity** at the property
|
|
44
|
+
level we had flattened to entity level.
|
|
45
|
+
|
|
46
|
+
Sources: [reverse-linear-sync-engine (CTO-endorsed)](https://github.com/wzhudev/reverse-linear-sync-engine/blob/main/SUMMARY.md),
|
|
47
|
+
[Architectures for Central Server Collaboration — Weidner](https://mattweidner.com/2024/06/04/server-architectures.html).
|
|
48
|
+
|
|
49
|
+
## The constraint that shapes the design
|
|
50
|
+
|
|
51
|
+
`sync_deltas.data` stores the **full post-update row**, not the changed columns.
|
|
52
|
+
This was a deliberate change (see `feedback_partial_update_delta_ui_drift`) so
|
|
53
|
+
the live-pool update path fires MobX reactivity for nested fields. Consequence:
|
|
54
|
+
we **cannot** recover "which fields did this delta change" from `data` — a
|
|
55
|
+
full-row snapshot does not tell you what moved.
|
|
56
|
+
|
|
57
|
+
We do have the changed set for free at write time: `Object.keys(snakeInput)` at
|
|
58
|
+
`commit.ts` UPDATE branch, after the unknown-column strip and before the
|
|
59
|
+
`updated_at` injection. So this is a write-side capture + a read-side
|
|
60
|
+
intersection, not a diff-the-snapshots problem.
|
|
61
|
+
|
|
62
|
+
## Design
|
|
63
|
+
|
|
64
|
+
### 1. Schema: `sync_deltas.changed_fields text[]` (nullable)
|
|
65
|
+
|
|
66
|
+
- Populate **only for UPDATE** with the real changed columns
|
|
67
|
+
(`Object.keys(snakeInput)` after strip, before `updated_at`).
|
|
68
|
+
- Leave `null` for CREATE / DELETE / ARCHIVE / UNARCHIVE.
|
|
69
|
+
|
|
70
|
+
`null` is semantically "whole-entity change" and always conflicts. This gives a
|
|
71
|
+
**safe migration**: every pre-migration delta is `null`, so detection falls back
|
|
72
|
+
to exactly today's entity-level behavior. Field granularity phases in only as
|
|
73
|
+
new deltas land — no risky backfill.
|
|
74
|
+
|
|
75
|
+
We store **field names only**, not previous values. LWW needs no value
|
|
76
|
+
comparison (latest `sync_id` wins); names are sufficient for the overlap check
|
|
77
|
+
that drives the optional reject. Prev-values would only matter for
|
|
78
|
+
"same field, same value ⇒ not a conflict" tie-breaking — defer it.
|
|
79
|
+
|
|
80
|
+
### 2. Detection rewrite: Step 0 (`commit.ts`)
|
|
81
|
+
|
|
82
|
+
Replace the scalar `MAX(id)` with a field-aware scan:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// op's own field set (snake-cased, framework cols excluded)
|
|
86
|
+
const opFields = new Set(Object.keys(op.input ?? {}).map(toSnakeCase));
|
|
87
|
+
|
|
88
|
+
const rows = await tx.unsafe(
|
|
89
|
+
`SELECT id, changed_fields FROM sync_deltas
|
|
90
|
+
WHERE model_name = $1 AND model_id = $2 AND id > $3
|
|
91
|
+
ORDER BY id DESC`,
|
|
92
|
+
[mapping.modelName, op.id, op.readAt] as never[],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Conflict iff a newer delta touched a field this op also writes,
|
|
96
|
+
// OR a newer delta is whole-entity (changed_fields IS NULL → CREATE/DELETE).
|
|
97
|
+
const overlap = rows.find(
|
|
98
|
+
(r) => r.changedFields === null || r.changedFields.some((f) => opFields.has(f)),
|
|
99
|
+
);
|
|
100
|
+
if (overlap) {
|
|
101
|
+
conflicts.push({
|
|
102
|
+
/* ...existing fields... */
|
|
103
|
+
observedSyncId: overlap.id,
|
|
104
|
+
conflictingFields: intersect(overlap.changedFields, opFields),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Disjoint-field concurrent writes now produce **no conflict** — they both apply.
|
|
110
|
+
That is LWW-per-field achieved by *not rejecting*; no merge code.
|
|
111
|
+
|
|
112
|
+
### 3. Policy type: additive (`packages/transaction/src/policy/types.ts`)
|
|
113
|
+
|
|
114
|
+
Extend `StaleContextConflict` with:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
readonly conflictingFields?: readonly string[];
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Pure addition. `defaultPolicy` still rejects; existing policies compile
|
|
121
|
+
unchanged. A policy can now reason at field granularity, e.g. allow when the
|
|
122
|
+
only conflicting field is cosmetic.
|
|
123
|
+
|
|
124
|
+
### 4. Scope boundary (honest)
|
|
125
|
+
|
|
126
|
+
Granularity is **column-level**, not JSON-path. Two writers editing different
|
|
127
|
+
keys *inside* one `content_json` column still conflict — that is the rich-text
|
|
128
|
+
case (Track B / CRDT), not this. JSON Merge Patch (RFC 7386) sub-column
|
|
129
|
+
granularity is a later refinement on the same column; v1 stops at columns.
|
|
130
|
+
|
|
131
|
+
## Relationship to the existing `readAt` reject
|
|
132
|
+
|
|
133
|
+
Linear is pure LWW-per-property with no stale check. We keep `readAt` /
|
|
134
|
+
`onStale: 'reject'` as an **opt-in** for the agent-reasoned-against-stale-state
|
|
135
|
+
case (an LLM that read a stale value and reasoned on it is a real failure mode
|
|
136
|
+
humans rarely hit). After Track A:
|
|
137
|
+
|
|
138
|
+
- **No `readAt`** → LWW-per-field, Linear parity: disjoint fields never conflict.
|
|
139
|
+
- **`readAt` set** → reject only if a newer delta touched a field this op also
|
|
140
|
+
writes. The `changed_fields` column makes that intersection computable.
|
|
141
|
+
- **`onStale: 'overwrite'`** → unchanged; still skips detection entirely.
|
|
142
|
+
|
|
143
|
+
## Index
|
|
144
|
+
|
|
145
|
+
Verify a `(model_name, model_id, id)` index exists on `sync_deltas` (the old
|
|
146
|
+
`MAX(id)` relied on it too). The new query is a bounded range scan (`id >
|
|
147
|
+
readAt`, usually a small recent window) on the same index prefix.
|
|
148
|
+
|
|
149
|
+
## Tests (vitest, sync-server)
|
|
150
|
+
|
|
151
|
+
- Disjoint fields (A: `status`, B: `assignee`, same row, both stale `readAt`) →
|
|
152
|
+
**both commit, no `AbloStaleContextError`** — the regression that proves the win.
|
|
153
|
+
- Same field, both stale → still rejects (default policy unchanged).
|
|
154
|
+
- DELETE after `readAt` → conflicts regardless of op fields (`null`).
|
|
155
|
+
- Pre-migration delta (`null`) in the window → conflicts (back-compat).
|
|
156
|
+
- `onStale: 'overwrite'` → still skips detection.
|
|
157
|
+
|
|
158
|
+
## Touch list
|
|
159
|
+
|
|
160
|
+
- `sync_deltas` migration — add `changed_fields text[]`.
|
|
161
|
+
- `commit.ts` — Step 0 detect rewrite + UPDATE write path populates
|
|
162
|
+
`changed_fields` + `deltaInfos` shape.
|
|
163
|
+
- `apps/sync-server/src/db/deltas.ts` — insert path carries `changed_fields`.
|
|
164
|
+
- `packages/transaction/src/policy/types.ts` — additive `conflictingFields`.
|
|
165
|
+
- vitest in sync-server.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Postgres replication: internal architecture
|
|
2
|
+
|
|
3
|
+
> **Status: wired and covered by unit and real-Postgres journeys.** This is server-internal code under `apps/sync-server/src/replication/postgres/`; it is not an SDK surface.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Ablo observes a customer's Postgres through a publication and logical-replication slot. The customer owns the schema and write path. Ablo decodes committed changes, appends them to its control-plane log, and serves sync from that log. The low-level decoder and lifecycle draw on Zero and PowerSync patterns; ADR 0002 governs the product boundary.
|
|
8
|
+
|
|
9
|
+
## The one job
|
|
10
|
+
|
|
11
|
+
**Postgres `pgoutput` messages → `PreparedDelta[]` + a confirmed LSN.** The consumer writes through `appendExternalDeltas`; deltas land in the control-plane `sync_deltas` log and use the normal fan-out pipeline.
|
|
12
|
+
|
|
13
|
+
## Module map (`apps/sync-server/src/replication/postgres/`)
|
|
14
|
+
|
|
15
|
+
| file | role | provenance |
|
|
16
|
+
| -------------------------------------------------------- | ---------------------------------------------------------- | ----------------------------- |
|
|
17
|
+
| `binaryReader.ts` | big-endian protocol reader | modeled on Zero |
|
|
18
|
+
| `pgoutputTypes.ts`, `pgoutput.ts` | typed `pgoutput` messages and decoder | modeled on Zero |
|
|
19
|
+
| `lsn.ts` | `LSN` string ↔ `bigint` (`toBigInt`/`fromBigInt`) | ported subset ← Zero `lsn.ts` |
|
|
20
|
+
| `connection.ts`, `stream.ts`, `streamAdapter.ts` | dedicated query/replication connections and stream adapter | Zero/PowerSync patterns |
|
|
21
|
+
| `assembler.ts` | buffers a transaction and maps changes to `PreparedDelta` | Ablo adapter |
|
|
22
|
+
| `consumer.ts` | persist-before-ack consume loop with retry | PowerSync/Ablo patterns |
|
|
23
|
+
| `slot.ts`, `slotLease.ts`, `backfill.ts`, `watermark.ts` | slot ownership, initial snapshot, and durable progress | Ablo |
|
|
24
|
+
| `sources.ts`, `fleet.ts`, `start.ts` | registry resolution, reconciliation, start/stop lifecycle | Ablo |
|
|
25
|
+
| `preflight.ts`, `readiness.ts`, `publicationDrift.ts` | registration checks and runtime diagnostics | Ablo |
|
|
26
|
+
|
|
27
|
+
## Data flow
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
registered Postgres source
|
|
31
|
+
→ replication slot + initial snapshot
|
|
32
|
+
→ pgoutput stream
|
|
33
|
+
→ TransactionAssembler
|
|
34
|
+
→ WalConsumer
|
|
35
|
+
→ appendExternalDeltas(controlSql, deltas, context)
|
|
36
|
+
→ persist watermark
|
|
37
|
+
→ acknowledge commit LSN
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Mapping (in `TransactionAssembler`, mirrors `events.ts:eventsToDeltas`)
|
|
41
|
+
|
|
42
|
+
- `actionType`: `insert→'I'`, `update→'U'`, `delete→'D'` (the 1:1 pgoutput↔Ablo coincidence).
|
|
43
|
+
- `modelName`: `mapping.tableToModel(schema, table)` — `null` skips the change.
|
|
44
|
+
- `modelId`: `mapping.rowToModelId(key)` over the replica-identity key.
|
|
45
|
+
- `data`: the row bound as an **object, never pre-stringified** (the jsonb double-encode trap at `deltaAppend.ts`).
|
|
46
|
+
- `transactionId`: `String(xid)`.
|
|
47
|
+
|
|
48
|
+
## Load-bearing invariants
|
|
49
|
+
|
|
50
|
+
- **Persist-before-ack** (`WalConsumer`): `appendExternalDeltas` and the watermark transaction resolve before `ack(commitLsn)`. A crash between them replays work instead of losing it.
|
|
51
|
+
- **Keepalive watermark** (`streamAdapter.ts`, `stream.ts`): every reply carries the last confirmed LSN, never the server's live position — the timed status update included.
|
|
52
|
+
- **Liveness off the socket** (`stream.ts`): a status update goes out every 75% of the upstream's `wal_sender_timeout` whether or not the consumer is reading, so backpressure cannot get the connection terminated for silence; inbound silence on a stream we are reading for twice that long destroys it and falls into the per-source backoff. A `wal_sender_timeout` of 0 runs untimed.
|
|
53
|
+
- **Failover-capable slot** (`slot.ts`): from PostgreSQL 17 the slot is created with `FAILOVER true`, so a customer failover leaves our position intact instead of forcing the re-snapshot path. It only takes effect where the standby has `sync_replication_slots = on`, which the preflight recommends and never requires.
|
|
54
|
+
- **Fresh subscription per retry** (`WalConsumer`): every backoff iteration opens a new subscription so a half-dead socket / stale relation cache never carries into the retry.
|
|
55
|
+
- **Per-source isolation** (`start.ts`, `fleet.ts`): one broken source reports and retries without stopping healthy sources.
|
|
56
|
+
- **Runtime reconciliation** (`fleet.ts`): registrations, removals, schema changes, and secret rotations converge without a server restart.
|
|
57
|
+
|
|
58
|
+
## Tests
|
|
59
|
+
|
|
60
|
+
Unit tests live beside the implementation in `replication/postgres/__tests__`. Real-Postgres coverage is grouped under `src/__journeys__/postgres-replication-*.journey.test.ts`: registration, registry migration, backfill, live streaming, source changes, bootstrap, query serving, read cutover, and customer-database isolation.
|
|
61
|
+
|
|
62
|
+
## Operations
|
|
63
|
+
|
|
64
|
+
Registration is the enable signal. `startPostgresReplication` starts the fleet after the server begins listening; `postgresReplicationReady` is drained during graceful shutdown. Use `docs/runbooks/connect-customer-database-postgres-replication.md` for source setup and live verification.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# A `Schema` is serializable
|
|
2
|
+
|
|
3
|
+
A `Schema` (output of `defineSchema`) is JSON-serializable except for two
|
|
4
|
+
things, both of which are client-only:
|
|
5
|
+
|
|
6
|
+
- **Zod validators:** `model().schema` / `.shape`, `Schema.validators`. Used
|
|
7
|
+
by the client for type inference + validation. The server never reads them
|
|
8
|
+
(it checks `information_schema.columns` and does no field-shape validation in
|
|
9
|
+
the commit path).
|
|
10
|
+
|
|
11
|
+
Everything the server reads — `typename`, `tableName`, `mutable`, `load`, the
|
|
12
|
+
canonical `tenancy` descriptor (the `policy` authoring option is normalized away
|
|
13
|
+
at build), bootstrap hints, `relations` (`foreignKeyColumn`), field names, and
|
|
14
|
+
`identityRoles` — is plain data.
|
|
15
|
+
|
|
16
|
+
## Identity roles are pure data
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
interface IdentityRole {
|
|
20
|
+
kind: string;
|
|
21
|
+
template: string; // 'org:{id}'
|
|
22
|
+
source: IdentityRoleSource; // { field: 'organizationId', multi: false }
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The runtime behaviour lives in `extractIdentityIds(identity, source)`, a pure
|
|
27
|
+
function `composeIdentitySyncGroups` calls once per role. `identityRole({ kind,
|
|
28
|
+
template, source, multi? })` is the factory. Absent/falsy fields yield `[]`, so
|
|
29
|
+
a role whose field isn't present (a user with no `teamIds`) is a silent no-op —
|
|
30
|
+
org-only, org+user, and org+team are just different `identityRoles` arrays, not
|
|
31
|
+
different code paths. The engine ships zero prefixes; `org:`/`user:`/`team:`
|
|
32
|
+
live only in `ablo.schema.ts`.
|
|
33
|
+
|
|
34
|
+
## Why this matters
|
|
35
|
+
|
|
36
|
+
Because a `Schema` carries no closures, the same object works in-process and,
|
|
37
|
+
for a hosted multi-tenant server, after being reconstructed from JSON over the
|
|
38
|
+
control plane (the GraphQL `printSchema` / `buildSchema` model). One type,
|
|
39
|
+
both places — no separate server-side schema type.
|
|
40
|
+
|
|
41
|
+
`apps/sync-server` reads the live `schema` directly today
|
|
42
|
+
(`buildModelMap(schema)`, `composeIdentitySyncGroups` via the `@ablo/schema`
|
|
43
|
+
wrapper).
|
|
44
|
+
|
|
45
|
+
## Trust boundary
|
|
46
|
+
|
|
47
|
+
Never trust a client-connection schema for authz (Zero/Convex/Instant). A
|
|
48
|
+
client connection may carry only the schema **version** for compatibility
|
|
49
|
+
gating; the authoritative `Schema` arrives over an authenticated control-plane
|
|
50
|
+
path. The identity passed to `composeIdentitySyncGroups` is server-resolved
|
|
51
|
+
trusted claims.
|
|
52
|
+
|
|
53
|
+
## Wire form (`serialize.ts`)
|
|
54
|
+
|
|
55
|
+
`serializeSchema(schema): string` / `parseSchema(json): Schema` are the
|
|
56
|
+
control-plane transport — the GraphQL `printSchema`/`buildSchema` model. The
|
|
57
|
+
JSON (`SchemaJSON`, envelope `{ v, models, identityRoles }`) carries every
|
|
58
|
+
model's routing/scoping metadata, relations (incl. resolved
|
|
59
|
+
`foreignKeyColumn`), field metadata, and identity roles. `parseSchema` rebuilds
|
|
60
|
+
each model's Zod permissively from `FieldMeta` (the server does no field-shape
|
|
61
|
+
validation) and drops `computed` closures. `schemaHash(schema)` is the stable
|
|
62
|
+
FNV-1a content hash used for connect-time gating. Round-trip tested in
|
|
63
|
+
`__tests__/serialize.test.ts`.
|
|
64
|
+
|
|
65
|
+
## Storage + runtime resolution (`apps/sync-server/src/schema/`): built
|
|
66
|
+
|
|
67
|
+
- **`ablo_schemas` table** (`packages/database/prisma/models/sync.prisma`,
|
|
68
|
+
`SchemaArtifact`) — `(organizationId, version, schemaJson, schemaHash, state,
|
|
69
|
+
error, createdBy, createdAt, activatedAt)`, unique `(orgId, version)`. State
|
|
70
|
+
`pending|validated|active|overwritten|failed`, ≤1 active per tenant (Convex
|
|
71
|
+
`_schemas` machine; Zero's "row in the operational DB"). *Migration written,
|
|
72
|
+
not applied — 0 users, Neon direct-endpoint rule.*
|
|
73
|
+
- **`pgSchemaStore` / `memorySchemaStore`** (`schemaStore.ts`) — mirrors
|
|
74
|
+
`pgApiKeyStore`. `insertPending` assigns `MAX(version)+1`; `activate` is a
|
|
75
|
+
transaction that demotes the current active → `overwritten` then promotes the
|
|
76
|
+
target. State-machine invariants tested.
|
|
77
|
+
- **`createSchemaRegistry(store)`** (`schemaRegistry.ts`) — `load(orgId)` parses
|
|
78
|
+
the active artifact's `schemaJson` to a `Schema` and caches it (shared
|
|
79
|
+
in-flight promise across concurrent cold loads); `invalidate(orgId)` busts it
|
|
80
|
+
on activation (Convex `schema_registry`). This is the seam that turns the
|
|
81
|
+
boot-time `import { schema }` into per-tenant runtime resolution.
|
|
82
|
+
|
|
83
|
+
## Push route (`apps/sync-server/src/routes/schema.ts`): built
|
|
84
|
+
|
|
85
|
+
`POST /api/schema`, mounted in `index.ts` (`schemaRoutes({ provider, store,
|
|
86
|
+
registry })`). Auth: secret `sk_` key carrying the `schema:push` scope —
|
|
87
|
+
`Identity.scopes` was added and `apiKeyProvider` now populates it from the key
|
|
88
|
+
row's `scopes` column (restricted `rk_` keys get no `scopes`, so they're
|
|
89
|
+
excluded). Tenant comes from `identity.organizationId`, never the body. Flow:
|
|
90
|
+
read `{ schema, force? }` → validate via `parseSchema` (throws → 400) →
|
|
91
|
+
authoritative hash via `schemaHash(parsed)` → reject removed-model changes (409)
|
|
92
|
+
unless `force` → no-op fast path on identical hash (200) → `insertPending` →
|
|
93
|
+
`activate` → `registry.invalidate(org)` → 201 `{ schemaId, version, hash }`. 6
|
|
94
|
+
route tests.
|
|
95
|
+
|
|
96
|
+
## CLI (`packages/ablo-cli`): built
|
|
97
|
+
|
|
98
|
+
`ablo push` (`src/push.ts`, dispatched from `index.ts`). Imports the
|
|
99
|
+
user's `sync/schema.ts` at runtime via tsx's `tsImport` (the real object —
|
|
100
|
+
`migrate`'s regex parse can't produce a faithful AST), then `serializeSchema`
|
|
101
|
+
+ `schemaHash` and POSTs `{ schema, force, renames }` to `POST /api/schema`
|
|
102
|
+
with `Authorization: Bearer $ABLO_API_KEY`. Flags: `--schema`, `--export`,
|
|
103
|
+
`--url` (`$ABLO_API_URL`, default `https://api.abloatai.com`), `--force`,
|
|
104
|
+
`--rename old:new` (repeatable). The route honors `renames` so a renamed model
|
|
105
|
+
isn't flagged as a removed-model incompatibility. `parsePushArgs` unit-tested.
|
|
106
|
+
|
|
107
|
+
## Schema drift is advisory
|
|
108
|
+
|
|
109
|
+
Bootstrap includes the tenant's active schema hash. The client compares it to
|
|
110
|
+
its built-in hash and warns once when they differ. Hash drift never closes the
|
|
111
|
+
WebSocket: an additive rollout must allow old and new clients to overlap while
|
|
112
|
+
data is expanded, dual-read/written, backfilled, verified, and finally
|
|
113
|
+
contracted. Breaking wire shapes use the protocol-version codec registry
|
|
114
|
+
instead.
|
|
115
|
+
|
|
116
|
+
## Not built yet
|
|
117
|
+
- **Switch the hot paths to per-tenant `registry.load(org)`:** boot still does
|
|
118
|
+
the single-tenant `import { schema }`; `buildModelMap`/bootstrap/commit
|
|
119
|
+
reading the registry per request is the final multi-tenant wiring.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Repository Structure
|
|
2
|
+
|
|
3
|
+
The public repository preserves the same ownership boundaries as the main
|
|
4
|
+
monorepo. `@abloatai/ablo` is the product package; the packages beneath it are
|
|
5
|
+
implementation owners and first-party extension surfaces.
|
|
6
|
+
|
|
7
|
+
| Workspace | Responsibility |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `packages/ablo` | Branded SDK, public entrypoints, docs, examples, release assets |
|
|
10
|
+
| `packages/transaction` | Headless HTTP client, canonical contracts, reads, commits, settlement, claims, durable observation |
|
|
11
|
+
| `packages/humans` | Reactive materializer, WebSocket transport, presence, browser persistence, React |
|
|
12
|
+
| `packages/agent` | Agent behavior, perception, and coordination helpers |
|
|
13
|
+
| `packages/cli` | Project setup, database connection, schema operations, and diagnostics |
|
|
14
|
+
| `packages/tsconfig` | Private shared compiler configuration |
|
|
15
|
+
|
|
16
|
+
Applications install and import `@abloatai/ablo`. The root entrypoint is the
|
|
17
|
+
headless HTTP API. Human-facing reactive behavior is explicit:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import Ablo from '@abloatai/ablo';
|
|
21
|
+
import ReactiveAblo from '@abloatai/ablo/client';
|
|
22
|
+
import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The backend implementation remains in `apps/sync-server` in the private
|
|
26
|
+
monorepo. It consumes transaction contracts but is not part of the public SDK
|
|
27
|
+
repository.
|
|
28
|
+
|
|
29
|
+
Internal packages must not import the branded facade. Dependencies point from
|
|
30
|
+
the facade to the owners, from humans and agents to transaction, and never back
|
|
31
|
+
upward. The public mirror copies these workspaces as workspaces; it does not
|
|
32
|
+
flatten them or generate compatibility source.
|