@dopamint-fun/open-sdk 0.1.0-dev.0 → 0.2.0-dev.1
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 +890 -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 +12 -4
- package/dist/index.js +15 -4
- 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 +88 -0
- package/dist/room.js +184 -0
- package/dist/seatState.d.ts +146 -0
- package/dist/seatState.js +286 -0
- package/dist/seatTurn.d.ts +151 -0
- package/dist/seatTurn.js +519 -0
- package/dist/session.d.ts +158 -5
- package/dist/session.js +433 -49
- package/dist/sessionCodec.d.ts +164 -2
- package/dist/sessionCodec.js +665 -11
- package/dist/sessionWire.d.ts +21 -0
- package/dist/sessionWire.js +45 -0
- package/dist/tour.d.ts +55 -4
- package/dist/tour.js +88 -11
- package/package.json +1 -1
package/dist/sessionCodec.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* appends the signature. Authority messages are decoded just far enough for
|
|
5
5
|
* the seat loop: views, commits, terminal, errors, and seat-auth challenges.
|
|
6
6
|
*/
|
|
7
|
-
import { ByteReader, ByteWriter } from "./bytes.js";
|
|
8
|
-
import { encodeContext } from "./sessionWire.js";
|
|
7
|
+
import { ByteReader, ByteWriter, equalBytes } from "./bytes.js";
|
|
8
|
+
import { encodeContext, requireSessionVersion, } from "./sessionWire.js";
|
|
9
9
|
const AUTHORITY_EVENT_ACKNOWLEDGEMENT_TAG = 0x09;
|
|
10
|
+
/** The only wire contract this build frames and decodes. */
|
|
11
|
+
const CANONICAL_WIRE_VERSION = 1;
|
|
10
12
|
export const SESSION_JOINED_TAG = 0x02;
|
|
11
13
|
export const PARTICIPANT_VIEW_TAG = 0x03;
|
|
12
14
|
export const ACTION_ACKNOWLEDGED_TAG = 0x05;
|
|
@@ -18,6 +20,9 @@ export const SESSION_TERMINAL_TAG = 0x0c;
|
|
|
18
20
|
export const SESSION_ERROR_TAG = 0x0d;
|
|
19
21
|
export const SEAT_AUTHORIZATION_CHALLENGE_TAG = 0x0f;
|
|
20
22
|
export const SEAT_AUTHORIZATION_RESPONSE_TAG = 0x10;
|
|
23
|
+
export const PREDICTION_GATE_RELEASED_TAG = 0x11;
|
|
24
|
+
export const PREDICTION_GATE_PREPARED_TAG = 0x12;
|
|
25
|
+
export const PREDICTION_GATE_OPENED_TAG = 0x13;
|
|
21
26
|
export const MAX_TRANSPORT_FRAME_BYTES = 1 << 20;
|
|
22
27
|
export function encodeAckFrame(context, cursor) {
|
|
23
28
|
const sequence = typeof cursor === "object" ? cursor.sequence : cursor;
|
|
@@ -50,6 +55,10 @@ export function decodeEnvelope(envelope) {
|
|
|
50
55
|
throw new Error("session frame exceeds transport ceiling");
|
|
51
56
|
return new Uint8Array(wire);
|
|
52
57
|
}
|
|
58
|
+
/** ADR-0096's ceiling on added pacing for one next-action prediction window,
|
|
59
|
+
* as `arena_session::prediction_gate::MAX_PREDICTION_PACING_MS`. */
|
|
60
|
+
export const MAX_PREDICTION_PACING_MS = 25000n;
|
|
61
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
53
62
|
/* The authority's session-protocol error tags, by name.
|
|
54
63
|
*
|
|
55
64
|
* `arena-session/src/wire.rs` numbers `SessionProtocolError` on the wire and
|
|
@@ -97,9 +106,11 @@ export function sessionErrorHint(tag) {
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
function readContext(reader, wireVersion) {
|
|
109
|
+
const sessionVersion = reader.readU16("session version");
|
|
110
|
+
requireSessionVersion(sessionVersion);
|
|
100
111
|
return {
|
|
101
112
|
wireVersion,
|
|
102
|
-
sessionVersion
|
|
113
|
+
sessionVersion,
|
|
103
114
|
sessionId: reader.readFixed(32, "session id"),
|
|
104
115
|
executionId: reader.readFixed(32, "execution id"),
|
|
105
116
|
executionManifestDigest: reader.readFixed(32, "execution manifest digest"),
|
|
@@ -145,6 +156,13 @@ function readView(reader) {
|
|
|
145
156
|
const legal = readPayload(reader, "legal action schema byte length", "legal action schema bytes");
|
|
146
157
|
const participantDeadlineMs = reader.readU64("participant deadline milliseconds");
|
|
147
158
|
const latestReceipt = readOption(reader, "latest receipt option", () => readReceipt(reader));
|
|
159
|
+
/* `ParticipantViewSnapshot::new` refuses a receipt whose resulting state is
|
|
160
|
+
not the state the view is on, and Rust decodes through that constructor.
|
|
161
|
+
Skipping it here accepts a view whose own commitment and receipt disagree
|
|
162
|
+
-- which is exactly what a mutated frame looks like, and what a seat would
|
|
163
|
+
then answer a turn on. */
|
|
164
|
+
if (latestReceipt && !equalStateRef(latestReceipt.resultingState, state))
|
|
165
|
+
throw new Error("view receipt does not result in the state the view is on");
|
|
148
166
|
return {
|
|
149
167
|
state,
|
|
150
168
|
participantView: participantView.bytes,
|
|
@@ -155,6 +173,156 @@ function readView(reader) {
|
|
|
155
173
|
latestReceipt,
|
|
156
174
|
};
|
|
157
175
|
}
|
|
176
|
+
/** `validate_authority_event`'s view rule: an event that carries a view names
|
|
177
|
+
* that view's own receipt in its cursor. The two are one fact, and a frame
|
|
178
|
+
* where they disagree describes a receipt floor no session ever held. */
|
|
179
|
+
function requireViewCursor(view, cursor) {
|
|
180
|
+
if (!equalReceiptRef(cursor.witnessedReceipt, view.latestReceipt))
|
|
181
|
+
throw new Error("the event cursor does not name the receipt its view is on");
|
|
182
|
+
}
|
|
183
|
+
/* ── The prediction-gate lifecycle values ────────────────────────────────
|
|
184
|
+
*
|
|
185
|
+
* Byte-for-byte the layouts in `arena_session::wire`:
|
|
186
|
+
* `encode_prediction_gate_preparation`, `_opening` and `_release`. Every
|
|
187
|
+
* structural refusal the Rust constructors make is made here, so a frame this
|
|
188
|
+
* side accepts is one the shared crate would also accept — a permissive
|
|
189
|
+
* client would hand a seat a window identity the authority never committed. */
|
|
190
|
+
export function equalStateRef(left, right) {
|
|
191
|
+
return (left.nonce === right.nonce && equalBytes(left.commitment, right.commitment));
|
|
192
|
+
}
|
|
193
|
+
export function equalReceiptRef(left, right) {
|
|
194
|
+
if (left === null || right === null)
|
|
195
|
+
return left === right;
|
|
196
|
+
return (equalBytes(left.digest, right.digest) &&
|
|
197
|
+
equalStateRef(left.resultingState, right.resultingState));
|
|
198
|
+
}
|
|
199
|
+
export function equalPredictionGatePreparation(left, right) {
|
|
200
|
+
return (left.window.windowId === right.window.windowId &&
|
|
201
|
+
left.window.marketId === right.window.marketId &&
|
|
202
|
+
left.window.contract === right.window.contract &&
|
|
203
|
+
left.actingSeat === right.actingSeat &&
|
|
204
|
+
equalStateRef(left.state, right.state) &&
|
|
205
|
+
equalReceiptRef(left.receipt, right.receipt) &&
|
|
206
|
+
left.originalDeadlineMs === right.originalDeadlineMs &&
|
|
207
|
+
left.preparedAtMs === right.preparedAtMs);
|
|
208
|
+
}
|
|
209
|
+
export function equalPredictionGateRelease(left, right) {
|
|
210
|
+
return (left.window.windowId === right.window.windowId &&
|
|
211
|
+
left.window.marketId === right.window.marketId &&
|
|
212
|
+
left.window.contract === right.window.contract &&
|
|
213
|
+
left.actingSeat === right.actingSeat &&
|
|
214
|
+
equalStateRef(left.state, right.state) &&
|
|
215
|
+
equalReceiptRef(left.receipt, right.receipt) &&
|
|
216
|
+
left.terminal === right.terminal &&
|
|
217
|
+
left.originalDeadlineMs === right.originalDeadlineMs &&
|
|
218
|
+
left.arrivalMs === right.arrivalMs &&
|
|
219
|
+
left.lockedAtMs === right.lockedAtMs &&
|
|
220
|
+
left.budgetMs === right.budgetMs);
|
|
221
|
+
}
|
|
222
|
+
function readPredictionWindow(reader) {
|
|
223
|
+
const windowId = reader.readU64("prediction window id");
|
|
224
|
+
const marketId = reader.readU64("prediction market id");
|
|
225
|
+
const tag = reader.readByte("prediction window contract");
|
|
226
|
+
if (tag !== 1)
|
|
227
|
+
throw new Error(`invalid prediction window contract tag ${tag}`);
|
|
228
|
+
if (windowId === 0n || marketId === 0n)
|
|
229
|
+
throw new Error("prediction window names neither a window nor a market");
|
|
230
|
+
return { windowId, marketId, contract: "pokerActionV1" };
|
|
231
|
+
}
|
|
232
|
+
function readPredictionGatePreparation(reader) {
|
|
233
|
+
const window = readPredictionWindow(reader);
|
|
234
|
+
const actingSeat = reader.readU16("prediction gate acting seat");
|
|
235
|
+
const state = readState(reader, "prediction gate state nonce", "prediction gate state commitment");
|
|
236
|
+
const receipt = readOption(reader, "prediction gate receipt option", () => readReceipt(reader));
|
|
237
|
+
const originalDeadlineMs = reader.readU64("original participant deadline milliseconds");
|
|
238
|
+
const preparedAtMs = reader.readU64("prediction gate prepared-at milliseconds");
|
|
239
|
+
return bindPredictionGatePreparation({
|
|
240
|
+
window,
|
|
241
|
+
actingSeat,
|
|
242
|
+
state,
|
|
243
|
+
receipt,
|
|
244
|
+
originalDeadlineMs,
|
|
245
|
+
preparedAtMs,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/** The structural refusals of `PredictionGatePreparation::new`, made wherever
|
|
249
|
+
* a preparation enters this process - the wire, or a retained seat state. */
|
|
250
|
+
export function bindPredictionGatePreparation(fields) {
|
|
251
|
+
if (fields.window.windowId === 0n || fields.window.marketId === 0n)
|
|
252
|
+
throw new Error("prediction window names neither a window nor a market");
|
|
253
|
+
if (fields.originalDeadlineMs === 0n)
|
|
254
|
+
throw new Error("prediction gate preparation has no original deadline");
|
|
255
|
+
if (fields.preparedAtMs === 0n)
|
|
256
|
+
throw new Error("prediction gate preparation has no persisted instant");
|
|
257
|
+
if (fields.receipt &&
|
|
258
|
+
!equalStateRef(fields.receipt.resultingState, fields.state))
|
|
259
|
+
throw new Error("prediction gate preparation receipt does not result in its target state");
|
|
260
|
+
return fields;
|
|
261
|
+
}
|
|
262
|
+
function readPredictionGateOpening(reader) {
|
|
263
|
+
const preparation = readPredictionGatePreparation(reader);
|
|
264
|
+
const openedAtMs = reader.readU64("prediction gate opened-at milliseconds");
|
|
265
|
+
const closesAtMs = reader.readU64("prediction gate closes-at milliseconds");
|
|
266
|
+
return bindPredictionGateOpening({ preparation, openedAtMs, closesAtMs });
|
|
267
|
+
}
|
|
268
|
+
/** The structural refusals of `PredictionGateOpening::new`. */
|
|
269
|
+
export function bindPredictionGateOpening(fields) {
|
|
270
|
+
bindPredictionGatePreparation(fields.preparation);
|
|
271
|
+
if (fields.openedAtMs === 0n)
|
|
272
|
+
throw new Error("prediction gate opening has no publication instant");
|
|
273
|
+
if (fields.openedAtMs < fields.preparation.preparedAtMs)
|
|
274
|
+
throw new Error("prediction gate opened before it was prepared");
|
|
275
|
+
if (fields.closesAtMs <= fields.openedAtMs)
|
|
276
|
+
throw new Error("prediction gate closes at or before it opened");
|
|
277
|
+
return fields;
|
|
278
|
+
}
|
|
279
|
+
function readPredictionGateRelease(reader) {
|
|
280
|
+
const window = readPredictionWindow(reader);
|
|
281
|
+
const actingSeat = reader.readU16("prediction gate acting seat");
|
|
282
|
+
const state = readState(reader, "prediction gate state nonce", "prediction gate state commitment");
|
|
283
|
+
const receipt = readOption(reader, "prediction gate receipt option", () => readReceipt(reader));
|
|
284
|
+
const terminalTag = reader.readByte("prediction gate terminal");
|
|
285
|
+
if (terminalTag !== 1 && terminalTag !== 2)
|
|
286
|
+
throw new Error(`invalid prediction gate terminal tag ${terminalTag}`);
|
|
287
|
+
const originalDeadlineMs = reader.readU64("original participant deadline milliseconds");
|
|
288
|
+
const arrivalMs = reader.readU64("prediction gate arrival milliseconds");
|
|
289
|
+
const lockedAtMs = reader.readU64("prediction gate locked-at milliseconds");
|
|
290
|
+
const budgetMs = reader.readU64("participant deadline budget milliseconds");
|
|
291
|
+
return bindPredictionGateRelease({
|
|
292
|
+
window,
|
|
293
|
+
actingSeat,
|
|
294
|
+
state,
|
|
295
|
+
receipt,
|
|
296
|
+
terminal: terminalTag === 1 ? "locked" : "cancelled",
|
|
297
|
+
originalDeadlineMs,
|
|
298
|
+
arrivalMs,
|
|
299
|
+
lockedAtMs,
|
|
300
|
+
budgetMs,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** The structural refusals of `PredictionGateRelease::new`, with the bounded
|
|
304
|
+
* overlay derived here rather than trusted from a sender or a stored file:
|
|
305
|
+
* the lock instant capped at the pacing ceiling, plus the budget. */
|
|
306
|
+
export function bindPredictionGateRelease(fields) {
|
|
307
|
+
if (fields.window.windowId === 0n || fields.window.marketId === 0n)
|
|
308
|
+
throw new Error("prediction window names neither a window nor a market");
|
|
309
|
+
if (fields.arrivalMs === 0n)
|
|
310
|
+
throw new Error("prediction gate release has no arrival instant");
|
|
311
|
+
if (fields.lockedAtMs < fields.arrivalMs)
|
|
312
|
+
throw new Error("prediction gate closed before the overlay arrived");
|
|
313
|
+
if (fields.budgetMs === 0n)
|
|
314
|
+
throw new Error("prediction gate release has no deadline budget");
|
|
315
|
+
if (fields.receipt &&
|
|
316
|
+
!equalStateRef(fields.receipt.resultingState, fields.state))
|
|
317
|
+
throw new Error("prediction gate release receipt does not result in its target state");
|
|
318
|
+
const uncapped = fields.arrivalMs + MAX_PREDICTION_PACING_MS;
|
|
319
|
+
const ceiling = uncapped > U64_MAX ? U64_MAX : uncapped;
|
|
320
|
+
const lockCappedMs = fields.lockedAtMs < ceiling ? fields.lockedAtMs : ceiling;
|
|
321
|
+
const participantDeadlineMs = lockCappedMs + fields.budgetMs;
|
|
322
|
+
if (participantDeadlineMs > U64_MAX)
|
|
323
|
+
throw new Error("prediction gate overlay deadline exceeds u64");
|
|
324
|
+
return { ...fields, lockCappedMs, participantDeadlineMs };
|
|
325
|
+
}
|
|
158
326
|
function readPolicy(reader) {
|
|
159
327
|
return {
|
|
160
328
|
maxParticipantViewBytes: reader.readU64("maximum participant view bytes"),
|
|
@@ -183,12 +351,28 @@ function skipAuthorization(reader) {
|
|
|
183
351
|
throw new Error(`invalid authorization principal tag ${principalTag}`);
|
|
184
352
|
}
|
|
185
353
|
}
|
|
354
|
+
/* `ActionRejection::retry_disposition` by wire tag: every rejection has one
|
|
355
|
+
canonical answer, and `validate_authority_event` refuses a frame that names
|
|
356
|
+
another. Without this a "retry later" byte on an unauthorised key reads as
|
|
357
|
+
a transient failure and a seat resubmits into a permanent refusal. */
|
|
358
|
+
const REJECTION_RETRY = [
|
|
359
|
+
/* ProtocolRejected */ 0, /* StaleState */ 2, /* UnauthorizedSeatOrKey */ 0,
|
|
360
|
+
/* DeadlineExpired */ 2, /* ActionIdentityConflict */ 0,
|
|
361
|
+
/* ConflictingAlreadyCommitted */ 2, /* UnsupportedSchema */ 0,
|
|
362
|
+
/* MalformedState */ 0, /* MalformedInput */ 0, /* WrongOrigin */ 0,
|
|
363
|
+
/* WrongActor */ 0, /* TerminalState */ 0, /* DeadlineMismatch */ 2,
|
|
364
|
+
/* InvalidTimeEvidence */ 0,
|
|
365
|
+
];
|
|
366
|
+
/** The rejection and the retry disposition that must accompany it. */
|
|
186
367
|
function readActionRejection(reader) {
|
|
187
368
|
const tag = reader.readByte("action rejection tag");
|
|
369
|
+
if (tag >= REJECTION_RETRY.length)
|
|
370
|
+
throw new Error(`invalid action rejection tag ${tag}`);
|
|
188
371
|
if (tag === 0 || tag === 6)
|
|
189
372
|
reader.readU16("action rejection extra");
|
|
190
|
-
|
|
191
|
-
|
|
373
|
+
const retry = reader.readByte("retry disposition");
|
|
374
|
+
if (retry !== REJECTION_RETRY[tag])
|
|
375
|
+
throw new Error(`action rejection ${tag} names retry disposition ${retry}, not ${REJECTION_RETRY[tag]}`);
|
|
192
376
|
}
|
|
193
377
|
function readSeatAuthChallenge(reader, wireVersion) {
|
|
194
378
|
const context = readContext(reader, wireVersion);
|
|
@@ -209,9 +393,20 @@ function readSeatAuthChallenge(reader, wireVersion) {
|
|
|
209
393
|
export function decodeAuthorityMessage(bytes) {
|
|
210
394
|
const reader = new ByteReader(bytes);
|
|
211
395
|
const wireVersion = reader.readU16("wire version");
|
|
396
|
+
/* The envelope names the wire contract it was framed under, and this build
|
|
397
|
+
speaks exactly one. Rust refuses any other value at the same point. */
|
|
398
|
+
if (wireVersion !== CANONICAL_WIRE_VERSION)
|
|
399
|
+
throw new Error(`unsupported wire version ${wireVersion}, expected ${CANONICAL_WIRE_VERSION}`);
|
|
212
400
|
const tag = reader.readByte("message tag");
|
|
213
401
|
if (tag === SESSION_ERROR_TAG) {
|
|
214
|
-
|
|
402
|
+
// A present selected version is the authority's answer for this session,
|
|
403
|
+
// so it has to be one this build can speak; the unsupported-version
|
|
404
|
+
// payload read below is a raw diagnostic and stays readable without one.
|
|
405
|
+
readOption(reader, "session version option", () => {
|
|
406
|
+
const version = reader.readU16("session version");
|
|
407
|
+
requireSessionVersion(version);
|
|
408
|
+
return version;
|
|
409
|
+
});
|
|
215
410
|
const errorTag = reader.readByte("session protocol error tag");
|
|
216
411
|
if (errorTag === 0 || errorTag === 1)
|
|
217
412
|
reader.readU16("unsupported version");
|
|
@@ -237,8 +432,12 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
237
432
|
switch (tag) {
|
|
238
433
|
case SESSION_JOINED_TAG: {
|
|
239
434
|
const policy = readPolicy(reader);
|
|
240
|
-
|
|
435
|
+
/* Option-tagged: a seat held behind a committed gate is admitted with
|
|
436
|
+
no private view, and the tag is the only thing that says so. */
|
|
437
|
+
const view = readOption(reader, "joined view option", () => readView(reader));
|
|
241
438
|
const cursor = readCursor(reader);
|
|
439
|
+
if (view)
|
|
440
|
+
requireViewCursor(view, cursor);
|
|
242
441
|
message = {
|
|
243
442
|
type: "sessionJoined",
|
|
244
443
|
context,
|
|
@@ -252,6 +451,7 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
252
451
|
case PARTICIPANT_VIEW_TAG: {
|
|
253
452
|
const view = readView(reader);
|
|
254
453
|
const cursor = readCursor(reader);
|
|
454
|
+
requireViewCursor(view, cursor);
|
|
255
455
|
message = { type: "participantView", context, sequence, view, cursor };
|
|
256
456
|
break;
|
|
257
457
|
}
|
|
@@ -271,9 +471,9 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
271
471
|
case ACTION_REJECTED_TAG: {
|
|
272
472
|
const actionId = reader.readFixed(32, "action id");
|
|
273
473
|
readActionRejection(reader);
|
|
274
|
-
reader.readByte("retry disposition");
|
|
275
474
|
const view = readView(reader);
|
|
276
475
|
const cursor = readCursor(reader);
|
|
476
|
+
requireViewCursor(view, cursor);
|
|
277
477
|
message = {
|
|
278
478
|
type: "actionRejected",
|
|
279
479
|
context,
|
|
@@ -289,6 +489,11 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
289
489
|
const receipt = readReceipt(reader);
|
|
290
490
|
const view = readView(reader);
|
|
291
491
|
const cursor = readCursor(reader);
|
|
492
|
+
/* A commit is the receipt its own view is on: the pair is one committed
|
|
493
|
+
fact, not two the recipient has to reconcile. */
|
|
494
|
+
if (!equalReceiptRef(view.latestReceipt, receipt))
|
|
495
|
+
throw new Error("the commit receipt is not the receipt its view is on");
|
|
496
|
+
requireViewCursor(view, cursor);
|
|
292
497
|
message = {
|
|
293
498
|
type: "actionCommitted",
|
|
294
499
|
context,
|
|
@@ -301,10 +506,19 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
301
506
|
break;
|
|
302
507
|
}
|
|
303
508
|
case SESSION_RESUMED_TAG: {
|
|
304
|
-
reader.readU64("replay from sequence");
|
|
305
|
-
const view = readView(reader);
|
|
509
|
+
const replayFrom = reader.readU64("replay from sequence");
|
|
510
|
+
const view = readOption(reader, "resumed view option", () => readView(reader));
|
|
306
511
|
const cursor = readCursor(reader);
|
|
307
|
-
|
|
512
|
+
if (view)
|
|
513
|
+
requireViewCursor(view, cursor);
|
|
514
|
+
message = {
|
|
515
|
+
type: "sessionResumed",
|
|
516
|
+
context,
|
|
517
|
+
sequence,
|
|
518
|
+
replayFrom,
|
|
519
|
+
view,
|
|
520
|
+
cursor,
|
|
521
|
+
};
|
|
308
522
|
break;
|
|
309
523
|
}
|
|
310
524
|
case SESSION_TERMINAL_TAG: {
|
|
@@ -312,6 +526,11 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
312
526
|
const finalState = readState(reader, "final state nonce", "final state commitment");
|
|
313
527
|
const finalReceipt = readOption(reader, "final receipt option", () => readReceipt(reader));
|
|
314
528
|
const cursor = readCursor(reader);
|
|
529
|
+
if (finalReceipt &&
|
|
530
|
+
!equalStateRef(finalReceipt.resultingState, finalState))
|
|
531
|
+
throw new Error("the terminal receipt does not result in its final state");
|
|
532
|
+
if (!equalReceiptRef(cursor.witnessedReceipt, finalReceipt))
|
|
533
|
+
throw new Error("the terminal cursor does not name the final receipt");
|
|
315
534
|
message = {
|
|
316
535
|
type: "sessionTerminal",
|
|
317
536
|
context,
|
|
@@ -323,6 +542,42 @@ export function decodeAuthorityMessage(bytes) {
|
|
|
323
542
|
};
|
|
324
543
|
break;
|
|
325
544
|
}
|
|
545
|
+
case PREDICTION_GATE_PREPARED_TAG: {
|
|
546
|
+
const preparation = readPredictionGatePreparation(reader);
|
|
547
|
+
const cursor = readCursor(reader);
|
|
548
|
+
message = {
|
|
549
|
+
type: "predictionGatePrepared",
|
|
550
|
+
context,
|
|
551
|
+
sequence,
|
|
552
|
+
preparation,
|
|
553
|
+
cursor,
|
|
554
|
+
};
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
case PREDICTION_GATE_OPENED_TAG: {
|
|
558
|
+
const opening = readPredictionGateOpening(reader);
|
|
559
|
+
const cursor = readCursor(reader);
|
|
560
|
+
message = {
|
|
561
|
+
type: "predictionGateOpened",
|
|
562
|
+
context,
|
|
563
|
+
sequence,
|
|
564
|
+
opening,
|
|
565
|
+
cursor,
|
|
566
|
+
};
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
case PREDICTION_GATE_RELEASED_TAG: {
|
|
570
|
+
const release = readPredictionGateRelease(reader);
|
|
571
|
+
const cursor = readCursor(reader);
|
|
572
|
+
message = {
|
|
573
|
+
type: "predictionGateReleased",
|
|
574
|
+
context,
|
|
575
|
+
sequence,
|
|
576
|
+
release,
|
|
577
|
+
cursor,
|
|
578
|
+
};
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
326
581
|
default:
|
|
327
582
|
throw new Error(`unknown authority message tag ${tag}`);
|
|
328
583
|
}
|
|
@@ -371,3 +626,402 @@ export function takePrincipalProof(bytes) {
|
|
|
371
626
|
rest: bytes.slice(reader.consumed()),
|
|
372
627
|
};
|
|
373
628
|
}
|
|
629
|
+
/* ── The named gate lifecycle, as a recipient folds it ───────────────────
|
|
630
|
+
*
|
|
631
|
+
* One fold, shared by both consumer loops. The rules are
|
|
632
|
+
* `arena_session::semantics::{validate_gate_preparation, validate_gate_opening,
|
|
633
|
+
* validate_gate_release}`: a preparation opens a revision's gate and never
|
|
634
|
+
* replaces a retained one, an opening must carry the exact stored preparation,
|
|
635
|
+
* and a release closes a prepared gate only by cancelling it. Writing these
|
|
636
|
+
* twice - once in `playSeat` and once in `openTurn` - is how the two doors
|
|
637
|
+
* would come to disagree about which phase a seat is in. */
|
|
638
|
+
/** A refused phase transition, named as the shared crate names it. */
|
|
639
|
+
export class PredictionGateConflictError extends Error {
|
|
640
|
+
conflict;
|
|
641
|
+
constructor(conflict, detail) {
|
|
642
|
+
super(`${conflict}: ${detail}`);
|
|
643
|
+
this.conflict = conflict;
|
|
644
|
+
this.name = "PredictionGateConflictError";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/** The lifecycle step an authority message carries, or null where it carries
|
|
648
|
+
* none. The fold takes the value, not the envelope. */
|
|
649
|
+
export function predictionGateMessage(message) {
|
|
650
|
+
switch (message.type) {
|
|
651
|
+
case "predictionGatePrepared":
|
|
652
|
+
return { kind: "prepared", preparation: message.preparation };
|
|
653
|
+
case "predictionGateOpened":
|
|
654
|
+
return { kind: "opened", opening: message.opening };
|
|
655
|
+
case "predictionGateReleased":
|
|
656
|
+
return { kind: "released", release: message.release };
|
|
657
|
+
default:
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/** The committed preparation behind any phase, or null once released - a
|
|
662
|
+
* release carries the identity but not the persisted preparation instant. */
|
|
663
|
+
export function predictionGatePreparationOf(status) {
|
|
664
|
+
switch (status.phase) {
|
|
665
|
+
case "prepared":
|
|
666
|
+
return status.preparation;
|
|
667
|
+
case "open":
|
|
668
|
+
return status.opening.preparation;
|
|
669
|
+
case "released":
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/** The receipt the gate targets. A recipient behind it still receives every
|
|
674
|
+
* notice, because each notice cursor names that recipient's own floor. */
|
|
675
|
+
export function predictionGateTargetReceipt(status) {
|
|
676
|
+
switch (status.phase) {
|
|
677
|
+
case "prepared":
|
|
678
|
+
return status.preparation.receipt;
|
|
679
|
+
case "open":
|
|
680
|
+
return status.opening.preparation.receipt;
|
|
681
|
+
case "released":
|
|
682
|
+
return status.release.receipt;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
/** Whether a newly named preparation supersedes the retained facts.
|
|
686
|
+
*
|
|
687
|
+
* Only a released gate can be superseded: a held gate's turn has not closed,
|
|
688
|
+
* so a second preparation contradicts it. Once released, the next selected
|
|
689
|
+
* turn always targets a later committed state and therefore its own receipt.
|
|
690
|
+
* A seat that is itself withheld for that next turn never witnesses the view
|
|
691
|
+
* that would otherwise retire the closed gate - which is exactly the second
|
|
692
|
+
* window of a hand, held on the seat that was a bystander for the first. A
|
|
693
|
+
* preparation naming the same revision is the same-target conflict, not a
|
|
694
|
+
* supersession. */
|
|
695
|
+
export function predictionGateSupersededByPreparation(status, nextTurn) {
|
|
696
|
+
if (status.phase !== "released")
|
|
697
|
+
return false;
|
|
698
|
+
return (nextTurn.state.nonce > status.release.state.nonce &&
|
|
699
|
+
!equalReceiptRef(nextTurn.receipt, status.release.receipt));
|
|
700
|
+
}
|
|
701
|
+
export function applyPredictionGateStatus(current, message) {
|
|
702
|
+
switch (message.kind) {
|
|
703
|
+
case "prepared":
|
|
704
|
+
if (current !== null &&
|
|
705
|
+
!predictionGateSupersededByPreparation(current, message.preparation))
|
|
706
|
+
throw new PredictionGateConflictError("PredictionGatePhaseConflict", "a preparation arrived while this revision still retains a gate");
|
|
707
|
+
return { phase: "prepared", preparation: message.preparation };
|
|
708
|
+
case "opened":
|
|
709
|
+
if (current?.phase !== "prepared" ||
|
|
710
|
+
!equalPredictionGatePreparation(current.preparation, message.opening.preparation))
|
|
711
|
+
throw new PredictionGateConflictError("PredictionGatePhaseConflict", "an opening must carry the exact durably prepared window");
|
|
712
|
+
return { phase: "open", opening: message.opening };
|
|
713
|
+
case "released": {
|
|
714
|
+
const release = message.release;
|
|
715
|
+
if (current === null)
|
|
716
|
+
throw new PredictionGateConflictError("PredictionGatePhaseConflict", "a release closes no retained gate");
|
|
717
|
+
if (current.phase === "released") {
|
|
718
|
+
/* A replayed release is the same overlay again; a different one would
|
|
719
|
+
be a second budget for one revision. */
|
|
720
|
+
if (!equalPredictionGateRelease(current.release, release))
|
|
721
|
+
throw new PredictionGateConflictError("PredictionGateReleaseConflict", "a second release for one revision");
|
|
722
|
+
return current;
|
|
723
|
+
}
|
|
724
|
+
const preparation = current.phase === "prepared"
|
|
725
|
+
? current.preparation
|
|
726
|
+
: current.opening.preparation;
|
|
727
|
+
const identical = release.window.windowId === preparation.window.windowId &&
|
|
728
|
+
release.window.marketId === preparation.window.marketId &&
|
|
729
|
+
release.window.contract === preparation.window.contract &&
|
|
730
|
+
release.actingSeat === preparation.actingSeat &&
|
|
731
|
+
equalStateRef(release.state, preparation.state) &&
|
|
732
|
+
equalReceiptRef(release.receipt, preparation.receipt) &&
|
|
733
|
+
release.originalDeadlineMs === preparation.originalDeadlineMs;
|
|
734
|
+
if (!identical)
|
|
735
|
+
throw new PredictionGateConflictError("PredictionGatePhaseConflict", "the release names another window than the retained gate");
|
|
736
|
+
if (current.phase === "prepared" && release.terminal !== "cancelled")
|
|
737
|
+
throw new PredictionGateConflictError("PredictionGatePhaseConflict", "a gate that never opened can only close by cancellation");
|
|
738
|
+
return { phase: "released", release };
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/** A refused event, named as `SessionSemanticError` names it. */
|
|
743
|
+
export class SessionBoundaryError extends Error {
|
|
744
|
+
reason;
|
|
745
|
+
constructor(reason, detail) {
|
|
746
|
+
super(`${reason}: ${detail}`);
|
|
747
|
+
this.reason = reason;
|
|
748
|
+
this.name = "SessionBoundaryError";
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** The boundary a seat that has accepted nothing holds. */
|
|
752
|
+
export function freshSessionBoundary() {
|
|
753
|
+
return {
|
|
754
|
+
context: null,
|
|
755
|
+
sequence: 0n,
|
|
756
|
+
receiptFloor: null,
|
|
757
|
+
view: null,
|
|
758
|
+
predictionGate: null,
|
|
759
|
+
awaitingGatePrefix: false,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function equalContext(left, right) {
|
|
763
|
+
return (left.wireVersion === right.wireVersion &&
|
|
764
|
+
left.sessionVersion === right.sessionVersion &&
|
|
765
|
+
equalBytes(left.sessionId, right.sessionId) &&
|
|
766
|
+
equalBytes(left.executionId, right.executionId) &&
|
|
767
|
+
equalBytes(left.executionManifestDigest, right.executionManifestDigest) &&
|
|
768
|
+
equalBytes(left.protocolId, right.protocolId) &&
|
|
769
|
+
left.protocolVersion === right.protocolVersion &&
|
|
770
|
+
equalBytes(left.participantId, right.participantId) &&
|
|
771
|
+
left.seat === right.seat);
|
|
772
|
+
}
|
|
773
|
+
/** While the gate holds this session's acting seat, the withheld private view
|
|
774
|
+
* cannot arrive - whether the event carrying it is new or a re-delivery. A
|
|
775
|
+
* bystander's own view of the same receipt is ordinary, so only the acting
|
|
776
|
+
* seat is refused. */
|
|
777
|
+
function refuseHeldActingView(accepted, view) {
|
|
778
|
+
const gate = accepted.predictionGate;
|
|
779
|
+
if (gate === null || gate.phase === "released")
|
|
780
|
+
return;
|
|
781
|
+
if (!equalReceiptRef(predictionGateTargetReceipt(gate), view.latestReceipt))
|
|
782
|
+
return;
|
|
783
|
+
if (predictionGatePreparationOf(gate)?.actingSeat !== accepted.context?.seat)
|
|
784
|
+
return;
|
|
785
|
+
throw new SessionBoundaryError("PredictionGatePhaseConflict", "a held acting seat cannot be shown the gated view");
|
|
786
|
+
}
|
|
787
|
+
/** A view this seat may be shown, and what it does to the retained gate.
|
|
788
|
+
*
|
|
789
|
+
* The refusals are the two a held seat depends on: the withheld private view
|
|
790
|
+
* cannot arrive while this seat's own turn is gated, and a later deadline on
|
|
791
|
+
* an unchanged receipt is legal only under the admitted overlay. */
|
|
792
|
+
function admitView(accepted, view) {
|
|
793
|
+
const gate = accepted.predictionGate;
|
|
794
|
+
refuseHeldActingView(accepted, view);
|
|
795
|
+
if (accepted.view &&
|
|
796
|
+
equalReceiptRef(view.latestReceipt, accepted.receiptFloor) &&
|
|
797
|
+
view.participantDeadlineMs !== accepted.view.participantDeadlineMs) {
|
|
798
|
+
const release = gate?.phase === "released" ? gate.release : null;
|
|
799
|
+
const admitted = release !== null &&
|
|
800
|
+
equalReceiptRef(release.receipt, view.latestReceipt) &&
|
|
801
|
+
equalStateRef(release.state, view.state) &&
|
|
802
|
+
release.participantDeadlineMs === view.participantDeadlineMs &&
|
|
803
|
+
release.originalDeadlineMs === accepted.view.participantDeadlineMs;
|
|
804
|
+
if (!admitted)
|
|
805
|
+
throw new SessionBoundaryError("DeadlineChangesWithoutTransition", `${accepted.view.participantDeadlineMs} to ${view.participantDeadlineMs} with no admitted overlay`);
|
|
806
|
+
}
|
|
807
|
+
if (!view.latestReceipt)
|
|
808
|
+
return { receiptFloor: accepted.receiptFloor, predictionGate: gate };
|
|
809
|
+
/* A view on a later receipt than the gate targeted leaves the gate facts
|
|
810
|
+
obsolete; one on the gated receipt keeps the release identity, so a second
|
|
811
|
+
release for the same revision is still refused. */
|
|
812
|
+
const superseded = !equalReceiptRef(accepted.receiptFloor, view.latestReceipt) &&
|
|
813
|
+
gate !== null &&
|
|
814
|
+
!equalReceiptRef(predictionGateTargetReceipt(gate), view.latestReceipt);
|
|
815
|
+
return {
|
|
816
|
+
receiptFloor: view.latestReceipt,
|
|
817
|
+
predictionGate: superseded ? null : gate,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/** Where a preparation may target: the receipt this recipient has witnessed,
|
|
821
|
+
* or the immediate next committed one for an actor still behind it. */
|
|
822
|
+
function admitGatePreparation(accepted, preparation) {
|
|
823
|
+
const view = accepted.view;
|
|
824
|
+
if (view) {
|
|
825
|
+
if (equalReceiptRef(preparation.receipt, view.latestReceipt)) {
|
|
826
|
+
if (!equalStateRef(view.state, preparation.state))
|
|
827
|
+
throw new SessionBoundaryError("CommittedStateWithoutReceipt", "the preparation targets another state on the witnessed receipt");
|
|
828
|
+
if (view.participantDeadlineMs !== preparation.originalDeadlineMs)
|
|
829
|
+
throw new SessionBoundaryError("DeadlineChangesWithoutTransition", "the preparation names another original deadline than the open turn");
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (preparation.state.nonce !== view.state.nonce + 1n)
|
|
833
|
+
throw new SessionBoundaryError("NonSequentialCommittedState", `the preparation targets nonce ${preparation.state.nonce} after ${view.state.nonce}`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (equalReceiptRef(preparation.receipt, accepted.receiptFloor))
|
|
837
|
+
return;
|
|
838
|
+
const floorNonce = accepted.receiptFloor?.resultingState.nonce;
|
|
839
|
+
if (floorNonce !== undefined && floorNonce + 1n === preparation.state.nonce)
|
|
840
|
+
return;
|
|
841
|
+
throw new SessionBoundaryError("PredictionGatePhaseConflict", "the preparation targets neither the retained floor nor the receipt after it");
|
|
842
|
+
}
|
|
843
|
+
/** A re-delivered event moves nothing, but it is not unexamined.
|
|
844
|
+
*
|
|
845
|
+
* `ParticipantSessionState::apply_event` compares a re-delivery against the
|
|
846
|
+
* event it recorded at that sequence and refuses any disagreement. This
|
|
847
|
+
* boundary keeps facts rather than the event log, so the comparison is
|
|
848
|
+
* against those facts: the session it names, the receipt floor it claims,
|
|
849
|
+
* and the withheld view it must not carry. Skipping the comparison is how a
|
|
850
|
+
* foreign-context event or the gated view of a held seat used to arrive
|
|
851
|
+
* unchecked at a consumer that then acted on it. */
|
|
852
|
+
function admitReplay(accepted, message) {
|
|
853
|
+
const conflict = (detail) => {
|
|
854
|
+
throw new SessionBoundaryError("SequenceConflict", `${detail} at re-delivered event ${message.sequence}`);
|
|
855
|
+
};
|
|
856
|
+
if (message.type === "sessionJoined" && message.sequence !== 1n)
|
|
857
|
+
conflict("a join outside sequence 1");
|
|
858
|
+
/* Nothing follows a terminal, so the only terminal a boundary can be handed
|
|
859
|
+
again is its own last event - the one a resume at the terminal cursor
|
|
860
|
+
re-delivers. An older sequence claiming to be terminal contradicts every
|
|
861
|
+
event this seat applied after it. */
|
|
862
|
+
if (message.type === "sessionTerminal" &&
|
|
863
|
+
message.sequence !== accepted.sequence)
|
|
864
|
+
conflict("a terminal before the boundary's own last event");
|
|
865
|
+
const view = "view" in message ? message.view : null;
|
|
866
|
+
if (view)
|
|
867
|
+
refuseHeldActingView(accepted, view);
|
|
868
|
+
const named = message.cursor.witnessedReceipt;
|
|
869
|
+
if (!named)
|
|
870
|
+
return;
|
|
871
|
+
const floor = accepted.receiptFloor;
|
|
872
|
+
/* History cannot name a receipt this seat has not witnessed: every event at
|
|
873
|
+
or below the boundary projected a floor the boundary already passed. */
|
|
874
|
+
if (!floor)
|
|
875
|
+
conflict("a receipt where the boundary has witnessed none");
|
|
876
|
+
else if (named.resultingState.nonce > floor.resultingState.nonce)
|
|
877
|
+
conflict(`receipt nonce ${named.resultingState.nonce} past the floor`);
|
|
878
|
+
else if (named.resultingState.nonce === floor.resultingState.nonce &&
|
|
879
|
+
!equalReceiptRef(named, floor))
|
|
880
|
+
conflict("another receipt for the floor's own state");
|
|
881
|
+
}
|
|
882
|
+
export function admitAuthorityEvent(accepted, message) {
|
|
883
|
+
/* Acknowledgements, pendings, errors and challenges carry no cursor: they
|
|
884
|
+
move no boundary and are not acknowledged. */
|
|
885
|
+
if (!("cursor" in message))
|
|
886
|
+
return { disposition: "boundaryless", boundary: accepted };
|
|
887
|
+
const { context, sequence, cursor } = message;
|
|
888
|
+
if (cursor.sequence !== sequence)
|
|
889
|
+
throw new SessionBoundaryError("EventCursorMismatch", `event ${sequence} carries cursor ${cursor.sequence}`);
|
|
890
|
+
/* A resume names the boundary it replayed from, which must precede it. */
|
|
891
|
+
if (message.type === "sessionResumed" &&
|
|
892
|
+
message.replayFrom >= message.sequence)
|
|
893
|
+
throw new SessionBoundaryError("ReplayFromAhead", `the resume replayed from ${message.replayFrom} at event ${message.sequence}`);
|
|
894
|
+
/* A join is an admission only against a boundary that has applied nothing.
|
|
895
|
+
A consumer that joins again hands this seam a fresh boundary, because a
|
|
896
|
+
join is a new session and nothing retained for the old one describes it;
|
|
897
|
+
a `SessionJoined` arriving on an established boundary is therefore never
|
|
898
|
+
an admission, and cannot be used to reset the phase a held seat holds or
|
|
899
|
+
the floor it witnessed. */
|
|
900
|
+
const admission = message.type === "sessionJoined" && accepted.sequence === 0n;
|
|
901
|
+
if (admission) {
|
|
902
|
+
if (sequence !== 1n)
|
|
903
|
+
throw new SessionBoundaryError("ExpectedSequenceOne", `the join opens the session at sequence 1, not ${sequence}`);
|
|
904
|
+
/* A consumer that has already been handed a context - by the very join
|
|
905
|
+
this event reports - is admitted into that session and no other. */
|
|
906
|
+
if (accepted.context && !equalContext(accepted.context, context))
|
|
907
|
+
throw new SessionBoundaryError("ContextMismatch", "the join names another session than the one this boundary opened");
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
/* Before anything is applied the protocol admits exactly one shape: the
|
|
911
|
+
join at sequence 1. Anything else would process a view or a notice
|
|
912
|
+
against no session at all. */
|
|
913
|
+
if (accepted.sequence === 0n || !accepted.context)
|
|
914
|
+
throw new SessionBoundaryError("ExpectedJoin", `a session opens with its own join, not ${message.type}`);
|
|
915
|
+
if (!equalContext(accepted.context, context))
|
|
916
|
+
throw new SessionBoundaryError("ContextMismatch", "the event names another session than the accepted one");
|
|
917
|
+
if (sequence <= accepted.sequence) {
|
|
918
|
+
admitReplay(accepted, message);
|
|
919
|
+
return { disposition: "replay", boundary: accepted };
|
|
920
|
+
}
|
|
921
|
+
if (sequence !== accepted.sequence + 1n)
|
|
922
|
+
throw new SessionBoundaryError("EventGap", `expected ${accepted.sequence + 1n}, got ${sequence}`);
|
|
923
|
+
if (message.type === "sessionJoined")
|
|
924
|
+
throw new SessionBoundaryError("UnexpectedJoin", "an established session cannot be joined again");
|
|
925
|
+
}
|
|
926
|
+
/* A fresh join is a new session: nothing retained for the previous one
|
|
927
|
+
describes it, so obsolete context state is dropped rather than carried. */
|
|
928
|
+
const base = admission
|
|
929
|
+
? { ...freshSessionBoundary(), context }
|
|
930
|
+
: { ...accepted, context };
|
|
931
|
+
const gateMessage = predictionGateMessage(message);
|
|
932
|
+
const viewlessResume = message.type === "sessionResumed" && message.view === null;
|
|
933
|
+
if (base.awaitingGatePrefix &&
|
|
934
|
+
gateMessage?.kind !== "prepared" &&
|
|
935
|
+
!viewlessResume)
|
|
936
|
+
/* The primed prefix: after a viewless admission the next durable event is
|
|
937
|
+
the matching preparation. A view, an opening or a release before it
|
|
938
|
+
would either disclose the withheld view or skip a held phase. */
|
|
939
|
+
throw new SessionBoundaryError("PredictionGatePhaseConflict", "the named prepared prefix must arrive before anything else");
|
|
940
|
+
let receiptFloor = base.receiptFloor;
|
|
941
|
+
let view = base.view;
|
|
942
|
+
let predictionGate = base.predictionGate;
|
|
943
|
+
let awaitingGatePrefix = base.awaitingGatePrefix;
|
|
944
|
+
let projected;
|
|
945
|
+
switch (message.type) {
|
|
946
|
+
case "sessionJoined":
|
|
947
|
+
case "sessionResumed":
|
|
948
|
+
if (message.view) {
|
|
949
|
+
({ receiptFloor, predictionGate } = admitView(base, message.view));
|
|
950
|
+
view = message.view;
|
|
951
|
+
projected = message.view.latestReceipt;
|
|
952
|
+
}
|
|
953
|
+
else if (message.type === "sessionJoined") {
|
|
954
|
+
/* A held seat's admission cursor names the receipt floor; only a later
|
|
955
|
+
real view may advance it. */
|
|
956
|
+
receiptFloor = cursor.witnessedReceipt;
|
|
957
|
+
view = null;
|
|
958
|
+
awaitingGatePrefix = true;
|
|
959
|
+
projected = cursor.witnessedReceipt;
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
/* A viewless reattach repeats the retained floor and still owes the
|
|
963
|
+
prefix it has not been shown. */
|
|
964
|
+
awaitingGatePrefix = awaitingGatePrefix || predictionGate === null;
|
|
965
|
+
projected = receiptFloor;
|
|
966
|
+
}
|
|
967
|
+
break;
|
|
968
|
+
case "participantView":
|
|
969
|
+
({ receiptFloor, predictionGate } = admitView(base, message.view));
|
|
970
|
+
view = message.view;
|
|
971
|
+
projected = message.view.latestReceipt;
|
|
972
|
+
break;
|
|
973
|
+
case "actionRejected":
|
|
974
|
+
if (!equalReceiptRef(message.view.latestReceipt, receiptFloor))
|
|
975
|
+
throw new SessionBoundaryError("RejectedEventAdvancesReceipt", "a rejection cannot advance the receipt it was rejected on");
|
|
976
|
+
({ receiptFloor, predictionGate } = admitView(base, message.view));
|
|
977
|
+
view = message.view;
|
|
978
|
+
projected = receiptFloor;
|
|
979
|
+
break;
|
|
980
|
+
case "actionCommitted":
|
|
981
|
+
if (equalReceiptRef(message.receipt, receiptFloor))
|
|
982
|
+
throw new SessionBoundaryError("CommitDoesNotAdvanceReceipt", "a commit must advance the witnessed receipt");
|
|
983
|
+
({ predictionGate } = admitView(base, message.view));
|
|
984
|
+
receiptFloor = message.receipt;
|
|
985
|
+
view = message.view;
|
|
986
|
+
projected = message.receipt;
|
|
987
|
+
break;
|
|
988
|
+
case "sessionTerminal":
|
|
989
|
+
if (message.finalReceipt)
|
|
990
|
+
receiptFloor = message.finalReceipt;
|
|
991
|
+
projected = message.finalReceipt;
|
|
992
|
+
break;
|
|
993
|
+
default: {
|
|
994
|
+
/* The three named notices. They never move the floor, never carry a
|
|
995
|
+
view, and never schedule a decision. */
|
|
996
|
+
const step = gateMessage;
|
|
997
|
+
if (!step)
|
|
998
|
+
throw new SessionBoundaryError("InvalidMessageTag", `${message.type} carries a cursor but no boundary`);
|
|
999
|
+
if (step.kind === "prepared")
|
|
1000
|
+
admitGatePreparation(base, step.preparation);
|
|
1001
|
+
if (step.kind === "released" && view) {
|
|
1002
|
+
const release = step.release;
|
|
1003
|
+
if (equalReceiptRef(release.receipt, view.latestReceipt) &&
|
|
1004
|
+
view.participantDeadlineMs !== release.originalDeadlineMs)
|
|
1005
|
+
throw new SessionBoundaryError("DeadlineChangesWithoutTransition", "the release names another original deadline than the open turn");
|
|
1006
|
+
}
|
|
1007
|
+
predictionGate = applyPredictionGateStatus(predictionGate, step);
|
|
1008
|
+
if (step.kind === "prepared")
|
|
1009
|
+
awaitingGatePrefix = false;
|
|
1010
|
+
projected = receiptFloor;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (!equalReceiptRef(cursor.witnessedReceipt, projected))
|
|
1015
|
+
throw new SessionBoundaryError("ReceiptHeadMismatch", "the cursor does not name the receipt this event projects");
|
|
1016
|
+
return {
|
|
1017
|
+
disposition: "applied",
|
|
1018
|
+
boundary: {
|
|
1019
|
+
context,
|
|
1020
|
+
sequence,
|
|
1021
|
+
receiptFloor,
|
|
1022
|
+
view,
|
|
1023
|
+
predictionGate,
|
|
1024
|
+
awaitingGatePrefix,
|
|
1025
|
+
},
|
|
1026
|
+
};
|
|
1027
|
+
}
|