@abloatai/transaction 0.59.1 → 0.60.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/capabilityLifecycle.d.ts +1 -1
- 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/claims/events.d.ts +1 -1
- package/dist/claims/events.d.ts.map +1 -1
- package/dist/client/ablo.d.ts +15 -11
- package/dist/client/ablo.d.ts.map +1 -1
- package/dist/client/ablo.js +5 -14
- 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 +0 -9
- package/dist/client/resources/modelOperations.d.ts.map +1 -1
- package/dist/client/surface.d.ts +2 -2
- package/dist/client/surface.d.ts.map +1 -1
- package/dist/client/surface.js +8 -3
- package/dist/client/surface.js.map +1 -1
- package/dist/commit/contract.d.ts +10 -10
- package/dist/commit/recordRuntime.d.ts +3 -2
- package/dist/commit/recordRuntime.d.ts.map +1 -1
- package/dist/commit/recordRuntime.js +1 -0
- package/dist/commit/recordRuntime.js.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 +3 -4
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +3 -47
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -69
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +2 -2
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +5 -9
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +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/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/http/client.d.ts +44 -16
- package/dist/transport/http/client.d.ts.map +1 -1
- package/dist/transport/http/client.js +125 -12
- package/dist/transport/http/client.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 +25 -10
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +102 -56
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/commitFrames.d.ts +5 -5
- package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
- package/dist/transport/websocket/commitFrames.js +1 -0
- package/dist/transport/websocket/commitFrames.js.map +1 -1
- package/dist/transport/websocket/contextSubscription.d.ts +6 -0
- package/dist/transport/websocket/contextSubscription.d.ts.map +1 -0
- package/dist/transport/websocket/contextSubscription.js +45 -0
- package/dist/transport/websocket/contextSubscription.js.map +1 -0
- 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 +1 -71
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/session.d.ts +4 -0
- package/dist/transport/websocket/session.d.ts.map +1 -0
- package/dist/transport/websocket/session.js +425 -0
- package/dist/transport/websocket/session.js.map +1 -0
- package/dist/transport/websocket/sessionContract.d.ts +78 -0
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -0
- package/dist/transport/websocket/sessionContract.js +2 -0
- package/dist/transport/websocket/sessionContract.js.map +1 -0
- package/dist/transport/websocket/transport.d.ts +27 -42
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js +77 -102
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/inboundFrames.d.ts +0 -10
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +1 -6
- package/dist/wire/inboundFrames.js.map +1 -1
- package/package.json +7 -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/events.ts +1 -2
- package/src/client/ablo.ts +23 -13
- package/src/client/resources/httpResources.ts +2 -174
- package/src/client/resources/modelOperations.ts +0 -10
- package/src/client/surface.ts +9 -4
- package/src/commit/recordRuntime.ts +4 -2
- package/src/coordination/index.ts +2 -9
- package/src/coordination/schema.ts +7 -81
- package/src/errorCodes.ts +8 -13
- package/src/errors.ts +18 -1
- package/src/index.ts +1 -0
- 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/client.ts +183 -26
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +140 -78
- package/src/transport/websocket/commitFrames.ts +5 -5
- package/src/transport/websocket/contextSubscription.ts +55 -0
- package/src/transport/websocket/frameHandlers.ts +0 -102
- package/src/transport/websocket/session.ts +522 -0
- package/src/transport/websocket/sessionContract.ts +106 -0
- package/src/transport/websocket/transport.ts +85 -137
- package/src/wire/inboundFrames.ts +0 -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
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keeps the short-lived access credential fresh. It owns the re-mint hook, a
|
|
3
3
|
* single-flight guard that stops concurrent triggers from minting more than
|
|
4
|
-
* once, and
|
|
5
|
-
* — that renews the credential ahead of expiry. It reaches the rest of the
|
|
4
|
+
* once, and proactive refresh — an expiry-aware timer plus a browser OS-wake
|
|
5
|
+
* listener — that renews the credential ahead of expiry. It reaches the rest of the
|
|
6
6
|
* client only through the small {@link CredentialLifecycleContext} interface,
|
|
7
7
|
* so the two can reference each other without an import cycle.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { RecoveryClass } from '
|
|
11
|
-
import { noopLogger, type Logger } from '
|
|
10
|
+
import type { RecoveryClass } from '../errorCodes.js';
|
|
11
|
+
import { noopLogger, type Logger } from '../logger.js';
|
|
12
12
|
import {
|
|
13
13
|
credentialExpiry,
|
|
14
14
|
credentialToken,
|
|
15
15
|
type CredentialProvider,
|
|
16
16
|
type CredentialProviderResult,
|
|
17
|
-
} from '
|
|
17
|
+
} from '../auth/credentialResult.js';
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Tri-state outcome of a credential re-mint, mirroring the `getToken`
|
|
@@ -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,
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
createHttpTransport,
|
|
22
22
|
type HttpTransport,
|
|
23
23
|
} from './transport.js';
|
|
24
|
-
import { modelWireNames } from '../../auth/capability.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';
|
|
@@ -56,6 +52,7 @@ import {
|
|
|
56
52
|
type FunctionalUpdateOptions,
|
|
57
53
|
} from '../../client/resources/functionalUpdate.js';
|
|
58
54
|
import type { HeldClaim, HeldLease } from '../../types/streams.js';
|
|
55
|
+
import type { JsonValue } from '../../types/streams.js';
|
|
59
56
|
import { AbloConnectionError, AbloValidationError } from '../../errors.js';
|
|
60
57
|
import type { ReadDependency } from '../../coordination/schema.js';
|
|
61
58
|
import {
|
|
@@ -65,8 +62,22 @@ import {
|
|
|
65
62
|
prepareReadSet,
|
|
66
63
|
type ReadSetContext,
|
|
67
64
|
} from '../../commit/readSetContext.js';
|
|
68
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
recordHttpCommitReceipt,
|
|
67
|
+
recordWebSocketCommitReceipt,
|
|
68
|
+
} from '../../commit/recordRuntime.js';
|
|
69
69
|
import type { EffectiveAuthority } from '../../auth/capability.js';
|
|
70
|
+
import {
|
|
71
|
+
createWebSocketSession,
|
|
72
|
+
} from '../websocket/session.js';
|
|
73
|
+
import type {
|
|
74
|
+
AbloWebSocketSession,
|
|
75
|
+
WebSocketObservedDelta,
|
|
76
|
+
} from '../websocket/sessionContract.js';
|
|
77
|
+
import { subscribeWebSocketReadChanges } from '../websocket/contextSubscription.js';
|
|
78
|
+
import type {
|
|
79
|
+
CoreSyncEventMap,
|
|
80
|
+
} from '../websocket/transport.js';
|
|
70
81
|
|
|
71
82
|
export interface AbloHttpClientOptions<S extends SchemaRecord>
|
|
72
83
|
extends HttpClientConfig<S> {
|
|
@@ -76,6 +87,16 @@ export interface AbloHttpClientOptions<S extends SchemaRecord>
|
|
|
76
87
|
* @default 30_000
|
|
77
88
|
*/
|
|
78
89
|
readonly timeoutMs?: number;
|
|
90
|
+
/** Initial groups observed by a session-backed WebSocket client. */
|
|
91
|
+
readonly groups?: readonly string[];
|
|
92
|
+
/** Application frame names accepted by `subscribe` on the WebSocket transport. */
|
|
93
|
+
readonly collaborationEvents?: readonly string[];
|
|
94
|
+
/** Durable resume position for WebSocket observation. */
|
|
95
|
+
readonly cursorStore?: import('../../client/contract.js').ObserveCursorStore;
|
|
96
|
+
readonly cursorKey?: string;
|
|
97
|
+
readonly reconnectDelay?: number;
|
|
98
|
+
readonly maxReconnectDelay?: number;
|
|
99
|
+
readonly connectTimeoutMs?: number;
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
declare const capturedRowBrand: unique symbol;
|
|
@@ -98,7 +119,7 @@ export type HttpModelMutationParams<P> = Omit<P, 'reads'> & {
|
|
|
98
119
|
* coordinated writes.
|
|
99
120
|
* It deliberately omits the stateful client's local-only
|
|
100
121
|
* reads (the `local` namespace) and live subscriptions (`onChange`), which need
|
|
101
|
-
* a
|
|
122
|
+
* a local graph and a persistent socket; those are absent from the type, so
|
|
102
123
|
* reaching for one is a compile error rather than a runtime gap.
|
|
103
124
|
*
|
|
104
125
|
* This is also the base the reactive per-model surface is composed from, rather
|
|
@@ -174,7 +195,7 @@ export interface HttpModelClient<T, C = T> {
|
|
|
174
195
|
|
|
175
196
|
/**
|
|
176
197
|
* The type of the stateless HTTP client: a typed {@link HttpModelClient} per
|
|
177
|
-
* schema model, plus `commits
|
|
198
|
+
* schema model, plus `commits` and lifecycle operations. It
|
|
178
199
|
* exposes only what request/response transport can do, so reaching for a
|
|
179
200
|
* stateful-only capability — the `local` reads, `onChange`, or the synchronous
|
|
180
201
|
* `claim.state`/`queue`/`reorder` reads — is a compile error rather than a value
|
|
@@ -210,13 +231,30 @@ export type AbloHttpClient<S extends SchemaRecord> = {
|
|
|
210
231
|
dispose(): Promise<void>;
|
|
211
232
|
/** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
|
|
212
233
|
getAuthToken(): Promise<string | null>;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** The same typed resource client, with server-pushed capabilities carried by WebSocket. */
|
|
237
|
+
export type AbloWebSocketClient<S extends SchemaRecord> = AbloHttpClient<S> & {
|
|
238
|
+
observe(options?: { signal?: AbortSignal }): AsyncIterable<WebSocketObservedDelta>;
|
|
239
|
+
subscribe<K extends keyof CoreSyncEventMap>(
|
|
240
|
+
event: K,
|
|
241
|
+
listener: (...args: CoreSyncEventMap[K]) => void,
|
|
242
|
+
): () => void;
|
|
243
|
+
updateSubscription(
|
|
244
|
+
groups: readonly string[],
|
|
245
|
+
options?: { timeoutMs?: number },
|
|
246
|
+
): Promise<{ groups: string[] }>;
|
|
247
|
+
readonly presence: {
|
|
248
|
+
update(input?: {
|
|
249
|
+
readonly status?: 'online' | 'away' | 'offline';
|
|
250
|
+
readonly customStatus?: string;
|
|
251
|
+
readonly timezone?: string;
|
|
252
|
+
readonly activity?: Readonly<Record<string, JsonValue>>;
|
|
253
|
+
}): Promise<void>;
|
|
254
|
+
};
|
|
255
|
+
readonly collaboration: {
|
|
256
|
+
send(event: string, payload: Readonly<Record<string, JsonValue>>): Promise<void>;
|
|
257
|
+
};
|
|
220
258
|
};
|
|
221
259
|
|
|
222
260
|
/**
|
|
@@ -232,7 +270,6 @@ const PROTOCOL_MEMBERS = new Set<string>([
|
|
|
232
270
|
'logs',
|
|
233
271
|
'identity',
|
|
234
272
|
'getAuthToken',
|
|
235
|
-
'sessions',
|
|
236
273
|
]);
|
|
237
274
|
|
|
238
275
|
/** Narrows a bare property name to a transport key so the facade can index it typed. */
|
|
@@ -489,26 +526,39 @@ function createHttpModelClient<T, C = T>(
|
|
|
489
526
|
}
|
|
490
527
|
|
|
491
528
|
/**
|
|
492
|
-
* Builds the
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
* credential is the identity.
|
|
529
|
+
* Builds the typed headless client. An API-key identity uses HTTP. A session
|
|
530
|
+
* identity uses one lazily opened WebSocket by default for commits and live
|
|
531
|
+
* coordination until `dispose()`; point reads and administration remain HTTP.
|
|
496
532
|
*/
|
|
497
|
-
/** @internal Constructed only through the public `Ablo(
|
|
533
|
+
/** @internal Constructed only through the public `Ablo()` factory. */
|
|
498
534
|
export function createAbloHttpClient<S extends SchemaRecord>(
|
|
499
535
|
options: AbloHttpClientOptions<S>,
|
|
500
|
-
): AbloHttpClient<S> {
|
|
536
|
+
): AbloHttpClient<S> | AbloWebSocketClient<S> {
|
|
501
537
|
const { schema, onCommitReceipt, ...rest } = options;
|
|
538
|
+
const usesWebSocket = options.transport === 'websocket'
|
|
539
|
+
|| (options.transport === undefined && options.session != null);
|
|
502
540
|
const readSetContext = createReadSetContext();
|
|
503
541
|
const transport: HttpTransport = createHttpTransport({
|
|
504
542
|
...rest,
|
|
505
|
-
// Derived from the schema this client is bound to, never assembled by hand
|
|
506
|
-
// — see `auth/capability.ts`.
|
|
507
|
-
modelTypenames: modelWireNames(schema.models),
|
|
508
543
|
onCommitReceipt: (observation) => {
|
|
509
544
|
recordHttpCommitReceipt(readSetContext, observation);
|
|
510
545
|
onCommitReceipt?.(observation);
|
|
511
546
|
},
|
|
547
|
+
...(usesWebSocket ? {
|
|
548
|
+
dispatchCommit: async (input) => {
|
|
549
|
+
const receipt = await (await webSocketSession()).commit(input);
|
|
550
|
+
recordWebSocketCommitReceipt(readSetContext, {
|
|
551
|
+
receipt,
|
|
552
|
+
operations: input.operations,
|
|
553
|
+
reads: input.reads,
|
|
554
|
+
});
|
|
555
|
+
return receipt;
|
|
556
|
+
},
|
|
557
|
+
dispatchClaim: async (input) => (await webSocketSession()).claim(input),
|
|
558
|
+
releaseDispatchedClaim: (input) => {
|
|
559
|
+
void webSocketSession().then((session) => session.release(input));
|
|
560
|
+
},
|
|
561
|
+
} : {}),
|
|
512
562
|
});
|
|
513
563
|
const schemaModels = new Set(Object.keys(schema.models));
|
|
514
564
|
const clientIdentity = {};
|
|
@@ -517,6 +567,56 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
517
567
|
string,
|
|
518
568
|
HttpModelClient<Record<string, unknown>, Record<string, unknown>>
|
|
519
569
|
>();
|
|
570
|
+
let webSocket: AbloWebSocketSession | null = null;
|
|
571
|
+
let webSocketPromise: Promise<AbloWebSocketSession> | null = null;
|
|
572
|
+
const webSocketOpen = new AbortController();
|
|
573
|
+
let disposed = false;
|
|
574
|
+
|
|
575
|
+
const webSocketSession = (): Promise<AbloWebSocketSession> => {
|
|
576
|
+
if (disposed) {
|
|
577
|
+
return Promise.reject(new AbloConnectionError('This Ablo client is disposed.'));
|
|
578
|
+
}
|
|
579
|
+
if (webSocket) return Promise.resolve(webSocket);
|
|
580
|
+
if (webSocketPromise) return webSocketPromise;
|
|
581
|
+
webSocketPromise = (async () => {
|
|
582
|
+
await transport.ready();
|
|
583
|
+
const session = await createWebSocketSession({
|
|
584
|
+
baseUrl: rest.baseURL ?? undefined,
|
|
585
|
+
access: transport.access,
|
|
586
|
+
syncGroups: options.groups,
|
|
587
|
+
collaborationEvents: options.collaborationEvents,
|
|
588
|
+
cursorStore: options.cursorStore,
|
|
589
|
+
cursorKey: options.cursorKey,
|
|
590
|
+
reconnectDelay: options.reconnectDelay,
|
|
591
|
+
maxReconnectDelay: options.maxReconnectDelay,
|
|
592
|
+
connectTimeoutMs: options.connectTimeoutMs,
|
|
593
|
+
}, webSocketOpen.signal);
|
|
594
|
+
if (disposed) {
|
|
595
|
+
await session.close();
|
|
596
|
+
throw new AbloConnectionError('This Ablo client is disposed.');
|
|
597
|
+
}
|
|
598
|
+
webSocket = session;
|
|
599
|
+
return session;
|
|
600
|
+
})().finally(() => {
|
|
601
|
+
webSocketPromise = null;
|
|
602
|
+
});
|
|
603
|
+
return webSocketPromise;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const ready = async (): Promise<void> => {
|
|
607
|
+
await transport.ready();
|
|
608
|
+
if (usesWebSocket) await webSocketSession();
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
const dispose = async (): Promise<void> => {
|
|
612
|
+
if (disposed) return;
|
|
613
|
+
disposed = true;
|
|
614
|
+
webSocketOpen.abort();
|
|
615
|
+
const opening = webSocketPromise;
|
|
616
|
+
if (webSocket) await webSocket.close();
|
|
617
|
+
else await opening?.then((session) => session.close()).catch(() => undefined);
|
|
618
|
+
await transport.dispose();
|
|
619
|
+
};
|
|
520
620
|
|
|
521
621
|
const model = (
|
|
522
622
|
name: string,
|
|
@@ -563,11 +663,55 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
563
663
|
return {
|
|
564
664
|
context: readSetContext,
|
|
565
665
|
client: clientIdentity,
|
|
566
|
-
onChange: transport.onChange
|
|
666
|
+
onChange: (reads: readonly ReadDependency[], listener: Parameters<typeof transport.onChange>[1]) =>
|
|
667
|
+
usesWebSocket
|
|
668
|
+
? subscribeWhenConnected(webSocketSession, reads, listener)
|
|
669
|
+
: transport.onChange(reads, listener),
|
|
567
670
|
};
|
|
568
671
|
}
|
|
569
672
|
if (typeof prop !== 'string') return undefined;
|
|
570
673
|
if (prop === 'commits') return commits;
|
|
674
|
+
if (prop === 'ready') return ready;
|
|
675
|
+
if (prop === 'dispose') return dispose;
|
|
676
|
+
if (usesWebSocket && prop === 'observe') {
|
|
677
|
+
return async function* (observeOptions?: { signal?: AbortSignal }) {
|
|
678
|
+
yield* (await webSocketSession()).observe(observeOptions);
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
if (usesWebSocket && prop === 'subscribe') {
|
|
682
|
+
return <K extends keyof CoreSyncEventMap>(
|
|
683
|
+
event: K,
|
|
684
|
+
listener: (...args: CoreSyncEventMap[K]) => void,
|
|
685
|
+
) => {
|
|
686
|
+
let stop: (() => void) | undefined;
|
|
687
|
+
let cancelled = false;
|
|
688
|
+
void webSocketSession().then((session) => {
|
|
689
|
+
if (!cancelled) stop = session.subscribe(event, listener);
|
|
690
|
+
});
|
|
691
|
+
return () => { cancelled = true; stop?.(); };
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
if (usesWebSocket && prop === 'updateSubscription') {
|
|
695
|
+
return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) => {
|
|
696
|
+
const result = await (await webSocketSession()).updateSubscription(
|
|
697
|
+
groups,
|
|
698
|
+
subscriptionOptions,
|
|
699
|
+
);
|
|
700
|
+
return { groups: result.syncGroups };
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
if (usesWebSocket && prop === 'presence') {
|
|
704
|
+
return {
|
|
705
|
+
update: async (input?: Parameters<AbloWebSocketSession['presence']['update']>[0]) =>
|
|
706
|
+
(await webSocketSession()).presence.update(input),
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (usesWebSocket && prop === 'collaboration') {
|
|
710
|
+
return {
|
|
711
|
+
send: async (event: string, payload: Readonly<Record<string, JsonValue>>) =>
|
|
712
|
+
(await webSocketSession()).collaboration.send(event, payload),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
571
715
|
// Real protocol members pass through unchanged.
|
|
572
716
|
if (isProtocolMember(prop)) {
|
|
573
717
|
return transport[prop];
|
|
@@ -586,5 +730,18 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
586
730
|
// A single boundary cast. `AbloHttpClient<S>` declares only what the model
|
|
587
731
|
// accessor and the passed-through protocol members actually implement, so no
|
|
588
732
|
// method on this type is missing at runtime.
|
|
589
|
-
return facade as AbloHttpClient<S>;
|
|
733
|
+
return facade as AbloHttpClient<S> | AbloWebSocketClient<S>;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function subscribeWhenConnected(
|
|
737
|
+
session: () => Promise<AbloWebSocketSession>,
|
|
738
|
+
reads: readonly ReadDependency[],
|
|
739
|
+
listener: Parameters<typeof subscribeWebSocketReadChanges>[2],
|
|
740
|
+
): () => void {
|
|
741
|
+
let stop: (() => void) | undefined;
|
|
742
|
+
let cancelled = false;
|
|
743
|
+
void session().then((connected) => {
|
|
744
|
+
if (!cancelled) stop = subscribeWebSocketReadChanges(connected, reads, listener);
|
|
745
|
+
});
|
|
746
|
+
return () => { cancelled = true; stop?.(); };
|
|
590
747
|
}
|
|
@@ -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: {
|