@dopamint-fun/open-sdk 0.2.0-dev.0 → 0.2.0-dev.2
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/cli.js +37 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/openTournament.d.ts +22 -0
- package/dist/openTournament.js +31 -0
- package/dist/room.d.ts +29 -7
- package/dist/room.js +38 -8
- package/dist/seatState.d.ts +60 -5
- package/dist/seatState.js +156 -7
- package/dist/seatTurn.d.ts +15 -2
- package/dist/seatTurn.js +98 -21
- package/dist/session.js +90 -46
- package/dist/sessionCodec.d.ts +158 -2
- package/dist/sessionCodec.js +642 -19
- package/dist/sessionWire.d.ts +6 -5
- package/dist/sessionWire.js +7 -6
- package/package.json +1 -1
package/dist/seatTurn.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { AgentKeypair } from "./keypair.js";
|
|
2
2
|
import type { TexasAction, TexasLegalActions } from "./texas.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type AuthorityMessage, type ResumeCursor } from "./sessionCodec.js";
|
|
4
4
|
import type { SessionContext } from "./sessionWire.js";
|
|
5
|
-
import type { ResumeCursor } from "./sessionCodec.js";
|
|
6
5
|
import { type OpenTableSeatView, type OpenTableTalkLine, type OpenTableView, type SeatSession, type OpenSittingStatus } from "./session.js";
|
|
7
6
|
import { type SeatSessionState } from "./seatState.js";
|
|
8
7
|
export interface SeatTurnArgs {
|
|
@@ -11,6 +10,19 @@ export interface SeatTurnArgs {
|
|
|
11
10
|
agentId: Uint8Array;
|
|
12
11
|
fetchImpl?: typeof fetch;
|
|
13
12
|
}
|
|
13
|
+
/** Persist the accepted boundary before it is acknowledged.
|
|
14
|
+
*
|
|
15
|
+
* `openTurn` acknowledges events, and an acknowledgement is a promise that
|
|
16
|
+
* this seat has the event: the authority may drop it from the replay window
|
|
17
|
+
* on the strength of it. Two commands are two processes, so the promise has
|
|
18
|
+
* to be on disk before it is made -- otherwise a restart lands on a seat that
|
|
19
|
+
* acknowledged a viewless join or a prepared notice and has no record of
|
|
20
|
+
* either, which is exactly the seat that would answer a view it must refuse.
|
|
21
|
+
*
|
|
22
|
+
* Supplied by the CLI from `saveSeatState`. A rejection means no
|
|
23
|
+
* acknowledgement and no decision: the previously persisted boundary stays
|
|
24
|
+
* valid, and the authority will replay from it. */
|
|
25
|
+
export type SeatStateCheckpoint = (state: SeatSessionState) => void | Promise<void>;
|
|
14
26
|
/** What this seat is looking at, in the shape a reader can act on. */
|
|
15
27
|
export interface SeatTurnPosition {
|
|
16
28
|
seat: number;
|
|
@@ -121,6 +133,7 @@ export declare function attachFailureOutcome(error: unknown, sitting: OpenSittin
|
|
|
121
133
|
* an answer, not a failure. */
|
|
122
134
|
export declare function openTurn(args: SeatTurnArgs & {
|
|
123
135
|
waitMs?: number;
|
|
136
|
+
checkpoint: SeatStateCheckpoint;
|
|
124
137
|
}): Promise<SeatTurnOutcome>;
|
|
125
138
|
export interface SubmitTurnResult {
|
|
126
139
|
committed: boolean;
|
package/dist/seatTurn.js
CHANGED
|
@@ -2,8 +2,9 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { toHex0x } from "./bytes.js";
|
|
3
3
|
import { acceptAndAwaitAdmission, readOffer } from "./offer.js";
|
|
4
4
|
import { decodeLegalActions, decodeParticipantView, encodeAction, } from "./texas.js";
|
|
5
|
+
import { admitAuthorityEvent, equalReceiptRef, freshSessionBoundary, predictionGatePreparationOf, predictionGateTargetReceipt, } from "./sessionCodec.js";
|
|
5
6
|
import { agentReadCapability, openSeatSession, readPublicTable, readTableTalk, turnIsStillOpen, SessionRefusal, afterRefusal, readSittingStatus, } from "./session.js";
|
|
6
|
-
import { decodeContext, decodeCursor, decodeView, encodeContext, encodeCursor, encodeView, } from "./seatState.js";
|
|
7
|
+
import { decodeContext, decodeCursor, decodeGate, decodeView, encodeContext, encodeCursor, encodeGate, encodeView, } from "./seatState.js";
|
|
7
8
|
/** Whether `view` is a turn this seat has already answered.
|
|
8
9
|
*
|
|
9
10
|
* `act` does not move the cursor, so the next `turn` resumes from before the
|
|
@@ -87,6 +88,12 @@ export async function attach(session, state, reopen) {
|
|
|
87
88
|
clientNonce: toHex0x(nonce),
|
|
88
89
|
context: joined.context ? encodeContext(joined.context) : null,
|
|
89
90
|
token: opened.client.token ?? null,
|
|
91
|
+
/* A fresh admission resets the gate boundary with everything else:
|
|
92
|
+
the phase belonged to the session that just ended. */
|
|
93
|
+
cursor: { sequence: "0", witnessedReceipt: null },
|
|
94
|
+
view: null,
|
|
95
|
+
predictionGate: null,
|
|
96
|
+
awaitingGatePrefix: false,
|
|
90
97
|
},
|
|
91
98
|
context: joined.context ?? null,
|
|
92
99
|
cursor: { sequence: 0n, witnessedReceipt: null },
|
|
@@ -124,13 +131,21 @@ const holeFromView = (view) => {
|
|
|
124
131
|
return null;
|
|
125
132
|
}
|
|
126
133
|
};
|
|
127
|
-
|
|
134
|
+
/** The file as it stands after one accepted event: the session it belongs to,
|
|
135
|
+
* the boundary that was accepted, and the position to answer if there is one.
|
|
136
|
+
* The gate phase travels with the cursor because they are one promise - a
|
|
137
|
+
* cursor that outran its phase is a seat that forgot it was held. */
|
|
138
|
+
function persisted(session, state, context, cursor, view, boundary) {
|
|
128
139
|
return {
|
|
129
140
|
...state,
|
|
130
141
|
context: context ? encodeContext(context) : state.context,
|
|
131
142
|
token: session.client.token ?? state.token,
|
|
132
143
|
cursor: encodeCursor(cursor),
|
|
133
144
|
view: view ? encodeView(view) : state.view,
|
|
145
|
+
predictionGate: boundary.predictionGate
|
|
146
|
+
? encodeGate(boundary.predictionGate)
|
|
147
|
+
: null,
|
|
148
|
+
awaitingGatePrefix: boundary.awaitingGatePrefix,
|
|
134
149
|
};
|
|
135
150
|
}
|
|
136
151
|
/** What `turn` answers when it could not open a session.
|
|
@@ -201,18 +216,66 @@ export async function openTurn(args) {
|
|
|
201
216
|
session = attached.session;
|
|
202
217
|
args = { ...args, state: attached.state };
|
|
203
218
|
let { context, cursor, messages } = attached;
|
|
219
|
+
/* What this seat has already accepted, restored from the file: the sequence
|
|
220
|
+
and receipt floor its cursor names, the gate phase it holds, and whether a
|
|
221
|
+
viewless admission still owes its prepared prefix. The view is not
|
|
222
|
+
restored - a restart drops it exactly as
|
|
223
|
+
`ParticipantSessionState::restore` does, and the committed floor carries
|
|
224
|
+
what the boundary still needs. */
|
|
225
|
+
let boundary = {
|
|
226
|
+
...freshSessionBoundary(),
|
|
227
|
+
context,
|
|
228
|
+
sequence: cursor.sequence,
|
|
229
|
+
receiptFloor: cursor.witnessedReceipt,
|
|
230
|
+
predictionGate: args.state.predictionGate
|
|
231
|
+
? decodeGate(args.state.predictionGate)
|
|
232
|
+
: null,
|
|
233
|
+
awaitingGatePrefix: args.state.awaitingGatePrefix,
|
|
234
|
+
};
|
|
204
235
|
const until = Date.now() + (args.waitMs ?? 0);
|
|
236
|
+
/* Accept, persist, acknowledge - in that order, for every event this door
|
|
237
|
+
takes. The acknowledgement tells the authority this seat holds the event,
|
|
238
|
+
and it may drop the event from the replay window on the strength of it; a
|
|
239
|
+
promise made before the file is on disk is one a restart cannot keep.
|
|
240
|
+
|
|
241
|
+
A refusal from `admitAuthorityEvent` throws out of here with nothing
|
|
242
|
+
written and nothing acknowledged, which is what leaves the previously
|
|
243
|
+
persisted boundary resumable. A re-delivery answers `null`: it was
|
|
244
|
+
persisted and acknowledged by the process that first applied it, and
|
|
245
|
+
writing its cursor again would walk this seat's boundary backwards. */
|
|
246
|
+
const accept = async (message, view) => {
|
|
247
|
+
const admitted = admitAuthorityEvent(boundary, message);
|
|
248
|
+
if (admitted.disposition !== "applied")
|
|
249
|
+
return null;
|
|
250
|
+
const originToken = session.client.token;
|
|
251
|
+
if (!originToken)
|
|
252
|
+
throw new Error("session token missing");
|
|
253
|
+
/* A fresh join is a new session: the view the previous one printed
|
|
254
|
+
describes a turn that no longer exists. */
|
|
255
|
+
const previous = message.type === "sessionJoined"
|
|
256
|
+
? { ...args.state, view: null }
|
|
257
|
+
: args.state;
|
|
258
|
+
const state = persisted(session, previous, message.context, message.cursor, view, admitted.boundary);
|
|
259
|
+
await args.checkpoint(state);
|
|
260
|
+
boundary = admitted.boundary;
|
|
261
|
+
context = message.context;
|
|
262
|
+
cursor = message.cursor;
|
|
263
|
+
args = { ...args, state };
|
|
264
|
+
await session.client.acknowledge(message.context, message.cursor, originToken);
|
|
265
|
+
return state;
|
|
266
|
+
};
|
|
205
267
|
for (;;) {
|
|
206
268
|
for (const message of messages) {
|
|
207
269
|
if (message.type === "error" && !message.retryable)
|
|
208
270
|
throw new SessionRefusal(message);
|
|
209
|
-
|
|
210
|
-
|
|
271
|
+
/* Acknowledgements, pendings, errors and challenges carry no boundary. */
|
|
272
|
+
if (!("cursor" in message))
|
|
273
|
+
continue;
|
|
211
274
|
if (message.type === "sessionTerminal") {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
const state = await accept(message, null);
|
|
276
|
+
/* A re-delivered terminal is this file's own last event: a resume at
|
|
277
|
+
the terminal cursor repeats it, so there is nothing to write or
|
|
278
|
+
promise again - and the sitting is still over. */
|
|
216
279
|
return {
|
|
217
280
|
kind: "terminal",
|
|
218
281
|
/* Carried out rather than dropped: `consent` takes exactly these two,
|
|
@@ -220,21 +283,21 @@ export async function openTurn(args) {
|
|
|
220
283
|
two-command tour had no settle at all. */
|
|
221
284
|
terminalNonce: message.finalState.nonce.toString(),
|
|
222
285
|
terminalCommitment: toHex0x(message.finalState.commitment),
|
|
223
|
-
state:
|
|
286
|
+
state: state ?? args.state,
|
|
224
287
|
};
|
|
225
288
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
throw new Error("session token missing");
|
|
230
|
-
await session.client.acknowledge(message.context, message.cursor, originToken);
|
|
231
|
-
cursor = message.cursor;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
289
|
+
/* A named notice and a viewless admission are acknowledged and waited
|
|
290
|
+
on: no private view arrived, so there is nothing here to answer and
|
|
291
|
+
nothing to decide. The phase they carry is persisted first. */
|
|
234
292
|
const view = "view" in message ? message.view : null;
|
|
235
|
-
|
|
293
|
+
const state = await accept(message, view);
|
|
294
|
+
/* A re-delivery is not a turn: the view it carries was answered or
|
|
295
|
+
waited out by the process that applied it, and nothing was written
|
|
296
|
+
here to hand back. */
|
|
297
|
+
if (!state)
|
|
298
|
+
continue;
|
|
299
|
+
if (!view)
|
|
236
300
|
continue;
|
|
237
|
-
cursor = message.cursor;
|
|
238
301
|
/* A view with no legal actions is the table moving without this seat.
|
|
239
302
|
One whose deadline has passed is this seat's turn already lost, and
|
|
240
303
|
answering it would sign for a turn that is closed. And one at or before
|
|
@@ -275,11 +338,11 @@ export async function openTurn(args) {
|
|
|
275
338
|
deadlineMs: view.participantDeadlineMs.toString(),
|
|
276
339
|
msRemaining: Number(view.participantDeadlineMs) - Date.now(),
|
|
277
340
|
},
|
|
278
|
-
state
|
|
341
|
+
state,
|
|
279
342
|
};
|
|
280
343
|
}
|
|
281
344
|
if (Date.now() >= until) {
|
|
282
|
-
const waiting = persisted(session, args.state, context, cursor, null);
|
|
345
|
+
const waiting = persisted(session, args.state, context, cursor, null, boundary);
|
|
283
346
|
/* Asked only here, where the loop is about to say "waiting" anyway: a
|
|
284
347
|
seat with no chips is not waiting for a turn, it is out, and a caller
|
|
285
348
|
polling `waiting` at the default `--wait 0` would spin at full rate for
|
|
@@ -309,6 +372,18 @@ export async function submitTurn(args) {
|
|
|
309
372
|
const view = decodeView(args.state.view);
|
|
310
373
|
if (!turnIsStillOpen(view, Date.now()))
|
|
311
374
|
throw new Error("the deadline for that turn has passed; run `turn` again for the next one");
|
|
375
|
+
/* A retained gate that still holds this seat's own turn means the authority
|
|
376
|
+
has not released it: answering the stored position would be inferring on
|
|
377
|
+
a turn nobody may act in yet. `turn` refuses to hand such a view out, so
|
|
378
|
+
this only fires on a stored position the gate later reclaimed. */
|
|
379
|
+
const retained = args.state.predictionGate
|
|
380
|
+
? decodeGate(args.state.predictionGate)
|
|
381
|
+
: null;
|
|
382
|
+
if (retained &&
|
|
383
|
+
retained.phase !== "released" &&
|
|
384
|
+
equalReceiptRef(predictionGateTargetReceipt(retained), view.latestReceipt) &&
|
|
385
|
+
predictionGatePreparationOf(retained)?.actingSeat === args.state.seat)
|
|
386
|
+
throw new Error("this turn is held behind a prediction gate; run `turn` again once it releases");
|
|
312
387
|
const openWith = (clientNonce) => openSeatSession({
|
|
313
388
|
productUrl: args.state.productUrl,
|
|
314
389
|
offerId: args.state.offerId,
|
|
@@ -438,5 +513,7 @@ export function newSeatState(productUrl, offerId, seat) {
|
|
|
438
513
|
token: null,
|
|
439
514
|
cursor: { sequence: "0", witnessedReceipt: null },
|
|
440
515
|
view: null,
|
|
516
|
+
predictionGate: null,
|
|
517
|
+
awaitingGatePrefix: false,
|
|
441
518
|
};
|
|
442
519
|
}
|
package/dist/session.js
CHANGED
|
@@ -3,7 +3,7 @@ import { blake2b256 } from "./crypto.js";
|
|
|
3
3
|
import { equalBytes, fromHex, textBytes, toHex0x } from "./bytes.js";
|
|
4
4
|
import { AGENT_HTTP_CAPABILITY_HEADER, mintAgentHttpCapability, } from "./agentHttp.js";
|
|
5
5
|
import { signRaw } from "./keypair.js";
|
|
6
|
-
import { MAX_TRANSPORT_FRAME_BYTES, decodeAuthorityMessage, decodeEnvelope, encodeAckFrame, encodeEnvelope, encodeSeatAuthSuccessFrame, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
|
|
6
|
+
import { MAX_TRANSPORT_FRAME_BYTES, PredictionGateConflictError, SessionBoundaryError, admitAuthorityEvent, decodeAuthorityMessage, freshSessionBoundary, decodeEnvelope, encodeAckFrame, encodeEnvelope, encodeSeatAuthSuccessFrame, sessionErrorHint, sessionErrorName, } from "./sessionCodec.js";
|
|
7
7
|
import { actionSigningBytes, encodeActionFrame, encodeJoinFrame, encodeResumeFrame, joinSigningBytes, requireSessionVersion, resumeSigningBytes, SESSION_VERSION, UnsupportedSessionVersionError, } from "./sessionWire.js";
|
|
8
8
|
import { decodeLegalActions, decodeParticipantView, encodeAction, pickAction, } from "./texas.js";
|
|
9
9
|
import { authorizeSeatChallenge } from "./seatAuth.js";
|
|
@@ -245,7 +245,7 @@ export class SessionClient {
|
|
|
245
245
|
typeof ceiling !== "number" ||
|
|
246
246
|
!Number.isFinite(ceiling) ||
|
|
247
247
|
ceiling < MAX_TRANSPORT_FRAME_BYTES)
|
|
248
|
-
throw new Error("authority discovery does not support participant session
|
|
248
|
+
throw new Error("authority discovery does not support participant session V3");
|
|
249
249
|
}
|
|
250
250
|
async answeredDiscovery() {
|
|
251
251
|
const { boundMs, pauseMs, sleep } = this.discoveryRetry;
|
|
@@ -611,6 +611,11 @@ export async function playSeat(args) {
|
|
|
611
611
|
let pending = null;
|
|
612
612
|
let lastContext = null;
|
|
613
613
|
let lastCursor = { sequence: 0n, witnessedReceipt: null };
|
|
614
|
+
/* What this seat has accepted: the event sequence, the receipt floor, the
|
|
615
|
+
view it was last shown, and the named gate phase it holds. Every event
|
|
616
|
+
goes through it before anything is acknowledged, so the two doors -- this
|
|
617
|
+
loop and `openTurn` -- refuse the same streams for the same reasons. */
|
|
618
|
+
let accepted = freshSessionBoundary();
|
|
614
619
|
const inbox = [];
|
|
615
620
|
let joined = await client.join();
|
|
616
621
|
if (joined.context)
|
|
@@ -634,7 +639,17 @@ export async function playSeat(args) {
|
|
|
634
639
|
if (message.type === "actionPending" ||
|
|
635
640
|
message.type === "actionAcknowledged")
|
|
636
641
|
return null;
|
|
637
|
-
if (message.type === "
|
|
642
|
+
if (message.type === "predictionGatePrepared" ||
|
|
643
|
+
message.type === "predictionGateOpened" ||
|
|
644
|
+
message.type === "predictionGateReleased") {
|
|
645
|
+
/* A named notice moves no receipt and carries no view: it is accepted,
|
|
646
|
+
acknowledged, and waited on. Only the real view that follows a
|
|
647
|
+
release is a turn to decide. */
|
|
648
|
+
const admitted = admitAuthorityEvent(accepted, message);
|
|
649
|
+
if (admitted.disposition !== "applied")
|
|
650
|
+
return null;
|
|
651
|
+
accepted = admitted.boundary;
|
|
652
|
+
lastContext = message.context;
|
|
638
653
|
await client.acknowledge(message.context, message.cursor, originToken);
|
|
639
654
|
lastCursor = message.cursor;
|
|
640
655
|
return null;
|
|
@@ -642,8 +657,16 @@ export async function playSeat(args) {
|
|
|
642
657
|
if ("context" in message)
|
|
643
658
|
lastContext = message.context;
|
|
644
659
|
if (message.type === "sessionTerminal") {
|
|
645
|
-
|
|
646
|
-
|
|
660
|
+
const admitted = admitAuthorityEvent(accepted, message);
|
|
661
|
+
/* A re-delivered terminal is this boundary's own last event - a resume
|
|
662
|
+
at the terminal cursor repeats it. There is nothing to apply and its
|
|
663
|
+
acknowledgement is spent, but the sitting is over either way and the
|
|
664
|
+
caller still needs what `consent` takes. */
|
|
665
|
+
if (admitted.disposition === "applied") {
|
|
666
|
+
accepted = admitted.boundary;
|
|
667
|
+
await client.acknowledge(message.context, message.cursor, originToken);
|
|
668
|
+
lastCursor = message.cursor;
|
|
669
|
+
}
|
|
647
670
|
return {
|
|
648
671
|
outcome: "terminal",
|
|
649
672
|
committedActions,
|
|
@@ -660,52 +683,61 @@ export async function playSeat(args) {
|
|
|
660
683
|
}
|
|
661
684
|
let view = null;
|
|
662
685
|
let cursor = null;
|
|
663
|
-
if (message.type === "sessionJoined"
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
view = message.view;
|
|
669
|
-
cursor = message.cursor;
|
|
670
|
-
}
|
|
671
|
-
else if (message.type === "actionCommitted") {
|
|
672
|
-
pending = null;
|
|
673
|
-
committedActions += 1;
|
|
674
|
-
view = message.view;
|
|
675
|
-
cursor = message.cursor;
|
|
676
|
-
}
|
|
677
|
-
else if (message.type === "actionRejected") {
|
|
678
|
-
pending = null;
|
|
679
|
-
view = message.view;
|
|
680
|
-
cursor = message.cursor;
|
|
681
|
-
}
|
|
682
|
-
else if (message.type === "sessionResumed") {
|
|
686
|
+
if (message.type === "sessionJoined" ||
|
|
687
|
+
message.type === "participantView" ||
|
|
688
|
+
message.type === "actionCommitted" ||
|
|
689
|
+
message.type === "actionRejected" ||
|
|
690
|
+
message.type === "sessionResumed") {
|
|
683
691
|
view = message.view;
|
|
684
692
|
cursor = message.cursor;
|
|
685
693
|
}
|
|
686
|
-
if (
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
/*
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
694
|
+
if (cursor !== null && lastContext) {
|
|
695
|
+
/* Accepted before anything else happens: a refusal here leaves the
|
|
696
|
+
retained boundary untouched and nothing acknowledged. */
|
|
697
|
+
const admitted = admitAuthorityEvent(accepted, message);
|
|
698
|
+
/* A re-delivery is the suffix a resume repeats. It was applied and
|
|
699
|
+
acknowledged once, so nothing here moves for it: no view, no pending
|
|
700
|
+
resolution, no commit count, no table read, no decision, no
|
|
701
|
+
acknowledgement. */
|
|
702
|
+
if (admitted.disposition !== "applied")
|
|
703
|
+
return null;
|
|
704
|
+
accepted = admitted.boundary;
|
|
705
|
+
/* Resolved only for the applied event, so a replayed commit cannot
|
|
706
|
+
count twice or drop a proposal that is still in flight. */
|
|
707
|
+
if (message.type === "actionCommitted") {
|
|
708
|
+
pending = null;
|
|
709
|
+
committedActions += 1;
|
|
701
710
|
}
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
711
|
+
else if (message.type === "actionRejected")
|
|
712
|
+
pending = null;
|
|
713
|
+
if (view &&
|
|
714
|
+
view.legalActions.length === 0 &&
|
|
715
|
+
!elimination.eliminated &&
|
|
716
|
+
Date.now() - lastIdleTableReadMs >= IDLE_TABLE_READ_MS) {
|
|
717
|
+
/* Nothing to decide: somebody else's turn, a hand this seat folded, or
|
|
718
|
+
a sitting this seat is out of. Read now and then, so the hand lines go
|
|
719
|
+
on and a seat that has lost its last chip says so once. */
|
|
720
|
+
lastIdleTableReadMs = Date.now();
|
|
721
|
+
const idle = await readPublicTable(fetchImpl, product, executionHex, names, tableCapability);
|
|
722
|
+
if (idle && idle.handNumber !== lastHandSeen) {
|
|
723
|
+
lastHandSeen = idle.handNumber;
|
|
724
|
+
args.onHand?.({
|
|
725
|
+
number: idle.handNumber,
|
|
726
|
+
stack: idle.seats.find((entry) => entry.seat === args.seat)?.stack ??
|
|
727
|
+
null,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (elimination.observe(idle))
|
|
731
|
+
args.onEliminated?.();
|
|
732
|
+
}
|
|
733
|
+
const nonceKey = view?.state.nonce.toString() ?? "";
|
|
707
734
|
const now = BigInt(Date.now());
|
|
708
|
-
if (
|
|
735
|
+
if (
|
|
736
|
+
/* A viewless admission is acknowledged and waited on: a held seat has
|
|
737
|
+
nothing to answer, and nothing to infer on, until its gate releases
|
|
738
|
+
and the real view arrives. */
|
|
739
|
+
view !== null &&
|
|
740
|
+
view.legalActions.length > 0 &&
|
|
709
741
|
!acted.has(nonceKey) &&
|
|
710
742
|
view.participantDeadlineMs > now) {
|
|
711
743
|
const legal = decodeLegalActions(view.legalActions);
|
|
@@ -876,6 +908,12 @@ export async function playSeat(args) {
|
|
|
876
908
|
look successful while the seat cannot read the gated stream. */
|
|
877
909
|
if (error instanceof UnsupportedSessionVersionError)
|
|
878
910
|
throw error;
|
|
911
|
+
/* Nor is a refused projection: the event the authority sent is one this
|
|
912
|
+
contract forbids, and resuming would fetch the same bytes again while
|
|
913
|
+
the loop reported it as flaky transport. */
|
|
914
|
+
if (error instanceof SessionBoundaryError ||
|
|
915
|
+
error instanceof PredictionGateConflictError)
|
|
916
|
+
throw error;
|
|
879
917
|
if (!lastContext || reconnects >= 5)
|
|
880
918
|
throw error;
|
|
881
919
|
/* Three refusals, three answers. A gone session (`UnknownSession`, or a
|
|
@@ -907,6 +945,12 @@ export async function playSeat(args) {
|
|
|
907
945
|
if (joined.context)
|
|
908
946
|
lastContext = joined.context;
|
|
909
947
|
lastCursor = { sequence: 0n, witnessedReceipt: null };
|
|
948
|
+
/* A rejoin is a new session: nothing the old one retained - its phase,
|
|
949
|
+
its floor, its sequence - describes this one, and only a boundary
|
|
950
|
+
this loop opened itself may be reset. The stale inbox belongs to
|
|
951
|
+
the session that is gone. */
|
|
952
|
+
accepted = freshSessionBoundary();
|
|
953
|
+
inbox.length = 0;
|
|
910
954
|
inbox.push(...joined.messages);
|
|
911
955
|
rejoins += 1;
|
|
912
956
|
reconnects += 1;
|
package/dist/sessionCodec.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const SESSION_ERROR_TAG = 13;
|
|
|
11
11
|
export declare const SEAT_AUTHORIZATION_CHALLENGE_TAG = 15;
|
|
12
12
|
export declare const SEAT_AUTHORIZATION_RESPONSE_TAG = 16;
|
|
13
13
|
export declare const PREDICTION_GATE_RELEASED_TAG = 17;
|
|
14
|
+
export declare const PREDICTION_GATE_PREPARED_TAG = 18;
|
|
15
|
+
export declare const PREDICTION_GATE_OPENED_TAG = 19;
|
|
14
16
|
export declare const MAX_TRANSPORT_FRAME_BYTES: number;
|
|
15
17
|
export interface WireEnvelope {
|
|
16
18
|
message: string;
|
|
@@ -57,12 +59,80 @@ export interface SeatAuthChallenge {
|
|
|
57
59
|
coordinatorProof: Uint8Array;
|
|
58
60
|
coordinatorPublicKey: Uint8Array;
|
|
59
61
|
}
|
|
62
|
+
/** ADR-0096's ceiling on added pacing for one next-action prediction window,
|
|
63
|
+
* as `arena_session::prediction_gate::MAX_PREDICTION_PACING_MS`. */
|
|
64
|
+
export declare const MAX_PREDICTION_PACING_MS = 25000n;
|
|
65
|
+
/** The in-play window a gate names. The session reuses this identity; it
|
|
66
|
+
* never mints a second one. */
|
|
67
|
+
export interface PredictionWindowRef {
|
|
68
|
+
windowId: bigint;
|
|
69
|
+
marketId: bigint;
|
|
70
|
+
/** The only contract kind a gate can hold a seat behind (wire tag 1). */
|
|
71
|
+
contract: "pokerActionV1";
|
|
72
|
+
}
|
|
73
|
+
/** A durably committed preparation: the frozen window identity, the target
|
|
74
|
+
* the acting seat is held on, its original committed deadline, and the
|
|
75
|
+
* persisted instant publication started. */
|
|
76
|
+
export interface PredictionGatePreparation {
|
|
77
|
+
window: PredictionWindowRef;
|
|
78
|
+
actingSeat: number;
|
|
79
|
+
state: StateRef;
|
|
80
|
+
receipt: ReceiptRef | null;
|
|
81
|
+
originalDeadlineMs: bigint;
|
|
82
|
+
preparedAtMs: bigint;
|
|
83
|
+
}
|
|
84
|
+
/** Acknowledged publication. `closesAtMs` is the service-committed close
|
|
85
|
+
* instant, never the action deadline. */
|
|
86
|
+
export interface PredictionGateOpening {
|
|
87
|
+
preparation: PredictionGatePreparation;
|
|
88
|
+
openedAtMs: bigint;
|
|
89
|
+
closesAtMs: bigint;
|
|
90
|
+
}
|
|
91
|
+
/** The terminal overlay: how the gate closed, and the bounded same-receipt
|
|
92
|
+
* deadline it admits. `lockCappedMs` and `participantDeadlineMs` are derived
|
|
93
|
+
* the way the shared crate derives them, so a recipient never has to repeat
|
|
94
|
+
* the arithmetic to know which later deadline is legal. */
|
|
95
|
+
export interface PredictionGateRelease {
|
|
96
|
+
window: PredictionWindowRef;
|
|
97
|
+
actingSeat: number;
|
|
98
|
+
state: StateRef;
|
|
99
|
+
receipt: ReceiptRef | null;
|
|
100
|
+
terminal: "locked" | "cancelled";
|
|
101
|
+
originalDeadlineMs: bigint;
|
|
102
|
+
arrivalMs: bigint;
|
|
103
|
+
lockedAtMs: bigint;
|
|
104
|
+
lockCappedMs: bigint;
|
|
105
|
+
budgetMs: bigint;
|
|
106
|
+
participantDeadlineMs: bigint;
|
|
107
|
+
}
|
|
108
|
+
/** The gate facts a participant retains for one revision. */
|
|
109
|
+
export type PredictionGateStatus = {
|
|
110
|
+
phase: "prepared";
|
|
111
|
+
preparation: PredictionGatePreparation;
|
|
112
|
+
} | {
|
|
113
|
+
phase: "open";
|
|
114
|
+
opening: PredictionGateOpening;
|
|
115
|
+
} | {
|
|
116
|
+
phase: "released";
|
|
117
|
+
release: PredictionGateRelease;
|
|
118
|
+
};
|
|
119
|
+
/** One named lifecycle step, as a value: what the fold below applies. */
|
|
120
|
+
export type PredictionGateMessage = {
|
|
121
|
+
kind: "prepared";
|
|
122
|
+
preparation: PredictionGatePreparation;
|
|
123
|
+
} | {
|
|
124
|
+
kind: "opened";
|
|
125
|
+
opening: PredictionGateOpening;
|
|
126
|
+
} | {
|
|
127
|
+
kind: "released";
|
|
128
|
+
release: PredictionGateRelease;
|
|
129
|
+
};
|
|
60
130
|
export type AuthorityMessage = {
|
|
61
131
|
type: "sessionJoined";
|
|
62
132
|
context: SessionContext;
|
|
63
133
|
sequence: bigint;
|
|
64
134
|
policy: SessionPolicy;
|
|
65
|
-
view: ViewSnapshot;
|
|
135
|
+
view: ViewSnapshot | null;
|
|
66
136
|
cursor: ResumeCursor;
|
|
67
137
|
} | {
|
|
68
138
|
type: "participantView";
|
|
@@ -98,7 +168,10 @@ export type AuthorityMessage = {
|
|
|
98
168
|
type: "sessionResumed";
|
|
99
169
|
context: SessionContext;
|
|
100
170
|
sequence: bigint;
|
|
101
|
-
|
|
171
|
+
/** The boundary the authority replayed from. It must precede the event
|
|
172
|
+
itself, or the resume names a suffix it cannot have replayed. */
|
|
173
|
+
replayFrom: bigint;
|
|
174
|
+
view: ViewSnapshot | null;
|
|
102
175
|
cursor: ResumeCursor;
|
|
103
176
|
} | {
|
|
104
177
|
type: "sessionTerminal";
|
|
@@ -108,10 +181,23 @@ export type AuthorityMessage = {
|
|
|
108
181
|
finalState: StateRef;
|
|
109
182
|
cursor: ResumeCursor;
|
|
110
183
|
finalReceipt: ReceiptRef | null;
|
|
184
|
+
} | {
|
|
185
|
+
type: "predictionGatePrepared";
|
|
186
|
+
context: SessionContext;
|
|
187
|
+
sequence: bigint;
|
|
188
|
+
preparation: PredictionGatePreparation;
|
|
189
|
+
cursor: ResumeCursor;
|
|
190
|
+
} | {
|
|
191
|
+
type: "predictionGateOpened";
|
|
192
|
+
context: SessionContext;
|
|
193
|
+
sequence: bigint;
|
|
194
|
+
opening: PredictionGateOpening;
|
|
195
|
+
cursor: ResumeCursor;
|
|
111
196
|
} | {
|
|
112
197
|
type: "predictionGateReleased";
|
|
113
198
|
context: SessionContext;
|
|
114
199
|
sequence: bigint;
|
|
200
|
+
release: PredictionGateRelease;
|
|
115
201
|
cursor: ResumeCursor;
|
|
116
202
|
} | {
|
|
117
203
|
type: "error";
|
|
@@ -131,6 +217,19 @@ export declare function sessionErrorName(tag: number): string;
|
|
|
131
217
|
* join supersedes the older binding, and the older client's next message
|
|
132
218
|
* finds its session gone. Both used to read as transport flakiness. */
|
|
133
219
|
export declare function sessionErrorHint(tag: number): string | null;
|
|
220
|
+
export declare function equalStateRef(left: StateRef, right: StateRef): boolean;
|
|
221
|
+
export declare function equalReceiptRef(left: ReceiptRef | null, right: ReceiptRef | null): boolean;
|
|
222
|
+
export declare function equalPredictionGatePreparation(left: PredictionGatePreparation, right: PredictionGatePreparation): boolean;
|
|
223
|
+
export declare function equalPredictionGateRelease(left: PredictionGateRelease, right: PredictionGateRelease): boolean;
|
|
224
|
+
/** The structural refusals of `PredictionGatePreparation::new`, made wherever
|
|
225
|
+
* a preparation enters this process - the wire, or a retained seat state. */
|
|
226
|
+
export declare function bindPredictionGatePreparation(fields: PredictionGatePreparation): PredictionGatePreparation;
|
|
227
|
+
/** The structural refusals of `PredictionGateOpening::new`. */
|
|
228
|
+
export declare function bindPredictionGateOpening(fields: PredictionGateOpening): PredictionGateOpening;
|
|
229
|
+
/** The structural refusals of `PredictionGateRelease::new`, with the bounded
|
|
230
|
+
* overlay derived here rather than trusted from a sender or a stored file:
|
|
231
|
+
* the lock instant capped at the pacing ceiling, plus the budget. */
|
|
232
|
+
export declare function bindPredictionGateRelease(fields: Omit<PredictionGateRelease, "lockCappedMs" | "participantDeadlineMs">): PredictionGateRelease;
|
|
134
233
|
export declare function decodeAuthorityMessage(bytes: Uint8Array): AuthorityMessage;
|
|
135
234
|
export declare function encodeSeatAuthSuccessFrame(context: SessionContext, actionId: Uint8Array, signature: Uint8Array): Uint8Array;
|
|
136
235
|
export declare function extractProtocolInput(payload: Uint8Array): Uint8Array;
|
|
@@ -143,3 +242,60 @@ export declare function takePrincipalProof(bytes: Uint8Array): {
|
|
|
143
242
|
signature: Uint8Array;
|
|
144
243
|
rest: Uint8Array;
|
|
145
244
|
};
|
|
245
|
+
/** A refused phase transition, named as the shared crate names it. */
|
|
246
|
+
export declare class PredictionGateConflictError extends Error {
|
|
247
|
+
readonly conflict: "PredictionGatePhaseConflict" | "PredictionGateReleaseConflict";
|
|
248
|
+
constructor(conflict: "PredictionGatePhaseConflict" | "PredictionGateReleaseConflict", detail: string);
|
|
249
|
+
}
|
|
250
|
+
/** The lifecycle step an authority message carries, or null where it carries
|
|
251
|
+
* none. The fold takes the value, not the envelope. */
|
|
252
|
+
export declare function predictionGateMessage(message: AuthorityMessage): PredictionGateMessage | null;
|
|
253
|
+
/** The committed preparation behind any phase, or null once released - a
|
|
254
|
+
* release carries the identity but not the persisted preparation instant. */
|
|
255
|
+
export declare function predictionGatePreparationOf(status: PredictionGateStatus): PredictionGatePreparation | null;
|
|
256
|
+
/** The receipt the gate targets. A recipient behind it still receives every
|
|
257
|
+
* notice, because each notice cursor names that recipient's own floor. */
|
|
258
|
+
export declare function predictionGateTargetReceipt(status: PredictionGateStatus): ReceiptRef | null;
|
|
259
|
+
/** Whether a newly named preparation supersedes the retained facts.
|
|
260
|
+
*
|
|
261
|
+
* Only a released gate can be superseded: a held gate's turn has not closed,
|
|
262
|
+
* so a second preparation contradicts it. Once released, the next selected
|
|
263
|
+
* turn always targets a later committed state and therefore its own receipt.
|
|
264
|
+
* A seat that is itself withheld for that next turn never witnesses the view
|
|
265
|
+
* that would otherwise retire the closed gate - which is exactly the second
|
|
266
|
+
* window of a hand, held on the seat that was a bystander for the first. A
|
|
267
|
+
* preparation naming the same revision is the same-target conflict, not a
|
|
268
|
+
* supersession. */
|
|
269
|
+
export declare function predictionGateSupersededByPreparation(status: PredictionGateStatus, nextTurn: PredictionGatePreparation): boolean;
|
|
270
|
+
export declare function applyPredictionGateStatus(current: PredictionGateStatus | null, message: PredictionGateMessage): PredictionGateStatus;
|
|
271
|
+
export interface AcceptedSessionBoundary {
|
|
272
|
+
context: SessionContext | null;
|
|
273
|
+
/** The last accepted authority event sequence; zero before the first. */
|
|
274
|
+
sequence: bigint;
|
|
275
|
+
receiptFloor: ReceiptRef | null;
|
|
276
|
+
/** The view last disclosed to this seat, or null while it is withheld. */
|
|
277
|
+
view: ViewSnapshot | null;
|
|
278
|
+
predictionGate: PredictionGateStatus | null;
|
|
279
|
+
/** Whether a viewless admission still owes its named prepared prefix. */
|
|
280
|
+
awaitingGatePrefix: boolean;
|
|
281
|
+
}
|
|
282
|
+
/** A refused event, named as `SessionSemanticError` names it. */
|
|
283
|
+
export declare class SessionBoundaryError extends Error {
|
|
284
|
+
readonly reason: string;
|
|
285
|
+
constructor(reason: string, detail: string);
|
|
286
|
+
}
|
|
287
|
+
/** The boundary a seat that has accepted nothing holds. */
|
|
288
|
+
export declare function freshSessionBoundary(): AcceptedSessionBoundary;
|
|
289
|
+
/** What the seam did with an event.
|
|
290
|
+
*
|
|
291
|
+
* `applied` is the only disposition that moved the boundary, and so the only
|
|
292
|
+
* one a consumer may act on: a re-delivery was applied and acknowledged once
|
|
293
|
+
* already, and acting on it a second time would decide a turn that is over,
|
|
294
|
+
* count a commit twice, or walk the acknowledged cursor backwards. */
|
|
295
|
+
export type AuthorityEventDisposition = "applied" | "replay" | "boundaryless";
|
|
296
|
+
export interface AdmittedAuthorityEvent {
|
|
297
|
+
disposition: AuthorityEventDisposition;
|
|
298
|
+
/** The boundary to retain. Unchanged unless the event was applied. */
|
|
299
|
+
boundary: AcceptedSessionBoundary;
|
|
300
|
+
}
|
|
301
|
+
export declare function admitAuthorityEvent(accepted: AcceptedSessionBoundary, message: AuthorityMessage): AdmittedAuthorityEvent;
|