p5.party
Version:
Pre-release! An easy to use library for simple multi-user sketches with p5.js.
364 lines (310 loc) • 10.2 kB
text/typescript
/* 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();
}
};
}