mirror of
https://github.com/HeyPuter/puter.git
synced 2026-08-27 16:37:18 +00:00
Ignore fs events replayed by replication
Raised in review: could an event from another node re-trigger the fan-out? Not today — broadcast carries outer.* and pubsub.* only, so fs.* never crosses a node boundary, and the emitted outer.gui.* is consumed on the peer by SocketService while ShareService listens to fs.* alone, so nothing re-enters. That safety is a property of what broadcast happens to replicate, which is not this service's to rely on. The handlers now skip anything tagged from_outside: the node that did the write has already told the audience, and a second fan-out would only duplicate it.
This commit is contained in:
@@ -1703,6 +1703,43 @@ describe('ShareService', () => {
|
||||
expect(lookups).toBe(1);
|
||||
});
|
||||
|
||||
it('stays quiet for an event another node already handled', async () => {
|
||||
const owner = await makeUser();
|
||||
const recipient = await makeUser();
|
||||
const { dir, file } = await makeDirWithFile(owner.user);
|
||||
|
||||
await share(owner.actor, {
|
||||
uid: dir.uuid,
|
||||
recipient: { email: recipient.email },
|
||||
mode: 'read',
|
||||
});
|
||||
|
||||
const seen: unknown[] = [];
|
||||
const listener = (_key: string, data: unknown) => seen.push(data);
|
||||
server.clients.event.on('outer.gui.item.added', listener);
|
||||
server.clients.event.on('outer.gui.item.updated', listener);
|
||||
try {
|
||||
// What replication looks like: the writing node already told
|
||||
// this audience, so a second fan-out would only duplicate it.
|
||||
await server.clients.event.emitAndWait(
|
||||
'fs.create.file',
|
||||
{ node: file, entry: file, uid: file.uuid },
|
||||
{ from_outside: true },
|
||||
);
|
||||
await server.clients.event.emitAndWait(
|
||||
'fs.write.file',
|
||||
{ node: file, entry: file, target: file },
|
||||
{ from_outside: true },
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
} finally {
|
||||
server.clients.event.off('outer.gui.item.added', listener);
|
||||
server.clients.event.off('outer.gui.item.updated', listener);
|
||||
}
|
||||
|
||||
expect(seen).toEqual([]);
|
||||
});
|
||||
|
||||
it('says where a directly shared file moved from', async () => {
|
||||
const owner = await makeUser();
|
||||
const recipient = await makeUser();
|
||||
|
||||
@@ -224,6 +224,15 @@ const holderPayload = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* An `fs.*` event replayed onto this bus by replication rather than raised by a
|
||||
* write here. Broadcast only carries `outer.*` and `pubsub.*`, so today nothing
|
||||
* reaches these handlers that way — but the node that did the write has already
|
||||
* told the audience, and a second fan-out would only duplicate it.
|
||||
*/
|
||||
const fromAnotherNode = (meta?: { from_outside?: boolean }): boolean =>
|
||||
Boolean(meta?.from_outside);
|
||||
|
||||
/** The GUI events a share recipient is an audience for. */
|
||||
type HolderGuiEvent =
|
||||
| 'outer.gui.item.added'
|
||||
@@ -288,7 +297,8 @@ export class ShareService extends PuterService {
|
||||
* first and cannot depend on this service.
|
||||
*/
|
||||
override onServerStart(): void {
|
||||
this.clients.event.on('fs.remove.node', (_key, data) => {
|
||||
this.clients.event.on('fs.remove.node', (_key, data, meta) => {
|
||||
if (fromAnotherNode(meta)) return;
|
||||
const entry = (data as { node?: FSEntry })?.node;
|
||||
if (!entry?.uuid) return;
|
||||
// Returned so an `emitAndWait` caller can observe the cleanup; the
|
||||
@@ -302,7 +312,8 @@ export class ShareService extends PuterService {
|
||||
});
|
||||
});
|
||||
|
||||
this.clients.event.on('fs.move.node', (_key, data) => {
|
||||
this.clients.event.on('fs.move.node', (_key, data, meta) => {
|
||||
if (fromAnotherNode(meta)) return;
|
||||
const { node, fromPath, fromUserId } = (data ?? {}) as {
|
||||
node?: FSEntry;
|
||||
fromPath?: string;
|
||||
@@ -380,7 +391,8 @@ export class ShareService extends PuterService {
|
||||
return claimFor(user_id, new_email);
|
||||
});
|
||||
|
||||
this.clients.event.on('fs.write.file', (_key, data) => {
|
||||
this.clients.event.on('fs.write.file', (_key, data, meta) => {
|
||||
if (fromAnotherNode(meta)) return;
|
||||
const entry = (data as { node?: FSEntry })?.node;
|
||||
if (!entry?.uuid) return;
|
||||
return this.#fanOutToHolders(entry, 'outer.gui.item.updated').catch(
|
||||
@@ -391,13 +403,15 @@ export class ShareService extends PuterService {
|
||||
});
|
||||
|
||||
// A create is `fs.create.<flavor>`, not `fs.write.file`.
|
||||
this.clients.event.on('fs.create.*', (_key, data) => {
|
||||
this.clients.event.on('fs.create.*', (_key, data, meta) => {
|
||||
if (fromAnotherNode(meta)) return;
|
||||
const entry = (data as { node?: FSEntry })?.node;
|
||||
if (!entry?.uuid) return;
|
||||
return this.#scheduleCreateFanOut(entry).catch(() => {});
|
||||
});
|
||||
|
||||
this.clients.event.on('fs.rename', (_key, data) => {
|
||||
this.clients.event.on('fs.rename', (_key, data, meta) => {
|
||||
if (fromAnotherNode(meta)) return;
|
||||
const { node: entry, old_path: oldPath } = (data ?? {}) as {
|
||||
node?: FSEntry;
|
||||
old_path?: string;
|
||||
|
||||
Reference in New Issue
Block a user