178 lines
7.6 KiB
TypeScript
Raw Normal View History

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
FederationPeer, TabInstanceResolver,
InMemoryFilterBroker,
} from '@open-matrix/federation';
import { getInternalTopic } from '@open-matrix/federation/topics';
/**
* END-TO-END test for FederationPeer with InMemoryFilterBroker.
*
* This test verifies the FULL flow that FlowPad uses:
* 1. Component subscribes via transport adapter (rewriteLocalTopic)
* 2. Component publishes via transport adapter (rewriteLocalTopic + route)
* 3. Router routes locally (rewriteLocalTopic again)
* 4. InMemoryFilterBroker matches subscription handler called
*
* If this test passes, the protocol works end-to-end.
* If this test fails, the browser will fail too.
*/
test('E2E: FederationPeer full pub/sub flow with tab isolation', async () => {
// ═══════════════════════════════════════════════════════════════════════════
// Setup - mirrors what createBrowserRouter() does
// ═══════════════════════════════════════════════════════════════════════════
const localBroker = new InMemoryFilterBroker();
const localRealm = 'flowpad.tab-abc123';
const router = new FederationPeer({
localRealm,
localBus: localBroker,
instanceResolver: new TabInstanceResolver(localRealm),
});
// ═══════════════════════════════════════════════════════════════════════════
// Transport adapter - mirrors wrapRouterAsTransport() in setup.ts
// ═══════════════════════════════════════════════════════════════════════════
const rewriteLocalTopic = (topic: string): string => {
return router.rewriteLocalTopic(topic);
};
const adapter = {
publish: (topic: string, payload: unknown) => {
const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);
const rewrittenTopic = rewriteLocalTopic(topic);
console.log(`[E2E] publish: ${topic}${rewrittenTopic}`);
router.route(rewrittenTopic, payloadStr);
},
subscribe: (topic: string, handler: (payload: any, meta?: any) => void) => {
const rewrittenTopic = rewriteLocalTopic(topic);
console.log(`[E2E] subscribe: ${topic}${rewrittenTopic}`);
return localBroker.subscribe(rewrittenTopic, (t: string, p: string) => {
const meta = { topic: t, ts: Date.now() };
try {
handler(JSON.parse(p), meta);
} catch {
handler(p, meta);
}
});
},
};
// ═══════════════════════════════════════════════════════════════════════════
// Test - component subscribes and publishes
// ═══════════════════════════════════════════════════════════════════════════
const received: any[] = [];
// Component subscribes to db.accepts.Query (base realm, no tab suffix)
// This should be rewritten to flowpad.tab-abc123.db.accepts.Query
adapter.subscribe('flowpad.db.accepts.Query', (payload, meta) => {
console.log(`[E2E] handler received:`, payload, meta);
received.push({ payload, meta });
});
// Component publishes to the same topic
// This should be rewritten and routed locally
adapter.publish('flowpad.db.accepts.Query', { table: 'products' });
// Wait a tick for async routing
await new Promise(resolve => setTimeout(resolve, 10));
// ═══════════════════════════════════════════════════════════════════════════
// Verify
// ═══════════════════════════════════════════════════════════════════════════
console.log(`[E2E] Received ${received.length} messages`);
assert.equal(received.length, 1, 'Should receive exactly 1 message');
assert.deepEqual(received[0].payload, { table: 'products' });
assert.ok(received[0].meta.topic.includes('flowpad.tab-abc123'), 'Topic should include tab realm');
});
test('E2E: FederationPeer request/reply flow with tab isolation', async () => {
// Setup
const localBroker = new InMemoryFilterBroker();
const localRealm = 'flowpad.tab-xyz789';
const router = new FederationPeer({
localRealm,
localBus: localBroker,
instanceResolver: new TabInstanceResolver(localRealm),
});
// Set up a responder on the local bus using browser local bus format ().
// TabInstanceResolver returns semantic path for browser local bus.
const browserTopic = `${localRealm}.db.accepts.Query`;
localBroker.subscribe(browserTopic, (_topic: string, payload: string) => {
const request = JSON.parse(payload);
// Simulate response by publishing to replyTo
if (request.replyTo) {
localBroker.publish(request.replyTo, JSON.stringify({
ok: true,
result: [{ id: 1, name: 'Product 1' }],
}));
}
});
// Make a request using router.invoke
const result = await router.invoke<any[]>(
'flowpad/db.accepts.Query',
{ table: 'products' },
1000
);
assert.ok(Array.isArray(result), 'Result should be an array');
assert.equal(result[0].name, 'Product 1');
});
test('E2E: Multiple subscriptions on same topic all receive messages', async () => {
const localBroker = new InMemoryFilterBroker();
const router = new FederationPeer({
localRealm: 'flowpad.tab-multi',
localBus: localBroker,
instanceResolver: new TabInstanceResolver('flowpad.tab-multi'),
});
const rewriteLocalTopic = (topic: string) => router.rewriteLocalTopic(topic);
const adapter = {
publish: (topic: string, payload: unknown) => {
const payloadStr = typeof payload === 'string' ? payload : JSON.stringify(payload);
const rewrittenTopic = rewriteLocalTopic(topic);
router.route(rewrittenTopic, payloadStr);
},
subscribe: (topic: string, handler: (payload: any, meta?: any) => void) => {
const rewrittenTopic = rewriteLocalTopic(topic);
return localBroker.subscribe(rewrittenTopic, (t: string, p: string) => {
try {
handler(JSON.parse(p), { topic: t });
} catch {
handler(p, { topic: t });
}
});
},
};
const received1: any[] = [];
const received2: any[] = [];
// Two subscribers on same topic
adapter.subscribe('flowpad.tree.emits.stateChanged', (payload) => {
received1.push(payload);
});
adapter.subscribe('flowpad.tree.emits.stateChanged', (payload) => {
received2.push(payload);
});
// Publish
adapter.publish('flowpad.tree.emits.stateChanged', { value: 42 });
await new Promise(resolve => setTimeout(resolve, 10));
assert.equal(received1.length, 1, 'First subscriber should receive');
assert.equal(received2.length, 1, 'Second subscriber should receive');
assert.deepEqual(received1[0], { value: 42 });
assert.deepEqual(received2[0], { value: 42 });
});