@automerge/automerge-repo
Advanced tools
Comparing version 1.1.0 to 1.1.1
@@ -204,7 +204,6 @@ import * as A from "@automerge/automerge/next"; | ||
#statePromise(awaitStates) { | ||
if (!Array.isArray(awaitStates)) | ||
awaitStates = [awaitStates]; | ||
return Promise.any(awaitStates.map(state => waitFor(this.#machine, s => s.matches(state), { | ||
timeout: this.#timeoutDelay * 2, // use a longer delay here so as not to race with other delays | ||
}))); | ||
const awaitStatesArray = Array.isArray(awaitStates) ? awaitStates : [awaitStates]; | ||
return waitFor(this.#machine, s => awaitStatesArray.some((state) => s.matches(state)), | ||
// use a longer delay here so as not to race with other delays | ||
{ timeout: this.#timeoutDelay * 2 }); | ||
} | ||
@@ -211,0 +210,0 @@ // PUBLIC |
@@ -1,2 +0,2 @@ | ||
import { type NetworkAdapter } from "../../index.js"; | ||
import type { NetworkAdapterInterface } from "../../network/NetworkAdapterInterface.js"; | ||
/** | ||
@@ -15,3 +15,3 @@ * Runs a series of tests against a set of three peers, each represented by one or more instantiated | ||
export declare function runAdapterTests(_setup: SetupFn, title?: string): void; | ||
type Network = NetworkAdapter | NetworkAdapter[]; | ||
type Network = NetworkAdapterInterface | NetworkAdapterInterface[]; | ||
export type SetupFn = () => Promise<{ | ||
@@ -18,0 +18,0 @@ adapters: [Network, Network, Network]; |
import assert from "assert"; | ||
import { describe, it } from "vitest"; | ||
import { describe, expect, it } from "vitest"; | ||
import { Repo } from "../../index.js"; | ||
@@ -113,2 +113,17 @@ import { eventPromise, eventPromises } from "../eventPromise.js"; | ||
}); | ||
it("emits a peer-candidate event with proper peer metadata when a peer connects", async () => { | ||
const { adapters, teardown } = await setup(); | ||
const a = adapters[0][0]; | ||
const b = adapters[1][0]; | ||
const bPromise = eventPromise(b, "peer-candidate"); | ||
const aPeerMetadata = { storageId: "a" }; | ||
b.connect("b", { storageId: "b" }); | ||
a.connect("a", aPeerMetadata); | ||
const peerCandidate = await bPromise; | ||
expect(peerCandidate).toMatchObject({ | ||
peerId: "a", | ||
peerMetadata: aPeerMetadata, | ||
}); | ||
teardown(); | ||
}); | ||
}); | ||
@@ -115,0 +130,0 @@ } |
@@ -32,4 +32,6 @@ /** | ||
export { NetworkAdapter } from "./network/NetworkAdapter.js"; | ||
export type { NetworkAdapterInterface } from "./network/NetworkAdapterInterface.js"; | ||
export { isRepoMessage } from "./network/messages.js"; | ||
export { StorageAdapter } from "./storage/StorageAdapter.js"; | ||
export type { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js"; | ||
/** @hidden **/ | ||
@@ -39,3 +41,3 @@ export * as cbor from "./helpers/cbor.js"; | ||
export type { DeleteDocumentPayload, DocumentPayload, RepoConfig, RepoEvents, SharePolicy, } from "./Repo.js"; | ||
export type { NetworkAdapterEvents, OpenPayload, PeerCandidatePayload, PeerDisconnectedPayload, PeerMetadata, } from "./network/NetworkAdapter.js"; | ||
export type { NetworkAdapterEvents, OpenPayload, PeerCandidatePayload, PeerDisconnectedPayload, PeerMetadata, } from "./network/NetworkAdapterInterface.js"; | ||
export type { DocumentUnavailableMessage, EphemeralMessage, Message, RepoMessage, RequestMessage, SyncMessage, } from "./network/messages.js"; | ||
@@ -42,0 +44,0 @@ export type { Chunk, ChunkInfo, ChunkType, StorageKey, StorageId, } from "./storage/types.js"; |
import { EventEmitter } from "eventemitter3"; | ||
import { NetworkAdapterEvents, PeerMetadata } from "../index.js"; | ||
import { PeerId } from "../types.js"; | ||
import { Message } from "./messages.js"; | ||
import { StorageId } from "../storage/types.js"; | ||
/** | ||
* Describes a peer intent to the system | ||
* storageId: the key for syncState to decide what the other peer already has | ||
* isEphemeral: to decide if we bother recording this peer's sync state | ||
* | ||
*/ | ||
export interface PeerMetadata { | ||
storageId?: StorageId; | ||
isEphemeral?: boolean; | ||
} | ||
import { NetworkAdapterInterface } from "./NetworkAdapterInterface.js"; | ||
/** An interface representing some way to connect to other peers | ||
* @deprecated use {@link NetworkAdapterInterface} | ||
* | ||
@@ -22,3 +14,3 @@ * @remarks | ||
*/ | ||
export declare abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> { | ||
export declare abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> implements NetworkAdapterInterface { | ||
peerId?: PeerId; | ||
@@ -40,24 +32,2 @@ peerMetadata?: PeerMetadata; | ||
} | ||
export interface NetworkAdapterEvents { | ||
/** Emitted when the network is ready to be used */ | ||
ready: (payload: OpenPayload) => void; | ||
/** Emitted when the network is closed */ | ||
close: () => void; | ||
/** Emitted when the network adapter learns about a new peer */ | ||
"peer-candidate": (payload: PeerCandidatePayload) => void; | ||
/** Emitted when the network adapter learns that a peer has disconnected */ | ||
"peer-disconnected": (payload: PeerDisconnectedPayload) => void; | ||
/** Emitted when the network adapter receives a message from a peer */ | ||
message: (payload: Message) => void; | ||
} | ||
export interface OpenPayload { | ||
network: NetworkAdapter; | ||
} | ||
export interface PeerCandidatePayload { | ||
peerId: PeerId; | ||
peerMetadata: PeerMetadata; | ||
} | ||
export interface PeerDisconnectedPayload { | ||
peerId: PeerId; | ||
} | ||
//# sourceMappingURL=NetworkAdapter.d.ts.map |
/* c8 ignore start */ | ||
import { EventEmitter } from "eventemitter3"; | ||
/** An interface representing some way to connect to other peers | ||
* @deprecated use {@link NetworkAdapterInterface} | ||
* | ||
@@ -5,0 +6,0 @@ * @remarks |
import { EventEmitter } from "eventemitter3"; | ||
import { PeerId } from "../types.js"; | ||
import type { NetworkAdapter, PeerDisconnectedPayload, PeerMetadata } from "./NetworkAdapter.js"; | ||
import type { NetworkAdapterInterface, PeerDisconnectedPayload, PeerMetadata } from "./NetworkAdapterInterface.js"; | ||
import { MessageContents, RepoMessage } from "./messages.js"; | ||
@@ -9,4 +9,4 @@ export declare class NetworkSubsystem extends EventEmitter<NetworkSubsystemEvents> { | ||
private peerMetadata; | ||
constructor(adapters: NetworkAdapter[], peerId: PeerId, peerMetadata: Promise<PeerMetadata>); | ||
addNetworkAdapter(networkAdapter: NetworkAdapter): void; | ||
constructor(adapters: NetworkAdapterInterface[], peerId: PeerId, peerMetadata: Promise<PeerMetadata>); | ||
addNetworkAdapter(networkAdapter: NetworkAdapterInterface): void; | ||
send(message: MessageContents): void; | ||
@@ -13,0 +13,0 @@ isReady: () => boolean; |
@@ -70,5 +70,7 @@ import debug from "debug"; | ||
}); | ||
this.peerMetadata.then(peerMetadata => { | ||
this.peerMetadata | ||
.then(peerMetadata => { | ||
networkAdapter.connect(this.peerId, peerMetadata); | ||
}).catch(err => { | ||
}) | ||
.catch(err => { | ||
this.#log("error connecting to network", err); | ||
@@ -75,0 +77,0 @@ }); |
import { EventEmitter } from "eventemitter3"; | ||
import { DocHandle } from "./DocHandle.js"; | ||
import { NetworkAdapter, type PeerMetadata } from "./network/NetworkAdapter.js"; | ||
import { NetworkAdapterInterface, type PeerMetadata } from "./network/NetworkAdapterInterface.js"; | ||
import { NetworkSubsystem } from "./network/NetworkSubsystem.js"; | ||
import { StorageAdapter } from "./storage/StorageAdapter.js"; | ||
import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js"; | ||
import { StorageSubsystem } from "./storage/StorageSubsystem.js"; | ||
@@ -93,5 +93,5 @@ import { StorageId } from "./storage/types.js"; | ||
/** A storage adapter can be provided, or not */ | ||
storage?: StorageAdapter; | ||
storage?: StorageAdapterInterface; | ||
/** One or more network adapters must be provided */ | ||
network: NetworkAdapter[]; | ||
network: NetworkAdapterInterface[]; | ||
/** | ||
@@ -98,0 +98,0 @@ * Normal peers typically share generously with everyone (meaning we sync all our documents with |
@@ -117,8 +117,6 @@ import { next as Automerge } from "@automerge/automerge"; | ||
// The network subsystem deals with sending and receiving messages to and from peers. | ||
const myPeerMetadata = new Promise( | ||
// eslint-disable-next-line no-async-promise-executor -- TODO: fix | ||
async (resolve) => resolve({ | ||
const myPeerMetadata = (async () => ({ | ||
storageId: await storageSubsystem?.id(), | ||
isEphemeral, | ||
})); | ||
}))(); | ||
const networkSubsystem = new NetworkSubsystem(network, peerId, myPeerMetadata); | ||
@@ -125,0 +123,0 @@ this.networkSubsystem = networkSubsystem; |
@@ -0,3 +1,5 @@ | ||
import { StorageAdapterInterface } from "./StorageAdapterInterface.js"; | ||
import { StorageKey, Chunk } from "./types.js"; | ||
/** A storage adapter represents some way of storing binary data for a {@link Repo} | ||
* @deprecated use {@link StorageAdapterInterface} | ||
* | ||
@@ -8,3 +10,3 @@ * @remarks | ||
*/ | ||
export declare abstract class StorageAdapter { | ||
export declare abstract class StorageAdapter implements StorageAdapterInterface { | ||
/** Load the single value corresponding to `key` */ | ||
@@ -11,0 +13,0 @@ abstract load(key: StorageKey): Promise<Uint8Array | undefined>; |
/** A storage adapter represents some way of storing binary data for a {@link Repo} | ||
* @deprecated use {@link StorageAdapterInterface} | ||
* | ||
@@ -3,0 +4,0 @@ * @remarks |
import * as A from "@automerge/automerge/next"; | ||
import { type DocumentId } from "../types.js"; | ||
import { StorageAdapter } from "./StorageAdapter.js"; | ||
import { StorageAdapterInterface } from "./StorageAdapterInterface.js"; | ||
import { StorageId } from "./types.js"; | ||
@@ -11,3 +11,3 @@ /** | ||
#private; | ||
constructor(storageAdapter: StorageAdapter); | ||
constructor(storageAdapter: StorageAdapterInterface); | ||
id(): Promise<StorageId>; | ||
@@ -14,0 +14,0 @@ /** Loads a value from storage. */ |
@@ -155,3 +155,3 @@ import * as A from "@automerge/automerge/next"; | ||
beginSync(peerIds) { | ||
const newPeers = new Set(peerIds.filter(peerId => !this.#peers.includes(peerId))); | ||
const noPeersWithDocument = peerIds.every((peerId) => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"]); | ||
// At this point if we don't have anything in our storage, we need to use an empty doc to sync | ||
@@ -166,3 +166,3 @@ // with; but we don't want to surface that state to the front end | ||
const wasUnavailable = doc === undefined; | ||
if (wasUnavailable && newPeers.size == 0) { | ||
if (wasUnavailable && noPeersWithDocument) { | ||
return; | ||
@@ -169,0 +169,0 @@ } |
{ | ||
"name": "@automerge/automerge-repo", | ||
"version": "1.1.0", | ||
"version": "1.1.1", | ||
"description": "A repository object to manage a collection of automerge documents", | ||
@@ -58,3 +58,3 @@ "repository": "https://github.com/automerge/automerge-repo/tree/master/packages/automerge-repo", | ||
}, | ||
"gitHead": "e9e7d3f27ec2ac8a2e9d122ece80598918940067" | ||
"gitHead": "7e0681014b8c5f672e2abc2a653a954ccb6d7aba" | ||
} |
@@ -41,4 +41,4 @@ # Automerge Repo | ||
- `create<T>()` | ||
Creates a new, empty `Automerge.Doc` and returns a `DocHandle` for it. | ||
- `create<T>(initialValue: T?)` | ||
Creates a new `Automerge.Doc` and returns a `DocHandle` for it. Accepts an optional initial value for the document. Produces an empty document (potentially violating the type!) otherwise. | ||
- `find<T>(docId: DocumentId)` | ||
@@ -45,0 +45,0 @@ Looks up a given document either on the local machine or (if necessary) over any configured |
@@ -42,3 +42,3 @@ import * as A from "@automerge/automerge/next" | ||
#machine: DocHandleXstateMachine<T> | ||
#timeoutDelay: number = 60_000 | ||
#timeoutDelay = 60_000 | ||
#remoteHeads: Record<StorageId, A.Heads> = {} | ||
@@ -248,9 +248,8 @@ | ||
#statePromise(awaitStates: HandleState | HandleState[]) { | ||
if (!Array.isArray(awaitStates)) awaitStates = [awaitStates] | ||
return Promise.any( | ||
awaitStates.map(state => | ||
waitFor(this.#machine, s => s.matches(state), { | ||
timeout: this.#timeoutDelay * 2, // use a longer delay here so as not to race with other delays | ||
}) | ||
) | ||
const awaitStatesArray = Array.isArray(awaitStates) ? awaitStates : [awaitStates] | ||
return waitFor( | ||
this.#machine, | ||
s => awaitStatesArray.some((state) => s.matches(state)), | ||
// use a longer delay here so as not to race with other delays | ||
{timeout: this.#timeoutDelay * 2} | ||
) | ||
@@ -257,0 +256,0 @@ } |
import assert from "assert" | ||
import { describe, it } from "vitest" | ||
import { PeerId, Repo, type NetworkAdapter } from "../../index.js" | ||
import { describe, expect, it } from "vitest" | ||
import { PeerId, PeerMetadata, Repo, StorageId } from "../../index.js" | ||
import type { NetworkAdapterInterface } from "../../network/NetworkAdapterInterface.js" | ||
import { eventPromise, eventPromises } from "../eventPromise.js" | ||
@@ -32,3 +33,6 @@ import { pause } from "../pause.js" | ||
it("can sync 2 repos", async () => { | ||
const doTest = async (a: NetworkAdapter[], b: NetworkAdapter[]) => { | ||
const doTest = async ( | ||
a: NetworkAdapterInterface[], | ||
b: NetworkAdapterInterface[] | ||
) => { | ||
const aliceRepo = new Repo({ network: a, peerId: alice }) | ||
@@ -145,2 +149,24 @@ const bobRepo = new Repo({ network: b, peerId: bob }) | ||
}) | ||
it("emits a peer-candidate event with proper peer metadata when a peer connects", async () => { | ||
const { adapters, teardown } = await setup() | ||
const a = adapters[0][0] | ||
const b = adapters[1][0] | ||
const bPromise = eventPromise(b, "peer-candidate") | ||
const aPeerMetadata: PeerMetadata = { storageId: "a" as StorageId } | ||
b.connect("b" as PeerId, { storageId: "b" as StorageId }) | ||
a.connect("a" as PeerId, aPeerMetadata) | ||
const peerCandidate = await bPromise | ||
expect(peerCandidate).toMatchObject({ | ||
peerId: "a", | ||
peerMetadata: aPeerMetadata, | ||
}) | ||
teardown() | ||
}) | ||
}) | ||
@@ -151,3 +177,3 @@ } | ||
type Network = NetworkAdapter | NetworkAdapter[] | ||
type Network = NetworkAdapterInterface | NetworkAdapterInterface[] | ||
@@ -154,0 +180,0 @@ export type SetupFn = () => Promise<{ |
@@ -37,4 +37,6 @@ /** | ||
export { NetworkAdapter } from "./network/NetworkAdapter.js" | ||
export type { NetworkAdapterInterface } from "./network/NetworkAdapterInterface.js" | ||
export { isRepoMessage } from "./network/messages.js" | ||
export { StorageAdapter } from "./storage/StorageAdapter.js" | ||
export type { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js" | ||
@@ -72,3 +74,3 @@ /** @hidden **/ | ||
PeerMetadata, | ||
} from "./network/NetworkAdapter.js" | ||
} from "./network/NetworkAdapterInterface.js" | ||
@@ -75,0 +77,0 @@ export type { |
/* c8 ignore start */ | ||
import { EventEmitter } from "eventemitter3" | ||
import { NetworkAdapterEvents, PeerMetadata } from "../index.js" | ||
import { PeerId } from "../types.js" | ||
import { Message } from "./messages.js" | ||
import { StorageId } from "../storage/types.js" | ||
import { NetworkAdapterInterface } from "./NetworkAdapterInterface.js" | ||
/** | ||
* Describes a peer intent to the system | ||
* storageId: the key for syncState to decide what the other peer already has | ||
* isEphemeral: to decide if we bother recording this peer's sync state | ||
* | ||
*/ | ||
export interface PeerMetadata { | ||
storageId?: StorageId | ||
isEphemeral?: boolean | ||
} | ||
/** An interface representing some way to connect to other peers | ||
* @deprecated use {@link NetworkAdapterInterface} | ||
* | ||
@@ -26,3 +17,6 @@ * @remarks | ||
*/ | ||
export abstract class NetworkAdapter extends EventEmitter<NetworkAdapterEvents> { | ||
export abstract class NetworkAdapter | ||
extends EventEmitter<NetworkAdapterEvents> | ||
implements NetworkAdapterInterface | ||
{ | ||
peerId?: PeerId | ||
@@ -47,33 +41,1 @@ peerMetadata?: PeerMetadata | ||
} | ||
// events & payloads | ||
export interface NetworkAdapterEvents { | ||
/** Emitted when the network is ready to be used */ | ||
ready: (payload: OpenPayload) => void | ||
/** Emitted when the network is closed */ | ||
close: () => void | ||
/** Emitted when the network adapter learns about a new peer */ | ||
"peer-candidate": (payload: PeerCandidatePayload) => void | ||
/** Emitted when the network adapter learns that a peer has disconnected */ | ||
"peer-disconnected": (payload: PeerDisconnectedPayload) => void | ||
/** Emitted when the network adapter receives a message from a peer */ | ||
message: (payload: Message) => void | ||
} | ||
export interface OpenPayload { | ||
network: NetworkAdapter | ||
} | ||
export interface PeerCandidatePayload { | ||
peerId: PeerId | ||
peerMetadata: PeerMetadata | ||
} | ||
export interface PeerDisconnectedPayload { | ||
peerId: PeerId | ||
} |
@@ -5,6 +5,6 @@ import debug from "debug" | ||
import type { | ||
NetworkAdapter, | ||
NetworkAdapterInterface, | ||
PeerDisconnectedPayload, | ||
PeerMetadata, | ||
} from "./NetworkAdapter.js" | ||
} from "./NetworkAdapterInterface.js" | ||
import { | ||
@@ -25,3 +25,3 @@ EphemeralMessage, | ||
#log: debug.Debugger | ||
#adaptersByPeer: Record<PeerId, NetworkAdapter> = {} | ||
#adaptersByPeer: Record<PeerId, NetworkAdapterInterface> = {} | ||
@@ -32,6 +32,6 @@ #count = 0 | ||
#readyAdapterCount = 0 | ||
#adapters: NetworkAdapter[] = [] | ||
#adapters: NetworkAdapterInterface[] = [] | ||
constructor( | ||
adapters: NetworkAdapter[], | ||
adapters: NetworkAdapterInterface[], | ||
public peerId = randomPeerId(), | ||
@@ -45,3 +45,3 @@ private peerMetadata: Promise<PeerMetadata> | ||
addNetworkAdapter(networkAdapter: NetworkAdapter) { | ||
addNetworkAdapter(networkAdapter: NetworkAdapterInterface) { | ||
this.#adapters.push(networkAdapter) | ||
@@ -112,7 +112,9 @@ networkAdapter.once("ready", () => { | ||
this.peerMetadata.then(peerMetadata => { | ||
networkAdapter.connect(this.peerId, peerMetadata) | ||
}).catch(err => { | ||
this.#log("error connecting to network", err) | ||
}) | ||
this.peerMetadata | ||
.then(peerMetadata => { | ||
networkAdapter.connect(this.peerId, peerMetadata) | ||
}) | ||
.catch(err => { | ||
this.#log("error connecting to network", err) | ||
}) | ||
} | ||
@@ -119,0 +121,0 @@ |
@@ -5,5 +5,5 @@ import { next as Automerge } from "@automerge/automerge" | ||
import { | ||
generateAutomergeUrl, | ||
interpretAsDocumentId, | ||
parseAutomergeUrl, | ||
generateAutomergeUrl, | ||
interpretAsDocumentId, | ||
parseAutomergeUrl, | ||
} from "./AutomergeUrl.js" | ||
@@ -14,6 +14,6 @@ import { DocHandle, DocHandleEncodedChangePayload } from "./DocHandle.js" | ||
import { throttle } from "./helpers/throttle.js" | ||
import { NetworkAdapter, type PeerMetadata } from "./network/NetworkAdapter.js" | ||
import { NetworkAdapterInterface, type PeerMetadata } from "./network/NetworkAdapterInterface.js" | ||
import { NetworkSubsystem } from "./network/NetworkSubsystem.js" | ||
import { RepoMessage } from "./network/messages.js" | ||
import { StorageAdapter } from "./storage/StorageAdapter.js" | ||
import { StorageAdapterInterface } from "./storage/StorageAdapterInterface.js" | ||
import { StorageSubsystem } from "./storage/StorageSubsystem.js" | ||
@@ -160,10 +160,6 @@ import { StorageId } from "./storage/types.js" | ||
const myPeerMetadata: Promise<PeerMetadata> = new Promise( | ||
// eslint-disable-next-line no-async-promise-executor -- TODO: fix | ||
async resolve => | ||
resolve({ | ||
storageId: await storageSubsystem?.id(), | ||
isEphemeral, | ||
} as PeerMetadata) | ||
) | ||
const myPeerMetadata: Promise<PeerMetadata> = (async () => ({ | ||
storageId: await storageSubsystem?.id(), | ||
isEphemeral, | ||
}))() | ||
@@ -523,6 +519,6 @@ const networkSubsystem = new NetworkSubsystem( | ||
/** A storage adapter can be provided, or not */ | ||
storage?: StorageAdapter | ||
storage?: StorageAdapterInterface | ||
/** One or more network adapters must be provided */ | ||
network: NetworkAdapter[] | ||
network: NetworkAdapterInterface[] | ||
@@ -529,0 +525,0 @@ /** |
@@ -0,4 +1,6 @@ | ||
import { StorageAdapterInterface } from "./StorageAdapterInterface.js" | ||
import { StorageKey, Chunk } from "./types.js" | ||
/** A storage adapter represents some way of storing binary data for a {@link Repo} | ||
* @deprecated use {@link StorageAdapterInterface} | ||
* | ||
@@ -9,3 +11,3 @@ * @remarks | ||
*/ | ||
export abstract class StorageAdapter { | ||
export abstract class StorageAdapter implements StorageAdapterInterface { | ||
/** Load the single value corresponding to `key` */ | ||
@@ -12,0 +14,0 @@ abstract load(key: StorageKey): Promise<Uint8Array | undefined> |
@@ -6,3 +6,3 @@ import * as A from "@automerge/automerge/next" | ||
import { type DocumentId } from "../types.js" | ||
import { StorageAdapter } from "./StorageAdapter.js" | ||
import { StorageAdapterInterface } from "./StorageAdapterInterface.js" | ||
import { ChunkInfo, StorageKey, StorageId } from "./types.js" | ||
@@ -19,3 +19,3 @@ import { keyHash, headsHash } from "./keyHash.js" | ||
/** The storage adapter to use for saving and loading documents */ | ||
#storageAdapter: StorageAdapter | ||
#storageAdapter: StorageAdapterInterface | ||
@@ -33,3 +33,3 @@ /** Record of the latest heads we've loaded or saved for each document */ | ||
constructor(storageAdapter: StorageAdapter) { | ||
constructor(storageAdapter: StorageAdapterInterface) { | ||
this.#storageAdapter = storageAdapter | ||
@@ -36,0 +36,0 @@ } |
@@ -230,4 +230,4 @@ import * as A from "@automerge/automerge/next" | ||
beginSync(peerIds: PeerId[]) { | ||
const newPeers = new Set( | ||
peerIds.filter(peerId => !this.#peers.includes(peerId)) | ||
const noPeersWithDocument = peerIds.every( | ||
(peerId) => this.#peerDocumentStatuses[peerId] in ["unavailable", "wants"] | ||
) | ||
@@ -246,3 +246,3 @@ | ||
const wasUnavailable = doc === undefined | ||
if (wasUnavailable && newPeers.size == 0) { | ||
if (wasUnavailable && noPeersWithDocument) { | ||
return | ||
@@ -249,0 +249,0 @@ } |
import * as A from "@automerge/automerge/next" | ||
import assert from "assert" | ||
import { decode } from "cbor-x" | ||
import { describe, it } from "vitest" | ||
import { describe, it, vi } from "vitest" | ||
import { generateAutomergeUrl, parseAutomergeUrl } from "../src/AutomergeUrl.js" | ||
@@ -75,2 +75,25 @@ import { eventPromise } from "../src/helpers/eventPromise.js" | ||
/** | ||
* Once there's a Repo#stop API this case should be covered in accompanying | ||
* tests and the following test removed. | ||
*/ | ||
it("no pending timers after a document is loaded", async () => { | ||
vi.useFakeTimers() | ||
const timerCount = vi.getTimerCount() | ||
const handle = new DocHandle<TestDoc>(TEST_ID) | ||
assert.equal(handle.isReady(), false) | ||
handle.doc() | ||
assert(vi.getTimerCount() > timerCount) | ||
// simulate loading from storage | ||
handle.update(doc => docFromMockStorage(doc)) | ||
assert.equal(handle.isReady(), true) | ||
assert.equal(vi.getTimerCount(), timerCount) | ||
vi.useRealTimers() | ||
}) | ||
it("should block changes until ready()", async () => { | ||
@@ -77,0 +100,0 @@ const handle = new DocHandle<TestDoc>(TEST_ID) |
@@ -1,11 +0,14 @@ | ||
import { NetworkAdapter } from "../../src/index.js" | ||
import { pause } from "../../src/helpers/pause.js"; | ||
import { Message, NetworkAdapter, PeerId } from "../../src/index.js" | ||
export class DummyNetworkAdapter extends NetworkAdapter { | ||
#startReady: boolean | ||
#sendMessage?: SendMessageFn; | ||
constructor({ startReady = true }: Options = {}) { | ||
constructor(opts: Options = {startReady: true}) { | ||
super() | ||
this.#startReady = startReady | ||
this.#startReady = opts.startReady; | ||
this.#sendMessage = opts.sendMessage; | ||
} | ||
send() {} | ||
connect(_: string) { | ||
@@ -16,7 +19,36 @@ if (this.#startReady) { | ||
} | ||
disconnect() {} | ||
peerCandidate(peerId: PeerId) { | ||
this.emit('peer-candidate', { peerId, peerMetadata: {} }); | ||
} | ||
override send(message: Message) { | ||
this.#sendMessage?.(message); | ||
} | ||
receive(message: Message) { | ||
this.emit('message', message); | ||
} | ||
static createConnectedPair() { | ||
const adapter1: DummyNetworkAdapter = new DummyNetworkAdapter({ | ||
startReady: true, | ||
sendMessage: (message: Message) => pause(10).then(() => adapter2.receive(message)), | ||
}); | ||
const adapter2: DummyNetworkAdapter = new DummyNetworkAdapter({ | ||
startReady: true, | ||
sendMessage: (message: Message) => pause(10).then(() => adapter1.receive(message)), | ||
}); | ||
return [adapter1, adapter2]; | ||
} | ||
} | ||
type SendMessageFn = (message: Message) => void; | ||
type Options = { | ||
startReady?: boolean | ||
startReady?: boolean; | ||
sendMessage?: SendMessageFn; | ||
} |
@@ -6,3 +6,3 @@ import { next as A } from "@automerge/automerge" | ||
import { describe, expect, it } from "vitest" | ||
import { READY } from "../src/DocHandle.js" | ||
import { HandleState, READY } from "../src/DocHandle.js" | ||
import { parseAutomergeUrl } from "../src/AutomergeUrl.js" | ||
@@ -1027,2 +1027,33 @@ import { | ||
it('peer receives a document when connection is recovered', async () => { | ||
const alice = "alice" as PeerId; | ||
const bob = "bob" as PeerId; | ||
const [aliceAdapter, bobAdapter] = DummyNetworkAdapter.createConnectedPair(); | ||
const aliceRepo = new Repo({ | ||
network: [aliceAdapter], | ||
peerId: alice | ||
}) | ||
const bobRepo = new Repo({ | ||
network: [bobAdapter], | ||
peerId: bob | ||
}) | ||
const aliceDoc = aliceRepo.create(); | ||
aliceDoc.change((doc: any) => doc.text = 'Hello world'); | ||
const bobDoc = bobRepo.find(aliceDoc.url); | ||
bobDoc.unavailable() | ||
await bobDoc.whenReady([HandleState.UNAVAILABLE]); | ||
aliceAdapter.peerCandidate(bob); | ||
// Bob isn't yet connected to Alice and can't respond to her sync message | ||
await pause(100); | ||
bobAdapter.peerCandidate(alice); | ||
await bobDoc.whenReady([HandleState.READY]); | ||
assert.equal(bobDoc.isReady(), true); | ||
}); | ||
describe("with peers (mesh network)", () => { | ||
@@ -1029,0 +1060,0 @@ const setup = async () => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
393099
140
9107