1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
import type {
InvokeMessage,
SubscribeMessage,
SurfaceServerMessage,
UnsubscribeMessage,
} from "@dispatch/ui-contract";
import type { ProtocolResult, ProtocolState } from "./types";
/** The initial protocol state: empty catalog, no subscriptions, no error. */
export function initialState(): ProtocolState {
return {
catalog: [],
subscriptions: new Map(),
lastError: null,
};
}
/** Fold an inbound server message into the next protocol state. */
export function applyServerMessage(state: ProtocolState, msg: SurfaceServerMessage): ProtocolState {
switch (msg.type) {
case "catalog":
return { ...state, catalog: msg.catalog };
case "surface": {
const surfaceId = msg.spec.id;
if (!state.subscriptions.has(surfaceId)) return state;
const subs = new Map(state.subscriptions);
subs.set(surfaceId, msg.spec);
return { ...state, subscriptions: subs };
}
case "update": {
const surfaceId = msg.update.surfaceId;
if (!state.subscriptions.has(surfaceId)) return state;
const subs = new Map(state.subscriptions);
subs.set(surfaceId, msg.update.spec);
return { ...state, subscriptions: subs };
}
case "error":
return { ...state, lastError: msg };
}
}
/**
* Subscribe to a surface. Idempotent: if already subscribed, returns the same
* state with no outgoing message.
*/
export function subscribe(state: ProtocolState, surfaceId: string): ProtocolResult {
if (state.subscriptions.has(surfaceId)) {
return { state, outgoing: [] };
}
const subs = new Map(state.subscriptions);
subs.set(surfaceId, null);
const outgoing: SubscribeMessage = { type: "subscribe", surfaceId };
return { state: { ...state, subscriptions: subs }, outgoing: [outgoing] };
}
/**
* Unsubscribe from a surface. Drops the local spec and emits one unsubscribe.
* If not subscribed, returns the same state with no outgoing.
*/
export function unsubscribe(state: ProtocolState, surfaceId: string): ProtocolResult {
if (!state.subscriptions.has(surfaceId)) {
return { state, outgoing: [] };
}
const subs = new Map(state.subscriptions);
subs.delete(surfaceId);
const outgoing: UnsubscribeMessage = { type: "unsubscribe", surfaceId };
return { state: { ...state, subscriptions: subs }, outgoing: [outgoing] };
}
/** Invoke a field's action on a surface. Emits an InvokeMessage; no state change. */
export function invoke(
state: ProtocolState,
surfaceId: string,
actionId: string,
payload?: unknown,
): ProtocolResult {
const outgoing: InvokeMessage = { type: "invoke", surfaceId, actionId, payload };
return { state, outgoing: [outgoing] };
}
|