| … | … | @@ -0,0 +1,111 @@ |
| 1 | // A Corestore FROM A DIFFERENT COPY OF THE MODULE is still a Corestore. |
| 2 | // |
| 3 | // `instanceof` compares against one module instance's class object. Bundle two copies of corestore — |
| 4 | // which is the normal outcome of a nested node_modules tree, and exactly what bare-pack ships to the |
| 5 | // phone — and a store built by copy A fails `instanceof` against copy B. Every discriminator in this |
| 6 | // package was written as `!(storage instanceof Corestore)`, i.e. "not a store, so it must be a PATH": |
| 7 | // |
| 8 | // TypeError: Cannot read properties of undefined (reading 'length') |
| 9 | // at Object.join (bare-path/lib/posix.js:67) |
| 10 | // at new CorestoreStorage (hypercore-storage/index.js:496) |
| 11 | // at new Corestore (corestore/index.js:242) |
| 12 | // at new RepoStore (atproto-store/lib/repo-store.js) |
| 13 | // |
| 14 | // Measured on a real device (hyper-social #40): the daemon passed |
| 15 | // `shared.store.namespace('membrane:' + did)` into serve-membrane's RepoStore, the check said "path", |
| 16 | // and Corestore tried to path.join an OBJECT. The node had already opened its record store, taken the |
| 17 | // joiner posture, opened blobs and served two control connections — it died at the very last step, and |
| 18 | // the message named a JS idiom rather than a cause. |
| 19 | // |
| 20 | // It never reproduced on a workstation because a deduped/symlinked tree has exactly one copy. The |
| 21 | // difference between the two environments is the module graph, not the code — so the discriminator |
| 22 | // must not depend on module identity. A store is identified by what it DOES. |
| 23 | // |
| 24 | // Run: brittle-node test/corestore-cross-copy.test.js |
| 25 | import test from 'brittle' |
| 26 | import fs from 'fs' |
| 27 | import os from 'os' |
| 28 | import path from 'path' |
| 29 | import Corestore from 'corestore' |
| 30 | import { isCorestore, openCorestore } from '../lib/corestore.js' |
| 31 | import { RepoStore } from '../lib/repo-store.js' |
| 32 | import { BlobStore } from '../lib/blob-store.js' |
| 33 | |
| 34 | const tmp = (t) => { |
| 35 | const d = fs.mkdtempSync(path.join(os.tmpdir(), 'atproto-store-crosscopy-')) |
| 36 | t.teardown(() => { try { fs.rmSync(d, { recursive: true, force: true }) } catch {} }) |
| 37 | return d |
| 38 | } |
| 39 | |
| 40 | // A store from ANOTHER copy of corestore. Same shape, same behaviour, different class object — which |
| 41 | // is all a second copy in the bundle amounts to. Delegating to a real store keeps the test honest: |
| 42 | // whatever the code does with it has to actually work, not merely type-check. |
| 43 | function foreignCopy (store) { |
| 44 | const surface = {} |
| 45 | for (const m of ['get', 'namespace', 'replicate', 'ready', 'close', 'session']) { |
| 46 | if (typeof store[m] === 'function') surface[m] = store[m].bind(store) |
| 47 | } |
| 48 | return surface |
| 49 | } |
| 50 | |
| 51 | test('the premise: a foreign-copy store fails instanceof', async (t) => { |
| 52 | const store = new Corestore(tmp(t)) |
| 53 | await store.ready() |
| 54 | const foreign = foreignCopy(store) |
| 55 | t.absent(foreign instanceof Corestore, 'it is NOT instanceof the local copy — this is the whole bug') |
| 56 | t.ok(typeof foreign.get === 'function' && typeof foreign.namespace === 'function', 'but it is a store by every behaviour that matters') |
| 57 | await store.close() |
| 58 | }) |
| 59 | |
| 60 | test('isCorestore recognises a store by behaviour, not module identity', async (t) => { |
| 61 | const store = new Corestore(tmp(t)) |
| 62 | await store.ready() |
| 63 | t.ok(isCorestore(store), 'the local copy') |
| 64 | t.ok(isCorestore(foreignCopy(store)), 'and one from another copy of the module') |
| 65 | t.absent(isCorestore('/some/path'), 'a path is not a store') |
| 66 | t.absent(isCorestore(null), 'and neither is null') |
| 67 | t.absent(isCorestore({ get: 1, namespace: 2 }), 'nor an object that merely has the names') |
| 68 | await store.close() |
| 69 | }) |
| 70 | |
| 71 | test('RepoStore accepts a foreign-copy store instead of path.join-ing it', async (t) => { |
| 72 | const store = new Corestore(tmp(t)) |
| 73 | await store.ready() |
| 74 | const repo = await new RepoStore(foreignCopy(store), { name: 'membrane-repo:did:plc:test' }).ready() |
| 75 | |
| 76 | // Before the fix this threw inside the constructor. Assert it is USABLE, not merely constructed. |
| 77 | await repo.put('app.bsky.feed.post', 'abc', { $type: 'app.bsky.feed.post', text: 'hello', createdAt: '2026-08-10T00:00:00.000Z' }) |
| 78 | const got = await repo.get('app.bsky.feed.post', 'abc') |
| 79 | t.is(got && got.text, 'hello', 'a record round-trips through the foreign-copy store') |
| 80 | |
| 81 | await repo.close() |
| 82 | await store.close() |
| 83 | }) |
| 84 | |
| 85 | test('BlobStore too — the same discriminator, the same trap', async (t) => { |
| 86 | const store = new Corestore(tmp(t)) |
| 87 | await store.ready() |
| 88 | const blobs = await new BlobStore(foreignCopy(store), { name: 'membrane-blobs:did:plc:test' }).ready() |
| 89 | const put = await blobs.put(Buffer.from('some bytes'), 'application/octet-stream') |
| 90 | const back = await blobs.get(put.cid.toString()) |
| 91 | t.is(back && back.size, 10, 'a blob round-trips through the foreign-copy store') |
| 92 | t.is(back && Buffer.from(back.bytes).toString(), 'some bytes', 'and its bytes are intact') |
| 93 | await store.close() |
| 94 | }) |
| 95 | |
| 96 | test('a PATH still gets its own store — the other half of the discriminator', async (t) => { |
| 97 | const dir = tmp(t) |
| 98 | const repo = await new RepoStore(dir).ready() |
| 99 | await repo.put('app.bsky.feed.post', 'p1', { $type: 'app.bsky.feed.post', text: 'from a path', createdAt: '2026-08-10T00:00:00.000Z' }) |
| 100 | t.is((await repo.get('app.bsky.feed.post', 'p1')).text, 'from a path', 'a string path is still opened as a directory') |
| 101 | await repo.close() |
| 102 | t.ok(fs.existsSync(dir), 'and it wrote there') |
| 103 | }) |
| 104 | |
| 105 | test('openCorestore is idempotent on a foreign-copy store', async (t) => { |
| 106 | const store = new Corestore(tmp(t)) |
| 107 | await store.ready() |
| 108 | const foreign = foreignCopy(store) |
| 109 | t.is(openCorestore(foreign), foreign, 'it is handed back untouched, not wrapped in a second Corestore') |
| 110 | await store.close() |
| 111 | }) |