Improved tauri events type safety

This commit is contained in:
2026-03-07 14:48:19 +08:00
parent f372e86457
commit f65d837841
20 changed files with 215 additions and 141 deletions

View File

@@ -2,10 +2,11 @@ import { listen } from "@tauri-apps/api/event";
import { writable } from "svelte/store";
import type { CursorPositions } from "../types/bindings/CursorPositions";
import type { DollDto } from "../types/bindings/DollDto";
import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload";
import type { FriendActiveDollChangedPayload } from "../types/bindings/FriendActiveDollChangedPayload";
import { AppEvents } from "../types/bindings/AppEventsConstants";
import {
createMultiListenerSubscription,
parseEventPayload,
removeFromStore,
setupHmrCleanup,
} from "./listener-utils";
@@ -59,63 +60,51 @@ export async function startFriendCursorTracking() {
);
subscription.addUnlisten(unlistenFriendCursor);
const unlistenFriendDisconnected = await listen<
[{ userId: string }] | { userId: string } | string
>(AppEvents.FriendDisconnected, (event) => {
const payload = parseEventPayload<
[{ userId: string }] | { userId: string }
>(event.payload, AppEvents.FriendDisconnected);
if (!payload) return;
const unlistenFriendDisconnected = await listen<FriendDisconnectedPayload>(
AppEvents.FriendDisconnected,
(event) => {
const data = event.payload;
const data = Array.isArray(payload) ? payload[0] : payload;
if (friendCursorState[data.userId]) {
delete friendCursorState[data.userId];
}
if (friendCursorState[data.userId]) {
delete friendCursorState[data.userId];
}
friendsCursorPositions.update((current) =>
removeFromStore(current, data.userId),
friendsCursorPositions.update((current) =>
removeFromStore(current, data.userId),
);
},
);
});
subscription.addUnlisten(unlistenFriendDisconnected);
subscription.addUnlisten(unlistenFriendDisconnected);
const unlistenFriendActiveDollChanged = await listen<
| string
| {
friendId: string;
doll: DollDto | null;
}
>(AppEvents.FriendActiveDollChanged, (event) => {
const data = parseEventPayload<{
friendId: string;
doll: DollDto | null;
}>(event.payload, AppEvents.FriendActiveDollChanged);
if (!data) return;
const unlistenFriendActiveDollChanged =
await listen<FriendActiveDollChangedPayload>(
AppEvents.FriendActiveDollChanged,
(event) => {
const payload = event.payload;
const payload = data as { friendId: string; doll: DollDto | null };
if (!payload.doll) {
friendsActiveDolls.update((current) => {
const next = { ...current };
next[payload.friendId] = null;
return next;
});
if (!payload.doll) {
friendsActiveDolls.update((current) => {
const next = { ...current };
next[payload.friendId] = null;
return next;
});
friendsCursorPositions.update((current) =>
removeFromStore(current, payload.friendId),
friendsCursorPositions.update((current) =>
removeFromStore(current, payload.friendId),
);
} else {
friendsActiveDolls.update((current) => {
return {
...current,
[payload.friendId]: payload.doll,
};
});
}
},
);
} else {
friendsActiveDolls.update((current) => {
return {
...current,
[payload.friendId]: payload.doll!,
};
});
}
});
subscription.addUnlisten(unlistenFriendActiveDollChanged);
subscription.addUnlisten(unlistenFriendActiveDollChanged);
subscription.setListening(true);
subscription.setListening(true);
} catch (err) {
console.error("Failed to initialize friend cursor tracking:", err);
throw err;

View File

@@ -74,22 +74,6 @@ export function setupHmrCleanup(cleanup: () => void) {
}
}
export function parseEventPayload<T>(
payload: unknown,
errorLabel: string,
): T | null {
if (typeof payload === "string") {
try {
return JSON.parse(payload) as T;
} catch (error) {
console.error(`Failed to parse ${errorLabel} payload`, error);
return null;
}
}
return payload as T;
}
export function removeFromStore<T>(
current: Record<string, T>,
key: string,

View File

@@ -1,21 +1,19 @@
import { listen } from "@tauri-apps/api/event";
import { writable } from "svelte/store";
import type { PresenceStatus } from "../types/bindings/PresenceStatus";
import type { FriendDisconnectedPayload } from "../types/bindings/FriendDisconnectedPayload";
import type { FriendUserStatusPayload } from "../types/bindings/FriendUserStatusPayload";
import type { UserStatusPayload } from "../types/bindings/UserStatusPayload";
import { AppEvents } from "../types/bindings/AppEventsConstants";
import {
createMultiListenerSubscription,
parseEventPayload,
removeFromStore,
setupHmrCleanup,
} from "./listener-utils";
export type PresenceState = {
presenceStatus: PresenceStatus;
state: "idle" | "resting";
};
export const friendsPresenceStates = writable<Record<string, PresenceState>>({});
export const currentPresenceState = writable<PresenceState | null>(null);
export const friendsPresenceStates = writable<
Record<string, UserStatusPayload>
>({});
export const currentPresenceState = writable<UserStatusPayload | null>(null);
const subscription = createMultiListenerSubscription();
@@ -26,22 +24,11 @@ export async function startUserStatus() {
if (subscription.isListening()) return;
try {
const unlistenStatus = await listen<unknown>(
const unlistenStatus = await listen<FriendUserStatusPayload>(
AppEvents.FriendUserStatus,
(event) => {
const payload = parseEventPayload<{
userId?: string;
status?: PresenceState;
}>(event.payload, AppEvents.FriendUserStatus);
if (!payload) return;
const { userId, status } = event.payload;
const userId = payload.userId;
const status = payload.status;
if (!userId || !status) return;
if (!status.presenceStatus) return;
// Validate that appMetadata has at least one valid name
const hasValidName =
(typeof status.presenceStatus.title === "string" &&
status.presenceStatus.title.trim() !== "") ||
@@ -49,20 +36,15 @@ export async function startUserStatus() {
status.presenceStatus.subtitle.trim() !== "");
if (!hasValidName) return;
if (status.state !== "idle" && status.state !== "resting") return;
friendsPresenceStates.update((current) => ({
...current,
[userId]: {
presenceStatus: status.presenceStatus,
state: status.state,
},
[userId]: status,
}));
},
);
subscription.addUnlisten(unlistenStatus);
const unlistenUserStatusChanged = await listen<PresenceState>(
const unlistenUserStatusChanged = await listen<UserStatusPayload>(
AppEvents.UserStatusChanged,
(event) => {
currentPresenceState.set(event.payload);
@@ -70,20 +52,15 @@ export async function startUserStatus() {
);
subscription.addUnlisten(unlistenUserStatusChanged);
const unlistenFriendDisconnected = await listen<
[{ userId: string }] | { userId: string } | string
>(AppEvents.FriendDisconnected, (event) => {
const payload = parseEventPayload<
[{ userId: string }] | { userId: string }
>(event.payload, AppEvents.FriendDisconnected);
if (!payload) return;
const data = Array.isArray(payload) ? payload[0] : payload;
const userId = data?.userId as string | undefined;
if (!userId) return;
friendsPresenceStates.update((current) => removeFromStore(current, userId));
});
const unlistenFriendDisconnected = await listen<FriendDisconnectedPayload>(
AppEvents.FriendDisconnected,
(event) => {
const { userId } = event.payload;
friendsPresenceStates.update((current) =>
removeFromStore(current, userId),
);
},
);
subscription.addUnlisten(unlistenFriendDisconnected);
subscription.setListening(true);

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { PresenceStatus } from "../../../types/bindings/PresenceStatus";
import type { PresenceState } from "../../../events/user-status";
import type { UserStatusPayload } from "../../../types/bindings/UserStatusPayload";
interface Friend {
friend?: {
@@ -15,7 +15,7 @@
presenceStatus: PresenceStatus | null;
friendsCursorPositions: Record<string, { mapped: { x: number; y: number } }>;
friends: Friend[];
friendsPresenceStates: Record<string, PresenceState>;
friendsPresenceStates: Record<string, UserStatusPayload>;
}
let {

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DollDto } from "./DollDto";
export type FriendActiveDollChangedPayload = { friendId: string, doll: DollDto | null, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FriendDisconnectedPayload = { userId: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestAcceptedPayload = { id: string, friend: UserBasicDto, acceptedAt: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestDeniedPayload = { id: string, denier: UserBasicDto, deniedAt: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserBasicDto } from "./UserBasicDto";
export type FriendRequestReceivedPayload = { id: string, sender: UserBasicDto, createdAt: string, };

View File

@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { UserStatusPayload } from "./UserStatusPayload";
export type FriendUserStatusPayload = { userId: string, status: UserStatusPayload, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UnfriendedPayload = { friendId: string, };

View File

@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PresenceStatus } from "./PresenceStatus";
import type { UserStatusState } from "./UserStatusState";
export type UserStatusPayload = { presenceStatus: PresenceStatus, state: UserStatusState, };

View File

@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type UserStatusState = "idle" | "resting";