diff --git a/src/core-ownership.js b/src/core-ownership.js index fa29ebd37..db96e124c 100644 --- a/src/core-ownership.js +++ b/src/core-ownership.js @@ -8,6 +8,7 @@ import { kTable, kSelect, kCreateWithDocId } from './datatype/index.js' import { eq, or } from 'drizzle-orm' import mapObject from 'map-obj' import { discoveryKey } from 'hypercore-crypto' +import pDefer from 'p-defer' /** * @typedef {import('./types.js').CoreOwnershipWithSignatures} CoreOwnershipWithSignatures @@ -15,6 +16,7 @@ import { discoveryKey } from 'hypercore-crypto' export class CoreOwnership { #dataType + #ownershipWriteDone /** * * @param {object} opts @@ -25,9 +27,30 @@ export class CoreOwnership { * import('@mapeo/schema').CoreOwnership, * import('@mapeo/schema').CoreOwnershipValue * >} opts.dataType + * @param {Record} opts.coreKeypairs + * @param {import('./types.js').KeyPair} opts.identityKeypair */ - constructor({ dataType }) { + constructor({ dataType, coreKeypairs, identityKeypair }) { this.#dataType = dataType + const authWriterCore = dataType.writerCore + const deferred = pDefer() + this.#ownershipWriteDone = deferred.promise + + const writeOwnership = () => { + if (authWriterCore.length > 0) { + deferred.resolve() + return + } + this.#writeOwnership(identityKeypair, coreKeypairs) + .then(deferred.resolve) + .catch(deferred.reject) + } + // @ts-ignore - opened missing from types + if (authWriterCore.opened) { + writeOwnership() + } else { + authWriterCore.on('ready', writeOwnership) + } } /** @@ -35,6 +58,7 @@ export class CoreOwnership { * @returns {Promise} deviceId of device that owns the core */ async getOwner(coreId) { + await this.#ownershipWriteDone const table = this.#dataType[kTable] const expressions = [] for (const namespace of NAMESPACES) { @@ -57,6 +81,7 @@ export class CoreOwnership { * @returns {Promise} coreId of core belonging to `deviceId` for `namespace` */ async getCoreId(deviceId, namespace) { + await this.#ownershipWriteDone const result = await this.#dataType.getByDocId(deviceId) return result[`${namespace}CoreId`] } @@ -66,7 +91,7 @@ export class CoreOwnership { * @param {import('./types.js').KeyPair} identityKeypair * @param {Record} coreKeypairs */ - async writeOwnership(identityKeypair, coreKeypairs) { + async #writeOwnership(identityKeypair, coreKeypairs) { /** @type {import('./types.js').CoreOwnershipWithSignaturesValue} */ const docValue = { schemaName: 'coreOwnership', diff --git a/src/datatype/index.d.ts b/src/datatype/index.d.ts index d12458cea..9397857b8 100644 --- a/src/datatype/index.d.ts +++ b/src/datatype/index.d.ts @@ -11,6 +11,7 @@ import { import { type BetterSQLite3Database } from 'drizzle-orm/better-sqlite3' import { SQLiteSelectBuilder } from 'drizzle-orm/sqlite-core' import { RunResult } from 'better-sqlite3' +import type Hypercore from 'hypercore' type MapeoDocTableName = `${MapeoDoc['schemaName']}Table` type GetMapeoDocTables = T[keyof T & MapeoDocTableName] @@ -59,6 +60,8 @@ export class DataType< get [kTable](): TTable + get writerCore(): Hypercore<'binary', Buffer> + [kCreateWithDocId]( docId: string, value: diff --git a/src/datatype/index.js b/src/datatype/index.js index 698c9f58b..2d8ef410f 100644 --- a/src/datatype/index.js +++ b/src/datatype/index.js @@ -91,6 +91,10 @@ export class DataType { return this.#table } + get writerCore() { + return this.#dataStore.writerCore + } + /** * @template {import('type-fest').Exact} T * @param {T} value diff --git a/src/mapeo-manager.js b/src/mapeo-manager.js index 660ba2717..b2b55d3b8 100644 --- a/src/mapeo-manager.js +++ b/src/mapeo-manager.js @@ -498,7 +498,6 @@ export class MapeoManager extends TypedEmitter { try { // 4. Write device info into project const project = await this.getProject(projectPublicId) - await project.ready() try { const deviceInfo = await this.getDeviceInfo() @@ -548,7 +547,6 @@ export class MapeoManager extends TypedEmitter { * @returns {Promise} */ async #waitForInitialSync(project, { timeoutMs = 5000 } = {}) { - await project.ready() const [capability, projectSettings] = await Promise.all([ project.$getOwnCapabilities(), project.$getProjectSettings(), diff --git a/src/mapeo-project.js b/src/mapeo-project.js index e3ada1a75..335af9bdf 100644 --- a/src/mapeo-project.js +++ b/src/mapeo-project.js @@ -4,7 +4,6 @@ import Database from 'better-sqlite3' import { decodeBlockPrefix } from '@mapeo/schema' import { drizzle } from 'drizzle-orm/better-sqlite3' import { migrate } from 'drizzle-orm/better-sqlite3/migrator' -import pDefer from 'p-defer' import { discoveryKey } from 'hypercore-crypto' import { CoreManager, NAMESPACES } from './core-manager/index.js' @@ -61,7 +60,6 @@ export class MapeoProject { #blobStore #coreOwnership #capabilities - #ownershipWriteDone #memberApi #iconApi #syncApi @@ -220,9 +218,16 @@ export class MapeoProject { db, }), } - + const identityKeypair = keyManager.getIdentityKeypair() + const coreKeypairs = getCoreKeypairs({ + projectKey, + projectSecretKey, + keyManager, + }) this.#coreOwnership = new CoreOwnership({ dataType: this.#dataTypes.coreOwnership, + coreKeypairs, + identityKeypair, }) this.#capabilities = new Capabilities({ dataType: this.#dataTypes.role, @@ -302,27 +307,6 @@ export class MapeoProject { this.#syncApi[kHandleDiscoveryKey](discoveryKey, stream) }) - ///////// 5. Write core ownership record - - const deferred = pDefer() - // Avoid uncaught rejection. If this is rejected then project.ready() will reject - deferred.promise.catch(() => {}) - this.#ownershipWriteDone = deferred.promise - - const authCore = this.#coreManager.getWriterCore('auth').core - authCore.on('ready', () => { - if (authCore.length > 0) return - const identityKeypair = keyManager.getIdentityKeypair() - const coreKeypairs = getCoreKeypairs({ - projectKey, - projectSecretKey, - keyManager, - }) - this.#coreOwnership - .writeOwnership(identityKeypair, coreKeypairs) - .then(deferred.resolve) - .catch(deferred.reject) - }) this.#l.log('Created project instance %h', projectKey) } @@ -355,13 +339,6 @@ export class MapeoProject { return this.#deviceId } - /** - * Resolves when hypercores have all loaded - */ - async ready() { - await Promise.all([this.#coreManager.ready(), this.#ownershipWriteDone]) - } - /** * @param {import('multi-core-indexer').Entry[]} entries * @param {{projectIndexWriter: IndexWriter, sharedIndexWriter: IndexWriter}} indexWriters @@ -544,11 +521,11 @@ function extractEditableProjectSettings(projectDoc) { * @param {Buffer} opts.projectKey * @param {Buffer} [opts.projectSecretKey] * @param {import('@mapeo/crypto').KeyManager} opts.keyManager - * @returns {Record} + * @returns {Record} */ function getCoreKeypairs({ projectKey, projectSecretKey, keyManager }) { const keypairs = - /** @type {Record} */ ({}) + /** @type {Record} */ ({}) for (const namespace of NAMESPACES) { keypairs[namespace] = diff --git a/test-e2e/capabilities.js b/test-e2e/capabilities.js index 984ff049c..30485dd1a 100644 --- a/test-e2e/capabilities.js +++ b/test-e2e/capabilities.js @@ -66,7 +66,6 @@ test('New device without capabilities', async (t) => { { waitForSync: false } ) const project = await manager.getProject(projectId) - await project.ready() const ownCapabilities = await project.$getOwnCapabilities() @@ -147,7 +146,6 @@ test('getMany() - on newly invited device before sync', async (t) => { { waitForSync: false } ) const project = await manager.getProject(projectId) - await project.ready() const expected = { [deviceId]: NO_ROLE_CAPABILITIES, diff --git a/test-e2e/core-ownership.js b/test-e2e/core-ownership.js index 97ec86061..910d4adb2 100644 --- a/test-e2e/core-ownership.js +++ b/test-e2e/core-ownership.js @@ -22,7 +22,6 @@ test('CoreOwnership', async (t) => { const projectId = await manager.createProject() const project = await manager.getProject(projectId) const coreOwnership = project[kCoreOwnership] - await project.ready() const identityKeypair = km.getIdentityKeypair() const deviceId = identityKeypair.publicKey.toString('hex') diff --git a/test-e2e/device-info.js b/test-e2e/device-info.js index 0ac3db0c4..25ebcb46c 100644 --- a/test-e2e/device-info.js +++ b/test-e2e/device-info.js @@ -45,8 +45,6 @@ test('device info written to projects', (t) => { const projectId = await manager.createProject() const project = await manager.getProject(projectId) - await project.ready() - const me = await project.$member.getById(project.deviceId) st.is(me.deviceId, project.deviceId) @@ -74,8 +72,6 @@ test('device info written to projects', (t) => { const project = await manager.getProject(projectId) - await project.ready() - const me = await project.$member.getById(project.deviceId) st.alike({ name: me.name }, { name: 'mapeo' }) @@ -101,7 +97,6 @@ test('device info written to projects', (t) => { const projects = await Promise.all( projectIds.map(async (projectId) => { const project = await manager.getProject(projectId) - await project.ready() return project }) ) diff --git a/test-e2e/media-server.js b/test-e2e/media-server.js index f7f68b2a3..9bc784156 100644 --- a/test-e2e/media-server.js +++ b/test-e2e/media-server.js @@ -33,8 +33,6 @@ test('retrieving blobs using url', async (t) => { const project = await manager.getProject(await manager.createProject()) - await project.ready() - const exceptionPromise1 = t.exception(async () => { await project.$blobs.getUrl({ driveId: randomBytes(32).toString('hex'), @@ -126,8 +124,6 @@ test('retrieving icons using url', async (t) => { const project = await manager.getProject(await manager.createProject()) - await project.ready() - const exceptionPromise1 = t.exception(async () => { await project.$icons.getIconUrl(randomBytes(32).toString('hex'), { mimeType: 'image/png', diff --git a/test-e2e/members.js b/test-e2e/members.js index 2934a531a..0a98dd45a 100644 --- a/test-e2e/members.js +++ b/test-e2e/members.js @@ -21,7 +21,6 @@ test('getting yourself after creating project', async (t) => { const deviceInfo = await manager.getDeviceInfo() const project = await manager.getProject(await manager.createProject()) - await project.ready() const me = await project.$member.getById(project.deviceId) @@ -62,7 +61,6 @@ test('getting yourself after adding project (but not yet synced)', async (t) => { waitForSync: false } ) ) - await project.ready() const me = await project.$member.getById(project.deviceId) @@ -98,7 +96,6 @@ test('getting invited member after invite rejected', async (t) => { const projectId = await invitor.createProject() const project = await invitor.getProject(projectId) - await project.ready() await invite({ invitor, @@ -131,7 +128,6 @@ test('getting invited member after invite accepted', async (t) => { const { name: inviteeName } = await invitee.getDeviceInfo() const projectId = await invitor.createProject() const project = await invitor.getProject(projectId) - await project.ready() await invite({ invitor,