Reutilización de Runtime y Daemon

Empieza por el ciclo de una orden CLI y descubre cómo un daemon posee un pool de Worker Runtimes y reutiliza Units cargadas entre llamadas.

Lo costoso de una orden CLI suele ser convertir una Unit1 en un Univer activo que pueda leerse y escribirse, no analizar argumentos: crear Univer, instalar plugins, abrir el backend colaborativo, cargar el punto de control, aplicar cambios y esperar el cálculo de fórmulas.

Este capítulo trata dos funciones:

  • @univer-cli/univer-collaboration-runtime: carga y opera una Unit en el proceso actual;
  • @univer-cli/univer-collaboration-runtime-pool: coloca Runtimes en Workers y los presta, reutiliza y retira según la identidad de Unit.

Para sobrevivir a varias invocaciones CLI, un Worker Runtime2 necesita un propietario duradero. Aquí lo proporciona un daemon local.

Tres ciclos de vida

Antes de reutilizar, distingue estos objetos:

ObjetoCreaciónFinPosee
Proceso CLICada ejecución de una ordenDespués de imprimir el resultadoArgumentos, una solicitud y formato de salida
Proceso DaemonPrimera solicitud o inicio directoParada, reinicio por actualización o falloPool Runtime, handlers de negocio y socket local
Worker RuntimePrimer encuentro de una clave en el poolExpulsión TTL/LRU3, invalidación, cierre daemon o fallo WorkerUn Worker, un handle backend, una Unit cargada y estado colaborativo

La CLI hace una llamada; el daemon posee el pool; el pool posee Workers; cada Worker posee un Runtime.

100%

El daemon solo reenvía solicitudes JSON definidas por la aplicación. Instantáneas, cambios y bloques se leen directamente desde el backend del Worker, sin pasar por CLI ni daemon.

Paso 1: cargar Runtime en cada orden

La implementación más simple crea @univer-cli/univer-collaboration-runtime dentro de una acción Commander y lo cierra al terminar:

TypeScript
const runtime = await runtimeFactory.load(unitId, unitType);try {  const pulled = await runtime.pull();  if (pulled.status === "conflict") throw new Error(pulled.conflict.message);  const execution = await runtime.execute({ mode: "write", code });  let commit = await runtime.commit();  if (commit.status === "pull-required") {    const repulled = await runtime.pull();    if (repulled.status === "conflict") throw new Error(repulled.conflict.message);    commit = await runtime.commit();  }  return { execution, commit };} finally {  await runtime.close();}

runtimeFactory se crea con createUniverCollaborationRuntimeFactory(). Runtime permanece ligado a una Unit desde carga hasta cierre y conserva revisión, mutaciones pendientes, cambios en espera y conflictos. El llamador también debe inspeccionar el commit final: solo confirmed y nothing-to-commit significan finalización.

load() hace más que leer un archivo. Sigue este orden:

  1. Abrir el backend colaborativo de la aplicación;
  2. Crear Headless Univer e instalar colaboración;
  3. Cargar UnitData o un punto de control Snapshot;
  4. Reproducir sus cambios;
  5. Obtener y reproducir el intervalo hasta la última revisión remota;
  6. Esperar la aplicación de los resultados más recientes de los módulos de fórmulas instalados;
  7. Devolver un Runtime listo para execute(), pull() y commit().

Una invocación puntual completa es:

100%

La propiedad es directa y un fallo no deja estado, por lo que sirve para órdenes poco frecuentes y tareas puntuales. A cambio, la siguiente orden inicia otro proceso Node.js y repite toda la carga, sin reutilizar Univer ni el estado Unit anterior.

Paso 2: alojar Runtime en un pool de Workers

@univer-cli/univer-collaboration-runtime-pool mantiene un proceso Worker y Runtime separados por clave residente. La aplicación aporta la entrada Worker; backend, credenciales y Univer se crean dentro:

TypeScript
import { defineUniverCollaborationRuntimeWorker } from "@univer-cli/univer-collaboration-runtime-pool";export default defineUniverCollaborationRuntimeWorker({  async createRuntime(init: { unitId: string; unitType: number }) {    return createRuntimeFromApplicationInit(init);  },});

La entrada debe apuntar a JavaScript ESM compilado. init se envía solo durante la creación en frío y debe admitir clonación estructurada.

No hace falta diseñar otra capa RPC: el lease ya representa las API públicas getState(), execute(), fetch(), pull(), commit() y exportUnitData():

TypeScript
import { createUniverCollaborationRuntimePool } from "@univer-cli/univer-collaboration-runtime-pool";const pool = createUniverCollaborationRuntimePool({  entry: new URL("./collaboration-worker.js", import.meta.url),});const lease = await pool.acquire({  key: targetKey,  init: { unitId, unitType },});try {  await lease.pull();  const result = await lease.execute({ mode: "read", code });  console.log(result.value);} catch (error) {  await lease.invalidate();  throw error;} finally {  await lease.release();}

key identifica el Runtime reutilizable, no es solo un argumento de consulta. La misma clave siempre debe representar la misma Unit, cadena de estado colaborativo e inicialización compatible. Una Unit normal y un borrador Worktree usan claves distintas aunque compartan unitId.

El pool tiene cuatro comportamientos principales:

  • Una clave se concede a un solo lease cada vez. Las solicitudes concurrentes esperan en FIFO y no modifican simultáneamente el mismo estado;
  • release() devuelve un Runtime fiable a la caché inactiva para la siguiente solicitud de la misma clave;
  • invalidate() indica un estado no fiable. Destruye Worker y Runtime y la siguiente solicitud carga en frío;
  • TTL y LRU pueden expulsar Runtimes inactivos. Tiempos agotados, fallos Worker y errores de protocolo también invalidan la instancia.

Si el pool se crea dentro de una CLI de corta vida, la reutilización sigue sin cruzar órdenes:

100%

Workers proporcionan aislamiento de procesos, préstamo exclusivo y terminación ante fallos. Para reutilizar entre invocaciones, el pool necesita un propietario más duradero que una orden.

Paso 3: usar Daemon como propietario duradero

El daemon es un proceso Node.js local iniciado a demanda. La CLI lo llama mediante socket Unix o canalización con nombre de Windows. Terminar la CLI no lo detiene, por lo que pool, Workers y Runtimes inactivos permanecen.

Crea el pool una vez en la entrada daemon y adquiere un lease en cada handler de negocio:

TypeScript
import { createDaemonServer, DAEMON_SOCKET_ENV } from "@univer-cli/daemon";import { createUniverCollaborationRuntimePool } from "@univer-cli/univer-collaboration-runtime-pool";const socketPath = process.env[DAEMON_SOCKET_ENV];if (!socketPath) throw new Error(`${DAEMON_SOCKET_ENV} is required`);const pool = createUniverCollaborationRuntimePool({  entry: new URL("./collaboration-worker.js", import.meta.url),  cache: { idleTtlMs: 5 * 60_000, maxEntries: 20 },});const server = createDaemonServer({  identity: { id: "my-cli", version: "1.0.0" },  socketPath,  onShutdown: () => pool.close(),});server.handle("unit.execute", async (payload) => {  const input = parseExecuteRequest(payload);  const lease = await pool.acquire({    key: input.targetKey,    init: { unitId: input.unitId, unitType: input.unitType },  });  try {    const pulled = await lease.pull();    if (pulled.status === "conflict") throw new Error(pulled.conflict.message);    const execution =      input.mode === "read"        ? await lease.execute({ mode: "read", code: input.code })        : await lease.execute({ mode: "write", code: input.code });    const commit = input.mode === "write" ? await commitWithPull(lease) : null;    return { commitStatus: commit?.status ?? null, value: execution.value };  } catch (error) {    await lease.invalidate();    throw error;  } finally {    await lease.release();  }});await server.listen();

La aplicación define parseExecuteRequest() y las reglas de targetKey. El daemon solo aporta transporte local y ciclo de vida; no conoce Units, Runtimes ni protocolo colaborativo. Su cierre debe llamar a pool.close() para mantener un propietario final claro de Workers activos e inactivos. Aquí commitWithPull() gestiona pull/commit tras pull-required y comprueba el estado final. Conservadoramente, cualquier error no manejado invalida el lease.

Una orden normal ahora envía una sola solicitud de negocio:

TypeScript
import { createDaemonClient } from "@univer-cli/daemon";const client = createDaemonClient({  entry: new URL("./daemon-entry.js", import.meta.url),  identity: { id: "my-cli", version: "1.0.0" },  socketPath,});const result = await client.request("unit.execute", {  targetKey,  unitId,  unitType,  mode,  code,});

Si no existe el socket, el cliente inicia la entrada daemon, espera el enlace de identidad y protocolo y envía la solicitud. Si hay un daemon compatible, lo reutiliza directamente.

Paso 4: comparar primera solicitud y siguientes

El primer acceso sigue la ruta fría: puede necesitar iniciar el daemon y el pool debe crear Worker y Runtime.

100%

Los accesos posteriores a la misma clave siguen la ruta caliente. El proceso CLI vuelve a arrancar, pero no repite la carga costosa:

100%

Se reutiliza un Runtime activo ya creado y cargado. No se omite la consistencia colaborativa. Normalmente debes ejecutar pull() al adquirir el lease, revisar el commit final y manejar pull-required, retry, unknown o conflict.

Costes que desaparecen y permanecen

CosteCargar directamente cada vezDaemon + pool Worker
Inicio CLI, argumentos, salidaCada vezCada vez
Solicitud/respuesta JSON por socketNingunoCada vez
Inicio daemon y módulosNingunoPrimera solicitud o reinicio
Inicio Worker y entradaSin WorkerCreación fría por clave
Crear Univer e instalar pluginsCada vezCreación fría por clave
Punto de control, cambios iniciales, espera de fórmulasCada vezCreación fría por clave
pull() de cambios durante inactividadCada vezNormalmente cada vez
execute() y commit()Cada vezCada vez
Memoria inactiva, handle backend, proceso WorkerLiberados al salirRetenidos hasta expulsión, invalidación o cierre daemon

La reutilización intercambia memoria residente y cantidad de Workers por menor latencia posterior. Elige idleTtlMs, maxEntries, tiempo de apertura y de operaciones según eventos del pool y mediciones, en lugar de guardar toda Unit indefinidamente.

Concurrencia, fallos y cierre

Cuatro reglas resumen el comportamiento:

  1. Las solicitudes de la misma clave esperan. Un Runtime pertenece a un solo lease y evita cambios entrelazados en el estado.
  2. Claves distintas trabajan en paralelo en Workers distintos. Cada clave residente requiere su propio Worker y Runtime.
  3. Solo se reutiliza estado fiable. Usa release() después del éxito y de limpiar mutaciones locales; invalidate() tras timeout, fallo, error de protocolo o estado incierto.
  4. La responsabilidad de cierre baja por niveles. La CLI termina su solicitud; el cierre daemon llama a pool.close(); el pool termina Workers y cierra Runtimes y backends.

Para órdenes poco frecuentes con latencia puntual aceptable, lo más simple es usar directamente @univer-cli/univer-collaboration-runtime. Combina daemon y @univer-cli/univer-collaboration-runtime-pool solo cuando las mediciones muestren que recargar domina el coste y se acepte el proceso residente y su memoria.

Consulta el catálogo de paquetes para la lista completa.

Footnotes

  1. Una Unit es una instancia de documento, como un libro de cálculo o un documento de texto. Una hoja forma parte de un libro y no es una Unit independiente. ↩

  2. Aquí, Runtime es la instancia activa de Univer y los recursos necesarios para operar sobre una Unit cargada, no el ejecutable de Node.js ni una instantánea guardada. ↩

  3. TTL retira un Runtime inactivo al vencer su tiempo de vida; LRU retira los menos usados recientemente cuando falta capacidad. Liberan recursos sin eliminar la Unit persistida. ↩

¿Cómo es esta guía?

© 2026 DreamNum Co., Ltd.