@columenlabs/lumen-person

中文文档
0.1.219发布于 2026/11/09MITTypeScriptES Module零依赖近 30 天下载 1.4万

Lumen's one store per person: a Durable Object whose SQLite holds every app's tables for that person, with migrations that fail per app, one alarm shared by every app, and backup, restore and delete per app or whole.

原始使用文档
文档目录6 节
  1. Install
  2. A store and its modules
  3. Row budgets: no loop can spend the account's day
  4. Rules it keeps
  5. Backup, restore, delete
  6. Moving rows in from an older store

@columenlabs/lumen-person

Lumen's one store per person: a Durable Object whose SQLite holds every app's tables for that person. Each app is a module that owns its tables (<module>_*), its migrations and its wake time, so app code stays apart while one person's data joins in one SQL query, one backup and one delete. No dependencies: it runs in a Worker with nodejs_compat (for AsyncLocalStorage) and any Durable Object with SQLite storage.

Install

bun add @columenlabs/lumen-person

Pin it to the same exact version as the other Lumen packages.

A store and its modules

import { DurableObject } from 'cloudflare:workers';
import { PersonObject, type PersonModule, personModule } from '@columenlabs/lumen-person';

const notes: PersonModule<Env> = {
  name: 'notes',
  migrations: [
    'CREATE TABLE notes_items (id INTEGER PRIMARY KEY, title TEXT NOT NULL, at INTEGER NOT NULL)',
  ],
  async call(ctx, method, args) {
    if (method === 'add') {
      ctx.sql.exec('INSERT INTO notes_items (title, at) VALUES (?, ?)', (args as { title: string }).title, Date.now());
      await ctx.wakeAt(Date.now() + 60_000);
      return { ok: true };
    }
    throw new Error(`no ${method}`);
  },
  async wake(ctx) {
    // small steps: stop by ctx.deadline and ask for the next time
  },
};

export class PersonStore extends DurableObject<Env> {
  private readonly person = new PersonObject(this.ctx, { modules: [notes], extra: this.env });
  call(owner: string, module: string, method: string, args?: unknown) {
    return this.person.call(owner, module, method, args);
  }
  alarm() {
    return this.person.alarm();
  }
}

// In the Worker
const mine = personModule(env.PERSON, email, 'notes');
await mine.call('add', { title: 'Buy saw blades' });

The first call names the person and claims the store; a later call for anyone else is refused (403).

Row budgets: no loop can spend the account's day

Cloudflare's free plan allows an account 5M Durable Objects rows read and 100k rows written a day, across every object. Past either, every SQL call in every object throws until 00:00 UTC. Every store is bounded (budget in its deps, RowBudget in ./budget for any other object):

BoundDefaultPast it
One call100k rows read, 20k writtenits next statement throws; the call answers 503
One wake200k read, 20k writtenthe same, for that wake
The store's UTC day600k read, 12k writtencalls answer 503 until 00:00 UTC
background share of the day0.6wakes wait for the next day (spread over its first half hour); calls keep working

Counts are the cursors' own rowsRead and rowsWritten, what Cloudflare bills. onLimit hears each level once a day (alert someone there). _object.budget reads today's totals and _object.brake {level: 'background' | 'stop' | 'ok', until?} sets a brake from outside, for when the whole account nears its cap; both answer on a spent day. A request that needs more than one call's budget (an import, a rebuild) is a background job in small wakes, never one request.

Rules it keeps

  • Migrations fail per module. Each module migrates on its first call or wake, each step in one transaction. A step that throws leaves only that module answering 503 for a minute before it tries again; every other module keeps working. Never edit or reorder a released step: add one.
  • One alarm for every module. _wakes keeps each module's next time and the store's alarm is the earliest. Each wake gets ctx.deadline (30 s by default, wakeMs) to stop by and set its next time. A wake that throws is tried again in 1 minute, doubling to an hour.
  • Waiting is shared, work is not. While one call awaits the network (a fetch, a model) other calls run. Only synchronous work holds the store, so keep heavy parsing out of it.
  • Calls stay in place. Code running in a person's store that calls the same person through personCall runs in place instead of a new request.

Backup, restore, delete

The store's own module _object (or personModule(...).own(method) for one module):

MethodDoes
dumpEvery module's tables as stored (sealed values stay sealed), with each module's migration level
dumpModule {module}One module's tables
loadModule {module, dump, prefix?}Replaces one module's tables from a dump. With prefix, tables named without it (items) load into <module>_items, so rows move in from an older store
loadReplaces every module's tables from a whole dump
wipeModule {module}Empties one module's tables and its wake
wipeDeletes the whole store (every table, key and the alarm)
countsRows per table, migration levels and any module whose migration is failing

Moving rows in from an older store

An app that kept its own Durable Object moves into the person's store a page at a time, so no step holds either store for long:

  • The old store answers exportRows(sql, table, after): rows after a rowid bookmark, at most 500 rows and about 4 MB a page (at least one row, so a big blob still moves), bytes as {b64}.
  • The module's wake writes each page with importRows(sql, table, page) inside ctx.transaction, keeps the bookmark and asks to wake again. Rows keep their rowids, so a page sent twice changes nothing.
  • When every table is across, rowStats on both sides (rows and highest rowid per table) must match before the app reads from the new store. Stop the old store's alarm and writes while it moves; a table whose stats differ goes again.
  • An old store that held many people's rows in one table moves each person's part: exportRows(sql, table, after, { where: { sql: 'scope = ?', args: [email] } }) and rowStats(sql, tables, where) with the same filter.

Point-in-time recovery (ctx.storage.getBookmarkForTime) restores the whole store, every module at once. Restore one module from its dump instead, and keep PITR for the whole store.

Virtual tables (an FTS5 word index from @columenlabs/lumen-ai's findSql) are not copied: their triggers fill them as rows load, and a rebuild runs after.

版本与发布记录

@columenlabs/lumen-person 共发布 76 个版本,首次发布于 2026/10/28;过去 12 个月发布了 76 个版本。

版本发布日期距今
0.1.219latest2026/11/091 天前
0.1.2182026/11/091 天前
0.1.2172026/11/091 天前
0.1.2162026/11/091 天前
0.1.2152026/11/091 天前
0.1.2142026/11/091 天前

关于 @columenlabs/lumen-person 的常见问题

@columenlabs/lumen-person 是什么?

Lumen's one store per person: a Durable Object whose SQLite holds every app's tables for that person, with migrations that fail per app, one alarm shared by every app, and backup, restore and delete per app or whole.

如何安装 @columenlabs/lumen-person?

在项目目录运行 npm install @columenlabs/lumen-person,或使用 pnpm add / yarn add / bun add。

@columenlabs/lumen-person 支持 TypeScript 吗?

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

@columenlabs/lumen-person 支持 ESM 还是 CommonJS?

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

@columenlabs/lumen-person 可以商用吗?

@columenlabs/lumen-person 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@columenlabs/lumen-person 的下载量有多少?

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

@columenlabs/lumen-person 还在维护吗?

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