@nodecg/types
Version:
Dynamic broadcast graphics rendered in a browser
198 lines • 8.31 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Replicator = void 0;
const node_crypto_1 = require("node:crypto");
const json_1 = require("klona/json");
const errors_1 = require("../../shared/utils/errors");
const logger_1 = require("../logger");
const throttle_name_1 = require("../util/throttle-name");
const server_replicant_1 = require("./server-replicant");
const log = (0, logger_1.createLogger)("replicator");
class Replicator {
io;
db;
declaredReplicants = new Map();
_uuid = (0, node_crypto_1.randomUUID)();
_repEntities;
_pendingSave = new WeakMap();
constructor(io, db, repEntities) {
this.io = io;
this.db = db;
this.io = io;
io.on("connection", (socket) => {
this._attachToSocket(socket);
});
this._repEntities = repEntities;
}
declare(name, namespace, opts) {
// If replicant already exists, return that.
const nsp = this.declaredReplicants.get(namespace);
if (nsp) {
const existing = nsp.get(name);
if (existing) {
existing.log.replicants("Existing replicant found, returning that instead of creating a new one.");
return existing;
}
}
else {
this.declaredReplicants.set(namespace, new Map());
}
// Look up the persisted value, if any.
let parsedPersistedValue;
const repEnt = this._repEntities.find((re) => re.namespace === namespace && re.name === name);
if (repEnt) {
try {
parsedPersistedValue =
repEnt.value === "" ? undefined : JSON.parse(repEnt.value);
}
catch (_) {
parsedPersistedValue = repEnt.value;
}
}
// Make the replicant and add it to the declaredReplicants map
const rep = new server_replicant_1.ServerReplicant(name, namespace, opts, parsedPersistedValue);
this.declaredReplicants.get(namespace).set(name, rep);
// Add persistence hooks
rep.on("change", () => {
this.saveReplicant(rep);
});
// Listen for server-side operations
rep.on("operations", (data) => {
this.emitToClients(rep, "replicant:operations", data);
});
return rep;
}
/**
* Applies an array of operations to a replicant.
* @param replicant {object} - The Replicant to perform these operation on.
* @param operations {array} - An array of operations.
*/
applyOperations(replicant, operations) {
const oldValue = (0, json_1.klona)(replicant.value);
operations.forEach((operation) => replicant._applyOperation(operation));
replicant.revision++;
replicant.emit("change", replicant.value, oldValue, operations);
this.emitToClients(replicant, "replicant:operations", {
name: replicant.name,
namespace: replicant.namespace,
revision: replicant.revision,
operations,
});
}
/**
* Emits an event to all remote Socket.IO listeners.
* @param namespace - The namespace in which to emit this event. Only applies to Socket.IO listeners.
* @param eventName - The name of the event to emit.
* @param data - The data to emit with the event.
*/
emitToClients(replicant, eventName, data) {
// Emit to clients (in the given namespace's room) using Socket.IO
const namespace = `replicant:${replicant.namespace}:${replicant.name}`;
log.replicants("emitting %s to %s:", eventName, namespace, JSON.stringify(data, undefined, 2));
this.io.to(namespace).emit(eventName, data); // TODO: figure out how to type this properly
}
saveAllReplicants() {
for (const replicants of this.declaredReplicants.values()) {
for (const replicant of replicants.values()) {
this.saveReplicant(replicant);
}
}
}
async saveAllReplicantsNow() {
const promises = [];
for (const replicants of this.declaredReplicants.values()) {
for (const replicant of replicants.values()) {
promises.push(this._saveReplicant(replicant));
}
}
await Promise.all(promises);
}
saveReplicant(replicant) {
if (!replicant.opts.persistent) {
return;
}
(0, throttle_name_1.throttleName)(`${this._uuid}:${replicant.namespace}:${replicant.name}`, () => {
this._saveReplicant(replicant).catch((error) => {
log.error("Error saving replicant:", error);
});
}, replicant.opts.persistenceInterval);
}
async _saveReplicant(replicant) {
if (!replicant.opts.persistent) {
return;
}
// Return the promise so that it can still be awaited
if (this._pendingSave.has(replicant)) {
return this._pendingSave.get(replicant);
}
try {
const promise = this.db.saveReplicant(replicant);
this._pendingSave.set(replicant, promise);
await promise;
}
catch (error) {
replicant.log.error("Failed to persist value:", (0, errors_1.stringifyError)(error));
}
finally {
this._pendingSave.delete(replicant);
}
}
_attachToSocket(socket) {
socket.on("replicant:declare", (data, cb) => {
log.replicants("received replicant:declare", JSON.stringify(data, undefined, 2));
try {
const replicant = this.declare(data.name, data.namespace, data.opts);
cb(undefined, {
value: replicant.value,
revision: replicant.revision,
schema: replicant.schema,
schemaSum: replicant.schemaSum,
});
}
catch (e) {
if (e.message.startsWith("Invalid value rejected for replicant")) {
cb(e.message, undefined);
}
else {
throw e;
}
}
});
socket.on("replicant:proposeOperations", (data, cb) => {
log.replicants("received replicant:proposeOperations", JSON.stringify(data, undefined, 2));
const serverReplicant = this.declare(data.name, data.namespace, data.opts);
if (serverReplicant.schema &&
(!("schemaSum" in data) || data.schemaSum !== serverReplicant.schemaSum)) {
log.replicants("Change request %s:%s had mismatched schema sum (ours %s, theirs %s), invoking callback with new schema and fullupdate", data.namespace, data.name, serverReplicant.schemaSum, "schemaSum" in data ? data.schemaSum : "(no schema)");
cb("Mismatched schema version, assignment rejected", {
schema: serverReplicant.schema,
schemaSum: serverReplicant.schemaSum,
value: serverReplicant.value,
revision: serverReplicant.revision,
});
}
else if (serverReplicant.revision !== data.revision) {
log.replicants("Change request %s:%s had mismatched revision (ours %s, theirs %s), invoking callback with fullupdate", data.namespace, data.name, serverReplicant.revision, data.revision);
cb("Mismatched revision number, assignment rejected", {
value: serverReplicant.value,
revision: serverReplicant.revision,
});
}
this.applyOperations(serverReplicant, data.operations);
});
socket.on("replicant:read", (data, cb) => {
log.replicants("replicant:read", JSON.stringify(data, undefined, 2));
const replicant = this.declare(data.name, data.namespace);
if (typeof cb === "function") {
if (replicant) {
cb(undefined, replicant.value);
}
else {
cb(undefined, undefined);
}
}
});
}
}
exports.Replicator = Replicator;
//# sourceMappingURL=replicator.js.map