181 lines
5.1 KiB
TypeScript
181 lines
5.1 KiB
TypeScript
const PROJECT_EVENTS_CHANNEL = 'jgjs-project-events'
|
|
const PROJECT_DELETED_STORAGE_KEY = 'jgjs-project-event:project-deleted'
|
|
const RESET_ALL_STORAGE_KEY = 'jgjs-project-event:reset-all'
|
|
const PROJECT_EVENT_SESSION_ID_KEY = 'jgjs-project-event-session-id'
|
|
|
|
type ProjectDeletedPayload = {
|
|
type: 'project-deleted'
|
|
projectId: string
|
|
sourceSessionId: string
|
|
at: number
|
|
}
|
|
|
|
type ResetAllPayload = {
|
|
type: 'reset-all'
|
|
sourceSessionId: string
|
|
at: number
|
|
}
|
|
|
|
const randomSessionId = () => `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
|
|
|
|
const getOrCreateSessionId = () => {
|
|
try {
|
|
const existing = String(window.sessionStorage.getItem(PROJECT_EVENT_SESSION_ID_KEY) || '').trim()
|
|
if (existing) return existing
|
|
const next = randomSessionId()
|
|
window.sessionStorage.setItem(PROJECT_EVENT_SESSION_ID_KEY, next)
|
|
return next
|
|
} catch {
|
|
return randomSessionId()
|
|
}
|
|
}
|
|
|
|
const parseProjectDeletedPayload = (raw: string | null): ProjectDeletedPayload | null => {
|
|
if (!raw) return null
|
|
try {
|
|
const parsed = JSON.parse(raw) as Partial<ProjectDeletedPayload>
|
|
if (!parsed || typeof parsed !== 'object') return null
|
|
if (parsed.type !== 'project-deleted') return null
|
|
if (typeof parsed.projectId !== 'string' || !parsed.projectId.trim()) return null
|
|
if (typeof parsed.sourceSessionId !== 'string' || !parsed.sourceSessionId.trim()) return null
|
|
if (typeof parsed.at !== 'number' || !Number.isFinite(parsed.at)) return null
|
|
return {
|
|
type: 'project-deleted',
|
|
projectId: parsed.projectId.trim(),
|
|
sourceSessionId: parsed.sourceSessionId.trim(),
|
|
at: parsed.at
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const parseResetAllPayload = (raw: string | null): ResetAllPayload | null => {
|
|
if (!raw) return null
|
|
try {
|
|
const parsed = JSON.parse(raw) as Partial<ResetAllPayload>
|
|
if (!parsed || typeof parsed !== 'object') return null
|
|
if (parsed.type !== 'reset-all') return null
|
|
if (typeof parsed.sourceSessionId !== 'string' || !parsed.sourceSessionId.trim()) return null
|
|
if (typeof parsed.at !== 'number' || !Number.isFinite(parsed.at)) return null
|
|
return {
|
|
type: 'reset-all',
|
|
sourceSessionId: parsed.sourceSessionId.trim(),
|
|
at: parsed.at
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const emitProjectDeleted = (projectIdRaw: string) => {
|
|
const projectId = String(projectIdRaw || '').trim()
|
|
if (!projectId) return
|
|
const payload: ProjectDeletedPayload = {
|
|
type: 'project-deleted',
|
|
projectId,
|
|
sourceSessionId: getOrCreateSessionId(),
|
|
at: Date.now()
|
|
}
|
|
try {
|
|
if (typeof BroadcastChannel !== 'undefined') {
|
|
const bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL)
|
|
bc.postMessage(payload)
|
|
bc.close()
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
localStorage.setItem(PROJECT_DELETED_STORAGE_KEY, JSON.stringify(payload))
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export const listenProjectDeleted = (onDeleted: (projectId: string) => void) => {
|
|
const localSessionId = getOrCreateSessionId()
|
|
let bc: BroadcastChannel | null = null
|
|
|
|
const handlePayload = (payload: ProjectDeletedPayload | null) => {
|
|
if (!payload) return
|
|
if (payload.sourceSessionId === localSessionId) return
|
|
onDeleted(payload.projectId)
|
|
}
|
|
|
|
const onStorage = (event: StorageEvent) => {
|
|
if (event.key !== PROJECT_DELETED_STORAGE_KEY) return
|
|
handlePayload(parseProjectDeletedPayload(event.newValue))
|
|
}
|
|
|
|
if (typeof BroadcastChannel !== 'undefined') {
|
|
bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL)
|
|
bc.onmessage = (event: MessageEvent<ProjectDeletedPayload>) => {
|
|
handlePayload(parseProjectDeletedPayload(JSON.stringify(event.data)))
|
|
}
|
|
}
|
|
window.addEventListener('storage', onStorage)
|
|
|
|
return () => {
|
|
window.removeEventListener('storage', onStorage)
|
|
if (bc) {
|
|
bc.close()
|
|
bc = null
|
|
}
|
|
}
|
|
}
|
|
|
|
export const emitResetAll = () => {
|
|
const payload: ResetAllPayload = {
|
|
type: 'reset-all',
|
|
sourceSessionId: getOrCreateSessionId(),
|
|
at: Date.now()
|
|
}
|
|
try {
|
|
if (typeof BroadcastChannel !== 'undefined') {
|
|
const bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL)
|
|
bc.postMessage(payload)
|
|
bc.close()
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
localStorage.setItem(RESET_ALL_STORAGE_KEY, JSON.stringify(payload))
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export const listenResetAll = (onResetAll: () => void) => {
|
|
const localSessionId = getOrCreateSessionId()
|
|
let bc: BroadcastChannel | null = null
|
|
|
|
const handlePayload = (payload: ResetAllPayload | null) => {
|
|
if (!payload) return
|
|
if (payload.sourceSessionId === localSessionId) return
|
|
onResetAll()
|
|
}
|
|
|
|
const onStorage = (event: StorageEvent) => {
|
|
if (event.key !== RESET_ALL_STORAGE_KEY) return
|
|
handlePayload(parseResetAllPayload(event.newValue))
|
|
}
|
|
|
|
if (typeof BroadcastChannel !== 'undefined') {
|
|
bc = new BroadcastChannel(PROJECT_EVENTS_CHANNEL)
|
|
bc.onmessage = (event: MessageEvent<ResetAllPayload>) => {
|
|
handlePayload(parseResetAllPayload(JSON.stringify(event.data)))
|
|
}
|
|
}
|
|
window.addEventListener('storage', onStorage)
|
|
|
|
return () => {
|
|
window.removeEventListener('storage', onStorage)
|
|
if (bc) {
|
|
bc.close()
|
|
bc = null
|
|
}
|
|
}
|
|
}
|