Realtime updates
IMVU pushes change notifications over IMQ, its websocket service. @imvu/client uses it only when you ask: ordinary fetches never open a connection.
Watching a resource or collection
Call watch() on anything that supports it. You get back a DocumentWatch that fires invalidate whenever the server says the document changed:
const wishlist = client.account.wishlist.list();
const watch = await wishlist.watch();
watch.on('invalidate', async ({ document, revision, receivedAt }) => {
// Something changed. Read again to see what.
const total = await document.count();
console.log(`wishlist now has ${total} items (change #${revision})`);
});
// When you're done:
watch.close();The first watch() opens the IMQ connection, using the details of the logged-in session. Later watches share it.
Invalidations say that something changed, not what
An invalidation carries no data. It increments the document's revision and sets stale = true, and leaves the next step to you:
For a resource, read it again with
refresh(). Because resources are canonical, that updates the very object you already hold, andstaleresets tofalse. A stale resource is also never served from the cache, so a plainfetch()re-reads it too:tsconst user = await client.users.fetch(id); const watch = await user.watch(); watch.on('invalidate', () => user.refresh()); // `user` is now freshFor a collection, read the range you care about again (
page(),count()orfor await). Collections never cache their membership, so every read is fresh.
Several invalidations can arrive close together. If re-reading is expensive, debounce it.
What can be watched
Only documents whose API response advertises an update channel can be watched. Check before you rely on it:
import { CollectionNotWatchableError } from '@imvu/client';
try {
const watch = await collection.watch();
} catch (error) {
if (error instanceof CollectionNotWatchableError) {
// This endpoint doesn't publish updates. Poll it instead.
} else {
throw error;
}
}After a collection has been read, collection.watchability tells you without throwing: 'supported', 'unsupported', or 'unknown' if it hasn't been read yet. For a resource, resource.updateTarget is null when it can't be watched.
Connection problems
The IMQ connection keeps itself alive and reconnects with backoff. Notifications sent while it was down are lost, so after a reconnect every watched document is invalidated once. Your normal invalidate handler then re-reads everything, and nothing is silently missed.
Connection errors are emitted on the client:
client.on('imqError', (error) => console.warn('IMQ:', error.message));Closing every watch on a document releases its subscription. client.logout() closes them all along with the connection.
Rooms
Room presence and chat are built on the same connection: chat.observe() follows who is in a room, and room.join() takes part in it. See Rooms and chat.
For your own subscriptions on the shared connection, client.realtime.listen(queue, mount, listener) delivers every message on a mount (sender and payload), and client.realtime.send() sends one. client.realtime.onReconnect(listener) tells you when to resynchronize.
Using @imvu/imq directly
To see raw traffic (chat queues, state mounts, presence), use IMQManager yourself. It needs the IMQ URL and session id from /login/me:
/**
* @imvu/rest + @imvu/imq — authenticate with the REST API, then open a live IMQ
* connection using the details from /login/me.
*
* node --env-file=.env examples/imq-listen.ts [queueName]
*
* Pass a queue name to subscribe and log its events; with no arguments it just
* connects and idles. See examples/README.md.
*/
import { IMQManager } from '@imvu/imq';
import { REST } from '@imvu/rest';
const username = process.env.IMVU_USERNAME;
const password = process.env.IMVU_PASSWORD ?? '';
if (!username) {
console.error('Set IMVU_USERNAME (and IMVU_PASSWORD) — see examples/README.md');
process.exit(1);
}
// 1. Authenticate (reuses a cached session when the username matches cookies.json).
const rest = new REST();
await rest.login(username, password);
console.log(`Logged in as ${username} (cid ${rest.cid})`);
// 2. The IMQ url + session token live on /login/me.
const me = await rest.request('/login/me');
const login = me.denormalized[me.id];
const imqUrl = login.relations?.imq;
if (!imqUrl) {
console.error('This session does not expose an IMQ endpoint');
process.exit(1);
}
const manager = new IMQManager({
url: imqUrl,
userId: String(rest.cid),
sessionId: login.data.session_id,
metadata: { app: 'imvu_next', platform_type: 'big' },
});
// Lifecycle is observable — no polling needed.
manager.on('status', (status) => console.log(`IMQ status: ${status}`));
manager.on('error', (error) => console.error('IMQ error:', error.message));
// 3. Connect — resolves once authenticated (reject after 15s rather than hang forever).
await manager.connect({ timeout: 15_000 });
// 4. Optionally subscribe to a queue and log everything it emits. Queue-level
// subscription discovers mounts as they appear — no need to know their names.
const queueName = process.argv[2];
if (queueName) {
const queue = manager.subscribe(queueName);
queue.on('mount', (mount) => console.log('mount:', mount));
queue.on('message', (message) => console.log('message:', message));
queue.on('state', (state) => console.log('state:', state));
queue.on('subscriber', (event) => console.log('subscriber:', event));
console.log(`Subscribed to ${queueName}. Waiting for events…`);
} else {
console.log('Tip: pass a queue name to subscribe, e.g. `… imq-listen.ts <queue>`.');
}
console.log('Press Ctrl+C to exit.');
process.on('SIGINT', () => {
manager.close();
process.exit(0);
});A queue groups mounts. Message mounts deliver a stream of messages, and state mounts hold a key → value map that changes over time. Subscribe to a queue to discover its mounts as they appear, or go straight to one you already know with manager.subscribeMessage(queue, mount) / manager.subscribeState(queue, mount). The manager emits status on every connection change (connecting, authenticating, connected, disconnected).