@aglyn/tenant-runtime

中文文档
1.18.0发布于 2026/11/07Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 1.2万

The server-side runtime that serves a published Aglyn site: the host-event seam that plugins listen on, and the screen-composition pipeline that turns a stored screen into the node tree a page renders. Install it if you are running Aglyn's tenant server,

原始使用文档
文档目录6 节
  1. Install
  2. What's in it
  3. Usage
  4. The render path never awaits unbounded I/O
  5. How it fits
  6. License

@aglyn/tenant-runtime

The server-side runtime that serves a published Aglyn site: the host-event seam that plugins listen on, and the screen-composition pipeline that turns a stored screen into the node tree a page renders. Install it if you are running Aglyn's tenant server, or writing plugin server code that needs to emit host events or compose a screen.

Published to npm from the Aglyn monorepo. Versions follow semantic versioning.

Install

npm install @aglyn/tenant-runtime

Peer dependencies:

  • firebase-admin

This package is server-only. It reads Firestore through firebase-admin and must not be imported from client code.

What's in it

From the package root, the host-event runtime. It hosts the event seam that no single plugin owns, and runs none of what an event triggers:

  • emitHostEvent is the one emit point. It hands an event to every registered host-event listener and returns any site alerts, so request/response emitters (a form submission, a booking) can surface them.
  • registerHostEventListener is how a plugin hears host events. A plugin registers from its server declarations entry, which the apps run at boot, so a core route that never loads the plugin still reaches it. The automation engine is one such listener, and lives in its own plugin.
  • dispatchHostAutomation runs one automation by id for a site event the published page evaluated itself, through the listeners that dispatch.
  • listHostEventListeners and runHostEventListeners for diagnostics and direct use.

Contact capture and owner assignment are the CRM's (@aglyn/plugins-crm/server/capture-host-contact, @aglyn/plugins-crm/server/assign-contact-owner): another plugin reports a person through the platform's capture contract (plugin-contact-capture). Finding a dataset by its id or name is the data plugin's (@aglyn/plugins-data/server/resolve-dataset).

By subpath, the screen-composition pipeline. This is the render read-path shared by the tenant app's server rendering and any plugin that needs to compose a screen on the server:

  • @aglyn/tenant-runtime/get-screen (getScreen) and @aglyn/tenant-runtime/get-screen-version (getScreenVersion)
  • @aglyn/tenant-runtime/compose-screen-nodes (composeScreenNodes, composeNodesWithChrome)
  • the loaders behind them: get-layout-version, get-components, get-variables, get-forms, get-plugin-installs — a repeat's rows come from the plugin that keeps them, through @aglyn/aglyn/plugin-manager/repeat-rows
  • page composers for collection, author and search pages: compose-collection-page, compose-author-page, compose-search-page
  • apply-publish-schedule (applyDuePublishSchedule)

Each subpath maps to a file under src/lib.

Usage

Listening to host events from a plugin's server code:

import { registerHostEventListener } from '@aglyn/tenant-runtime'

registerHostEventListener('my-plugin', {
  async onEvent(hostId, event, payload, context) {
    // react to the event; optionally return site alerts.
    // `context.actor` is who caused it, when the emitting door knew:
    // { kind: 'member' | 'visitor' | 'apiKey' | 'platform', uid?, email?, apiKeyName? }
  },
})

Raising one, with who caused it:

await emitHostEvent(hostId, 'contactStageChanged', payload, {
  actor: { kind: 'member', uid, email },
})

Loading a screen on the server:

import { getScreen } from '@aglyn/tenant-runtime/get-screen'

const screen = await getScreen({ hostId, screenId })

The render path never awaits unbounded I/O

A page render, and everything it awaits — the layouts, the page-data loader, this pipeline, plugin page enrichers, resolvers, redirect resolvers, repeat readers and computed variables, and the middleware in front of them — never waits on outbound network I/O without a deadline of its own (AGL-3565).

  • Wrap the await in boundedAwait(work, ms, fallback, label) from @aglyn/shared-util-http/bounded-await: a real timer race that answers with fallback when ms passes, whatever the work does. Pass its signal to fetch so the request is canceled too, and put the body read inside the work. An AbortSignal on its own is not a deadline: on Vercel, Next's patched fetch handed back a promise that never settled with AbortSignal.timeout(2500) on it, and every uncached page on the platform waited for the 60-second function limit.
  • Pick a fallback the page can render with, and say what it means: the linked font stylesheet, the verdict "unreachable", a redirect rule held.
  • A process cache that outlives a request holds settled values only (createSettledValueCache, which refuses a promise). A cached in-flight promise is awaited by every later render, each with a fresh deadline in front of nothing.
  • Work nothing needs the answer of is detached with void, never awaited.

apps/tenant/specs/render-path-bounded-io.spec.ts sweeps every symbol a render can reach for a fetch, a fetch passed on as a value, a token exchange or http(s).request, and refuses one that is not behind boundedAwait or void. Its allowlist carries a reason per row and may only shrink. Firestore reads through firebase-admin are outside it: they carry the client's own deadlines.

How it fits

This is the tenant scope of the package map. It depends only on @aglyn/aglyn, @aglyn/tenant-data-admin (its server data layer) and firebase-admin, so it is importable from the tenant app's API routes and from a plugin's server handlers without pulling app code into a library. A plugin may import it; it never imports a plugin. Plugins reach it by registering a listener, and the runtime knows them only through that registration and the loader manifests.

License

Apache-2.0. Source: https://github.com/aglyn/aglyn/tree/main/libs/tenant/runtime

版本与发布记录

@aglyn/tenant-runtime 共发布 152 个版本,首次发布于 2026/09/21;过去 12 个月发布了 152 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.18.0latest2026/11/07今天
1.17.02026/11/07今天
1.16.02026/11/07今天
1.15.22026/11/061 天前
1.15.12026/11/061 天前
1.15.02026/11/061 天前

其他发布通道:beta → 1.0.0-beta.296。

关于 @aglyn/tenant-runtime 的常见问题

@aglyn/tenant-runtime 是什么?

The server-side runtime that serves a published Aglyn site: the host-event seam that plugins listen on, and the screen-composition pipeline that turns a stored screen into the node tree a page renders. Install it if you are running Aglyn's tenant server,

如何安装 @aglyn/tenant-runtime?

在项目目录运行 npm install @aglyn/tenant-runtime,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:firebase-admin(^14.3.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@aglyn/tenant-runtime 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@aglyn/tenant-runtime 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@aglyn/tenant-runtime 可以商用吗?

@aglyn/tenant-runtime 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@aglyn/tenant-runtime 的下载量有多少?

根据 npm 官方下载统计,@aglyn/tenant-runtime 在 2026/10/05 – 2026/11/03 这 30 天内被下载 12,388 次,日均约 413 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@aglyn/tenant-runtime 还在维护吗?

最新版本 1.18.0 发布于 2026/11/07(今天);至今共发布 152 个版本,其中过去 12 个月发布了 152 个;首次发布于 2026/09/21。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。