@novasamatech/statement-store 0.8.12 → 0.9.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/dist/crypto.js +2 -1
- package/dist/helpers.d.ts +0 -1
- package/dist/helpers.js +0 -3
- package/dist/index.d.ts +7 -0
- package/dist/index.js +3 -0
- package/dist/model/session.d.ts +1 -1
- package/dist/model/session.js +1 -1
- package/dist/model/sessionAccount.js +2 -1
- package/dist/session/codec/decoder.d.ts +68 -0
- package/dist/session/codec/decoder.js +96 -0
- package/dist/session/codec/decoder.spec.d.ts +1 -0
- package/dist/session/codec/decoder.spec.js +161 -0
- package/dist/session/codec/envelope.d.ts +55 -0
- package/dist/session/codec/envelope.js +115 -0
- package/dist/session/codec/envelope.spec.d.ts +1 -0
- package/dist/session/codec/envelope.spec.js +114 -0
- package/dist/session/codec/incomingTopics.d.ts +46 -0
- package/dist/session/codec/incomingTopics.js +69 -0
- package/dist/session/codec/outgoingBody.d.ts +46 -0
- package/dist/session/codec/outgoingBody.js +64 -0
- package/dist/session/core.d.ts +58 -0
- package/dist/session/core.js +609 -0
- package/dist/session/encyption.js +7 -7
- package/dist/session/encyption.spec.d.ts +1 -0
- package/dist/session/encyption.spec.js +43 -0
- package/dist/session/messageMapper.d.ts +3 -3
- package/dist/session/messageMapper.js +8 -8
- package/dist/session/multiDeviceSession.d.ts +49 -0
- package/dist/session/multiDeviceSession.js +62 -0
- package/dist/session/multiDeviceSession.spec.d.ts +6 -0
- package/dist/session/multiDeviceSession.spec.js +354 -0
- package/dist/session/scale/statementData.d.ts +40 -0
- package/dist/session/scale/statementData.js +34 -1
- package/dist/session/session.d.ts +15 -5
- package/dist/session/session.js +31 -633
- package/dist/session/session.spec.js +24 -7
- package/dist/session/stateMachine.d.ts +135 -0
- package/dist/session/stateMachine.js +203 -0
- package/dist/session/stateMachine.spec.d.ts +1 -0
- package/dist/session/stateMachine.spec.js +276 -0
- package/package.json +4 -3
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { Bytes } from '@novasamatech/scale';
|
|
1
2
|
import { createExpiryFromDuration } from '@novasamatech/sdk-statement';
|
|
2
3
|
import { ResultAsync, err, errAsync, ok, okAsync } from 'neverthrow';
|
|
3
|
-
import {
|
|
4
|
+
import { Struct, str } from 'scale-ts';
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
6
|
import { createInMemoryStatementStore } from '../adapter/inMemory.js';
|
|
6
7
|
import { AccountFullError, ExpiryTooLowError } from '../adapter/types.js';
|
|
7
8
|
import { createAccountId, createLocalSessionAccount, createRemoteSessionAccount } from '../model/sessionAccount.js';
|
|
9
|
+
import { STATEMENT_OVERHEAD } from './core.js';
|
|
8
10
|
import { DecodingError, UnknownError } from './error.js';
|
|
9
11
|
import { StatementData } from './scale/statementData.js';
|
|
10
|
-
import {
|
|
12
|
+
import { createSession } from './session.js';
|
|
11
13
|
// Real signature work belongs in statementProver tests; this stub stamps a
|
|
12
14
|
// non-empty proof so submitted statements are well-formed.
|
|
13
15
|
const mockProver = {
|
|
@@ -159,11 +161,14 @@ describe('session', () => {
|
|
|
159
161
|
it('queries the outgoing and incoming topics on creation', async () => {
|
|
160
162
|
const { adapter } = makeSession();
|
|
161
163
|
await delay();
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
expect(
|
|
164
|
+
// Outgoing is our single publish topic (matchAll); incoming is matchAny because a
|
|
165
|
+
// multi-device session listens on one topic per peer device. Both carry exactly one
|
|
166
|
+
// topic here, and they must differ.
|
|
167
|
+
const filters = adapter.queryStatements.mock.calls.map(([f]) => f);
|
|
168
|
+
expect(filters).toHaveLength(2);
|
|
169
|
+
expect(filters[0].matchAll).toHaveLength(1);
|
|
170
|
+
expect(filters[1].matchAny).toHaveLength(1);
|
|
171
|
+
expect(filters[0].matchAll).not.toEqual(filters[1].matchAny);
|
|
167
172
|
});
|
|
168
173
|
it('seeds the expiry from the highest own statement expiry', async () => {
|
|
169
174
|
const highExpiry = createExpiryFromDuration(7 * 24 * 60 * 60) + 9999n;
|
|
@@ -532,6 +537,18 @@ describe('session', () => {
|
|
|
532
537
|
// One subscription: the incoming topic carries both peer requests and peer responses.
|
|
533
538
|
expect(store.activeSubscriptions()).toBe(1);
|
|
534
539
|
});
|
|
540
|
+
it('reopens the subscription when a subscriber returns after the last one left', async () => {
|
|
541
|
+
const { subscribeStatements } = capturingSubscribe();
|
|
542
|
+
const { session } = makeSession({ subscribeStatements });
|
|
543
|
+
await delay();
|
|
544
|
+
const unsubscribe = session.subscribe(rawCodec, vi.fn());
|
|
545
|
+
expect(subscribeStatements).toHaveBeenCalledTimes(1);
|
|
546
|
+
unsubscribe();
|
|
547
|
+
// The topic-set watcher and the store subscription must be torn down together —
|
|
548
|
+
// otherwise the session silently never listens again.
|
|
549
|
+
session.subscribe(rawCodec, vi.fn());
|
|
550
|
+
expect(subscribeStatements).toHaveBeenCalledTimes(2);
|
|
551
|
+
});
|
|
535
552
|
it('tears down the subscription when the last subscriber leaves', () => {
|
|
536
553
|
const store = createInMemoryStatementStore();
|
|
537
554
|
const session = makeHost(store);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's transport decision logic as a pure reducer: {@link transition} maps a
|
|
3
|
+
* {@link SessionState} and a {@link SessionEvent} to the next state plus the
|
|
4
|
+
* {@link SessionEffect}s the driver must perform. It performs no I/O, mutates nothing it is
|
|
5
|
+
* given, and reads no clock — sizing and id generation arrive in {@link TransitionContext},
|
|
6
|
+
* so tests make both deterministic.
|
|
7
|
+
*
|
|
8
|
+
* The state is what base-spec.md §"Session State" defines: the phase, the outgoing request
|
|
9
|
+
* plus the queue behind it, and the incoming requests.
|
|
10
|
+
*
|
|
11
|
+
* Deliberate deviation from base-spec.md: `incomingRequests` is a map rather than the
|
|
12
|
+
* spec's single `IncomingRequest(A, B)`. The spec's model assumes the Application Layer
|
|
13
|
+
* answers synchronously; this SDK lets it answer whenever, so an older request has to stay
|
|
14
|
+
* answerable after a newer one arrives. It is local bookkeeping only — the wire is
|
|
15
|
+
* unaffected, since the shared response channel still exposes just the latest response.
|
|
16
|
+
*/
|
|
17
|
+
import type { ResponseStatus } from './scale/statementData.js';
|
|
18
|
+
type QueuedMessage = {
|
|
19
|
+
encoded: Uint8Array;
|
|
20
|
+
tokens: string[];
|
|
21
|
+
};
|
|
22
|
+
type OutgoingRequest = {
|
|
23
|
+
requestIds: string[];
|
|
24
|
+
messages: Uint8Array[];
|
|
25
|
+
tokens: string[];
|
|
26
|
+
};
|
|
27
|
+
type IncomingRequest = {
|
|
28
|
+
responded: boolean;
|
|
29
|
+
};
|
|
30
|
+
export type SessionState = {
|
|
31
|
+
phase: 'initialization' | 'active' | 'failed';
|
|
32
|
+
initError: Error | null;
|
|
33
|
+
outgoingRequest: OutgoingRequest | null;
|
|
34
|
+
messageQueue: QueuedMessage[];
|
|
35
|
+
incomingRequests: Map<string, IncomingRequest>;
|
|
36
|
+
};
|
|
37
|
+
export type SessionEvent =
|
|
38
|
+
/** The application layer wants a message sent. */
|
|
39
|
+
{
|
|
40
|
+
type: 'messageSubmitted';
|
|
41
|
+
encoded: Uint8Array;
|
|
42
|
+
token: string;
|
|
43
|
+
}
|
|
44
|
+
/** A peer request we had not seen before. */
|
|
45
|
+
| {
|
|
46
|
+
type: 'requestReceived';
|
|
47
|
+
requestId: string;
|
|
48
|
+
}
|
|
49
|
+
/** A peer response arrived on our outgoing batch. */
|
|
50
|
+
| {
|
|
51
|
+
type: 'responseReceived';
|
|
52
|
+
requestId: string;
|
|
53
|
+
responseCode: ResponseStatus;
|
|
54
|
+
}
|
|
55
|
+
/** A request submission exhausted its retries. */
|
|
56
|
+
| {
|
|
57
|
+
type: 'requestSubmitFailed';
|
|
58
|
+
requestId: string;
|
|
59
|
+
error: Error;
|
|
60
|
+
}
|
|
61
|
+
/** We answered an incoming request (marked before the submit, so concurrent callers dedupe). */
|
|
62
|
+
| {
|
|
63
|
+
type: 'responseSubmitted';
|
|
64
|
+
requestId: string;
|
|
65
|
+
}
|
|
66
|
+
/** That answer failed after retries — let a later peer retransmit be answered. */
|
|
67
|
+
| {
|
|
68
|
+
type: 'responseSubmitFailed';
|
|
69
|
+
requestId: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The statement budget may now hold a different number of messages — the peer's device
|
|
73
|
+
* roster changed, so the envelope grew or shrank. Re-evaluate what the queue can ship.
|
|
74
|
+
*/
|
|
75
|
+
| {
|
|
76
|
+
type: 'capacityChanged';
|
|
77
|
+
}
|
|
78
|
+
/** Drop the live batch and everything queued behind it. */
|
|
79
|
+
| {
|
|
80
|
+
type: 'outgoingCleared';
|
|
81
|
+
}
|
|
82
|
+
/** An unacknowledged batch was found in the store during initialization. */
|
|
83
|
+
| {
|
|
84
|
+
type: 'outgoingRestored';
|
|
85
|
+
requestId: string;
|
|
86
|
+
messages: Uint8Array[];
|
|
87
|
+
}
|
|
88
|
+
/** An incoming request was found in the store during initialization. */
|
|
89
|
+
| {
|
|
90
|
+
type: 'incomingRestored';
|
|
91
|
+
requestId: string;
|
|
92
|
+
responded: boolean;
|
|
93
|
+
}
|
|
94
|
+
/** Initialization finished: go active and ship whatever the queue held. */
|
|
95
|
+
| {
|
|
96
|
+
type: 'activated';
|
|
97
|
+
}
|
|
98
|
+
/** Initialization failed terminally. */
|
|
99
|
+
| {
|
|
100
|
+
type: 'initFailed';
|
|
101
|
+
error: Error;
|
|
102
|
+
};
|
|
103
|
+
/** Work the driver must carry out. The reducer performs none of it. */
|
|
104
|
+
export type SessionEffect = {
|
|
105
|
+
type: 'submitRequest';
|
|
106
|
+
requestId: string;
|
|
107
|
+
messages: Uint8Array[];
|
|
108
|
+
} | {
|
|
109
|
+
type: 'resolveTokens';
|
|
110
|
+
tokens: string[];
|
|
111
|
+
requestId: string;
|
|
112
|
+
responseCode: ResponseStatus;
|
|
113
|
+
} | {
|
|
114
|
+
type: 'rejectTokens';
|
|
115
|
+
tokens: string[];
|
|
116
|
+
error: Error;
|
|
117
|
+
};
|
|
118
|
+
export type TransitionContext = {
|
|
119
|
+
/** Whether these messages fit one statement — the body builder is the size oracle. */
|
|
120
|
+
fits: (messages: Uint8Array[]) => boolean;
|
|
121
|
+
newRequestId: () => string;
|
|
122
|
+
};
|
|
123
|
+
export type Transition = {
|
|
124
|
+
state: SessionState;
|
|
125
|
+
effects: SessionEffect[];
|
|
126
|
+
};
|
|
127
|
+
export declare function initialSessionState(): SessionState;
|
|
128
|
+
export declare function incomingRequest(state: SessionState, requestId: string): IncomingRequest | undefined;
|
|
129
|
+
/**
|
|
130
|
+
* The newest submission of the live batch: the id still worth retrying, and the one whose
|
|
131
|
+
* on-chain statement must be superseded to clear the batch. Null when nothing is in flight.
|
|
132
|
+
*/
|
|
133
|
+
export declare function liveRequestId(state: SessionState): string | null;
|
|
134
|
+
export declare function transition(state: SessionState, event: SessionEvent, ctx: TransitionContext): Transition;
|
|
135
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's transport decision logic as a pure reducer: {@link transition} maps a
|
|
3
|
+
* {@link SessionState} and a {@link SessionEvent} to the next state plus the
|
|
4
|
+
* {@link SessionEffect}s the driver must perform. It performs no I/O, mutates nothing it is
|
|
5
|
+
* given, and reads no clock — sizing and id generation arrive in {@link TransitionContext},
|
|
6
|
+
* so tests make both deterministic.
|
|
7
|
+
*
|
|
8
|
+
* The state is what base-spec.md §"Session State" defines: the phase, the outgoing request
|
|
9
|
+
* plus the queue behind it, and the incoming requests.
|
|
10
|
+
*
|
|
11
|
+
* Deliberate deviation from base-spec.md: `incomingRequests` is a map rather than the
|
|
12
|
+
* spec's single `IncomingRequest(A, B)`. The spec's model assumes the Application Layer
|
|
13
|
+
* answers synchronously; this SDK lets it answer whenever, so an older request has to stay
|
|
14
|
+
* answerable after a newer one arrives. It is local bookkeeping only — the wire is
|
|
15
|
+
* unaffected, since the shared response channel still exposes just the latest response.
|
|
16
|
+
*/
|
|
17
|
+
import { toHex } from '@novasamatech/scale';
|
|
18
|
+
export function initialSessionState() {
|
|
19
|
+
return {
|
|
20
|
+
phase: 'initialization',
|
|
21
|
+
initError: null,
|
|
22
|
+
outgoingRequest: null,
|
|
23
|
+
messageQueue: [],
|
|
24
|
+
incomingRequests: new Map(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// ── selectors ────────────────────────────────────────────────────────────────
|
|
28
|
+
// Reads the driver needs that are not transitions.
|
|
29
|
+
export function incomingRequest(state, requestId) {
|
|
30
|
+
return state.incomingRequests.get(requestId);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The newest submission of the live batch: the id still worth retrying, and the one whose
|
|
34
|
+
* on-chain statement must be superseded to clear the batch. Null when nothing is in flight.
|
|
35
|
+
*/
|
|
36
|
+
export function liveRequestId(state) {
|
|
37
|
+
return state.outgoingRequest?.requestIds.at(-1) ?? null;
|
|
38
|
+
}
|
|
39
|
+
// ── internals ────────────────────────────────────────────────────────────────
|
|
40
|
+
const nothing = (state) => ({ state, effects: [] });
|
|
41
|
+
function withIncoming(state, requestId, value) {
|
|
42
|
+
return { ...state, incomingRequests: new Map(state.incomingRequests).set(requestId, value) };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Attach `token` to an identical message already in flight or queued, so the caller
|
|
46
|
+
* resolves on that message's response instead of the bytes going out twice.
|
|
47
|
+
* Returns null when nothing matches.
|
|
48
|
+
*/
|
|
49
|
+
function attachToDuplicate(state, encoded, token) {
|
|
50
|
+
const encodedHex = toHex(encoded);
|
|
51
|
+
const sameBytes = (m) => m.length === encoded.length && toHex(m) === encodedHex;
|
|
52
|
+
const outgoing = state.outgoingRequest;
|
|
53
|
+
if (outgoing?.messages.some(sameBytes)) {
|
|
54
|
+
return { ...state, outgoingRequest: { ...outgoing, tokens: [...outgoing.tokens, token] } };
|
|
55
|
+
}
|
|
56
|
+
// Only the first match takes the token; a later identical entry must not duplicate it.
|
|
57
|
+
const index = state.messageQueue.findIndex(entry => sameBytes(entry.encoded));
|
|
58
|
+
if (index === -1)
|
|
59
|
+
return null;
|
|
60
|
+
return {
|
|
61
|
+
...state,
|
|
62
|
+
messageQueue: state.messageQueue.map((entry, i) => i === index ? { ...entry, tokens: [...entry.tokens, token] } : entry),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Start a batch, extend the live one, or park the message behind it. */
|
|
66
|
+
function admit(state, encoded, tokens, ctx) {
|
|
67
|
+
const outgoing = state.outgoingRequest;
|
|
68
|
+
if (outgoing === null) {
|
|
69
|
+
const requestId = ctx.newRequestId();
|
|
70
|
+
const messages = [encoded];
|
|
71
|
+
return {
|
|
72
|
+
state: { ...state, outgoingRequest: { requestIds: [requestId], messages, tokens: [...tokens] } },
|
|
73
|
+
effects: [{ type: 'submitRequest', requestId, messages: [...messages] }],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const messages = [...outgoing.messages, encoded];
|
|
77
|
+
if (ctx.fits(messages)) {
|
|
78
|
+
const requestId = ctx.newRequestId();
|
|
79
|
+
return {
|
|
80
|
+
state: {
|
|
81
|
+
...state,
|
|
82
|
+
outgoingRequest: {
|
|
83
|
+
requestIds: [...outgoing.requestIds, requestId],
|
|
84
|
+
messages,
|
|
85
|
+
tokens: [...outgoing.tokens, ...tokens],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
// The statement store keeps one statement per channel, so every submission must
|
|
89
|
+
// carry the FULL unacknowledged batch — an offline peer only ever sees the survivor.
|
|
90
|
+
effects: [{ type: 'submitRequest', requestId, messages: [...messages] }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { state: { ...state, messageQueue: [...state.messageQueue, { encoded, tokens }] }, effects: [] };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Move queue heads into the batch for as long as the budget allows. FIFO: a later message
|
|
97
|
+
* never overtakes one already waiting.
|
|
98
|
+
*/
|
|
99
|
+
function drain(state, ctx) {
|
|
100
|
+
let current = state;
|
|
101
|
+
const effects = [];
|
|
102
|
+
while (current.messageQueue.length > 0) {
|
|
103
|
+
const [head, ...rest] = current.messageQueue;
|
|
104
|
+
if (!head)
|
|
105
|
+
break;
|
|
106
|
+
// Recomputed per iteration; `admit` grows the batch as it goes.
|
|
107
|
+
if (current.outgoingRequest !== null && !ctx.fits([...current.outgoingRequest.messages, head.encoded]))
|
|
108
|
+
break;
|
|
109
|
+
const admitted = admit({ ...current, messageQueue: rest }, head.encoded, head.tokens, ctx);
|
|
110
|
+
current = admitted.state;
|
|
111
|
+
effects.push(...admitted.effects);
|
|
112
|
+
}
|
|
113
|
+
return { state: current, effects };
|
|
114
|
+
}
|
|
115
|
+
// ── reducer ──────────────────────────────────────────────────────────────────
|
|
116
|
+
export function transition(state, event, ctx) {
|
|
117
|
+
switch (event.type) {
|
|
118
|
+
case 'messageSubmitted': {
|
|
119
|
+
const deduped = attachToDuplicate(state, event.encoded, event.token);
|
|
120
|
+
if (deduped)
|
|
121
|
+
return nothing(deduped);
|
|
122
|
+
// FIFO: never let a later (fitting) message overtake queued ones, and never submit
|
|
123
|
+
// before initialization has established the expiry floor.
|
|
124
|
+
if (state.phase === 'initialization' || state.messageQueue.length > 0) {
|
|
125
|
+
return nothing({
|
|
126
|
+
...state,
|
|
127
|
+
messageQueue: [...state.messageQueue, { encoded: event.encoded, tokens: [event.token] }],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return admit(state, event.encoded, [event.token], ctx);
|
|
131
|
+
}
|
|
132
|
+
case 'requestReceived': {
|
|
133
|
+
if (state.incomingRequests.has(event.requestId))
|
|
134
|
+
return nothing(state);
|
|
135
|
+
return nothing(withIncoming(state, event.requestId, { responded: false }));
|
|
136
|
+
}
|
|
137
|
+
case 'responseReceived': {
|
|
138
|
+
const outgoing = state.outgoingRequest;
|
|
139
|
+
// Any id the batch was ever submitted under counts — an early response to a
|
|
140
|
+
// superseded retransmit still answers the same messages.
|
|
141
|
+
if (!outgoing?.requestIds.includes(event.requestId))
|
|
142
|
+
return nothing(state);
|
|
143
|
+
const drained = drain({ ...state, outgoingRequest: null }, ctx);
|
|
144
|
+
return {
|
|
145
|
+
state: drained.state,
|
|
146
|
+
effects: [
|
|
147
|
+
{
|
|
148
|
+
type: 'resolveTokens',
|
|
149
|
+
tokens: outgoing.tokens,
|
|
150
|
+
requestId: event.requestId,
|
|
151
|
+
responseCode: event.responseCode,
|
|
152
|
+
},
|
|
153
|
+
...drained.effects,
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
case 'requestSubmitFailed': {
|
|
158
|
+
const outgoing = state.outgoingRequest;
|
|
159
|
+
// Superseded by a newer retransmit carrying the same tokens — that one owns the
|
|
160
|
+
// waiters now, so this failure is not the live batch's concern.
|
|
161
|
+
if (!outgoing || outgoing.requestIds.at(-1) !== event.requestId)
|
|
162
|
+
return nothing(state);
|
|
163
|
+
const drained = drain({ ...state, outgoingRequest: null }, ctx);
|
|
164
|
+
return {
|
|
165
|
+
state: drained.state,
|
|
166
|
+
effects: [{ type: 'rejectTokens', tokens: outgoing.tokens, error: event.error }, ...drained.effects],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
case 'responseSubmitted': {
|
|
170
|
+
const incoming = state.incomingRequests.get(event.requestId);
|
|
171
|
+
if (!incoming)
|
|
172
|
+
return nothing(state);
|
|
173
|
+
return nothing(withIncoming(state, event.requestId, { responded: true }));
|
|
174
|
+
}
|
|
175
|
+
case 'responseSubmitFailed': {
|
|
176
|
+
const incoming = state.incomingRequests.get(event.requestId);
|
|
177
|
+
if (!incoming)
|
|
178
|
+
return nothing(state);
|
|
179
|
+
return nothing(withIncoming(state, event.requestId, { responded: false }));
|
|
180
|
+
}
|
|
181
|
+
case 'capacityChanged':
|
|
182
|
+
// Never before initialization has established the expiry floor; `activated` drains.
|
|
183
|
+
return state.phase === 'active' ? drain(state, ctx) : nothing(state);
|
|
184
|
+
case 'outgoingCleared':
|
|
185
|
+
return nothing({ ...state, outgoingRequest: null, messageQueue: [] });
|
|
186
|
+
case 'outgoingRestored':
|
|
187
|
+
return nothing({
|
|
188
|
+
...state,
|
|
189
|
+
// Tokens from a previous run cannot be restored — nobody is awaiting them.
|
|
190
|
+
outgoingRequest: { requestIds: [event.requestId], messages: event.messages, tokens: [] },
|
|
191
|
+
});
|
|
192
|
+
case 'incomingRestored': {
|
|
193
|
+
// Don't clobber an entry a live delivery created while init was still awaiting.
|
|
194
|
+
if (state.incomingRequests.has(event.requestId))
|
|
195
|
+
return nothing(state);
|
|
196
|
+
return nothing(withIncoming(state, event.requestId, { responded: event.responded }));
|
|
197
|
+
}
|
|
198
|
+
case 'activated':
|
|
199
|
+
return drain({ ...state, phase: 'active' }, ctx);
|
|
200
|
+
case 'initFailed':
|
|
201
|
+
return nothing({ ...state, phase: 'failed', initError: event.error, messageQueue: [] });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|