@abloatai/transaction 0.59.2 → 0.61.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/dist/auth/apiKey.d.ts +10 -11
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +15 -29
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/browserCredentialSafety.js +1 -1
- package/dist/auth/browserCredentialSafety.js.map +1 -1
- package/dist/auth/capability.d.ts +15 -15
- package/dist/auth/capabilityLifecycle.d.ts +3 -3
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/capabilityLifecycle.js +2 -1
- package/dist/auth/capabilityLifecycle.js.map +1 -1
- package/dist/auth/credentialEndpoint.js +1 -1
- package/dist/auth/credentialEndpoint.js.map +1 -1
- package/dist/auth/credentialResult.d.ts +2 -1
- package/dist/auth/credentialResult.d.ts.map +1 -1
- package/dist/auth/credentialResult.js.map +1 -1
- package/dist/auth/credentialSource.js.map +1 -1
- package/dist/auth/identity.js.map +1 -1
- package/dist/auth/schemas.d.ts +5 -5
- package/dist/auth/sessionIssuanceProtocol.d.ts +8 -8
- package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -1
- package/dist/branches.d.ts +38 -38
- package/dist/claims/admission.d.ts +4 -0
- package/dist/claims/admission.d.ts.map +1 -0
- package/dist/claims/admission.js +32 -0
- package/dist/claims/admission.js.map +1 -0
- package/dist/claims/awaitGrant.d.ts.map +1 -1
- package/dist/claims/awaitGrant.js +4 -38
- package/dist/claims/awaitGrant.js.map +1 -1
- package/dist/claims/conflict.d.ts +55 -0
- package/dist/claims/conflict.d.ts.map +1 -0
- package/dist/claims/conflict.js +71 -0
- package/dist/claims/conflict.js.map +1 -0
- package/dist/claims/contract.d.ts +39 -27
- package/dist/claims/contract.d.ts.map +1 -1
- package/dist/claims/contract.js +2 -0
- package/dist/claims/contract.js.map +1 -1
- package/dist/claims/eventContract.d.ts +18 -18
- package/dist/claims/events.d.ts +1 -1
- package/dist/claims/events.d.ts.map +1 -1
- package/dist/claims/index.d.ts +2 -0
- package/dist/claims/index.d.ts.map +1 -1
- package/dist/claims/index.js +1 -0
- package/dist/claims/index.js.map +1 -1
- package/dist/claims/routes.d.ts +9 -9
- package/dist/claims/routes.d.ts.map +1 -1
- package/dist/claims/trace.d.ts.map +1 -1
- package/dist/claims/trace.js.map +1 -1
- package/dist/client/ablo.d.ts +11 -8
- package/dist/client/ablo.d.ts.map +1 -1
- package/dist/client/ablo.js +5 -6
- package/dist/client/ablo.js.map +1 -1
- package/dist/client/resources/httpResources.d.ts +1 -169
- package/dist/client/resources/httpResources.d.ts.map +1 -1
- package/dist/client/resources/httpResources.js +1 -1
- package/dist/client/resources/httpResources.js.map +1 -1
- package/dist/client/resources/modelOperations.d.ts +6 -15
- package/dist/client/resources/modelOperations.d.ts.map +1 -1
- package/dist/client/resources/where.d.ts +12 -12
- package/dist/client/surface.d.ts +1 -1
- package/dist/client/surface.d.ts.map +1 -1
- package/dist/client/surface.js +2 -4
- package/dist/client/surface.js.map +1 -1
- package/dist/commit/confirmation/commitEnvelope.d.ts +7 -7
- package/dist/commit/confirmation/httpCommitEnvelope.d.ts +1 -1
- package/dist/commit/confirmation/pendingWrite.d.ts +5 -5
- package/dist/commit/contract.d.ts +249 -249
- package/dist/commit/contract.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +4 -5
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +64 -101
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +14 -69
- package/dist/coordination/schema.js.map +1 -1
- package/dist/environment.d.ts +3 -3
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/errorCodes.d.ts +4 -5
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +15 -9
- package/dist/errorCodes.js.map +1 -1
- package/dist/errorObservation.d.ts +14 -14
- package/dist/errors.d.ts +17 -11
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +101 -5
- package/dist/errors.js.map +1 -1
- package/dist/keys/index.d.ts +1 -1
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js.map +1 -1
- package/dist/observation/contract.d.ts +34 -34
- package/dist/observation/feedContract.d.ts +86 -86
- package/dist/observation/persistence/syncDeltaRow.d.ts +23 -23
- package/dist/observation/persistence/syncDeltaRow.d.ts.map +1 -1
- package/dist/pricing.d.ts +12 -12
- package/dist/schema/audit.js.map +1 -1
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/ddlLock.js.map +1 -1
- package/dist/schema/deployment/contracts.d.ts +64 -64
- package/dist/schema/deployment/index.js.map +1 -1
- package/dist/schema/deployment/reconcile.js.map +1 -1
- package/dist/schema/diff.js.map +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.d.ts.map +1 -1
- package/dist/schema/loadStrategy.d.ts +2 -2
- package/dist/schema/loadStrategy.d.ts.map +1 -1
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/relation.d.ts +3 -3
- package/dist/schema/relation.d.ts.map +1 -1
- package/dist/schema/residency.d.ts +1 -1
- package/dist/schema/roles.d.ts +1 -1
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/roles.js.map +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/serialize.js.map +1 -1
- package/dist/schema/sugar.d.ts.map +1 -1
- package/dist/schema/tenancy.d.ts +2 -2
- package/dist/server/storageMode.d.ts +1 -1
- package/dist/sessions/client.d.ts +29 -0
- package/dist/sessions/client.d.ts.map +1 -0
- package/dist/sessions/client.js +69 -0
- package/dist/sessions/client.js.map +1 -0
- package/dist/sessions/contract.d.ts +97 -0
- package/dist/sessions/contract.d.ts.map +1 -0
- package/dist/sessions/contract.js +5 -0
- package/dist/sessions/contract.js.map +1 -0
- package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
- package/dist/sessions/create.d.ts.map +1 -0
- package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
- package/dist/sessions/create.js.map +1 -0
- package/dist/sessions/handler.d.ts +23 -0
- package/dist/sessions/handler.d.ts.map +1 -0
- package/dist/sessions/handler.js +55 -0
- package/dist/sessions/handler.js.map +1 -0
- package/dist/sessions/index.d.ts +14 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +14 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
- package/dist/sessions/lifecycle.d.ts.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
- package/dist/sessions/lifecycle.js.map +1 -0
- package/dist/sessions/source.d.ts +24 -0
- package/dist/sessions/source.d.ts.map +1 -0
- package/dist/sessions/source.js +56 -0
- package/dist/sessions/source.js.map +1 -0
- package/dist/source/adapters/adapterProfile.d.ts +6 -6
- package/dist/source/adapters/conformance.js.map +1 -1
- package/dist/source/adapters/contract.d.ts +14 -14
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/connector/client.d.ts.map +1 -1
- package/dist/source/connector/client.js.map +1 -1
- package/dist/source/connector/protocol.d.ts.map +1 -1
- package/dist/source/delivery/pushQueue.d.ts.map +1 -1
- package/dist/source/delivery/pushQueue.js.map +1 -1
- package/dist/source/endpoint/signing.d.ts +4 -4
- package/dist/source/endpoint/signing.d.ts.map +1 -1
- package/dist/source/outbox/contract.d.ts +4 -4
- package/dist/testing/fixtures/httpResponses.d.ts +2 -2
- package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
- package/dist/testing/fixtures/httpResponses.js.map +1 -1
- package/dist/transport/connection/index.d.ts +0 -1
- package/dist/transport/connection/index.d.ts.map +1 -1
- package/dist/transport/connection/index.js +0 -1
- package/dist/transport/connection/index.js.map +1 -1
- package/dist/transport/connection/manager.d.ts +1 -1
- package/dist/transport/connection/manager.d.ts.map +1 -1
- package/dist/transport/connection/manager.js.map +1 -1
- package/dist/transport/connection/networkProbe.d.ts +5 -18
- package/dist/transport/connection/networkProbe.d.ts.map +1 -1
- package/dist/transport/connection/networkProbe.js.map +1 -1
- package/dist/transport/http/claimWait.d.ts +9 -0
- package/dist/transport/http/claimWait.d.ts.map +1 -0
- package/dist/transport/http/claimWait.js +74 -0
- package/dist/transport/http/claimWait.js.map +1 -0
- package/dist/transport/http/client.d.ts +11 -19
- package/dist/transport/http/client.d.ts.map +1 -1
- package/dist/transport/http/client.js +15 -18
- package/dist/transport/http/client.js.map +1 -1
- package/dist/transport/http/contract.d.ts +57 -0
- package/dist/transport/http/contract.d.ts.map +1 -0
- package/dist/transport/http/contract.js +3 -0
- package/dist/transport/http/contract.js.map +1 -0
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/index.d.ts.map +1 -1
- package/dist/transport/http/index.js +1 -0
- package/dist/transport/http/index.js.map +1 -1
- package/dist/transport/http/options.d.ts +4 -1
- package/dist/transport/http/options.d.ts.map +1 -1
- package/dist/transport/http/transport.d.ts +1 -84
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +16 -134
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/commitFrames.d.ts +1 -2
- package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
- package/dist/transport/websocket/commitFrames.js.map +1 -1
- package/dist/transport/websocket/frameHandlers.d.ts +0 -14
- package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
- package/dist/transport/websocket/frameHandlers.js +4 -71
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/heartbeat.d.ts.map +1 -1
- package/dist/transport/websocket/heartbeat.js.map +1 -1
- package/dist/transport/websocket/session.d.ts.map +1 -1
- package/dist/transport/websocket/session.js +137 -64
- package/dist/transport/websocket/session.js.map +1 -1
- package/dist/transport/websocket/sessionContract.d.ts +4 -15
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
- package/dist/transport/websocket/transport.d.ts +6 -42
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js +14 -101
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/accountResponses.d.ts +31 -31
- package/dist/wire/auth.d.ts +3 -3
- package/dist/wire/bootstrapReason.d.ts +2 -2
- package/dist/wire/dataSourceResponses.d.ts +2 -2
- package/dist/wire/dataSourceResponses.d.ts.map +1 -1
- package/dist/wire/frames.d.ts +8 -8
- package/dist/wire/inboundFrames.d.ts +60 -63
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +5 -6
- package/dist/wire/inboundFrames.js.map +1 -1
- package/dist/wire/listEnvelope.js.map +1 -1
- package/dist/wire/modelResponses.d.ts +2 -2
- package/dist/wire/modelShape.d.ts +4 -4
- package/dist/wire/pricing.d.ts +3 -3
- package/dist/wire/protocolVersion.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/auth/apiKey.ts +25 -44
- package/src/auth/browserCredentialSafety.ts +1 -1
- package/src/auth/capabilityLifecycle.ts +3 -2
- package/src/auth/credentialEndpoint.ts +1 -1
- package/src/auth/credentialResult.ts +2 -0
- package/src/claims/admission.ts +49 -0
- package/src/claims/awaitGrant.ts +3 -52
- package/src/claims/conflict.ts +103 -0
- package/src/claims/contract.ts +2 -0
- package/src/claims/events.ts +1 -2
- package/src/claims/index.ts +11 -0
- package/src/client/ablo.ts +15 -10
- package/src/client/resources/httpResources.ts +2 -174
- package/src/client/resources/modelOperations.ts +0 -10
- package/src/client/surface.ts +2 -4
- package/src/coordination/index.ts +4 -9
- package/src/coordination/schema.ts +18 -81
- package/src/errorCodes.ts +21 -13
- package/src/errors.ts +116 -4
- package/src/sessions/client.ts +121 -0
- package/src/sessions/contract.ts +116 -0
- package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
- package/src/sessions/handler.ts +96 -0
- package/src/sessions/index.ts +13 -0
- package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
- package/src/sessions/source.ts +94 -0
- package/src/transport/connection/index.ts +0 -1
- package/src/transport/connection/manager.ts +1 -1
- package/src/transport/http/claimWait.ts +108 -0
- package/src/transport/http/client.ts +27 -36
- package/src/transport/http/contract.ts +71 -0
- package/src/transport/http/index.ts +1 -0
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +22 -275
- package/src/transport/websocket/commitFrames.ts +0 -2
- package/src/transport/websocket/frameHandlers.ts +7 -102
- package/src/transport/websocket/session.ts +162 -73
- package/src/transport/websocket/sessionContract.ts +4 -13
- package/src/transport/websocket/transport.ts +15 -137
- package/src/wire/inboundFrames.ts +5 -6
- package/dist/auth/sessionMint.d.ts.map +0 -1
- package/dist/auth/sessionMint.js.map +0 -1
- package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
- package/dist/transport/connection/credentialLifecycle.js.map +0 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime lifecycle for a renewable session supplied to `Ablo({ session })`.
|
|
3
|
+
*
|
|
4
|
+
* A session provider mints a credential, while this boundary decides when to
|
|
5
|
+
* reuse it. Keeping that policy here prevents HTTP requests and the WebSocket
|
|
6
|
+
* bootstrap from independently minting duplicate sessions for the same actor.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
CredentialProvider,
|
|
11
|
+
CredentialProviderResult,
|
|
12
|
+
} from '../auth/credentialResult.js';
|
|
13
|
+
import type {
|
|
14
|
+
SessionProvider,
|
|
15
|
+
SessionProviderResult,
|
|
16
|
+
SessionSource,
|
|
17
|
+
} from './contract.js';
|
|
18
|
+
import { AbloValidationError } from '../errors.js';
|
|
19
|
+
import {
|
|
20
|
+
createEndpointCredentialResolver,
|
|
21
|
+
isCredentialEndpoint,
|
|
22
|
+
} from '../auth/credentialEndpoint.js';
|
|
23
|
+
import { protectBrowserCredentialProvider } from '../auth/browserCredentialSafety.js';
|
|
24
|
+
|
|
25
|
+
const SESSION_REFRESH_SKEW_MS = 30_000;
|
|
26
|
+
|
|
27
|
+
function remainsUsable(session: Exclude<SessionProviderResult, null>, now: number): boolean {
|
|
28
|
+
const expiresAt = Date.parse(session.expiresAt);
|
|
29
|
+
return Number.isFinite(expiresAt) && expiresAt - now > SESSION_REFRESH_SKEW_MS;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Cache one minted session until it approaches expiry; concurrent refreshes share one mint. */
|
|
33
|
+
export function cachedSessionProvider(provider: SessionProvider): CredentialProvider {
|
|
34
|
+
let current: Exclude<SessionProviderResult, null> | null = null;
|
|
35
|
+
let refreshing: Promise<SessionProviderResult> | null = null;
|
|
36
|
+
|
|
37
|
+
return async () => {
|
|
38
|
+
if (current && remainsUsable(current, Date.now())) return current;
|
|
39
|
+
refreshing ??= provider().then((next) => {
|
|
40
|
+
current = next;
|
|
41
|
+
return next;
|
|
42
|
+
}).finally(() => {
|
|
43
|
+
refreshing = null;
|
|
44
|
+
});
|
|
45
|
+
return refreshing;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The one credential input a live transport consumes. */
|
|
50
|
+
export interface SessionAccess {
|
|
51
|
+
readonly renewable: boolean;
|
|
52
|
+
credential(): Promise<CredentialProviderResult>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Normalize a static or renewable session before it reaches a transport. */
|
|
56
|
+
export function createSessionAccess(
|
|
57
|
+
source: SessionSource | null | undefined,
|
|
58
|
+
credential: () => Promise<CredentialProviderResult>,
|
|
59
|
+
): SessionAccess {
|
|
60
|
+
return {
|
|
61
|
+
renewable:
|
|
62
|
+
typeof source === 'function'
|
|
63
|
+
|| (typeof source === 'object' && source !== null && 'endpoint' in source),
|
|
64
|
+
credential,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolve the public session forms into the credential shape auth consumes. */
|
|
69
|
+
export function resolveSessionCredential(
|
|
70
|
+
source: SessionSource,
|
|
71
|
+
options: {
|
|
72
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
73
|
+
readonly dangerouslyAllowBrowser?: boolean;
|
|
74
|
+
},
|
|
75
|
+
): string | CredentialProvider {
|
|
76
|
+
if (typeof source === 'function') {
|
|
77
|
+
return protectBrowserCredentialProvider(
|
|
78
|
+
cachedSessionProvider(source),
|
|
79
|
+
options.dangerouslyAllowBrowser,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!('endpoint' in source)) return source.token;
|
|
83
|
+
if (!isCredentialEndpoint(source.endpoint)) {
|
|
84
|
+
throw new AbloValidationError(
|
|
85
|
+
'`session.endpoint` expects a URL or path such as \'/api/ablo-session\'.',
|
|
86
|
+
{ code: 'invalid_options', param: 'session.endpoint' },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return protectBrowserCredentialProvider(createEndpointCredentialResolver(source.endpoint, {
|
|
90
|
+
fetch: options.fetch,
|
|
91
|
+
timeoutMs: source.timeoutMs,
|
|
92
|
+
allowCrossOrigin: source.allowCrossOrigin,
|
|
93
|
+
}), options.dangerouslyAllowBrowser);
|
|
94
|
+
}
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
import { probeNetwork, type ProbeResult } from './networkProbe.js';
|
|
44
44
|
import type { AuthTokenGetter } from '../../auth/credentialSource.js';
|
|
45
|
-
import type { CredentialRefreshOutcome } from '
|
|
45
|
+
import type { CredentialRefreshOutcome } from '../../sessions/lifecycle.js';
|
|
46
46
|
import { noopLogger, type Logger } from '../../logger.js';
|
|
47
47
|
import {
|
|
48
48
|
noopSocketObservability,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { normalizeClaimConflict } from '../../claims/conflict.js';
|
|
2
|
+
import type { ClaimQueuedResponse } from '../../claims/contract.js';
|
|
3
|
+
import type { HttpClaimsResource } from '../../client/resources/httpResources.js';
|
|
4
|
+
import {
|
|
5
|
+
emitClaimStatus,
|
|
6
|
+
type ResolvedClaimContentionOptions,
|
|
7
|
+
} from '../../client/resources/modelOperations.js';
|
|
8
|
+
import { AbloClaimedError } from '../../errors.js';
|
|
9
|
+
|
|
10
|
+
const GRANT_POLL_FIRST_MS = 250;
|
|
11
|
+
const GRANT_POLL_INTERVAL_MS = 1_000;
|
|
12
|
+
const ignoreBestEffortClaimReleaseFailure = (): undefined => undefined;
|
|
13
|
+
|
|
14
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
const done = (): void => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
signal?.removeEventListener('abort', done);
|
|
19
|
+
resolve();
|
|
20
|
+
};
|
|
21
|
+
const timer = setTimeout(done, ms);
|
|
22
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Wait for a queued HTTP claim while keeping its server-side slot alive. */
|
|
27
|
+
export async function awaitClaimGrantOverHttp(
|
|
28
|
+
claims: HttpClaimsResource,
|
|
29
|
+
targetLabel: string,
|
|
30
|
+
queued: ClaimQueuedResponse,
|
|
31
|
+
options: ResolvedClaimContentionOptions,
|
|
32
|
+
): Promise<{ id: string; fenceToken?: number }> {
|
|
33
|
+
const claimId = queued.id;
|
|
34
|
+
const { signal } = options;
|
|
35
|
+
const conflict = normalizeClaimConflict({
|
|
36
|
+
...(queued.heldBy !== undefined ? { heldBy: queued.heldBy } : {}),
|
|
37
|
+
...(queued.heldByKind !== undefined ? { heldByKind: queued.heldByKind } : {}),
|
|
38
|
+
...(queued.heldByClaimId !== undefined ? { heldByClaimId: queued.heldByClaimId } : {}),
|
|
39
|
+
...(queued.expiresAt !== undefined ? { heldByExpiresAt: queued.expiresAt } : {}),
|
|
40
|
+
...(queued.heldByClaim !== undefined ? { heldByClaim: queued.heldByClaim } : {}),
|
|
41
|
+
});
|
|
42
|
+
const waitingError = (
|
|
43
|
+
message: string,
|
|
44
|
+
code: 'queue_too_deep' | 'claim_wait_aborted' | 'grant_timeout' | 'claim_lost',
|
|
45
|
+
): AbloClaimedError => new AbloClaimedError(message, {
|
|
46
|
+
code,
|
|
47
|
+
claims: queued.heldByClaim ? [queued.heldByClaim] : undefined,
|
|
48
|
+
conflict,
|
|
49
|
+
});
|
|
50
|
+
const rejectAndLeave = async (error: AbloClaimedError): Promise<never> => {
|
|
51
|
+
await claims.release({ claimId }).catch(ignoreBestEffortClaimReleaseFailure);
|
|
52
|
+
throw error;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
emitClaimStatus(options.onStatus, {
|
|
56
|
+
type: 'queued',
|
|
57
|
+
claimId,
|
|
58
|
+
position: queued.position,
|
|
59
|
+
ahead: queued.position + 1,
|
|
60
|
+
});
|
|
61
|
+
if (options.maxDepth !== undefined && queued.position >= options.maxDepth) {
|
|
62
|
+
return rejectAndLeave(waitingError(
|
|
63
|
+
`Claim queue for ${targetLabel} is ${queued.position} deep (max ${options.maxDepth}).`,
|
|
64
|
+
'queue_too_deep',
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const deadline = options.timeoutMs !== undefined ? Date.now() + options.timeoutMs : undefined;
|
|
69
|
+
let delay = GRANT_POLL_FIRST_MS;
|
|
70
|
+
for (;;) {
|
|
71
|
+
if (signal?.aborted) {
|
|
72
|
+
return rejectAndLeave(waitingError(
|
|
73
|
+
`The wait for the claim on ${targetLabel} was aborted before the grant arrived.`,
|
|
74
|
+
'claim_wait_aborted',
|
|
75
|
+
));
|
|
76
|
+
}
|
|
77
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
78
|
+
return rejectAndLeave(waitingError(
|
|
79
|
+
`Timed out after ${options.timeoutMs}ms waiting for the queue grant on ${targetLabel}.`,
|
|
80
|
+
'grant_timeout',
|
|
81
|
+
));
|
|
82
|
+
}
|
|
83
|
+
await sleep(
|
|
84
|
+
deadline !== undefined ? Math.min(delay, Math.max(0, deadline - Date.now())) : delay,
|
|
85
|
+
signal,
|
|
86
|
+
);
|
|
87
|
+
if (signal?.aborted) {
|
|
88
|
+
return rejectAndLeave(waitingError(
|
|
89
|
+
`The wait for the claim on ${targetLabel} was aborted before the grant arrived.`,
|
|
90
|
+
'claim_wait_aborted',
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
delay = GRANT_POLL_INTERVAL_MS * (0.85 + Math.random() * 0.3);
|
|
94
|
+
const beat = await claims.heartbeat({ claimId });
|
|
95
|
+
if (beat.status !== 'held') continue;
|
|
96
|
+
const state = await claims.retrieve({ claimId });
|
|
97
|
+
if (state.status !== 'active') {
|
|
98
|
+
return rejectAndLeave(waitingError(
|
|
99
|
+
`Claim lost while queued for ${targetLabel}.`,
|
|
100
|
+
'claim_lost',
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
emitClaimStatus(options.onStatus, { type: 'granted', claimId, waited: true });
|
|
104
|
+
return state.fenceToken !== undefined
|
|
105
|
+
? { id: claimId, fenceToken: state.fenceToken }
|
|
106
|
+
: { id: claimId };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -19,9 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import {
|
|
21
21
|
createHttpTransport,
|
|
22
|
-
type HttpTransport,
|
|
23
22
|
} from './transport.js';
|
|
24
|
-
import {
|
|
23
|
+
import type { HttpTransport } from './contract.js';
|
|
25
24
|
import type { HttpClientConfig } from './options.js';
|
|
26
25
|
import type {
|
|
27
26
|
CommitResource,
|
|
@@ -33,9 +32,6 @@ import type {
|
|
|
33
32
|
ModelReadOptions,
|
|
34
33
|
ModelMutationOptions,
|
|
35
34
|
ModelList,
|
|
36
|
-
CreateSessionParams,
|
|
37
|
-
AbloSession,
|
|
38
|
-
SessionResource,
|
|
39
35
|
} from '../../client/resources/httpResources.js';
|
|
40
36
|
import { collectModelList, modelList } from '../../client/resources/httpResources.js';
|
|
41
37
|
import { resolveCreateId } from '../../client/resources/modelCreate.js';
|
|
@@ -91,8 +87,8 @@ export interface AbloHttpClientOptions<S extends SchemaRecord>
|
|
|
91
87
|
* @default 30_000
|
|
92
88
|
*/
|
|
93
89
|
readonly timeoutMs?: number;
|
|
94
|
-
/** Initial
|
|
95
|
-
readonly
|
|
90
|
+
/** Initial groups observed by a session-backed WebSocket client. */
|
|
91
|
+
readonly groups?: readonly string[];
|
|
96
92
|
/** Application frame names accepted by `subscribe` on the WebSocket transport. */
|
|
97
93
|
readonly collaborationEvents?: readonly string[];
|
|
98
94
|
/** Durable resume position for WebSocket observation. */
|
|
@@ -123,7 +119,7 @@ export type HttpModelMutationParams<P> = Omit<P, 'reads'> & {
|
|
|
123
119
|
* coordinated writes.
|
|
124
120
|
* It deliberately omits the stateful client's local-only
|
|
125
121
|
* reads (the `local` namespace) and live subscriptions (`onChange`), which need
|
|
126
|
-
* a
|
|
122
|
+
* a local graph and a persistent socket; those are absent from the type, so
|
|
127
123
|
* reaching for one is a compile error rather than a runtime gap.
|
|
128
124
|
*
|
|
129
125
|
* This is also the base the reactive per-model surface is composed from, rather
|
|
@@ -199,7 +195,7 @@ export interface HttpModelClient<T, C = T> {
|
|
|
199
195
|
|
|
200
196
|
/**
|
|
201
197
|
* The type of the stateless HTTP client: a typed {@link HttpModelClient} per
|
|
202
|
-
* schema model, plus `commits
|
|
198
|
+
* schema model, plus `commits` and lifecycle operations. It
|
|
203
199
|
* exposes only what request/response transport can do, so reaching for a
|
|
204
200
|
* stateful-only capability — the `local` reads, `onChange`, or the synchronous
|
|
205
201
|
* `claim.state`/`queue`/`reorder` reads — is a compile error rather than a value
|
|
@@ -235,13 +231,6 @@ export type AbloHttpClient<S extends SchemaRecord> = {
|
|
|
235
231
|
dispose(): Promise<void>;
|
|
236
232
|
/** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
|
|
237
233
|
getAuthToken(): Promise<string | null>;
|
|
238
|
-
/**
|
|
239
|
-
* Mints a short-lived, scoped session token. Minting is itself a stateless
|
|
240
|
-
* request, so it is available here even though the local-cache reads are not.
|
|
241
|
-
* Pass `{ user }` to mint an end-user key (`ek_`) or `{ agent, can }` to mint a
|
|
242
|
-
* scoped agent key (`rk_`). See {@link CreateSessionParams}.
|
|
243
|
-
*/
|
|
244
|
-
readonly sessions: SessionResource<S>;
|
|
245
234
|
};
|
|
246
235
|
|
|
247
236
|
/** The same typed resource client, with server-pushed capabilities carried by WebSocket. */
|
|
@@ -252,9 +241,9 @@ export type AbloWebSocketClient<S extends SchemaRecord> = AbloHttpClient<S> & {
|
|
|
252
241
|
listener: (...args: CoreSyncEventMap[K]) => void,
|
|
253
242
|
): () => void;
|
|
254
243
|
updateSubscription(
|
|
255
|
-
|
|
244
|
+
groups: readonly string[],
|
|
256
245
|
options?: { timeoutMs?: number },
|
|
257
|
-
): Promise<{
|
|
246
|
+
): Promise<{ groups: string[] }>;
|
|
258
247
|
readonly presence: {
|
|
259
248
|
update(input?: {
|
|
260
249
|
readonly status?: 'online' | 'away' | 'offline';
|
|
@@ -281,9 +270,10 @@ const PROTOCOL_MEMBERS = new Set<string>([
|
|
|
281
270
|
'logs',
|
|
282
271
|
'identity',
|
|
283
272
|
'getAuthToken',
|
|
284
|
-
'sessions',
|
|
285
273
|
]);
|
|
286
274
|
|
|
275
|
+
const ignoreAlreadySurfacedSessionOpeningFailure = (): undefined => undefined;
|
|
276
|
+
|
|
287
277
|
/** Narrows a bare property name to a transport key so the facade can index it typed. */
|
|
288
278
|
function isProtocolMember(prop: string): prop is keyof HttpTransport {
|
|
289
279
|
return PROTOCOL_MEMBERS.has(prop);
|
|
@@ -538,23 +528,20 @@ function createHttpModelClient<T, C = T>(
|
|
|
538
528
|
}
|
|
539
529
|
|
|
540
530
|
/**
|
|
541
|
-
* Builds the
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* credential is the identity.
|
|
531
|
+
* Builds the typed headless client. An API-key identity uses HTTP. A session
|
|
532
|
+
* identity uses one lazily opened WebSocket by default for commits and live
|
|
533
|
+
* coordination until `dispose()`; point reads and administration remain HTTP.
|
|
545
534
|
*/
|
|
546
|
-
/** @internal Constructed only through the public `Ablo(
|
|
535
|
+
/** @internal Constructed only through the public `Ablo()` factory. */
|
|
547
536
|
export function createAbloHttpClient<S extends SchemaRecord>(
|
|
548
537
|
options: AbloHttpClientOptions<S>,
|
|
549
538
|
): AbloHttpClient<S> | AbloWebSocketClient<S> {
|
|
550
539
|
const { schema, onCommitReceipt, ...rest } = options;
|
|
551
|
-
const usesWebSocket = options.transport === 'websocket'
|
|
540
|
+
const usesWebSocket = options.transport === 'websocket'
|
|
541
|
+
|| (options.transport === undefined && options.session != null);
|
|
552
542
|
const readSetContext = createReadSetContext();
|
|
553
543
|
const transport: HttpTransport = createHttpTransport({
|
|
554
544
|
...rest,
|
|
555
|
-
// Derived from the schema this client is bound to, never assembled by hand
|
|
556
|
-
// — see `auth/capability.ts`.
|
|
557
|
-
modelTypenames: modelWireNames(schema.models),
|
|
558
545
|
onCommitReceipt: (observation) => {
|
|
559
546
|
recordHttpCommitReceipt(readSetContext, observation);
|
|
560
547
|
onCommitReceipt?.(observation);
|
|
@@ -570,9 +557,8 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
570
557
|
return receipt;
|
|
571
558
|
},
|
|
572
559
|
dispatchClaim: async (input) => (await webSocketSession()).claim(input),
|
|
573
|
-
releaseDispatchedClaim: (input) =>
|
|
574
|
-
|
|
575
|
-
},
|
|
560
|
+
releaseDispatchedClaim: async (input) =>
|
|
561
|
+
(await webSocketSession()).release(input),
|
|
576
562
|
} : {}),
|
|
577
563
|
});
|
|
578
564
|
const schemaModels = new Set(Object.keys(schema.models));
|
|
@@ -597,8 +583,8 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
597
583
|
await transport.ready();
|
|
598
584
|
const session = await createWebSocketSession({
|
|
599
585
|
baseUrl: rest.baseURL ?? undefined,
|
|
600
|
-
|
|
601
|
-
syncGroups: options.
|
|
586
|
+
access: transport.access,
|
|
587
|
+
syncGroups: options.groups,
|
|
602
588
|
collaborationEvents: options.collaborationEvents,
|
|
603
589
|
cursorStore: options.cursorStore,
|
|
604
590
|
cursorKey: options.cursorKey,
|
|
@@ -629,7 +615,7 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
629
615
|
webSocketOpen.abort();
|
|
630
616
|
const opening = webSocketPromise;
|
|
631
617
|
if (webSocket) await webSocket.close();
|
|
632
|
-
else await opening?.then((session) => session.close()).catch(
|
|
618
|
+
else await opening?.then((session) => session.close()).catch(ignoreAlreadySurfacedSessionOpeningFailure);
|
|
633
619
|
await transport.dispose();
|
|
634
620
|
};
|
|
635
621
|
|
|
@@ -707,8 +693,13 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
707
693
|
};
|
|
708
694
|
}
|
|
709
695
|
if (usesWebSocket && prop === 'updateSubscription') {
|
|
710
|
-
return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) =>
|
|
711
|
-
(await webSocketSession()).updateSubscription(
|
|
696
|
+
return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) => {
|
|
697
|
+
const result = await (await webSocketSession()).updateSubscription(
|
|
698
|
+
groups,
|
|
699
|
+
subscriptionOptions,
|
|
700
|
+
);
|
|
701
|
+
return { groups: result.syncGroups };
|
|
702
|
+
};
|
|
712
703
|
}
|
|
713
704
|
if (usesWebSocket && prop === 'presence') {
|
|
714
705
|
return {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Private contract owned by the stateless HTTP transport boundary. */
|
|
2
|
+
|
|
3
|
+
import type { HttpClientConfig } from './options.js';
|
|
4
|
+
import type { CommitReceiptWire } from '../../commit/contract.js';
|
|
5
|
+
import type {
|
|
6
|
+
CommitResource,
|
|
7
|
+
HttpClaimsResource,
|
|
8
|
+
HttpLogsResource,
|
|
9
|
+
HttpTransportModel,
|
|
10
|
+
} from '../../client/resources/httpResources.js';
|
|
11
|
+
import type {
|
|
12
|
+
ClaimAcquired,
|
|
13
|
+
ClaimBeginPayload,
|
|
14
|
+
ClaimGranted,
|
|
15
|
+
ClaimQueued,
|
|
16
|
+
} from '../../coordination/schema.js';
|
|
17
|
+
import type { CoordinationObservability } from '../../observability.js';
|
|
18
|
+
import type { EffectiveAuthority } from '../../auth/capability.js';
|
|
19
|
+
import type { SessionAccess } from '../../sessions/index.js';
|
|
20
|
+
import type { CommitFrameOperation } from '../websocket/commitFrames.js';
|
|
21
|
+
import type { HttpReadOnChange } from './subscription.js';
|
|
22
|
+
|
|
23
|
+
/** @internal Private options for the schema-agnostic HTTP protocol transport. */
|
|
24
|
+
export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
|
|
25
|
+
readonly bootstrapBaseUrl?: string | undefined;
|
|
26
|
+
/** Observability forwarded from `Ablo({ observability })`. */
|
|
27
|
+
readonly observability?: CoordinationObservability;
|
|
28
|
+
/** Per-request deadline in milliseconds. Pass `0` to disable it. */
|
|
29
|
+
readonly timeoutMs?: number;
|
|
30
|
+
/** @internal Routes writes through the selected duplex transport. */
|
|
31
|
+
readonly dispatchCommit?: ((input: {
|
|
32
|
+
readonly clientTxId: string;
|
|
33
|
+
readonly operations: readonly CommitFrameOperation[];
|
|
34
|
+
readonly reads?: readonly import('../../coordination/schema.js').ReadDependency[] | null;
|
|
35
|
+
}) => Promise<CommitReceiptWire>) | undefined;
|
|
36
|
+
/** @internal Routes held-claim acquisition through the shared socket. */
|
|
37
|
+
readonly dispatchClaim?: ((input: ClaimBeginPayload & {
|
|
38
|
+
readonly timeoutMs?: number;
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
readonly onQueued?: (event: ClaimQueued) => Error | undefined;
|
|
41
|
+
}) => Promise<ClaimAcquired | ClaimGranted>) | undefined;
|
|
42
|
+
/** @internal Releases a socket-acquired claim on that same session. */
|
|
43
|
+
readonly releaseDispatchedClaim?: ((input: {
|
|
44
|
+
readonly claimId: string;
|
|
45
|
+
readonly entityType: string;
|
|
46
|
+
readonly entityId: string;
|
|
47
|
+
}) => Promise<void>) | undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** @internal Private protocol surface wrapped by `AbloHttpClient`. */
|
|
51
|
+
export interface HttpTransport {
|
|
52
|
+
ready(): Promise<void>;
|
|
53
|
+
waitForFlush(): Promise<void>;
|
|
54
|
+
/** Drains scheduled commits and active requests. */
|
|
55
|
+
dispose(): Promise<void>;
|
|
56
|
+
purge(): Promise<void>;
|
|
57
|
+
/** @internal Live check used only by `context().onChange`. */
|
|
58
|
+
readonly onChange: HttpReadOnChange;
|
|
59
|
+
readonly commits: CommitResource;
|
|
60
|
+
readonly claims: HttpClaimsResource;
|
|
61
|
+
readonly logs: HttpLogsResource;
|
|
62
|
+
/** Server-confirmed authority of the active bearer, populated by `ready()`. */
|
|
63
|
+
readonly identity: EffectiveAuthority | null;
|
|
64
|
+
model<T = Record<string, unknown>, Fields = T>(
|
|
65
|
+
name: string,
|
|
66
|
+
): HttpTransportModel<T, Fields>;
|
|
67
|
+
/** Resolve the bearer credential used by this transport. */
|
|
68
|
+
getAuthToken(): Promise<string | null>;
|
|
69
|
+
/** @internal One normalized source shared by HTTP bootstrap and live transport. */
|
|
70
|
+
readonly access: SessionAccess;
|
|
71
|
+
}
|
|
@@ -37,7 +37,10 @@ export interface HttpClientConfig<S extends SchemaRecord = SchemaRecord>
|
|
|
37
37
|
commitOutbox?: DurableWriteStore;
|
|
38
38
|
/** Compatibility input and internal child-client identity seam. */
|
|
39
39
|
commitOutboxScope?: CommitOutboxScope;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Which transport carries this client's traffic. Defaults to WebSocket for
|
|
42
|
+
* a scoped `session` and HTTP for an API key or direct auth token.
|
|
43
|
+
*/
|
|
41
44
|
transport?: 'websocket' | 'http' | undefined;
|
|
42
45
|
/** @internal Exact receipt phases observed while dispatching one HTTP commit. */
|
|
43
46
|
onCommitReceipt?: (observation: {
|