Skip to content

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:

ts
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, and stale resets to false. A stale resource is also never served from the cache, so a plain fetch() re-reads it too:

    ts
    const user = await client.users.fetch(id);
    const watch = await user.watch();
    watch.on('invalidate', () => user.refresh()); // `user` is now fresh
  • For a collection, read the range you care about again (page(), count() or for 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:

ts
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:

ts
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:

ts
/**
 * @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).

Released under the MIT License.