fix: stop losing the first local store call of a tab that just became the owner, which left it syncing for good

This commit is contained in:
Linus Rath
2026-11-03 07:17:23 +01:00
parent d3bd93b80d
commit fcbc6edb89
2 changed files with 30 additions and 2 deletions
+26
View File
@@ -107,6 +107,31 @@ describe('a tab that starts after the owner', () => {
});
});
describe('the owner tab calling its own worker', () => {
it('waits for the worker on the channel, since a call posted before that is lost', async () => {
const worker = new FakeChannel('bulwark-store-rpc');
const heard: Array<{ kind: string; id?: string; from?: string }> = [];
worker.onmessage = (event) => heard.push(event.data as { kind: string });
const client = new StoreClient();
client.start();
FakeWorker.all[0].becomeOwner();
expect(client.ownsStore).toBe(true);
const call = client.area('a', 'x.y');
await new Promise((resolve) => setTimeout(resolve, 5));
expect(heard.some(({ kind }) => kind === 'rpc')).toBe(false);
// It asked again, for when the first announcement was missed.
expect(heard.filter(({ kind }) => kind === 'who-owns')).toHaveLength(2);
worker.postMessage({ kind: 'owner-changed', owner: client.clientId });
await vi.waitFor(() => expect(heard.some(({ kind }) => kind === 'rpc')).toBe(true));
const rpc = heard.find(({ kind }) => kind === 'rpc')!;
worker.postMessage({ kind: 'reply', id: rpc.id, to: rpc.from, result: 'done' });
await expect(call).resolves.toBe('done');
client.halt();
});
});
describe('a sign-out in any tab', () => {
it('stops this tab\'s worker and rejects what was waiting', async () => {
const halted = vi.fn();
@@ -209,6 +234,7 @@ describe('the page\'s store', () => {
expect(await getAccountRecord('a@x')).not.toBeNull();
const owner = new FakeChannel('bulwark-store-rpc');
owner.postMessage({ kind: 'owner-changed', owner: 'worker' });
const removed: unknown[] = [];
owner.onmessage = (event) => {
const message = event.data as { kind: string; id: string; from: string; method: string; params: unknown };
+4 -2
View File
@@ -110,8 +110,10 @@ export class StoreClient {
switch (message.kind) {
case 'owner':
this.isOwner = true;
this.owner = this.clientId;
this.flushOwnerWaiters();
// Not an owner to call yet. This message can overtake the worker's
// channel getting registered, and a call posted then never reaches it,
// so calls wait for the worker's answer on the channel itself.
this.channel?.postMessage({ kind: 'who-owns' } satisfies ChannelMessage);
this.options.onOwner?.();
break;
case 'result': {