Now I have a complete picture. Let me analyze the current implementation and propose optimizations based on capnweb's capabilities.
## Current Implementation Analysis
### HTTP Mode (transport === "http"):
1. **create**: POST to `/api/share` with sessionID, then sync separately
2. **sync**: POST to `/api/share/{id}/sync` with secret and data
3. **delete**: DELETE to `/api/share/{id}` with secret
### RPC Mode (transport === "rpc"):
1. **create**: Call `session.createShare(sessionID, initialData)` - sends everything in one call
2. **sync**: Call `session.syncShare(shareID, secret, items)`
3. **delete**: Call `session.deleteShare(shareID, secret)`
## Key Capnweb Features We Can Use:
1. **Promise Pipelining** - We can chain calls without awaiting
2. **RpcTarget** - Objects passed by reference
3. **Batch vs WebSocket** - Current code uses HTTP batch, WebSocket would be persistent
4. **No need to send full data** - If we pass references/capabilities, the server can fetch what it needs
## Optimization Opportunities:
### For `create`:
- Current: Send full snapshot in one call (good for RPC)
- Could improve: Server could have a `Sessions` capability to fetch session directly
### For `sync`:
- Current: Send full data objects every time
- Optimization: Send only references/IDs, server fetches from Sessions capability
### For `delete`:
- Current: Need shareID and secret
- Optimization: Could pass the share object itself as a capability
Let me write up a plan for theuser.
Now I understand both the capnweb capabilities and your current implementation. Let me analyze each RPC call:
Current Implementation Analysis
RPC Contract (ShareRpc):
interface ShareRpc extends RpcTarget {
createShare: (sessionID: string, initialData?: SyncData[]) => Promise<SyncInfo>
syncShare: (shareID: string, secret: string, data: SyncData[]) => Promise<{ success: boolean; syncCount: number }>
deleteShare: (shareID: string, secret: string) => Promise<{ success: boolean }>
probeValue: (input: ProbeValueInput) => ProbeValueOutput
probeCallback: (cb: ProbeCallback) => Promise<string>
}
Analysis of Each RPC Call
1. createShare(sessionID, initialData)
Current (HTTP):
// 2 round trips:
// 1. POST /api/share → returns { id, secret, url }
// 2. POST /api/share/{id}/sync → sends initialData
Current (RPC):
// 1 round trip (good!):
const result = await session.createShare(sessionID, initialData)
✅ This is already optimal for RPC. Promise pipelining lets you send create + data in one batch.
Potential optimization using capnweb capabilities:
// Instead of sending full session/message data, could pass a capability:
interface Sessions extends RpcTarget {
get(sessionID: string): Promise<SDK.Session>
getMessages(sessionID: string): Promise<SDK.Message[]>
// ...
}
// Then create becomes:
const share = await api.createShare(sessionID, sessionsCapability)
// Server can fetch what it needs via the capability
But this adds complexity. Current approach is fine.
2. syncShare(shareID, secret, data)
Current (HTTP):
// 1 round trip:
POST /api/share/{id}/sync { secret, data }
Current (RPC):
// 1 round trip:
await session.syncShare(shareID, secret, items)
🔴 Problem: Sending full data objects every sync
The data array contains full SDK.Session, SDK.Message, SDK.Part objects. These can be large.
Optimization using capnweb:
// Instead of sending full objects, send only IDs + references:
type SyncDataLightweight =
| { type: "session"; id: string }
| { type: "message"; id: string }
| { type: "part"; id: string; messageID: string }
| { type: "session_diff"; data: SDK.FileDiff[] } // diffs must be sent
| { type: "model"; ids: string[] }
// Server can fetch full objects from Sessions capability:
interface ShareRpc extends RpcTarget {
// New: pass Sessions capability so server can fetch
syncShare: (
shareID: string,
secret: string,
refs: SyncDataLightweight[],
sessions: Sessions // capability passed by reference
) => Promise<{ success: boolean }>
}
But wait - capnweb already optimizes this! When you pass a capability over RPC, it's passed by reference. The server can call methods on it.
Better approach - pass the Sessions object as a capability:
// Client has a Sessions object with all the data
const sessionsCap = new SessionsCapability(/* local session data */)
// Pass it to sync - only a reference is sent!
const result = await api.syncShare(shareID, secret, refs, sessionsCap)
The server can then call sessionsCap.getMessage(id) etc. without the client sending the full data.
3. deleteShare(shareID, secret)
Current (HTTP):
DELETE /api/share/{id} { secret }
Current (RPC):
await session.deleteShare(shareID, secret)
🔴 Issue: Need shareID and secret from database
Current code:
const share = getShare(sessionID) // DB lookup
await session.deleteShare(share.id, share.secret)
Optimization using capnweb:
If createShare returned a capability (stub), you could dispose it directly:
// Instead of:
const share = await api.createShare(sessionID, initialData)
// share is { id, secret, url } - plain data
// Could be:
const share = await api.createShare(sessionID, initialData)
// share is RpcStub<Share> - a capability
// Then delete is:
await share[Symbol.dispose]() // or share.delete()
But this requires bigger architectural changes.
Recommended Plan
Phase 1: Switch to WebSocket RPC (Your Current Work)
- Already optimal for
createShare
- WebSocket gives persistent connection, no reconnect overhead
Phase 2: Use Capability Pattern for syncShare
// New capability interface
interface SessionData extends RpcTarget {
getSession(): Promise<SDK.Session>
getMessage(id: string): Promise<SDK.Message>
getPart(id: string): Promise<SDK.Part>
getModel(id: string): Promise<SDK.Model>
}
// New syncShare signature
interface ShareRpc extends RpcTarget {
// Old: syncShare(shareID, secret, data: SyncData[])
// New: syncShare(shareID, secret, refs: { type, id }[], sessions: SessionData)
syncShare: (
shareID: string,
secret: string,
refs: Array<{ type: "session" | "message" | "part" | "model"; id: string }>,
sessions: SessionData // capability - passed by reference!
) => Promise<{ success: boolean }>
}
Benefits:
- Only send IDs over wire, not full objects
- Server fetches what it needs via capability
- Session diffs still sent as data (they're already minimal)
Phase 3: Capability-Based Shares
interface ShareRpc extends RpcTarget {
// Returns a Share capability, not plain data
createShare: (sessionID: string, sessions: SessionData) => Promise<RpcStub<Share>>
}
interface Share extends RpcTarget {
id: string
url: string
sync(refs: SyncRef[], sessions: SessionData): Promise<void>
delete(): Promise<void>
}
Then the client just needs to keep the Share stub and call share.sync() or share.delete().
Questions for You
For createShare: Do you want to keep sending initialData for now, or move to capability pattern?
For syncShare: Should we implement the SessionData capability pattern? This would let the server pull only what it needs.
For deleteShare: The current approach needs DB lookup. Should we:
- Keep as-is (DB lookup → RPC call)
- Store the
Share capability from createShare and call share.delete() directly
WebSocket vs HTTP Batch: You're switching to WebSocket. Do you want to:
- Keep HTTP batch as fallback?
- Or fully migrate to WebSocket only?
Say "Approve" when ready to proceed, and I'll have the TypeScript agent implement.