/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ declare let p5: any; import { JSONObject, UserData } from "./validate"; import { SubscriptionCallback } from "@deepstream/client/dist/src/record/record"; import { version } from "../version"; import * as log from "./log"; import { Room } from "./Room"; import { Record } from "./Record"; import { createInfo, destroyInfo } from "./info"; if (typeof window !== "undefined") { (window as any).p5party = { Room, Record, }; const p5 = (window as any).p5; p5 ? init() : log.warn("p5.js not found."); } function init() { const version_string = p5.prototype.VERSION ? `p5.js v${p5.prototype.VERSION}` : "p5.js is older than 1.3.1"; log.styled("font-weight: bold", version_string); log.styled("font-weight: bold", `p5.party v${version}`); let room: Room | null = null; /// partyConnect (preload) p5.prototype.registerPreloadMethod("partyConnect", p5.prototype); p5.prototype.partyConnect = function ( host: string, appName: string, roomName = "_main", cb?: () => void ) { if (room !== null) { log.warn("You should call partyConnect() only one time"); return; } const load = async () => { // connect room room = new Room(host, appName, roomName); await room.whenConnected; window.addEventListener("beforeunload", () => { room?.disconnect(); }); // install ctrl-i inspector pane binding document.addEventListener( "keyup", (e) => { if (e.ctrlKey && e.key === "i") { p5.prototype.partyToggleInfo(); } }, false ); // check for auto reload await experimentalAutoReload(); // install p5PartyEvents room.subscribe("p5PartyEvent", (data: JSONObject) => { // function handleAction() { if (!room) return; // reload-others if ( data.action === "reload-others" && data.sender != room.info().guestName ) { log.log("Recieved reload-others p5PartyEvent. Reloading..."); window.location.reload(); } // disconnect-others if ( data.action === "disconnect-others" && data.sender != room.info().guestName ) { log.log("Recieved disconnect-others p5PartyEvent. Disconnecting..."); room.disconnect(); void createInfo(room); } // } // void handleAction(); }); // finish up log.log("partyConnect done!"); this._decrementPreload(); cb?.(); }; void load(); }; /// experimental auto reload /** EXPERIMENTAL FEATURE: Auto reloading When developing a sketch that uses p5.party, it is usually best to have all connected clients reload when the code changes. When working in VS Code with Live Server, saving the code auto reloads all local browser tabs running the code, which is usually good enough. When working with the p5 web editor saving the code will reload the sketch in the same tab, but not in other tabs/windows. This feature is intended to make working in the p5 web editor (or similar environments) easier by reloaded all other connected clients when the "primary" client reloads. To use this you would open the info panel (ctrl-i) and enable the "auto" checkbox. When enabled, reloading the tab will send a message to all other connected clients to reload. Reloading happens immediately after the "auto" guest connects, making the "auto" guest the host before it's setup() is called. */ p5.prototype.registerMethod("partyDisconnect", p5.prototype); p5.prototype.partyDisconnect = function () { if (room === null) { log.error("partyDisconnect() called before partyConnect()"); return; } room.disconnect(); }; async function experimentalAutoReload() { if (!room) return; // // check if this tab has "auto" enabled const auto = sessionStorage.getItem("auto") === "true"; log.log("Experimental Auto:", auto); // if auto is enabled... if (auto) { // ...reload others and... log.log("Auto enabled. Reloading others..."); room.emit("p5PartyEvent", { action: "auto-reload-others", sender: room.info().guestName, }); // ...wait until this tab becomes the host... while (!room.isHost()) { log.log("Waiting..."); await new Promise((r) => setTimeout(r, 100)); } } // install the auto-reload-others event listener room.subscribe("p5PartyEvent", async (data: JSONObject) => { // async function handleAction() { if (!room) return; // when we get an auto-reload-others message... if ( data.action === "auto-reload-others" && data.sender != room.info().guestName ) { // ... first remove the auto flag if it's set, otherwise this client // will send a reload message back, forming an endless loop ... const auto = sessionStorage.getItem("auto") === "true"; if (auto) { log.alert( "Recieved auto-reload-others p5PartyEvent, but auto is set. Disabling auto..." ); sessionStorage.setItem("auto", "false"); } // ... then disconnect and reload log.log("Recieved auto-reload-others p5PartyEvent. Disconnecting..."); room.disconnect(); await new Promise((r) => setTimeout(r, 500)); log.log("Reloading..."); window.location.reload(); } // } // void handleAction(); }); } /// partyLoadShared (preload) p5.prototype.registerPreloadMethod("partyLoadShared", p5.prototype); p5.prototype.partyLoadShared = function ( name: string, initObject?: UserData, cb?: (shared: JSONObject) => void ): JSONObject | undefined { if (room === null) { log.error("partyLoadShared() called before partyConnect()"); return undefined; } const record = room.getRecord(name); const load = async () => { await room?.whenConnected; // room null checked above const overwrite = room?.isHost() === true; await record.load(initObject, overwrite); log.log(`partyLoadShared "${name}" done!`); cb?.(record.shared); this._decrementPreload(); }; void load(); return record.shared; }; /// partyLoadMyShared p5.prototype.registerPreloadMethod("partyLoadMyShared", p5.prototype); p5.prototype.partyLoadMyShared = function ( initObject = {}, cb?: (shared: JSONObject) => void ) { if (room === null) { log.error("partyLoadMyShared() called before partyConnect()"); return undefined; } const record = room.myGuestRecord; const load = async () => { await room?.whenConnected; // room null checked above await record.whenLoaded; await record.initData(initObject); log.log(`partyLoadMyShared done!`); cb?.(record.shared); this._decrementPreload(); }; void load(); return record.shared; }; /// partyLoadGuestShareds p5.prototype.partyLoadGuestShareds = function () { if (room === null) { log.error("partyLoadGuestShareds() called before partyConnect()"); return undefined; } return room.guestShareds; }; p5.prototype.partyLoadParticipantShareds = function () { log.warn( "partyLoadParticipantShareds is deprecated. Use partyLoadGuestShareds instead." ); if (room === null) { log.error("partyLoadParticipantShareds() called before partyConnect()"); return undefined; } return room.guestShareds; }; /// partyIsHost p5.prototype.partyIsHost = function (): boolean { if (room === null) { log.error("partyIsHost() called before partyConnect()"); return false; } return room.isHost(); }; /// partySetShared p5.prototype.partySetShared = function ( shared: JSONObject, object: JSONObject ): void { if (!Record.recordForShared(shared)) { log.warn( "partySetShared() doesn't recognize the provided shared object.", shared ); return; } Record.recordForShared(shared)?.setData(object); }; /// partyWatchShared p5.prototype.partyWatchShared = function ( shared: JSONObject, a: any, b: any, c: any ): void { if (!Record.recordForShared(shared)) { log.warn( "partyWatchShared() expected shared (argument 1) to be shared object.", shared ); return; } /* eslint-disable-next-line @typescript-eslint/no-unsafe-argument */ Record.recordForShared(shared)?.watchShared(a, b, c); }; /// partySubscribe p5.prototype.partySubscribe = function ( event: string, cb: SubscriptionCallback ): void { if (room === null) { log.error("partySubscribe() called before partyConnect()"); return; } room.subscribe(event, cb); }; /// partyUnsubscribe p5.prototype.partyUnsubscribe = function ( event: string, cb?: SubscriptionCallback ): void { if (room === null) { log.error("partyUnsubscribe() called before partyConnect()"); return; } room.unsubscribe(event, cb); }; /// partyEmit p5.prototype.partyEmit = function (event: string, data?: UserData): void { if (room === null) { log.error("partyEmit() called before partyConnect()"); return; } room.emit(event, data); }; let isInfoShown = false; p5.prototype.partyToggleInfo = function (show?: boolean) { if (room === null) { log.error("partyToggleInfo() called before partyConnect()"); return; } isInfoShown = show ?? !isInfoShown; if (isInfoShown) { console.log("call createInfo"); void createInfo(room); } else { destroyInfo(); } }; }