@dopamint-fun/open-sdk 0.1.0-dev.0 → 0.2.0-dev.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/README.md +8 -1
- package/dist/acceptance.d.ts +6 -0
- package/dist/acceptance.js +34 -0
- package/dist/channel.d.ts +7 -6
- package/dist/channel.js +20 -16
- package/dist/claim.d.ts +19 -1
- package/dist/claim.js +47 -4
- package/dist/cli.js +866 -57
- package/dist/decide.d.ts +16 -0
- package/dist/decide.js +122 -0
- package/dist/equity.d.ts +34 -0
- package/dist/equity.js +99 -0
- package/dist/handRank.d.ts +36 -0
- package/dist/handRank.js +170 -0
- package/dist/identity.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +14 -3
- package/dist/keypair.js +8 -1
- package/dist/offer.d.ts +7 -0
- package/dist/offer.js +38 -7
- package/dist/openTournament.d.ts +211 -0
- package/dist/openTournament.js +337 -0
- package/dist/refusal.d.ts +45 -0
- package/dist/refusal.js +84 -0
- package/dist/room.d.ts +66 -0
- package/dist/room.js +154 -0
- package/dist/seatState.d.ts +91 -0
- package/dist/seatState.js +137 -0
- package/dist/seatTurn.d.ts +138 -0
- package/dist/seatTurn.js +442 -0
- package/dist/session.d.ts +158 -5
- package/dist/session.js +365 -25
- package/dist/sessionCodec.d.ts +6 -0
- package/dist/sessionCodec.js +34 -3
- package/dist/sessionWire.d.ts +20 -0
- package/dist/sessionWire.js +44 -0
- package/dist/tour.d.ts +55 -4
- package/dist/tour.js +88 -11
- package/package.json +1 -1
package/dist/sessionCodec.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* the seat loop: views, commits, terminal, errors, and seat-auth challenges.
|
|
6
6
|
*/
|
|
7
7
|
import { ByteReader, ByteWriter } from "./bytes.js";
|
|
8
|
-
import { encodeContext } from "./sessionWire.js";
|
|
8
|
+
import { encodeContext, requireSessionVersion, } from "./sessionWire.js";
|
|
9
9
|
const AUTHORITY_EVENT_ACKNOWLEDGEMENT_TAG = 0x09;
|
|
10
10
|
export const SESSION_JOINED_TAG = 0x02;
|
|
11
11
|
export const PARTICIPANT_VIEW_TAG = 0x03;
|
|
@@ -18,6 +18,7 @@ export const SESSION_TERMINAL_TAG = 0x0c;
|
|
|
18
18
|
export const SESSION_ERROR_TAG = 0x0d;
|
|
19
19
|
export const SEAT_AUTHORIZATION_CHALLENGE_TAG = 0x0f;
|
|
20
20
|
export const SEAT_AUTHORIZATION_RESPONSE_TAG = 0x10;
|
|
21
|
+
export const PREDICTION_GATE_RELEASED_TAG = 0x11;
|
|
21
22
|
export const MAX_TRANSPORT_FRAME_BYTES = 1 << 20;
|
|
22
23
|
export function encodeAckFrame(context, cursor) {
|
|
23
24
|
const sequence = typeof cursor === "object" ? cursor.sequence : cursor;
|
|
@@ -97,9 +98,11 @@ export function sessionErrorHint(tag) {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
function readContext(reader, wireVersion) {
|
|
101
|
+
const sessionVersion = reader.readU16("session version");
|
|
102
|
+
requireSessionVersion(sessionVersion);
|
|
100
103
|
return {
|
|
101
104
|
wireVersion,
|
|
102
|
-
sessionVersion
|
|
105
|
+
sessionVersion,
|
|
103
106
|
sessionId: reader.readFixed(32, "session id"),
|
|
104
107
|
executionId: reader.readFixed(32, "execution id"),
|
|
105
108
|
executionManifestDigest: reader.readFixed(32, "execution manifest digest"),
|
|
@@ -211,7 +214,14 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
211
214
|
const wireVersion = reader.readU16("wire version");
|
|
212
215
|
const tag = reader.readByte("message tag");
|
|
213
216
|
if (tag === SESSION_ERROR_TAG) {
|
|
214
|
-
|
|
217
|
+
// A present selected version is the authority's answer for this session,
|
|
218
|
+
// so it has to be one this build can speak; the unsupported-version
|
|
219
|
+
// payload read below is a raw diagnostic and stays readable without one.
|
|
220
|
+
readOption(reader, "session version option", () => {
|
|
221
|
+
const version = reader.readU16("session version");
|
|
222
|
+
requireSessionVersion(version);
|
|
223
|
+
return version;
|
|
224
|
+
});
|
|
215
225
|
const errorTag = reader.readByte("session protocol error tag");
|
|
216
226
|
if (errorTag === 0 || errorTag === 1)
|
|
217
227
|
reader.readU16("unsupported version");
|
|
@@ -323,6 +333,27 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
323
333
|
};
|
|
324
334
|
break;
|
|
325
335
|
}
|
|
336
|
+
case PREDICTION_GATE_RELEASED_TAG: {
|
|
337
|
+
reader.readU64("prediction window id");
|
|
338
|
+
reader.readU64("prediction market id");
|
|
339
|
+
reader.readByte("prediction window contract");
|
|
340
|
+
reader.readU16("prediction gate acting seat");
|
|
341
|
+
readState(reader, "prediction gate state nonce", "prediction gate state commitment");
|
|
342
|
+
readOption(reader, "prediction gate receipt option", () => readReceipt(reader));
|
|
343
|
+
reader.readByte("prediction gate terminal");
|
|
344
|
+
reader.readU64("original participant deadline milliseconds");
|
|
345
|
+
reader.readU64("prediction gate arrival milliseconds");
|
|
346
|
+
reader.readU64("prediction gate locked-at milliseconds");
|
|
347
|
+
reader.readU64("participant deadline budget milliseconds");
|
|
348
|
+
const cursor = readCursor(reader);
|
|
349
|
+
message = {
|
|
350
|
+
type: "predictionGateReleased",
|
|
351
|
+
context,
|
|
352
|
+
sequence,
|
|
353
|
+
cursor,
|
|
354
|
+
};
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
326
357
|
default:
|
|
327
358
|
throw new Error(`unknown authority message tag ${tag}`);
|
|
328
359
|
}
|
package/dist/sessionWire.d.ts
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { ByteWriter } from "./bytes.js";
|
|
2
|
+
/** The participant session version this build speaks.
|
|
3
|
+
*
|
|
4
|
+
* It is the contract that carries the named `PredictionGateReleased` overlay
|
|
5
|
+
* event, so an authority that cannot name it in its join advertisement is
|
|
6
|
+
* refused at discovery and a selected context from any other version is
|
|
7
|
+
* refused rather than re-encoded. */
|
|
8
|
+
export declare const SESSION_VERSION = 2;
|
|
9
|
+
/** A selected participant session version this build cannot speak.
|
|
10
|
+
*
|
|
11
|
+
* Thrown where a *selected* version enters or leaves the process -- a
|
|
12
|
+
* retained seat state, a signed context, or an authority event. It is never
|
|
13
|
+
* thrown for a raw join advertisement, which may carry any ascending `u16`
|
|
14
|
+
* values for the authority to choose among. */
|
|
15
|
+
export declare class UnsupportedSessionVersionError extends Error {
|
|
16
|
+
readonly version: number;
|
|
17
|
+
constructor(version: number);
|
|
18
|
+
}
|
|
19
|
+
/** The selected-context guard. Call it wherever a session context, or the
|
|
20
|
+
* selected version on an authority message, is about to be acted on. */
|
|
21
|
+
export declare function requireSessionVersion(version: number): void;
|
|
2
22
|
export interface SessionContext {
|
|
3
23
|
wireVersion: number;
|
|
4
24
|
sessionVersion: number;
|
package/dist/sessionWire.js
CHANGED
|
@@ -12,7 +12,50 @@ const RESUME_DOMAIN = textBytes("arena_session::resume");
|
|
|
12
12
|
const SESSION_JOIN_REQUEST_TAG = 0x01;
|
|
13
13
|
const ACTION_PROPOSAL_TAG = 0x04;
|
|
14
14
|
const SESSION_RESUME_REQUEST_TAG = 0x0a;
|
|
15
|
+
/** The participant session version this build speaks.
|
|
16
|
+
*
|
|
17
|
+
* It is the contract that carries the named `PredictionGateReleased` overlay
|
|
18
|
+
* event, so an authority that cannot name it in its join advertisement is
|
|
19
|
+
* refused at discovery and a selected context from any other version is
|
|
20
|
+
* refused rather than re-encoded. */
|
|
21
|
+
export const SESSION_VERSION = 2;
|
|
22
|
+
/** A selected participant session version this build cannot speak.
|
|
23
|
+
*
|
|
24
|
+
* Thrown where a *selected* version enters or leaves the process -- a
|
|
25
|
+
* retained seat state, a signed context, or an authority event. It is never
|
|
26
|
+
* thrown for a raw join advertisement, which may carry any ascending `u16`
|
|
27
|
+
* values for the authority to choose among. */
|
|
28
|
+
export class UnsupportedSessionVersionError extends Error {
|
|
29
|
+
version;
|
|
30
|
+
constructor(version) {
|
|
31
|
+
super(`unsupported participant session version ${version}; expected 2`);
|
|
32
|
+
this.version = version;
|
|
33
|
+
this.name = "UnsupportedSessionVersionError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The selected-context guard. Call it wherever a session context, or the
|
|
37
|
+
* selected version on an authority message, is about to be acted on. */
|
|
38
|
+
export function requireSessionVersion(version) {
|
|
39
|
+
if (version !== SESSION_VERSION)
|
|
40
|
+
throw new UnsupportedSessionVersionError(version);
|
|
41
|
+
}
|
|
42
|
+
/** Structural validation of a join advertisement, mirroring the shared codec:
|
|
43
|
+
* the list is raw, so it need not name a version this build supports, but an
|
|
44
|
+
* empty, non-u16, or non-ascending list is not encodable at all. */
|
|
45
|
+
function requireRawVersionList(versions) {
|
|
46
|
+
if (versions.length === 0)
|
|
47
|
+
throw new Error("supported session versions must not be empty");
|
|
48
|
+
if (versions.length > 0xffff)
|
|
49
|
+
throw new Error("supported session versions exceed the u16 count limit");
|
|
50
|
+
for (const [index, version] of versions.entries()) {
|
|
51
|
+
if (!Number.isInteger(version) || version < 0 || version > 0xffff)
|
|
52
|
+
throw new Error(`supported session version ${version} is not a u16`);
|
|
53
|
+
if (index > 0 && versions[index - 1] >= version)
|
|
54
|
+
throw new Error("supported session versions must be strictly ascending");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
15
57
|
export function encodeContext(writer, context) {
|
|
58
|
+
requireSessionVersion(context.sessionVersion);
|
|
16
59
|
writer
|
|
17
60
|
.pushU16(context.sessionVersion)
|
|
18
61
|
.pushFixed(context.sessionId, 32, "session id")
|
|
@@ -36,6 +79,7 @@ export function signedFrame(body, signature) {
|
|
|
36
79
|
.bytes();
|
|
37
80
|
}
|
|
38
81
|
export function joinBodyBytes(request) {
|
|
82
|
+
requireRawVersionList(request.supportedSessionVersions);
|
|
39
83
|
const writer = new ByteWriter()
|
|
40
84
|
.pushU16(request.wireVersion)
|
|
41
85
|
.pushByte(SESSION_JOIN_REQUEST_TAG)
|
package/dist/tour.d.ts
CHANGED
|
@@ -11,8 +11,16 @@ export type TourEntry = {
|
|
|
11
11
|
waiting: number;
|
|
12
12
|
seatsNeeded: number;
|
|
13
13
|
fillAtMs: number;
|
|
14
|
-
/** how many seats the house will take at `fillAtMs` if nobody else
|
|
14
|
+
/** how many seats the house will take at `fillAtMs` if nobody else
|
|
15
|
+
* comes; 0 when the fill will not seat this agent at all */
|
|
15
16
|
houseSeatsAtFill?: number;
|
|
17
|
+
/** what the fill at `fillAtMs` does with this agent: `stays_queued` when
|
|
18
|
+
* its floor (`minAgents`) cannot be met by who is queued, so it is not
|
|
19
|
+
* seated and waits for another real agent to arrive. Absent from an
|
|
20
|
+
* arena that predates floors, like `houseSeatsAtFill` before it */
|
|
21
|
+
atFill?: "seated" | "stays_queued";
|
|
22
|
+
/** the floor this agent stated, when above one */
|
|
23
|
+
minAgents?: number;
|
|
16
24
|
}
|
|
17
25
|
/** The queue composed an offer this agent accepts and plays itself. Since
|
|
18
26
|
* ADR-0175 this is the answer for every external agent: the product holds
|
|
@@ -37,8 +45,25 @@ export type TourEntry = {
|
|
|
37
45
|
state: "closed" | "unpaid";
|
|
38
46
|
[key: string]: unknown;
|
|
39
47
|
};
|
|
40
|
-
|
|
41
|
-
export interface
|
|
48
|
+
/** What an agent states when it enters the playground. */
|
|
49
|
+
export interface PlaygroundFloor {
|
|
50
|
+
/** The fewest real agents, this one included, it will sit with: 1 to 3.
|
|
51
|
+
* Omitted, the playground seats it with house seats after the wait, as it
|
|
52
|
+
* always has. */
|
|
53
|
+
minAgents?: number;
|
|
54
|
+
}
|
|
55
|
+
/** The origin answering as a restarting one does -- no answer at all, 408,
|
|
56
|
+
* 429 or a 5xx -- rather than the tour refusing the entry. A queue waits it
|
|
57
|
+
* out; every other refusal is the caller's to read. */
|
|
58
|
+
export declare class TourEntryUnavailable extends Error {
|
|
59
|
+
readonly status: number;
|
|
60
|
+
constructor(status: number, message: string);
|
|
61
|
+
}
|
|
62
|
+
/** How long a queue keeps entering while the origin stays unavailable. Long
|
|
63
|
+
* enough to cover a product restart, which is what this is for. */
|
|
64
|
+
export declare const QUEUE_RESTART_BOUND_MS = 90000;
|
|
65
|
+
export declare function enterTour(client: TourClient, tour: TourKind, floor?: PlaygroundFloor): Promise<TourEntry>;
|
|
66
|
+
export interface QueueOptions extends PlaygroundFloor {
|
|
42
67
|
/** How long to keep polling before giving up. */
|
|
43
68
|
timeoutMs?: number;
|
|
44
69
|
/** Gap between polls. The queue answers immediately; this paces the caller. */
|
|
@@ -46,6 +71,12 @@ export interface QueueOptions {
|
|
|
46
71
|
onWaiting?: (entry: Extract<TourEntry, {
|
|
47
72
|
state: "waiting";
|
|
48
73
|
}>) => void;
|
|
74
|
+
/** Told each time the origin answers as a restarting one does, so a run can
|
|
75
|
+
* say why it is still waiting. */
|
|
76
|
+
onUnavailable?: (error: unknown) => void;
|
|
77
|
+
/** How long the entry keeps being re-sent while the origin stays that way;
|
|
78
|
+
* tests shorten it. */
|
|
79
|
+
restartBoundMs?: number;
|
|
49
80
|
}
|
|
50
81
|
/** Enter and poll until seated.
|
|
51
82
|
*
|
|
@@ -89,13 +120,33 @@ export interface TourPlayReport {
|
|
|
89
120
|
outcome: string;
|
|
90
121
|
committedActions: number;
|
|
91
122
|
hands: number;
|
|
123
|
+
/** Whether this seat lost its last chip before the sitting ended. Kept
|
|
124
|
+
* separately from `outcome`, which is the table's ending rather than the
|
|
125
|
+
* seat's: a seat that busts on hand two and a seat that wins are both at the
|
|
126
|
+
* table when it reaches its terminal, and both have to be reported as
|
|
127
|
+
* having seen it. */
|
|
128
|
+
eliminated: boolean;
|
|
92
129
|
}
|
|
93
130
|
/** Drive a seated tour table to a terminal disposition.
|
|
94
131
|
*
|
|
95
132
|
* A lapsed turn is not an error: the authority acts for a silent seat on its
|
|
96
133
|
* own clock, so a slow decision costs the hand rather than the match, and the
|
|
97
|
-
* loop keeps reading. Only a refusal the server calls terminal stops it.
|
|
134
|
+
* loop keeps reading. Only a refusal the server calls terminal stops it.
|
|
135
|
+
*
|
|
136
|
+
* **Being eliminated does not end the sitting.** This used to return the
|
|
137
|
+
* moment the seat lost its last chip, which reads as the natural thing to do —
|
|
138
|
+
* there is no turn coming — and is how a seat ends up reporting a result
|
|
139
|
+
* before the table has one and walking away from a settlement the whole table
|
|
140
|
+
* is waiting on. A busted seat keeps watching to the table's terminal, which
|
|
141
|
+
* the route answers as `complete` for an out seat too, and says it busted in
|
|
142
|
+
* the report instead. */
|
|
98
143
|
export declare function playTour(client: TourClient, tableId: string, decide?: (turn: TurnView) => TableMove, options?: {
|
|
99
144
|
pollMs?: number;
|
|
145
|
+
/** How long to keep watching before giving up, as `queueUntilSeated` does.
|
|
146
|
+
* A table that stops dealing without reaching a terminal would otherwise
|
|
147
|
+
* hold this loop for as long as the process lives. */
|
|
148
|
+
timeoutMs?: number;
|
|
100
149
|
onTurn?: (turn: TurnView, move: TableMove) => void;
|
|
150
|
+
/** Called once, when this seat loses its last chip. */
|
|
151
|
+
onEliminated?: () => void;
|
|
101
152
|
}): Promise<TourPlayReport>;
|
package/dist/tour.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { AGENT_HTTP_CAPABILITY_HEADER, mintAgentHttpCapability, } from "./agentHttp.js";
|
|
15
15
|
import { textBytes } from "./bytes.js";
|
|
16
|
+
import { refusalMessage } from "./refusal.js";
|
|
16
17
|
/** One signed Product API call. `target` is origin-form, exactly as the server
|
|
17
18
|
* will reconstruct it - a leading-slash path plus any query string. */
|
|
18
19
|
async function signedFetch(client, method, target, body) {
|
|
@@ -41,13 +42,41 @@ async function signedFetch(client, method, target, body) {
|
|
|
41
42
|
json: text.length === 0 ? undefined : JSON.parse(text),
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
/** The origin answering as a restarting one does -- no answer at all, 408,
|
|
46
|
+
* 429 or a 5xx -- rather than the tour refusing the entry. A queue waits it
|
|
47
|
+
* out; every other refusal is the caller's to read. */
|
|
48
|
+
export class TourEntryUnavailable extends Error {
|
|
49
|
+
status;
|
|
50
|
+
constructor(status, message) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "TourEntryUnavailable";
|
|
53
|
+
this.status = status;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** How long a queue keeps entering while the origin stays unavailable. Long
|
|
57
|
+
* enough to cover a product restart, which is what this is for. */
|
|
58
|
+
export const QUEUE_RESTART_BOUND_MS = 90_000;
|
|
59
|
+
function unavailable(error) {
|
|
60
|
+
/* A fetch that never reached an origin throws `TypeError`; a stopped product
|
|
61
|
+
behind a proxy answers 502, and one coming back answers 5xx or 429. */
|
|
62
|
+
return error instanceof TourEntryUnavailable || error instanceof TypeError;
|
|
63
|
+
}
|
|
64
|
+
export async function enterTour(client, tour, floor = {}) {
|
|
45
65
|
const target = tour === "playground"
|
|
46
66
|
? "/open/v1/tours/playground/entries"
|
|
47
67
|
: "/open/v1/tours/tournament/entries";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
if (floor.minAgents !== undefined && tour !== "playground")
|
|
69
|
+
throw new Error("a floor of real agents only applies to the playground");
|
|
70
|
+
/* No body at all when nothing is stated: the join every client sent before
|
|
71
|
+
floors existed, signed over the same empty bytes. */
|
|
72
|
+
const body = floor.minAgents === undefined ? undefined : { minAgents: floor.minAgents };
|
|
73
|
+
const { status, json } = await signedFetch(client, "POST", target, body);
|
|
74
|
+
if (status !== 200) {
|
|
75
|
+
const said = refusalMessage("tour entry", status, json);
|
|
76
|
+
if (status === 408 || status === 429 || status >= 500)
|
|
77
|
+
throw new TourEntryUnavailable(status, said);
|
|
78
|
+
throw new Error(said);
|
|
79
|
+
}
|
|
51
80
|
return json;
|
|
52
81
|
}
|
|
53
82
|
/** Enter and poll until seated.
|
|
@@ -60,28 +89,50 @@ export async function queueUntilSeated(client, tour, options = {}) {
|
|
|
60
89
|
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
61
90
|
const pollMs = options.pollMs ?? 3_000;
|
|
62
91
|
const deadline = Date.now() + timeoutMs;
|
|
92
|
+
/* When the origin first went quiet on us, or null while it is answering. A
|
|
93
|
+
product restarted under a queued agent used to end its run on the proxy's
|
|
94
|
+
502, which is a deployment rather than a refusal. */
|
|
95
|
+
let unavailableSince = null;
|
|
63
96
|
for (;;) {
|
|
64
|
-
|
|
97
|
+
let entry;
|
|
98
|
+
try {
|
|
99
|
+
entry = await enterTour(client, tour, { minAgents: options.minAgents });
|
|
100
|
+
unavailableSince = null;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (!unavailable(error))
|
|
104
|
+
throw error;
|
|
105
|
+
unavailableSince ??= Date.now();
|
|
106
|
+
const bound = options.restartBoundMs ?? QUEUE_RESTART_BOUND_MS;
|
|
107
|
+
if (Date.now() - unavailableSince >= bound || Date.now() >= deadline)
|
|
108
|
+
throw error;
|
|
109
|
+
options.onUnavailable?.(error);
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
65
113
|
if (entry.state === "seated" || entry.state === "offered")
|
|
66
114
|
return entry;
|
|
67
115
|
if (entry.state !== "waiting")
|
|
68
116
|
throw new Error(`tour is not open: ${JSON.stringify(entry)}`);
|
|
69
117
|
options.onWaiting?.(entry);
|
|
70
118
|
if (Date.now() >= deadline)
|
|
71
|
-
throw new Error(
|
|
119
|
+
throw new Error(entry.atFill === "stays_queued"
|
|
120
|
+
? `still waiting for a seat after ${timeoutMs} ms; no fill seats this agent until ` +
|
|
121
|
+
`${entry.minAgents ?? options.minAgents} real agents are queued`
|
|
122
|
+
: `still waiting for a seat after ${timeoutMs} ms; the tour fills at ${entry.fillAtMs}`);
|
|
72
123
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
73
124
|
}
|
|
74
125
|
}
|
|
75
126
|
export async function readPosition(client, tableId) {
|
|
76
127
|
const { status, json } = await signedFetch(client, "GET", `/open/v1/tables/${tableId}/decision`);
|
|
77
128
|
if (status !== 200)
|
|
78
|
-
throw new Error(
|
|
129
|
+
throw new Error(refusalMessage("decision read", status, json));
|
|
79
130
|
return json;
|
|
80
131
|
}
|
|
81
132
|
export async function act(client, tableId, move) {
|
|
82
133
|
const { status, json } = await signedFetch(client, "POST", `/open/v1/tables/${tableId}/actions`, move);
|
|
83
134
|
if (status !== 200)
|
|
84
|
-
throw new Error(
|
|
135
|
+
throw new Error(refusalMessage("action", status, json));
|
|
85
136
|
return json;
|
|
86
137
|
}
|
|
87
138
|
/** The reference strategy: never wager, call only what is free. It exists so
|
|
@@ -98,15 +149,41 @@ export function foldHeavy(turn) {
|
|
|
98
149
|
*
|
|
99
150
|
* A lapsed turn is not an error: the authority acts for a silent seat on its
|
|
100
151
|
* own clock, so a slow decision costs the hand rather than the match, and the
|
|
101
|
-
* loop keeps reading. Only a refusal the server calls terminal stops it.
|
|
152
|
+
* loop keeps reading. Only a refusal the server calls terminal stops it.
|
|
153
|
+
*
|
|
154
|
+
* **Being eliminated does not end the sitting.** This used to return the
|
|
155
|
+
* moment the seat lost its last chip, which reads as the natural thing to do —
|
|
156
|
+
* there is no turn coming — and is how a seat ends up reporting a result
|
|
157
|
+
* before the table has one and walking away from a settlement the whole table
|
|
158
|
+
* is waiting on. A busted seat keeps watching to the table's terminal, which
|
|
159
|
+
* the route answers as `complete` for an out seat too, and says it busted in
|
|
160
|
+
* the report instead. */
|
|
102
161
|
export async function playTour(client, tableId, decide = foldHeavy, options = {}) {
|
|
103
162
|
const pollMs = options.pollMs ?? 1_000;
|
|
163
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
164
|
+
const deadline = Date.now() + timeoutMs;
|
|
104
165
|
let committedActions = 0;
|
|
166
|
+
let eliminated = false;
|
|
105
167
|
const hands = new Set();
|
|
106
168
|
for (;;) {
|
|
169
|
+
if (Date.now() >= deadline)
|
|
170
|
+
throw new Error(`the table reached no terminal after ${timeoutMs} ms; it committed ${committedActions} action(s) over ${hands.size} hand(s)`);
|
|
107
171
|
const turn = await readPosition(client, tableId);
|
|
108
|
-
if (turn.state === "complete"
|
|
109
|
-
return {
|
|
172
|
+
if (turn.state === "complete")
|
|
173
|
+
return {
|
|
174
|
+
outcome: turn.state,
|
|
175
|
+
committedActions,
|
|
176
|
+
hands: hands.size,
|
|
177
|
+
eliminated,
|
|
178
|
+
};
|
|
179
|
+
if (turn.state === "eliminated") {
|
|
180
|
+
if (!eliminated) {
|
|
181
|
+
eliminated = true;
|
|
182
|
+
options.onEliminated?.();
|
|
183
|
+
}
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
110
187
|
if (turn.state !== "your_turn") {
|
|
111
188
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
112
189
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dopamint-fun/open-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-dev.0",
|
|
4
4
|
"description": "DOPA-OPEN client SDK: generate and hold your own agent key, sign registrations, and play a Participant Session",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|