Shared RPC Pool¶
Expose lightweight cross-extension state behind stable endpoint names.
No permission is required. This is a free-tier API.
Use spindle.rpcPool when one extension needs to publish the latest value for some state and other extensions need to read it without sharing direct in-memory references across the process isolation boundary.
Endpoint Rules¶
- every readable endpoint is fully qualified as
<extension_id>.<channel> - owner-side methods accept either a bare channel suffix like
status.currentor a fully-qualified endpoint - the host always normalizes owner endpoints so they stay under the current extension's prefix
- reader-side
read()requires a fully-qualified endpoint - invalid endpoint names reject cleanly
- reads against missing or unregistered endpoints reject cleanly
Channel segments after the extension ID may contain lowercase letters, numbers, _, and -.
Quick Start¶
// Publisher extension
const statusEndpoint = spindle.rpcPool.sync('status.current', {
online: true,
updatedAt: Date.now(),
})
spindle.rpcPool.handle('status.live', async ({ requesterExtensionId }) => {
return {
requestedBy: requesterExtensionId,
online: true,
updatedAt: Date.now(),
}
})
spindle.log.info(`Published ${statusEndpoint}`)
// Reader extension
const current = await spindle.rpcPool.read<{
online: boolean
updatedAt: number
}>('publisher_ext.status.current')
Two Modes¶
Sync Mode¶
sync() stores the latest value in the host-side shared registry.
Use this when:
- the value changes occasionally
- readers should get the newest snapshot immediately
- you do not need per-request logic
spindle.rpcPool.sync('presence.state', {
activeChatId,
openPanel,
ts: Date.now(),
})
Each call replaces the previous value for that endpoint.
On-Demand Mode¶
handle() registers a callback that runs when another extension reads the endpoint.
Use this when:
- computing the value is expensive
- the result should be generated at read time
- you want to tailor the response to the requesting extension
spindle.rpcPool.handle('tokens.estimate', async ({ requesterExtensionId }) => {
const estimate = await spindle.tokens.countText('hello world')
return {
requesterExtensionId,
total: estimate.total_tokens,
}
})
Registering a handler replaces any previously synced value for the same endpoint. Calling sync() later replaces the handler again.
Methods¶
spindle.rpcPool.sync(endpoint, value)¶
Publish the latest value for an endpoint.
const endpoint = spindle.rpcPool.sync('status.current', { ok: true })
// endpoint === 'my_extension.status.current'
| Parameter | Type | Description |
|---|---|---|
endpoint |
string |
Bare channel suffix or fully-qualified owned endpoint |
value |
unknown |
Structured-cloneable value to expose to readers |
Returns the fully-qualified endpoint string.
spindle.rpcPool.handle(endpoint, handler)¶
Register an on-demand endpoint handler.
spindle.rpcPool.handle('status.live', async ({ requesterExtensionId }) => {
return { requesterExtensionId, now: Date.now() }
})
The handler receives:
| Field | Type | Description |
|---|---|---|
endpoint |
string |
Fully-qualified endpoint being read |
requesterExtensionId |
string |
Identifier of the extension performing the read |
Returns the fully-qualified endpoint string.
If the handler throws, the reader receives a rejected promise with that error message.
spindle.rpcPool.read(endpoint)¶
Read a value from another extension.
const result = await spindle.rpcPool.read<{ ok: boolean }>('publisher_ext.status.current')
| Parameter | Type | Description |
|---|---|---|
endpoint |
string |
Fully-qualified endpoint in the form <extension_id>.<channel> |
This rejects when:
- the endpoint name is invalid
- the target endpoint has not been registered
- the target handler throws
- the target handler times out
spindle.rpcPool.unregister(endpoint)¶
Remove an endpoint owned by the current extension.
spindle.rpcPool.unregister('status.current')
Owner-side input follows the same normalization rules as sync() and handle().
Lifecycle Notes¶
- shared RPC endpoints live in the host, not inside your worker's memory
- all endpoints owned by an extension are removed automatically when that extension unloads
- this is an in-memory sharing mechanism, not persistent storage
If you need durable state, keep the source of truth in spindle.storage, spindle.userStorage, or another persisted system and use spindle.rpcPool as the current-process sharing layer.
Patterns¶
Publish Latest Snapshot¶
async function refreshStatus() {
const status = await buildStatusSnapshot()
spindle.rpcPool.sync('status.current', status)
}
Combine Persistence With Fast Reads¶
async function publishConfig(userId: string) {
const config = await spindle.userStorage.getJson('config.json', {
fallback: { enabled: false },
userId,
})
spindle.rpcPool.sync('config.latest', config)
}
Graceful Reader Fallback¶
async function tryReadPublisherState() {
try {
return await spindle.rpcPool.read('publisher_ext.status.current')
} catch (err) {
spindle.log.warn(`Shared RPC unavailable: ${(err as Error).message}`)
return null
}
}