Skip to content

Commit 8de81c0

Browse files
committed
feat(stage-tamagotchi): new helper ReferencedWindow
1 parent 8275529 commit 8de81c0

File tree

2 files changed

+117
-0
lines changed

2 files changed

+117
-0
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { BrowserWindow } from 'electron'
2+
3+
import type { createRequestWindowEventa, RequestWindowPayload } from '../../../shared/eventa'
4+
5+
import { defineInvokeHandler } from '@moeru/eventa'
6+
import { createContext } from '@moeru/eventa/adapters/electron/main'
7+
import { ipcMain } from 'electron'
8+
9+
export interface ReferencedWindowHandle {
10+
id: string
11+
window: BrowserWindow
12+
context: ReturnType<typeof createContext>['context']
13+
eventa: ReturnType<typeof createRequestWindowEventa>
14+
}
15+
16+
export interface ReferencedWindowManager<Payload extends RequestWindowPayload = RequestWindowPayload> {
17+
open: (payload: Payload & { id?: string }) => Promise<ReferencedWindowHandle>
18+
close: (id: string) => void
19+
}
20+
21+
/**
22+
* Minimal per-id window manager used by notice/widgets-like windows.
23+
* It opens (or reuses) a window, loads the route with the id in query, and returns the window/context so
24+
* callers can register their own action handlers.
25+
*/
26+
export function createReferencedWindowManager<Payload extends RequestWindowPayload = RequestWindowPayload>(params: {
27+
eventa: ReturnType<typeof createRequestWindowEventa>
28+
createWindow: (id: string) => BrowserWindow
29+
loadRoute: (window: BrowserWindow, payload: Payload & { id: string }) => Promise<void>
30+
}): ReferencedWindowManager<Payload> {
31+
const windows = new Map<string, { window: BrowserWindow, context: ReturnType<typeof createContext>['context'] }>()
32+
33+
function bindContext(id: string, payload: Payload, win: BrowserWindow) {
34+
// TODO: once we refactored eventa to support window-namespaced contexts,
35+
// we can remove the setMaxListeners call below since eventa will be able to dispatch and
36+
// manage events within eventa's context system.
37+
ipcMain.setMaxListeners(0)
38+
const { context } = createContext(ipcMain, win)
39+
40+
defineInvokeHandler(context, params.eventa.pageMounted, (req) => {
41+
if (req?.id && req.id !== id)
42+
return undefined
43+
return { id, type: payload.type, payload: payload.payload }
44+
})
45+
46+
defineInvokeHandler(context, params.eventa.pageUnmounted, (req) => {
47+
if (req?.id && req.id !== id)
48+
return
49+
windows.delete(id)
50+
})
51+
52+
win.on('closed', () => windows.delete(id))
53+
54+
return { window: win, context }
55+
}
56+
57+
async function open(payload: Payload & { id?: string }): Promise<ReferencedWindowHandle> {
58+
const id = payload.id ?? Math.random().toString(36).slice(2, 10)
59+
let ctx = windows.get(id)
60+
61+
if (!ctx || ctx.window.isDestroyed()) {
62+
const win = params.createWindow(id)
63+
ctx = bindContext(id, payload, win)
64+
windows.set(id, ctx)
65+
}
66+
67+
try {
68+
await params.loadRoute(ctx.window, { ...payload, id })
69+
ctx.window.show()
70+
ctx.window.focus()
71+
}
72+
catch (error) {
73+
const wrapped = error ?? new Error('Failed to open referenced window')
74+
console.error('[referenced-window] open failed', wrapped)
75+
throw wrapped
76+
}
77+
78+
return { id, window: ctx.window, context: ctx.context, eventa: params.eventa }
79+
}
80+
81+
function close(id: string) {
82+
const ctx = windows.get(id)
83+
if (!ctx)
84+
return
85+
if (!ctx.window.isDestroyed())
86+
ctx.window.close()
87+
windows.delete(id)
88+
}
89+
90+
return { open, close }
91+
}

apps/stage-tamagotchi/src/shared/eventa.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ export const electronOpenSettingsDevtools = defineInvokeEventa('eventa:invoke:el
99
export const captionIsFollowingWindowChanged = defineEventa<boolean>('eventa:event:electron:windows:caption-overlay:is-following-window-changed')
1010
export const captionGetIsFollowingWindow = defineInvokeEventa<boolean>('eventa:invoke:electron:windows:caption-overlay:get-is-following-window')
1111

12+
export type RequestWindowActionDefault = 'confirm' | 'cancel' | 'close'
13+
export interface RequestWindowPayload {
14+
id?: string
15+
route: string
16+
type?: string
17+
payload?: Record<string, any>
18+
}
19+
export interface RequestWindowPending {
20+
id: string
21+
type?: string
22+
payload?: Record<string, any>
23+
}
24+
25+
// Reference window helpers are generic; callers can alias for clarity
26+
export type NoticeAction = 'confirm' | 'cancel' | 'close'
27+
28+
export function createRequestWindowEventa(namespace: string) {
29+
const prefix = (name: string) => `eventa:${name}:electron:windows:${namespace}`
30+
return {
31+
openWindow: defineInvokeEventa<boolean, RequestWindowPayload>(prefix('invoke:open')),
32+
windowAction: defineInvokeEventa<void, { id: string, action: RequestWindowActionDefault }>(prefix('invoke:action')),
33+
pageMounted: defineInvokeEventa<RequestWindowPending | undefined, { id?: string }>(prefix('invoke:page-mounted')),
34+
pageUnmounted: defineInvokeEventa<void, { id?: string }>(prefix('invoke:page-unmounted')),
35+
}
36+
}
37+
1238
// Widgets / Adhoc window events
1339
export interface WidgetsAddPayload {
1440
id?: string

0 commit comments

Comments
 (0)