@abloatai/transaction 0.60.0 → 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/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 +12 -0
- 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/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/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +1 -1
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +7 -0
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -0
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +12 -1
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +101 -5
- package/dist/errors.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.map +1 -1
- package/dist/transport/http/client.js +3 -4
- 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/transport.d.ts +1 -78
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +7 -83
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
- package/dist/transport/websocket/frameHandlers.js +3 -0
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/session.d.ts.map +1 -1
- package/dist/transport/websocket/session.js +69 -36
- package/dist/transport/websocket/session.js.map +1 -1
- package/dist/transport/websocket/sessionContract.d.ts +1 -1
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
- package/dist/transport/websocket/transport.d.ts +2 -1
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/inboundFrames.d.ts +7 -0
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +5 -1
- package/dist/wire/inboundFrames.js.map +1 -1
- package/package.json +1 -1
- 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/index.ts +11 -0
- package/src/coordination/index.ts +2 -0
- package/src/coordination/schema.ts +11 -0
- package/src/errorCodes.ts +18 -1
- package/src/errors.ts +116 -4
- package/src/transport/http/claimWait.ts +108 -0
- package/src/transport/http/client.ts +6 -5
- package/src/transport/http/contract.ts +71 -0
- package/src/transport/http/index.ts +1 -0
- package/src/transport/http/transport.ts +11 -204
- package/src/transport/websocket/frameHandlers.ts +7 -0
- package/src/transport/websocket/session.ts +77 -43
- package/src/transport/websocket/sessionContract.ts +1 -1
- package/src/transport/websocket/transport.ts +2 -0
- package/src/wire/inboundFrames.ts +5 -0
package/src/errors.ts
CHANGED
|
@@ -23,12 +23,24 @@ import { ABLO_DOCS_BASE_URL } from './auth/hostedEndpoints.js';
|
|
|
23
23
|
import { errorCodeSpec, classifyRecovery } from './errorCodes.js';
|
|
24
24
|
import {
|
|
25
25
|
wireClaimSummarySchema,
|
|
26
|
+
wireParticipantKindSchema,
|
|
27
|
+
modelTargetSchema,
|
|
26
28
|
descriptionFromMeta,
|
|
27
29
|
type WireClaimSummary,
|
|
28
30
|
type ModelClaim,
|
|
29
31
|
type ModelTarget,
|
|
30
32
|
type ParticipantKind,
|
|
31
33
|
} from './coordination/schema.js';
|
|
34
|
+
import {
|
|
35
|
+
claimConflictContextSchema,
|
|
36
|
+
normalizeClaimConflict,
|
|
37
|
+
type ClaimConflictContext,
|
|
38
|
+
} from './claims/conflict.js';
|
|
39
|
+
|
|
40
|
+
export type {
|
|
41
|
+
ClaimConflictContext,
|
|
42
|
+
ClaimConflictCounterparty,
|
|
43
|
+
} from './claims/conflict.js';
|
|
32
44
|
|
|
33
45
|
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errorCodes.js';
|
|
34
46
|
export {
|
|
@@ -368,6 +380,44 @@ export interface ClaimContext {
|
|
|
368
380
|
|
|
369
381
|
export type ClaimErrorClaim = WireClaimSummary | ClaimContext;
|
|
370
382
|
|
|
383
|
+
function conflictFromClaims(
|
|
384
|
+
claims: readonly ClaimErrorClaim[] | undefined,
|
|
385
|
+
): ClaimConflictContext | undefined {
|
|
386
|
+
const claim = claims?.[0];
|
|
387
|
+
if (!claim) return undefined;
|
|
388
|
+
const summary = wireClaimSummarySchema.safeParse(claim);
|
|
389
|
+
if (summary.success) {
|
|
390
|
+
const actor = 'actor' in claim && typeof claim.actor === 'string'
|
|
391
|
+
? claim.actor
|
|
392
|
+
: undefined;
|
|
393
|
+
return normalizeClaimConflict({
|
|
394
|
+
heldByClaim: summary.data,
|
|
395
|
+
...(actor !== undefined ? { heldBy: actor } : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const context = claim as ClaimContext;
|
|
399
|
+
return normalizeClaimConflict({
|
|
400
|
+
...(context.actor !== undefined ? { heldBy: context.actor } : {}),
|
|
401
|
+
...(context.participantKind !== undefined
|
|
402
|
+
? { heldByKind: context.participantKind }
|
|
403
|
+
: {}),
|
|
404
|
+
...(context.claimId !== undefined ? { heldByClaimId: context.claimId } : {}),
|
|
405
|
+
...(context.description !== undefined ? { description: context.description } : {}),
|
|
406
|
+
...(context.expiresAt !== undefined ? { expiresAt: context.expiresAt } : {}),
|
|
407
|
+
...(context.target !== undefined
|
|
408
|
+
? { target: context.target }
|
|
409
|
+
: context.entityType !== undefined || context.entityId !== undefined
|
|
410
|
+
? {
|
|
411
|
+
target: {
|
|
412
|
+
...(context.entityType !== undefined ? { model: context.entityType } : {}),
|
|
413
|
+
...(context.entityId !== undefined ? { id: context.entityId } : {}),
|
|
414
|
+
...(context.field !== undefined ? { field: context.field } : {}),
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
: {}),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
371
421
|
function claimDescription(claim: ClaimErrorClaim | undefined): string | undefined {
|
|
372
422
|
if (!claim) return undefined;
|
|
373
423
|
if ('description' in claim && typeof claim.description === 'string') {
|
|
@@ -434,6 +484,7 @@ export function formatClaimedErrorMessage(args: {
|
|
|
434
484
|
export class AbloClaimedError extends AbloError {
|
|
435
485
|
override readonly type = 'AbloClaimedError' as const;
|
|
436
486
|
readonly claims?: readonly ClaimErrorClaim[];
|
|
487
|
+
readonly conflict?: ClaimConflictContext;
|
|
437
488
|
|
|
438
489
|
constructor(
|
|
439
490
|
message: string,
|
|
@@ -443,10 +494,22 @@ export class AbloClaimedError extends AbloError {
|
|
|
443
494
|
requestId?: string;
|
|
444
495
|
cause?: unknown;
|
|
445
496
|
claims?: readonly ClaimErrorClaim[];
|
|
497
|
+
conflict?: ClaimConflictContext;
|
|
498
|
+
details?: Readonly<Record<string, unknown>>;
|
|
446
499
|
},
|
|
447
500
|
) {
|
|
448
|
-
|
|
501
|
+
const conflict = options?.conflict ?? conflictFromClaims(options?.claims);
|
|
502
|
+
const details = {
|
|
503
|
+
...(options?.details ?? {}),
|
|
504
|
+
...(options?.claims !== undefined ? { claims: options.claims } : {}),
|
|
505
|
+
...(conflict !== undefined ? { conflict } : {}),
|
|
506
|
+
};
|
|
507
|
+
super(message, {
|
|
508
|
+
...options,
|
|
509
|
+
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
510
|
+
});
|
|
449
511
|
if (options?.claims !== undefined) this.claims = options.claims;
|
|
512
|
+
if (conflict !== undefined) this.conflict = conflict;
|
|
450
513
|
}
|
|
451
514
|
}
|
|
452
515
|
|
|
@@ -681,8 +744,13 @@ const NestedErrorShapeSchema = z
|
|
|
681
744
|
field: OptionalWireStringSchema,
|
|
682
745
|
requiredCapability: requiredCapabilityWireSchema.optional().catch(undefined),
|
|
683
746
|
heldBy: OptionalWireStringSchema,
|
|
747
|
+
heldByKind: wireParticipantKindSchema.optional().catch(undefined),
|
|
748
|
+
heldByClaimId: OptionalWireStringSchema,
|
|
749
|
+
heldByExpiresAt: z.number().optional().catch(undefined),
|
|
684
750
|
heldByClaim: wireClaimSummarySchema.optional().catch(undefined),
|
|
685
751
|
claims: z.array(wireClaimSummarySchema).optional().catch(undefined),
|
|
752
|
+
conflict: claimConflictContextSchema.optional().catch(undefined),
|
|
753
|
+
target: modelTargetSchema.unwrap().partial().optional().catch(undefined),
|
|
686
754
|
})
|
|
687
755
|
.loose();
|
|
688
756
|
|
|
@@ -706,8 +774,13 @@ const ErrorBodyShapeSchema = z
|
|
|
706
774
|
message: OptionalWireStringSchema,
|
|
707
775
|
requiredCapability: requiredCapabilityWireSchema.optional().catch(undefined),
|
|
708
776
|
heldBy: OptionalWireStringSchema,
|
|
777
|
+
heldByKind: wireParticipantKindSchema.optional().catch(undefined),
|
|
778
|
+
heldByClaimId: OptionalWireStringSchema,
|
|
779
|
+
heldByExpiresAt: z.number().optional().catch(undefined),
|
|
709
780
|
heldByClaim: wireClaimSummarySchema.optional().catch(undefined),
|
|
710
781
|
claims: z.array(wireClaimSummarySchema).optional().catch(undefined),
|
|
782
|
+
conflict: claimConflictContextSchema.optional().catch(undefined),
|
|
783
|
+
target: modelTargetSchema.unwrap().partial().optional().catch(undefined),
|
|
711
784
|
})
|
|
712
785
|
.loose();
|
|
713
786
|
|
|
@@ -763,11 +836,12 @@ export function errorFromWire(
|
|
|
763
836
|
requestId?: string;
|
|
764
837
|
requiredCapability?: RequiredCapability;
|
|
765
838
|
claims?: readonly ClaimErrorClaim[];
|
|
839
|
+
conflict?: ClaimConflictContext;
|
|
766
840
|
/** Everything the producer attached beyond the envelope's own fields. */
|
|
767
841
|
details?: Readonly<Record<string, unknown>>;
|
|
768
842
|
} = {},
|
|
769
843
|
): AbloError {
|
|
770
|
-
const { code, requestId, requiredCapability, claims, details } = opts;
|
|
844
|
+
const { code, requestId, requiredCapability, claims, conflict, details } = opts;
|
|
771
845
|
// Effective status: an explicit HTTP status wins; otherwise fall back to
|
|
772
846
|
// the code's canonical status from the registry (undefined for unknown /
|
|
773
847
|
// forward-compat codes, which then map to the base AbloError).
|
|
@@ -802,9 +876,16 @@ export function errorFromWire(
|
|
|
802
876
|
if (
|
|
803
877
|
code === 'claim_conflict' ||
|
|
804
878
|
code === 'entity_claimed' ||
|
|
805
|
-
code === 'claim_lost'
|
|
879
|
+
code === 'claim_lost' ||
|
|
880
|
+
code === 'fence_token_stale' ||
|
|
881
|
+
code === 'model_claimed' ||
|
|
882
|
+
code === 'model_claimed_timeout' ||
|
|
883
|
+
code === 'claim_lease_unavailable' ||
|
|
884
|
+
code === 'claim_wait_aborted' ||
|
|
885
|
+
code === 'grant_timeout' ||
|
|
886
|
+
code === 'claim_queued'
|
|
806
887
|
) {
|
|
807
|
-
return new AbloClaimedError(message, { ...baseOpts, claims });
|
|
888
|
+
return new AbloClaimedError(message, { ...baseOpts, claims, conflict });
|
|
808
889
|
}
|
|
809
890
|
// A write whose `readAt` watermark went stale — callers re-read and retry.
|
|
810
891
|
if (code === 'stale_context') {
|
|
@@ -854,6 +935,12 @@ const ENVELOPE_KEYS: ReadonlySet<string> = new Set([
|
|
|
854
935
|
'requiredCapability',
|
|
855
936
|
'claims',
|
|
856
937
|
'heldByClaim',
|
|
938
|
+
'heldBy',
|
|
939
|
+
'heldByKind',
|
|
940
|
+
'heldByClaimId',
|
|
941
|
+
'heldByExpiresAt',
|
|
942
|
+
'conflict',
|
|
943
|
+
'target',
|
|
857
944
|
// `reason` is deliberately NOT here: it is domain detail (a driver's words
|
|
858
945
|
// beside the server's message), not an envelope field — the canonical
|
|
859
946
|
// envelope schema has no such key — so it must survive into `details` for a
|
|
@@ -916,6 +1003,30 @@ export function translateHttpError(
|
|
|
916
1003
|
: nested?.heldByClaim
|
|
917
1004
|
? [nested.heldByClaim]
|
|
918
1005
|
: undefined);
|
|
1006
|
+
const conflict =
|
|
1007
|
+
parsed.conflict ??
|
|
1008
|
+
nested?.conflict ??
|
|
1009
|
+
normalizeClaimConflict({
|
|
1010
|
+
...(parsed.heldBy ?? nested?.heldBy
|
|
1011
|
+
? { heldBy: parsed.heldBy ?? nested?.heldBy }
|
|
1012
|
+
: {}),
|
|
1013
|
+
...(parsed.heldByKind ?? nested?.heldByKind
|
|
1014
|
+
? { heldByKind: parsed.heldByKind ?? nested?.heldByKind }
|
|
1015
|
+
: {}),
|
|
1016
|
+
...(parsed.heldByClaimId ?? nested?.heldByClaimId
|
|
1017
|
+
? { heldByClaimId: parsed.heldByClaimId ?? nested?.heldByClaimId }
|
|
1018
|
+
: {}),
|
|
1019
|
+
...(parsed.heldByExpiresAt ?? nested?.heldByExpiresAt
|
|
1020
|
+
? { heldByExpiresAt: parsed.heldByExpiresAt ?? nested?.heldByExpiresAt }
|
|
1021
|
+
: {}),
|
|
1022
|
+
...(parsed.heldByClaim ?? nested?.heldByClaim
|
|
1023
|
+
? { heldByClaim: parsed.heldByClaim ?? nested?.heldByClaim }
|
|
1024
|
+
: {}),
|
|
1025
|
+
...(parsed.target ?? nested?.target
|
|
1026
|
+
? { target: parsed.target ?? nested?.target }
|
|
1027
|
+
: {}),
|
|
1028
|
+
}) ??
|
|
1029
|
+
conflictFromClaims(claims);
|
|
919
1030
|
|
|
920
1031
|
// The envelope carries `request_id`, so a caller that has only the body still
|
|
921
1032
|
// gets the correlation id — without this it was dropped unless the caller
|
|
@@ -935,6 +1046,7 @@ export function translateHttpError(
|
|
|
935
1046
|
requestId: requestId ?? (typeof bodyRequestId === 'string' ? bodyRequestId : undefined),
|
|
936
1047
|
requiredCapability,
|
|
937
1048
|
claims,
|
|
1049
|
+
conflict,
|
|
938
1050
|
details: Object.keys(details).length > 0 ? details : undefined,
|
|
939
1051
|
});
|
|
940
1052
|
}
|
|
@@ -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,8 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import {
|
|
21
21
|
createHttpTransport,
|
|
22
|
-
type HttpTransport,
|
|
23
22
|
} from './transport.js';
|
|
23
|
+
import type { HttpTransport } from './contract.js';
|
|
24
24
|
import type { HttpClientConfig } from './options.js';
|
|
25
25
|
import type {
|
|
26
26
|
CommitResource,
|
|
@@ -272,6 +272,8 @@ const PROTOCOL_MEMBERS = new Set<string>([
|
|
|
272
272
|
'getAuthToken',
|
|
273
273
|
]);
|
|
274
274
|
|
|
275
|
+
const ignoreAlreadySurfacedSessionOpeningFailure = (): undefined => undefined;
|
|
276
|
+
|
|
275
277
|
/** Narrows a bare property name to a transport key so the facade can index it typed. */
|
|
276
278
|
function isProtocolMember(prop: string): prop is keyof HttpTransport {
|
|
277
279
|
return PROTOCOL_MEMBERS.has(prop);
|
|
@@ -555,9 +557,8 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
555
557
|
return receipt;
|
|
556
558
|
},
|
|
557
559
|
dispatchClaim: async (input) => (await webSocketSession()).claim(input),
|
|
558
|
-
releaseDispatchedClaim: (input) =>
|
|
559
|
-
|
|
560
|
-
},
|
|
560
|
+
releaseDispatchedClaim: async (input) =>
|
|
561
|
+
(await webSocketSession()).release(input),
|
|
561
562
|
} : {}),
|
|
562
563
|
});
|
|
563
564
|
const schemaModels = new Set(Object.keys(schema.models));
|
|
@@ -614,7 +615,7 @@ export function createAbloHttpClient<S extends SchemaRecord>(
|
|
|
614
615
|
webSocketOpen.abort();
|
|
615
616
|
const opening = webSocketPromise;
|
|
616
617
|
if (webSocket) await webSocket.close();
|
|
617
|
-
else await opening?.then((session) => session.close()).catch(
|
|
618
|
+
else await opening?.then((session) => session.close()).catch(ignoreAlreadySurfacedSessionOpeningFailure);
|
|
618
619
|
await transport.dispose();
|
|
619
620
|
};
|
|
620
621
|
|
|
@@ -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
|
+
}
|