Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
d804aec
feat: Remove fast test option from test-e2e
Feb 16, 2026
81db3e0
feat: Remove random-access-storage from more test-e2e
Feb 16, 2026
592dd9e
test: Temporarily disable Consistent storage folders test
Feb 17, 2026
3781b7d
test: Remove RAM from server test
Feb 17, 2026
109dee8
test: Remove RAM from coreManager in tests
Feb 17, 2026
f91c239
fix: Close blobstore correctly
Feb 19, 2026
32bbb2b
feat: Add ready-resource as dependency + types
Feb 23, 2026
6afe70e
chore: Revert ready-resource to 1.0.0
Feb 24, 2026
fc53685
fix: Use ready-resource when loading/closing async resources
Feb 24, 2026
a8f8bc5
chore: Fix ts in test-e2e/members.js
Feb 24, 2026
f1aa0ef
test: Wait for blobStore to be ready for blob-api tests
Feb 24, 2026
fcf3d12
fix: Race conditions in core manager tests
Feb 24, 2026
addd53c
chore: make replicate async to fix race conditions
Feb 24, 2026
bf728da
test: update datastore test with new timings
Feb 24, 2026
7c4a522
fix: wait for project ready when loading
Feb 24, 2026
a3d252a
test: add create-core test utility
Feb 24, 2026
d75febe
Merge branch 'main' into feat/no-random-access-storage-in-tests
RangerMauve Mar 2, 2026
2e8d962
Merge branch 'main' into feat/no-random-access-storage-in-tests
RangerMauve Mar 3, 2026
986ce4b
Merge branch 'main' into feat/no-random-access-storage-in-tests
RangerMauve Mar 9, 2026
f27c79f
Merge branch 'main' into feat/no-random-access-storage-in-tests
RangerMauve Mar 10, 2026
63636ea
Merge branch 'main' into feat/no-random-access-storage-in-tests
RangerMauve Mar 10, 2026
76473d2
Merge branch 'main' into feat/no-random-access-storage-in-tests
RangerMauve Mar 12, 2026
2f0cad7
chore: auto-open resources (#1245)
gmaclennan Mar 16, 2026
7c102c6
chore: Update hypercore dependencies
Mar 12, 2026
144e1a1
chore: Use hypercore11 version of digidem types
Mar 12, 2026
5c2e707
chore: force ready-resource type definition
Mar 16, 2026
04695e6
chore: Update hyper constructor options
Mar 16, 2026
ffface3
Update types/ready-resource.d.ts
RangerMauve Mar 16, 2026
de4ced0
chore: re-add consistent folder name test
Mar 16, 2026
649f32c
fix: Only _close should be abstract in ready-resource
Mar 16, 2026
90165ec
Merge branch 'main' into feat/hypercore11-migration
RangerMauve Mar 16, 2026
6781da2
chore: Skip tests that rely on RAS
Mar 16, 2026
5b1c049
fix: Bug in corestore requires key specified in addition to keyPair
Mar 17, 2026
447df7a
fix: Update error message for blob block not available
Mar 18, 2026
ee962ea
fix: Use proper error message check in fastify blobs plugin
Mar 18, 2026
bbfcdcf
fix: Ignore request cancelled errors in downloader
Mar 18, 2026
42c8043
fix: Account for remoteContiguousLength in core sync state
Mar 19, 2026
cd25c5a
fix: Send bitfields for hypercore wants/ranges manually
Mar 19, 2026
1337383
Merge branch 'main' into feat/hypercore11-migration
RangerMauve Mar 19, 2026
4cd7e42
chore: Skip bitfield test temporarily
Mar 19, 2026
731dde1
feat: Remove random-access-storage from manager and project
Mar 19, 2026
874da60
feat: Initial migration size calculation code
Apr 22, 2026
bcdc4cd
feat: Check if hypercore11 migration needs to or can happen and migrate
Apr 23, 2026
a68d3f8
fix: Monkeypatch core purge
Apr 23, 2026
260bc92
Merge branch 'main' into feat/hypercore11-migration
RangerMauve Apr 23, 2026
0ae5bcf
chore: bump digidem types
Apr 23, 2026
2333771
test: Check data integrity post hypercore migration
Apr 27, 2026
d21a4d4
feat: export migration from package
Apr 28, 2026
833f3c2
fix: Include migration in d.ts build
Apr 28, 2026
f7bc348
feat: Specify if should use fallback in checkShouldMigrate
Apr 28, 2026
7f18169
fix: ensureKnownError should use error message
May 6, 2026
ac50456
fix: better detection of partial migration
May 6, 2026
b9ebf8d
fix: Error when writing to closed datastore
May 7, 2026
730bf35
fix: Don't update device info if there are no changes
May 7, 2026
710049d
chore: Bump hypercore dependency
May 11, 2026
eadddbc
fix: Account for local contiguous length in tests
May 11, 2026
9abaa76
fix: Calculate local core contiguousLength on get state
May 12, 2026
f0fc0b2
Merge branch 'main' into feat/hypercore11-migration
RangerMauve May 12, 2026
240cf20
fix: Correct formatter for role logging in PeerSyncController
May 12, 2026
92ee66e
fix: Invalidate namespace sync state on peer sync controller capabili…
May 12, 2026
66f1d82
Revert "fix: Invalidate namespace sync state on peer sync controller …
May 18, 2026
180f178
Merge branch 'main' into feat/hypercore11-migration
RangerMauve May 18, 2026
4f1170e
chore: rename psc dispose to close
May 18, 2026
530ffea
fix: compact storage after others core purge
May 18, 2026
e239619
fix: Remove RAM from types test
May 18, 2026
b81df59
fix: Migrate each core after migrating corestore
May 18, 2026
05b038b
feat: progress indicator for storage migration
RangerMauve Jun 18, 2026
baccb66
test: account for initial migration progress being 0
RangerMauve Jun 18, 2026
38408a5
chore: Update package-lock
Jun 23, 2026
83fe215
fix lockfile
achou11 Jun 23, 2026
c23097f
Merge branch 'main' into feat/hypercore11-migration
RangerMauve Jun 25, 2026
58e82e6
chore: Bump digidem/types to 2.4.0
Jun 25, 2026
1dd3a55
chore: Update digidem types, fix protomux and streamx typing.
Jun 29, 2026
3e8f976
chore: remove typedoc (#1289)
RangerMauve Jun 29, 2026
face919
chore: remove remaining API docs
Jun 29, 2026
4997b24
chore: Bump hypercore dependencies
Jun 29, 2026
9596731
Merge branch 'main' into feat/hypercore11-migration
RangerMauve Jul 2, 2026
cdf3eb6
fix: remote bitfield not being exchanged due to core length not being…
RangerMauve Jul 8, 2026
bc27584
fix: account for missing dir when checking for migration
RangerMauve Jul 8, 2026
bca71e6
chore: document needed avail space as 1.5x largest core
RangerMauve Jul 8, 2026
48d4f6e
fix: ensure onProgress gets called with total cores at least once
RangerMauve Jul 8, 2026
2d7469c
fix: want should behave the same as wantWord in sync state
RangerMauve Jul 8, 2026
95028d5
chore: fix jsdocs, remove unnecessary tests
RangerMauve Jul 9, 2026
d563236
Merge branch 'main' into feat/hypercore11-migration
gmaclennan Jul 13, 2026
9119ec6
test: fix create-core helper silently skipping bitfield exchange on h…
gmaclennan Jul 13, 2026
7a361b5
fix: don't reference unpublished hypercore-storage types in migration…
gmaclennan Jul 13, 2026
3a001e4
fix(core-manager): delete the head record when purging a core (#1307)
gmaclennan Jul 16, 2026
ad026b8
feat: Add spaceNeeded on migration reason no space
RangerMauve Jul 20, 2026
222cf91
chore: pin exact hypercore-storage version
RangerMauve Jul 20, 2026
830d811
fix: migration - don't wait for store ready to avoid double migrate
RangerMauve Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,294 changes: 1,216 additions & 78 deletions package-lock.json

Large diffs are not rendered by default.

23 changes: 14 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"./schema.js": {
"types": "./dist/schema.d.ts",
"default": "./src/schema.js"
},
"./migration.js": {
"types": "./dist/migration.d.ts",
"default": "./src/migration.js"
}
},
"type": "module",
Expand Down Expand Up @@ -124,6 +128,7 @@
"@comapeo/core2.0.1": "npm:@comapeo/core@2.0.1",
"@comapeo/core4.1.4": "npm:@comapeo/core@^4.1.4",
"@comapeo/ipc": "^2.1.0",
"@digidem/types": "^2.4.1",
"@mapeo/default-config": "^6.0.0",
"@mapeo/mock-data": "^5.0.0",
"@sinonjs/fake-timers": "^10.0.2",
Expand Down Expand Up @@ -175,23 +180,22 @@
"dependencies": {
"@comapeo/fallback-smp": "^1.0.0",
"@comapeo/schema": "2.3.0",
"@digidem/types": "^2.3.0",
"@fastify/error": "^3.4.1",
"@fastify/type-provider-typebox": "^4.1.0",
"@hyperswarm/secret-stream": "^6.6.3",
"@mapeo/crypto": "1.0.0-alpha.10",
"@mapeo/sqlite-indexer": "^1.0.2",
"@sinclair/typebox": "^0.33.17",
"@sindresorhus/merge-streams": "^4.0.0",
"b4a": "^1.6.3",
"b4a": "^1.8.0",
"bcp-47": "^2.1.0",
"bcp-47-normalize": "^2.3.0",
"better-sqlite3": "^11.10.0",
"big-sparse-array": "^1.0.3",
"bogon": "^1.1.0",
"comapeocat": "^1.1.0",
"compact-encoding": "^2.12.0",
"corestore": "6.8.4",
"corestore": "^7.9.2",
"custom-error-creator": "^1.1.1",
"debug": "^4.3.4",
"dot-prop": "^9.0.0",
Expand All @@ -200,10 +204,11 @@
"ensure-error": "^4.0.0",
"fastify": "^4.0.0",
"fastify-plugin": "^4.5.1",
"hyperblobs": "2.3.0",
"hypercore": "10.19.0",
"hypercore-crypto": "3.4.2",
"hyperdrive": "11.5.3",
"hyperblobs": "^2.12.1",
"hypercore": "^11.30.2",
"hypercore-crypto": "^3.7.0",
"hypercore-storage": "^2.9.0",
"hyperdrive": "^13.3.2",
"iso-3166": "^4.3.0",
"json-stable-stringify": "^1.1.1",
"magic-bytes.js": "^1.10.0",
Expand All @@ -218,9 +223,9 @@
"protomux": "^3.4.1",
"quickbit-universal": "^2.2.0",
"ready-resource": "^1.2.0",
"sodium-universal": "^4.0.0",
"sodium-universal": "^5.0.1",
"start-stop-state-machine": "^1.2.0",
"streamx": "^2.19.0",
"streamx": "^2.28.0",
"string-timing-safe-equal": "^0.1.0",
"styled-map-package": "^3.0.0",
"sub-encoder": "^2.1.1",
Expand Down
2 changes: 2 additions & 0 deletions src/blob-store/downloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ export class Downloader extends ReadyResource {
/** @param {Error} err */
#handleError = (err) => {
if (this.#ac.signal.aborted) return
// Ignore request cancelled errors since they are false positives
if ('code' in err && err.code === 'REQUEST_CANCELLED') return
this.emit('error', err)
this.#ac.abort(err)
}
Expand Down
12 changes: 8 additions & 4 deletions src/blob-store/hyperdrive-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,15 @@ class PretendCorestore {
if (b4a.isBuffer(opts)) {
opts = { publicKey: opts }
}
if ('key' in opts) {
if ('key' in opts && opts.key !== null) {
// @ts-ignore
opts.publicKey = opts.key
}
if ('publicKey' in opts) {
if ('keyPair' in opts && opts.keyPair !== null) {
// @ts-ignore
opts.publicKey = opts.keyPair.publicKey
}
if ('publicKey' in opts && opts.publicKey !== null) {
// NB! We should always add blobIndex (Hyperbee) cores to the core manager
// before we use them here. We would only reach the addCore path if the
// blob core is read from the hyperbee header (before it is added to the
Expand All @@ -117,9 +121,9 @@ class PretendCorestore {
this.#coreManager.getCoreByKey(opts.publicKey) ||
this.#coreManager.addCore(opts.publicKey, 'blob').core
)
} else if (opts.name === 'db') {
} else if ('name' in opts && opts.name === 'db') {
return this.#coreManager.getWriterCore('blobIndex').core
} else if (opts.name.includes('blobs')) {
} else if ('name' in opts && opts.name.includes('blobs')) {
return this.#coreManager.getWriterCore('blob').core
} else {
throw new UnsupportedCorestoreOptsError({ opts })
Expand Down
61 changes: 50 additions & 11 deletions src/core-manager/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ import {
} from '../generated/extensions.js'
import { Logger } from '../logger.js'
import { NAMESPACES } from '../constants.js'
import { noop } from '../utils.js'
import { forceBitfieldExchange, noop, patchCoreReplicator } from '../utils.js'
import { coresTable } from '../schema/project.js'
import * as rle from './bitfield-rle.js'
import { CoreIndex } from './core-index.js'
import mapObject from 'map-obj'
import ReadyResource from 'ready-resource'
import CorestoreStorage from 'hypercore-storage'
import {
InvalidProjectKeyError,
InvalidProjectSecretKeyError,
UnexpectedError,
} from '../errors.js'

/** @import Hypercore from 'hypercore' */
Expand All @@ -43,12 +45,14 @@ export const kCoreManagerReplicate = Symbol('replicate core manager')
export class CoreManager extends ReadyResource {
#corestore
#coreIndex
#storage
/** @type {CoreRecord} */
#creatorCoreRecord
#queries
#encryptionKeys
#projectExtension
/** @type {'opened' | 'closing' | 'closed'} */
// @ts-ignore
#state = 'opened'
#haveExtension
#deviceId
Expand All @@ -67,7 +71,7 @@ export class CoreManager extends ReadyResource {
* @param {Buffer} options.projectKey 32-byte public key of the project creator core
* @param {Buffer} [options.projectSecretKey] 32-byte secret key of the project creator core
* @param {Partial<Record<Namespace, Buffer>>} [options.encryptionKeys] Encryption keys for each namespace
* @param {import('hypercore').HypercoreStorage} options.storage Folder to store all hypercore data
* @param {string} options.storage Folder to store all hypercore data
* @param {boolean} [options.autoDownload=true] Immediately start downloading cores - should only be set to false for tests
* @param {Logger} [options.logger]
*/
Expand Down Expand Up @@ -110,11 +114,13 @@ export class CoreManager extends ReadyResource {
.prepare(),
}

this.#storage = new CorestoreStorage(storage)

// Note: the primary key here should not be used, because we do not rely on
// corestore for key storage (i.e. we do not get cores from corestore via a
// name, which would derive the keypair from the primary key), but setting
// this just in case a dependency does (e.g. hyperdrive) and we miss it.
this.#corestore = new Corestore(storage, { primaryKey })
this.#corestore = new Corestore(this.#storage, { primaryKey, unsafe: true })
// Persistent index of core keys and namespaces in the project
this.#coreIndex = new CoreIndex()

Expand Down Expand Up @@ -290,31 +296,34 @@ export class CoreManager extends ReadyResource {
const existingCore = this.#coreIndex.getByCoreKey(keyPair.publicKey)
if (existingCore) return existingCore

const encryptionKey = this.#encryptionKeys[namespace]
const { publicKey: key, secretKey } = keyPair
const writer = !!secretKey
// Pass both key and keyPair to work around corestore bug where keyPair alone
// doesn't result in the correct core key
const core = this.#corestore.get({
key,
keyPair,
encryptionKey: this.#encryptionKeys[namespace],
encryption: encryptionKey ? { key: encryptionKey } : undefined,
})
if (this.#autoDownload && namespace !== 'blob') {
// Blob downloads are managed by BlobStore
core.download({ start: 0, end: -1 })
}
// Every peer adds a listener, so could have many peers
core.setMaxListeners(0)
// @ts-ignore - ensure key is defined before hypercore is ready
core.key = key

this.#coreIndex.add({ core, key, namespace, writer })

// **Hack** As soon as a peer is added, eagerly send a "want" for the entire
// core. This ensures that the peer sends back its entire bitfield.
// Otherwise this would only happen once we call core.download()
core.on('peer-add', (peer) => {
core.on('peer-add', (/** @type {HypercorePeer} */ peer) => {
if (core.length === 0) return
// **Warning** uses internal method, but should be covered by tests
peer._maybeWant(0, core.length)
forceBitfieldExchange(core, peer)
})

patchCoreReplicator(core)
if (writer) {
const sendHaves = debounce(WRITER_CORE_PREHAVES_DEBOUNCE_DELAY, () => {
for (const peer of this.creatorCore.peers) {
Expand Down Expand Up @@ -345,7 +354,7 @@ export class CoreManager extends ReadyResource {
// TODO: It would be more efficient (in terms of network traffic) to
// send a want with start = length of previous want. Need to track
// "last want length" sent by peer.
peer._maybeWant(0, core.length)
forceBitfieldExchange(core, peer)
}
})
}
Expand Down Expand Up @@ -486,15 +495,45 @@ export class CoreManager extends ReadyResource {

for (const { core, key } of coreRecords) {
if (key.equals(ownWriterCore.key)) continue
deletionPromises.push(core.purge())
deletionPromises.push(purgeCore(core))
}

await Promise.all(deletionPromises)

this.#queries.removeCores.run({ namespace })

// This actually clears out the data
await this.#storage.compact()
}
}

/**
* Purge data inside a hypercore.
* Pending on this PR being merged into hypercore11
* https://github.com/holepunchto/hypercore/pull/788
* @param {Hypercore<Hypercore.ValueEncoding, Buffer>} core
*/
async function purgeCore(core) {
const privateCore = core.core
// Should never happen
if (!privateCore) throw new UnexpectedError('Core not initialized')
if (privateCore.opened === false) await privateCore.opening
// @ts-ignore Private methods on storage
const tx = privateCore.storage.write()
tx.deleteTreeNodeRange(0, -1)
tx.deleteBlockRange(0, -1)
// Also delete the head record (fork, length, root hash): a header claiming
// length > 0 with no merkle roots in storage puts the core in hypercore's
// repair mode on next open, in which it never sends a Synchronize and so
// never syncs again
tx.deleteHead()
// @ts-ignore Private methods on storage
privateCore.bitfield.clear(tx)
await tx.flush()
await privateCore.closeAllSessions()
await privateCore.close()
}

/**
* @typedef {object} HaveMsg
* @property {Buffer} discoveryKey
Expand Down
14 changes: 14 additions & 0 deletions src/datastore/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { discoveryKey } from 'hypercore-crypto'
import { NAMESPACE_SCHEMAS } from '../constants.js'
import { createMap } from '../utils.js'
import {
DataStoreClosedError,
InvalidDocSchemaError,
InvalidVersionIdError,
NotFoundError,
Expand Down Expand Up @@ -50,6 +51,7 @@ export class DataStore extends TypedEmitter {
#pendingAppends = new Set()
/** @type {Record<MapeoDoc['schemaName'], Set<string>>} */
#pendingEmits
#closed = false
/** @type {(coreRecord: import('../core-manager/index.js').CoreRecord) => void} */
#handleAddCore

Expand Down Expand Up @@ -145,6 +147,7 @@ export class DataStore extends TypedEmitter {
* @returns {Promise<Extract<MapeoDoc, TDoc>>}
*/
async write(doc) {
this.#checkClosed('write')
// @ts-ignore
if (!NAMESPACE_SCHEMAS[this.#namespace].includes(doc.schemaName)) {
throw new InvalidDocSchemaError({
Expand Down Expand Up @@ -196,6 +199,7 @@ export class DataStore extends TypedEmitter {

/** @param {Buffer} buf */
async writeRaw(buf) {
this.#checkClosed('write')
const { length } = await this.#writerCore.append(buf)
const index = length - 1
const coreDiscoveryKey = this.#writerCore.discoveryKey
Expand All @@ -217,13 +221,23 @@ export class DataStore extends TypedEmitter {
}

async close() {
if (this.#closed) return
this.#closed = true
// Remove the add-core listener before closing the indexer so a late
// 'add-core' event during teardown can't call addCore() on a closed indexer
// (which would throw 'Cannot add core after closing').
this.#coreManager.off('add-core', this.#handleAddCore)
await this.#coreIndexer.close()
}

/**
* Throw an error if the datastore is closed.
* @param {string} method
*/
#checkClosed(method) {
if (this.#closed) throw new DataStoreClosedError({ method })
}

/**
* Unlink all index files. This should only be called after `close()` has resolved.
*/
Expand Down
8 changes: 8 additions & 0 deletions src/datatype/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { validate } from '@comapeo/schema'
import { getTableConfig } from 'drizzle-orm/sqlite-core'
import { eq, inArray, sql } from 'drizzle-orm'
import { randomBytes } from 'node:crypto'
import { isDeepStrictEqual } from 'node:util'
import { noop, mutatingDeNullify } from '../utils.js'
import {
DocAlreadyDeletedError,
Expand Down Expand Up @@ -199,6 +200,13 @@ export class DataType extends TypedEmitter {
async [kCreateOrUpdateWithDocId](docId, value) {
const existing = await this.getByDocId(docId).catch(nullIfNotFound)
if (existing) {
const hasChanges = Object.keys(value).some(
// @ts-ignore Key will always be a valid property
(key) => !isDeepStrictEqual(existing[key], value[key])
)
if (!hasChanges) {
return existing
}
return this.update(existing.versionId, value)
} else {
return this[kCreateWithDocId](docId, value, { checkExisting: false })
Expand Down
6 changes: 6 additions & 0 deletions src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,12 @@ export const InvalidDrizzleJournalError = createErrorClass({
status: 400,
})

export const DataStoreClosedError = createErrorClass({
code: 'DATA_STORE_CLOSED_ERROR',
message: 'Unable to invoke {method}, DataStore is closed',
status: 503,
})

/**
* @param {unknown} err
* @returns {null}
Expand Down
2 changes: 1 addition & 1 deletion src/fastify-plugins/blobs.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async function routes(fastify, options) {
} catch (e) {
// This matches [how Hyperblobs checks if a blob is unavailable][0].
// [0]: https://github.com/holepunchto/hyperblobs/blob/518088d2b828082fd70a276fa2c8848a2cf2a56b/index.js#L49
if (ensureError(e).message === 'Block not available') {
if (ensureError(e).message.includes('BLOCK_NOT_AVAILABLE')) {
reply.code(404)
throw new BlobNotFoundError()
} else {
Expand Down
Loading
Loading