rockbed
v1.0.1
Published
Small TypeScript primitives for results, events, disposables, async helpers, JSON, assertions, responses, logging, and retries.
Maintainers
Readme
rockbed
rockbed is a small TypeScript primitives package extracted from shared CLI/runtime code.
Version 0.2.0 moves the error API to Result-style names and removes unused helper modules.
For 0.1.x migration details, see MIGRATION.md.
import { Result, err, ok } from 'rockbed/error';
import { Emitter } from 'rockbed/event';Module Map
| Import path | Main exports | Use it for |
| --- | --- | --- |
| rockbed/assert | assert, assertDefined, assertNever, unreachable | Runtime invariants and TypeScript narrowing. |
| rockbed/async | wait, Barrier, defer, withTimeout, pollUntil | Timers, promise coordination, polling, and timeout envelopes. |
| rockbed/dispose | IDisposable, EmptyDispose, Disposable, DisposableStore, MutableDisposable, toDisposable | Ownership and cleanup of resources or subscriptions. |
| rockbed/error | Result<T>, IOk<T>, IErr, ok, err, errFrom, defineError, isResult, runSafe, cancelledError, timeoutError, interruptedError | Typed result objects instead of throwing for expected failures. |
| rockbed/event | Emitter, Event, listenOnce, listener error handlers | Lightweight typed event emitters that return disposables. |
| rockbed/glassbox | installGlassbox, getGlassbox, registerGlass, GlassboxHost, check, makeDiagnosis, toSerializable, RingBuffer | Runtime introspection for humans and AI agents. |
| rockbed/json | safeJsonParse, safeJsonStringify | JSON helpers that never throw. |
| rockbed/keyboard | KeyboardShortcutManager, KeyboardShortcutScope, CompositeKeyboardShortcutScope, describeKeyboardShortcut, isEditableEventTarget | Platform-aware keyboard shortcuts: single chords, multi-key sequences, keydown/keyup, code-based matching, IME-safe guards, and introspectable metadata. |
| rockbed/limit | ConcurrencyLimiter, createLimiter, createSerialQueue, SingleFlight, createSingleFlight | Concurrency caps, serial queues, and same-key request de-duplication. |
| rockbed/logger | logger, createLogger, setLogLevel, addLogSink, LoggerHub, consoleSink, registerLoggerGlass | Structured, leveled logging that can feed glassbox. |
| rockbed/mobx-store | MobxStore, MobxStoreOptions, MobxStoreMutation | Manager-owned MobX state with shallow observable views and Immer mutations. Best for small, flat coordination state — see usage boundaries. |
| rockbed/response | IResponse<T>, makeSuccessResponse, makeErrorResponse | Simple { code, msg, data } response objects. |
| rockbed/retry | retry, IRetryOptions, RetryJitter | Retry with exponential backoff, jitter, and total timeout. |
Result Pattern
import { Result, defineError, err, ok } from 'rockbed/error';
const invalidPort = defineError(400, 'port must be an integer');
function parsePort(value: string): Result<number> {
const port = Number(value);
if (!Number.isInteger(port)) {
return invalidPort();
}
if (port <= 0) {
return err(400, 'port must be positive');
}
return ok(port);
}Every result keeps the runtime fields ok, code, msg, value, and pair():
const [error, value] = parsePort('8080').pair();
if (error) {
throw new Error(error.toString());
}
console.log(value);Dispose Pattern
import { Disposable, toDisposable } from 'rockbed/dispose';
class Session extends Disposable {
start() {
this._register(toDisposable(() => console.log('cleanup')));
}
}Disposable.dispose() is idempotent. DisposableStore.clear() disposes every child and throws an AggregateError only after all cleanup has been attempted.
Async Pattern
import { Barrier, pollUntil, wait, withTimeout } from 'rockbed/async';
await wait(100);
const barrier = new Barrier();
queueMicrotask(() => barrier.open());
await barrier.wait();
const timed = await withTimeout(fetch('/api/profile'), 1000);
if (!timed.ok) {
console.error(timed.msg);
}
const ready = await pollUntil(async () => (await fetchStatus()) === 'done', {
interval: 500,
timeout: 30_000,
});Use platform AbortController / AbortSignal for cancellation.
Optional MobX Store
rockbed/mobx-store is a separate subpath and is not re-exported from rockbed.
Consumers that use it must install its optional peers:
pnpm add mobx immerConsumers that only import other rockbed subpaths do not need those packages.
Usage boundaries
MobxStore keeps an immutable snapshot plus a shallow observable view, and every
write rebuilds the top-level snapshot object. Keep the following in mind:
- Keep the state root small and flat; each write is at least O(top-level keys).
- Store large or complex payloads (big lists,
Map/Sets, trees, buffers) as atomic references under a key and swap them wholesale — do not deep-mutate them. - Avoid
{ deep: true }for large object graphs; it proxies every nested node up front and re-wraps reassigned branches on each commit. - Park very large or high-churn data outside the store and keep only a handle, id, or derived summary in it.
It is coordination state, not a database. For naturally huge or hot-path-heavy data, use several small stores or an external structure instead.
Retry And Limit
import { createLimiter, SingleFlight } from 'rockbed/limit';
import { retry } from 'rockbed/retry';
const profile = await retry(() => fetchProfile(uid), {
retries: 5,
minDelay: 100,
maxDelay: 5000,
timeout: 15000,
shouldRetry: (error) => error.code === 1001,
});
const limiter = createLimiter(4);
const results = await Promise.all(urls.map((url) => limiter.run(() => fetch(url))));
await limiter.onIdle();
const flight = new SingleFlight();
const [a, b] = await Promise.all([
flight.run('user:42', () => loadUser(42)),
flight.run('user:42', () => loadUser(42)),
]);Glassbox And Logger
rockbed/glassbox installs a predictable introspection root on globalThis.__glass.
Register capabilities where the state lives:
import { check, makeDiagnosis, registerGlass } from 'rockbed/glassbox';
const handle = registerGlass({
name: 'player',
describe: '主播放器的实时状态、最近事件与健康自检',
snapshot: () => ({
state: player.paused ? 'paused' : 'playing',
currentTime: player.currentTime,
duration: player.duration,
}),
diagnose: () =>
makeDiagnosis([
check('currentTime in range', player.currentTime <= player.duration + 0.1),
]),
});
handle.dispose();rockbed/logger is structured, leveled logging and can expose recent logs through glassbox:
import { createLogger, setLogLevel } from 'rockbed/logger';
const log = createLogger('player');
log.info('connected', { id: 42 });
log.error('decode failed', error, { src });
setLogLevel('debug');Keyboard
rockbed/keyboard binds shortcuts to a single event target (defaults to window).
Handlers register as disposables; activate() / deactivate() bind and unbind the
listeners. Use primary for the platform command key (meta on mac, ctrl elsewhere).
import { KeyboardShortcutManager } from 'rockbed/keyboard';
const shortcuts = new KeyboardShortcutManager({ platform: 'darwin' });
// Single chord. In editable fields (input/textarea/contentEditable) and during
// IME composition it stays inert unless you opt in with allowEditableTarget /
// allowComposition. Non-text inputs (checkbox, radio, range, ...) never block.
shortcuts.registerShortcut({
id: 'save',
description: 'Save file',
category: 'File',
key: 's',
modifiers: ['primary'],
run: () => save(),
});
// Multi-key sequence (VS Code style). Resets after `sequenceTimeout` (default 1000ms).
shortcuts.registerShortcut({
id: 'save-all',
sequence: [
{ key: 'k', modifiers: ['primary'] },
{ key: 's', modifiers: ['primary'] },
],
run: () => saveAll(),
});
// Layout-independent matching via `code`, and keyup handling.
shortcuts.registerShortcut({ id: 'fold', code: 'BracketLeft', modifiers: ['primary'], run: fold });
shortcuts.registerShortcut({ id: 'pan-stop', key: ' ', eventType: 'keyup', run: stopPan });run may return false to signal "not handled", letting the manager fall through
to the next matching shortcut. Registering an id again replaces the previous one.
Build help sheets / command palettes from the registry:
for (const info of shortcuts.describeShortcuts()) {
// { id, description, category, kind, eventType, keys } e.g. keys: '⌘K ⌘S'
render(info.description, info.keys);
}Group independent managers with CompositeKeyboardShortcutScope to toggle them together.
Precedence for a key event: an in-flight sequence is consumed by that sequence first,
then chord/match shortcuts are tried newest-first (with fallthrough), then sequence
starts. A standalone chord equal to a sequence's first chord shadows that sequence, so
avoid overlapping them. Across managers sharing one target, a shortcut fires once only
because handled events call preventDefault(); a shortcut with preventDefault: false
can be handled by more than one manager.
Publishing
pnpm typecheck
pnpm build
pnpm publish --registry=https://registry.npmjs.orgThe npm package includes dist, src, and this README. Keeping source files in the published package makes the available primitives easier for humans and AI coding agents to inspect.
