summaryrefslogtreecommitdiffhomepage
path: root/src/core/protocol/reducer.ts
blob: 992a918cddbe81caaaf05c0b4cf802cd2a22c3a3 (plain)
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] };
}