attach-primitive
Contract and invariants for `attach*` composition primitives in `packages/workspace` (side-effectful building blocks like attachIndexedDb, attachSqlite, attachBroadcastChannel, attachBunSqliteMaterializer, attachMarkdownMaterializer, openCollaboration), and when to use `create*` (pure construction) instead. Use when writing or reviewing an `attach*` or `create*` function, naming a new workspace primitive, composing inside a workspace builder, or deciding whether a primitive registers listeners at call time.
Works with
---
name: attach-primitive
description: Contract and invariants for `attach*` composition primitives in `packages/workspace` (side-effectful building blocks like attachIndexedDb, attachSqlite, attachBroadcastChannel, attachBunSqliteMaterializer, attachMarkdownMaterializer, openCollaboration), and when to use `create*` (pure construction) instead. Use when writing or reviewing an `attach*` or `create*` function, naming a new workspace primitive, composing inside a workspace builder, or deciding whether a primitive registers listeners at call time.
license: MIT
---
# Attach Primitives
Every persistence, sync, materializer, and binding in `packages/workspace` (plus session-shaped primitives in `packages/cli`) follows one shape. Match the invariants exactly.
## Naming
| Prefix | Meaning |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `attach*` | Side-effectful. Registers observers, destroy listeners, or subscription state **onto a subject argument**. Returns a plain object whose surface is fixed at call time. |
| `create*` | Pure construction OR module-singleton bootstrap. No subject argument. Cache constructors qualify (e.g. `createFileContentDocs` returns a `createDisposableCache` result; nothing attaches until `.open(id)` is called). Module-singleton factories that bootstrap themselves at construction time (e.g. `createManualRecorder` registering a global event listener) also use `create*`: the side effects belong to the singleton itself, not to an external subject. |
Both return plain objects. The distinction is **whether the call modifies a subject argument**, not just whether side effects fire at call time.
### Scope of this contract
These rules apply to `packages/workspace` primitives and other functions that take a *subject* (a Y.Doc, an attachment, or a comparable composable target) and decorate it. They do **not** apply to:
- Module-level factory singletons in app code (e.g. UI state containers, service clients). Even if those factories perform I/O at construction time, use `create*`.
- Top-level orchestrators that own their own lifecycle and aren't attached to anything external.
The discriminator is **"what is being attached to what?"** If there's no `subject` on the left side of that question, it's `create*`.
## The shape
```ts
export function attachX(subject: TSubject, opts: XOptions): XAttachment;
```
**One rule.** The first argument is the subject being modified. The subject is almost always a `Y.Doc`. In the rare case where a primitive operates on an existing attachment rather than a ydoc, the subject is that attachment.
Examples of the common form:
```ts
attachIndexedDb(ydoc) // Y.Doc subject
attachSqlite(ydoc, { filePath })
openCollaboration(ydoc, { url, openWebSocket, replicaId, actions })
attachBroadcastChannel(ydoc)
attachAwareness(ydoc, defs)
attachRichText(ydoc) / attachPlainText(ydoc) / attachTimeline(ydoc)
```
Table and KV stores are no longer attached one-by-one. They are constructed as a bundle by `createWorkspace({ id, tables, kv })`, which owns the Y.Doc's lifecycle and exposes `workspace.ydoc`, `workspace.tables`, and `workspace.kv`. Pass `workspace.ydoc` into the remaining `attach*` primitives; pass the whole `workspace` bundle (the `{ ydoc, tables }` pair) into the materializers.
**Materializers** take the `workspace` bundle as the subject (not a bare ydoc): `attachX(workspace, opts)`. They read `workspace.tables` themselves; you do not pass a separate `tables` slot. The two materializer families select what to mirror differently, on purpose:
- **SQLite** mirrors *every* table in `workspace.tables`. A full queryable mirror is cheap and wanted, so there is no selection slot. `fts` is optional per-table config, keyed by table name.
- **Markdown** mirrors a *human-facing subset*. Dumping every internal table as `.md` files is unwanted, so selection is required: `perTable[name]` presence selects the table, and its value configures it. A table with no `perTable` entry is skipped. Pass `{}` to mirror a table with all defaults.
```ts
attachMarkdownMaterializer(workspace, {
dir,
waitFor,
perTable: {
// Presence selects; the value configures. Tables absent here are skipped.
files: {
filename: slugFilename('title'),
// Most real tables store body content in a separate Y.Doc (via
// createDisposableCache), so toMarkdown / fromMarkdown are typically
// bespoke callbacks; no sugar helper can abstract the async
// open/await/dispose cycle usefully.
toMarkdown: async (row) => {
using doc = fileContentDocs.open(row.id);
await doc.whenReady;
return { frontmatter: { id: row.id, name: row.name }, body: doc.content.read() };
},
},
devices: {}, // selected, all defaults
},
});
attachBunSqliteMaterializer(workspace, {
filePath,
waitFor,
fts: { posts: ['title'] },
});
```
Per-table customization lives in `perTable: { [tableName]: { ... } }` (markdown) and FTS column opt-in lives in `fts: { [tableName]: ColumnKey[] }` (SQLite). Both slots narrow against `keyof workspace.tables`, so keys autocomplete and typos error at the call site.
The SQLite result surfaces FTS on a nested namespace: `sqlite.fts.search({ table, query })` exists when `fts: {...}` was passed; when omitted, `sqlite.fts` is absent from the return type entirely. Single attach call, single `whenFlushed` barrier; the FTS DDL and triggers run between table DDL and the bulk insert so triggers populate `<table>_fts` for free.
## Non-Ydoc Subject (rare)
When a primitive operates on a sibling attachment rather than the Y.Doc itself (cross-package coordination, for example), the subject is that attachment and the call shape stays `attachX(subject, opts)`. No examples currently in the repo; the pattern is documented in case it ever surfaces.
## Constructor bundles (the workspace case)
When several sibling handles must be constructed atomically (one Y.Doc and N
stores activated together), that work lives in `createWorkspace`, not in an
`attach*` primitive. The factory takes definition records as named slots and
returns the constructed handles as a bundle:
```ts
const workspace = createWorkspace({
id: 'my-app',
tables: { posts },
kv: {},
});
workspace.ydoc; // Y.Doc bundled in
workspace.tables.posts; // constructed table helper
workspace.kv; // constructed KV bag
workspace[Symbol.dispose](); // cascades to ydoc.destroy()
```
The workspace bundle owns the stores' lifecycle: `using workspace =
createWorkspace(...)` triggers cascade disposal. One call gives atomic
registration, with no temporal window for mid-session attachment.
The materializer primitives are the remaining "subject in, constructed handle out" `attach*` shape: see the materializer block above. They take the `workspace` bundle as the subject, read `workspace.tables` themselves, and accept a `perTable` (markdown) or `fts` (SQLite) sibling slot for per-table customization.
## Invariants
1. **Synchronous return.** Construction never awaits. Startup work goes into semantic `when*` promises on the returned object. Genuine async teardown exposes a `whenDisposed` promise field resolved from the `ydoc.destroy()` cascade.
2. **Teardown hooked to the subject's lifecycle.**
- Y.Doc subject: `ydoc.once('destroy', ...)`. Never expose a `.destroy()` method on the attachment.
- Attachment subject: use the subject attachment's disposal signal; or no teardown if there are no listeners.
3. **Idempotent cleanup.** If the underlying library also registers a destroy handler (like `y-indexeddb`), your handler must be safe to run alongside it.
4. **Plain data returned.** The attachment is a record of promises, functions, and occasionally mutable state. No ES classes, no getters that lazy-init.
5. **No id option on ydoc-bound primitives.** `ydoc.guid` is the identity. Read it off the doc.
6. **Barrier naming is semantic, not mechanical.** Pick the name that describes the actual event:
- `whenLoaded`: local state replayed into the ydoc (IDB, SQLite)
- `whenConnected`: remote transport up + first exchange done (sync)
- `whenChecked`: configuration action settled (session-unlock; resolves even if nothing was applied)
- `whenFlushed`: initial side-effect pass done (materializer)
- `whenReady`: bundle-level aggregate only; not on individual attachments
## Composition inside a workspace builder
Primitives compose inside a build closure:
```ts
const cache = createDisposableCache((id: string) => {
const workspace = createWorkspace({
id,
tables: schema,
kv: kvDefs,
});
const { ydoc, tables, kv } = workspace;
const idb = attachIndexedDb(ydoc);
const collaboration = openCollaboration(ydoc, {
url, openWebSocket, replicaId, actions,
waitFor: idb.whenLoaded,
});
const markdown = attachMarkdownMaterializer(workspace, {
dir,
waitFor: collaboration.whenConnected,
perTable: { posts: { filename: slugFilename('title') } },
});
return {
workspace, ydoc, tables, kv, idb, collaboration, markdown,
whenReady: Promise.all([idb.whenLoaded, collaboration.whenConnected]),
async wipe() {
workspace[Symbol.dispose]();
await collaboration.whenDisposed;
await idb.whenDisposed;
await idb.clearLocal();
},
[Symbol.dispose]() { workspace[Symbol.dispose](); },
};
});
export const bundle = cache.open('my-app');
```
The bundle aggregates child `whenLoaded` / `whenConnected` / `whenChecked` into one `whenReady`. Browser bundles expose `wipe()` for reset flows that must dispose and delete local storage in the right order. Daemon bundles expose `[Symbol.asyncDispose]()` as the trigger and await attachment `whenDisposed` barriers before process exit.
## The `waitFor` convention
Primitives that perform a gated startup (collaboration, session-unlock) accept `waitFor?: Promise<unknown>` in their options. The primitive awaits it before taking its first action. This replaces the old extension-chain "init pipeline": sequencing is now explicit at the call site, visible in one file, with no hidden ordering.
Use it whenever a primitive's startup must follow another's. Examples:
- `openCollaboration` after local hydrate: `waitFor: idb.whenLoaded`
- `attachSessionUnlock` after hydrate (so stored keys don't clobber freshly-hydrated plaintext mid-replay): `waitFor: persistence.whenLoaded`
- `openCollaboration` after both hydrate AND unlock: `waitFor: Promise.all([idb.whenLoaded, unlock.whenChecked])`
## Anti-patterns
- **Don't revive `ExtensionContext` / `RawExtension` / `defineExtension`.** Those were deleted for a reason: the lifecycle framework added a registration indirection that primitives don't need.
- **Don't wrap attachments in a `createWorkspace().with(...)` chain.** Compose inline in the factory.
- **Don't expose `dispose()` on a ydoc-bound attachment.** Destroy the Y.Doc.
- **Don't duck-type an attachment.** If you need to brand it, use a `Symbol.for` marker. See `skills/typescript`: runtime shape-checking is a code smell.
- **Don't take an `id` on a ydoc-bound primitive.** Use `ydoc.guid`.
- **Don't use `createX` for a side-effectful primitive that takes a subject argument.** If it registers listeners on a subject passed in, it's `attach*`. Module-singleton factories that bootstrap themselves are still `create*`; see scope section.
- **Don't attach tables or KV stores one-by-one on a raw Y.Doc.** Construct them as a bundle via `createWorkspace`; pass `workspace.ydoc` into the remaining `attach*` primitives.
## Reference implementations
- `packages/workspace/src/document/attach-indexed-db.ts` ; the canonical 40-line example.
- `packages/workspace/src/document/open-collaboration.ts` ; document collaboration surface with sync, presence, peers, and action dispatch.
- `packages/workspace/src/create-workspace.ts` ; the bundle factory; takes `{ id, tables, kv }` and returns `{ ydoc, tables, kv, [Symbol.dispose] }` after one atomic construction.
- `packages/workspace/src/document/materializer/markdown/materializer.ts` ; `workspace` subject, `perTable` presence both selects and configures, keyed by table name.
- `apps/whispering/src/lib/client.ts`: full singleton composition.More Accessibility skills
skill-creator
anthropics/skills
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.
hyperframes-core
heygen-com/hyperframes
The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class="clip"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Also covers Tailwind projects and the STORYBOARD.md / SCRIPT.md plan formats. Read before writing composition HTML.
ui-ux-pro-max
nextlevelbuilder/ui-ux-pro-max-skill
UI/UX design intelligence for web, mobile, and desktop. This skill should be used when designing, building, reviewing, or fixing interfaces, including pages, components, design systems, accessibility, interaction, responsive layout, typography, color, charts, and stack-specific UI implementation. Searchable local data: 79 searchable styles (50 active), 192 product palettes and reasoning profiles, 74 font pairings, 119 UX guidelines, 105 icons, 17 GSAP presets, 25 chart types, and 22 stacks.

