# Recipes

## A per-user preference that the relay cannot correlate

```ts
// WRONG — the relay can join u:<sam>/pin_doc7 to g:acme/doc7
await engine.put('u', { id: `pin_${docId}`, kind: 'pin', data: { docId } });

// RIGHT
const id = await engine.personalId('pin', docId);
await engine.put('u', { id, kind: 'pin', data: { docId } });
```

Deterministic (upsert-by-id still merges across your devices), stable through
rotation, linking and recovery, and unlinkable to any shared ID without a key
only you hold. **This is a rule apps must follow**, not an optimisation.

## A "sealed, not empty" screen

```ts
const s = engine.status.graphs['g:acme'];
if (!s || s.key === 'none') return <NotSetUp />;
if (s.key === 'waiting') return <Sealed hint="Open the app on another device to finish linking." />;
return <TheActualUi />;
```

## Showing sync state honestly

```ts
engine.on('status', () => {
  const { pending, syncing, lastError, rejected } = engine.status;
  setBadge(rejected > 0 ? 'error' : lastError ? 'offline' : syncing ? 'syncing' : pending ? `${pending} pending` : 'synced');
});
```

## Telling somebody their work might not survive

```ts
const { persisted } = engine.status.storage;
if (persisted === false && engine.status.pending > 0) {
  banner('Some changes are only on this device, and this browser may clear them.');
}
```

`persisted` is `null` wherever the Storage API is unavailable, including Node,
native shells, and older browsers. That means "we do not know", not "no"; an
app that treats it as a warning will warn every server-side agent it runs.

Persistence is requested once at `open()`, never on a timer: `persist()` may
show a permission prompt in some browsers, and a library that prompts
repeatedly is a library people uninstall. The quota estimate is re-read after
a sync, at most once a minute.

## A bulk import that does not stall

```ts
for (let i = 0; i < rows.length; i += 200) {
  await engine.putMany('g:acme', rows.slice(i, i + 200).map(toRecord));
  await engine.flush(); // keep the queue bounded
}
```

A push is bounded by bytes as well as op count, so oversized records split
automatically — but flushing in batches keeps memory flat and gives you a
progress point.

## A server-side agent

```ts
import { ArgalaEngine, HttpTransport } from '@argala/client';
import { NodeSqliteGraphStore, FileKeyStore } from '@argala/client/node';

const agent = await ArgalaEngine.open({
  memberId: 'bot',
  deviceId: 'bot-worker-1',
  transport: new HttpTransport({ baseUrl, headers: () => ({ authorization: `Bearer ${token}` }) }),
  store: new NodeSqliteGraphStore({ path: '/var/lib/bot/graph.db' }),
  keyStore: new FileKeyStore('/var/lib/bot/keys'),
  kinds,
});
```

The agent is a device like any other: grant it, revoke it, rotate it out. It
holds real keys, so its disk is a real trust boundary.

## Testing an app built on argala

```ts
const engine = await ArgalaEngine.open({
  memberId: 'sam',
  deviceId: 'sam-test',
  transport: new InjectTransport(app), // or HttpTransport against a test server
  store: new MemoryGraphStore(),
  keyStore: new MemoryKeyStore(),
  kinds,
  pushDebounceMs: 0,
});
await engine.openGraph('u', { create: true });
```

`MemoryGraphStore` + `MemoryKeyStore` + a relay on an ephemeral port gives you
a full deployment per test in a few milliseconds. Always `await engine.flush()`
before asserting on what the relay holds.

---
