Skip to content

Commit

Permalink
Merge pull request #394 from automerge/denylist
Browse files Browse the repository at this point in the history
Add a denylist of automerge URLs to Repo constructor
  • Loading branch information
pvh authored Oct 14, 2024
2 parents f4504b4 + e1e5e40 commit 41af5ad
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 12 deletions.
21 changes: 19 additions & 2 deletions packages/automerge-repo/src/Repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ import {
DocSyncMetrics,
SyncStatePayload,
} from "./synchronizer/Synchronizer.js"
import type { AnyDocumentId, DocumentId, PeerId } from "./types.js"
import type {
AnyDocumentId,
AutomergeUrl,
DocumentId,
PeerId,
} from "./types.js"

function randomPeerId() {
return ("peer-" + Math.random().toString(36).slice(4)) as PeerId
Expand Down Expand Up @@ -80,6 +85,7 @@ export class Repo extends EventEmitter<RepoEvents> {
sharePolicy,
isEphemeral = storage === undefined,
enableRemoteHeadsGossiping = false,
denylist = [],
}: RepoConfig = {}) {
super()
this.#remoteHeadsGossipingEnabled = enableRemoteHeadsGossiping
Expand All @@ -99,7 +105,7 @@ export class Repo extends EventEmitter<RepoEvents> {

// SYNCHRONIZER
// The synchronizer uses the network subsystem to keep documents in sync with peers.
this.synchronizer = new CollectionSynchronizer(this)
this.synchronizer = new CollectionSynchronizer(this, denylist)

// When the synchronizer emits messages, send them to peers
this.synchronizer.on("message", message => {
Expand Down Expand Up @@ -627,6 +633,13 @@ export interface RepoConfig {
* Whether to enable the experimental remote heads gossiping feature
*/
enableRemoteHeadsGossiping?: boolean

/**
* A list of automerge URLs which should never be loaded regardless of what
* messages are received or what the share policy is. This is useful to avoid
* loading documents that are known to be too resource intensive.
*/
denylist?: AutomergeUrl[]
}

/** A function that determines whether we should share a document with a peer
Expand Down Expand Up @@ -670,3 +683,7 @@ export type DocMetrics =
numOps: number
numChanges: number
}
| {
type: "doc-denied"
documentId: DocumentId
}
22 changes: 19 additions & 3 deletions packages/automerge-repo/src/synchronizer/CollectionSynchronizer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import debug from "debug"
import { DocHandle } from "../DocHandle.js"
import { stringifyAutomergeUrl } from "../AutomergeUrl.js"
import { parseAutomergeUrl, stringifyAutomergeUrl } from "../AutomergeUrl.js"
import { Repo } from "../Repo.js"
import { DocMessage } from "../network/messages.js"
import { DocumentId, PeerId } from "../types.js"
import { AutomergeUrl, DocumentId, PeerId } from "../types.js"
import { DocSynchronizer } from "./DocSynchronizer.js"
import { Synchronizer } from "./Synchronizer.js"

Expand All @@ -21,8 +21,11 @@ export class CollectionSynchronizer extends Synchronizer {
/** Used to determine if the document is know to the Collection and a synchronizer exists or is being set up */
#docSetUp: Record<DocumentId, boolean> = {}

constructor(private repo: Repo) {
#denylist: DocumentId[]

constructor(private repo: Repo, denylist: AutomergeUrl[] = []) {
super()
this.#denylist = denylist.map(url => parseAutomergeUrl(url).documentId)
}

/** Returns a synchronizer for the given document, creating one if it doesn't already exist. */
Expand Down Expand Up @@ -91,6 +94,19 @@ export class CollectionSynchronizer extends Synchronizer {
throw new Error("received a message with an invalid documentId")
}

if (this.#denylist.includes(documentId)) {
this.emit("metrics", {
type: "doc-denied",
documentId,
})
this.emit("message", {
type: "doc-unavailable",
documentId,
targetId: message.senderId,
})
return
}

this.#docSetUp[documentId] = true

const docSynchronizer = this.#fetchDocSynchronizer(documentId)
Expand Down
19 changes: 12 additions & 7 deletions packages/automerge-repo/src/synchronizer/Synchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ export interface SyncStatePayload {
syncState: SyncState
}

export type DocSyncMetrics = {
type: "receive-sync-message"
documentId: DocumentId
durationMillis: number
numOps: number
numChanges: number
}
export type DocSyncMetrics =
| {
type: "receive-sync-message"
documentId: DocumentId
durationMillis: number
numOps: number
numChanges: number
}
| {
type: "doc-denied"
documentId: DocumentId
}
38 changes: 38 additions & 0 deletions packages/automerge-repo/test/Repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,44 @@ describe("Repo", () => {
teardown()
})
})

describe("the denylist", () => {
it("should immediately return an unavailable message in response to a request for a denylisted document", async () => {
const storage = new DummyStorageAdapter()

// first create the document in storage
const dummyRepo = new Repo({ network: [], storage })
const doc = dummyRepo.create({ foo: "bar" })
await dummyRepo.flush()

// Check that the document actually is in storage
let docId = doc.documentId
assert(storage.keys().some((k: string) => k.includes(docId)))

const channel = new MessageChannel()
const { port1: clientToServer, port2: serverToClient } = channel
const server = new Repo({
network: [new MessageChannelNetworkAdapter(serverToClient)],
storage,
denylist: [doc.url],
})
const client = new Repo({
network: [new MessageChannelNetworkAdapter(clientToServer)],
})

await Promise.all([
eventPromise(server.networkSubsystem, "peer"),
eventPromise(client.networkSubsystem, "peer"),
])

const clientDoc = client.find(doc.url)
await pause(100)
assert.strictEqual(clientDoc.docSync(), undefined)

const openDocs = Object.keys(server.metrics().documents).length
assert.deepEqual(openDocs, 0)
})
})
})

const warn = console.warn
Expand Down

0 comments on commit 41af5ad

Please sign in to comment.