| … | … | @@ -1071,18 +1071,25 @@ export class DidPeerRouter { |
| 1071 | 1071 | })().catch(() => {}) |
| 1072 | 1072 | } |
| 1073 | 1073 | |
| 1074 | async _emitSync (did, front) { | |
| 1074 | // `force`: the relay dedupes frames by key, and an automatic #sync is keyed by (did, rev) so a re-anchor | |
| 1075 | // never re-emits the same head. An OPERATOR re-announce must not be deduplicated: on 2026-10-13 bsky's | |
| 1076 | // relay held loser-club at a newer July rev and dropped every commit; the fix is one #sync at our head, | |
| 1077 | // and the (did, rev) key — already used by a #sync on 2026-07-26 — made emit-sync a silent no-op. So a | |
| 1078 | // forced emission's key carries its time: each operator request is its own announcement. | |
| 1079 | async _emitSync (did, front, { force = false } = {}) { | |
| 1075 | 1080 | if (!this.relay) return { did, emitted: false, reason: 'no relay' } |
| 1076 | 1081 | const signed = front.head && front.head.signed |
| 1077 | 1082 | if (!signed) return { did, emitted: false, reason: 'no commit' } |
| 1078 | 1083 | const time = new Date().toISOString() |
| 1079 | 1084 | const frameBytes = await syncFrame({ seq: 0, did, signedCommit: signed, time }) |
| 1080 | const emitted = await this.relay.emit(`sync:${did}:${signed.rev}`, frameBytes) | |
| 1081 | return { did, rev: signed.rev, emitted } | |
| 1085 | const key = force ? `sync:${did}:${signed.rev}:${time}` : `sync:${did}:${signed.rev}` | |
| 1086 | const emitted = await this.relay.emit(key, frameBytes) | |
| 1087 | return { did, rev: signed.rev, emitted, forced: !!force } | |
| 1082 | 1088 | } |
| 1083 | 1089 | |
| 1084 | // Public control-plane trigger: force a #sync re-baseline for a fronted DID (from its freshest front). | |
| 1085 | async emitSync (did) { const f = await this.front(did); return this._emitSync(did, f) } | |
| 1090 | // Public control-plane trigger (the operator's `emit-sync`): FORCE a #sync re-baseline for a fronted DID | |
| 1091 | // at its freshest front, even if that head was #synced before. | |
| 1092 | async emitSync (did) { const f = await this.front(did); return this._emitSync(did, f, { force: true }) } | |
| 1086 | 1093 | |
| 1087 | 1094 | // Pull-only staleness ceiling + relay failover (owns-swarm/relay mode). NEVER destroys a connection — |
| 1088 | 1095 | // cores live-follow via the standing download; this just nudges a front that saw no 'append' for a window |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "atproto-did-peer", |
| 3 | "version": "0.2.13", | |
| 3 | "version": "0.2.14", | |
| 4 | 4 | "description": "Keyless, direction-free P2P reader/seeder for an atproto DID's repo+firehose Hypercores: announce-push discovery + event-driven head reconciliation + a keyless read surface. The shared primitive beneath atproto-bridge, atproto-relay and favela (extracted from atproto-bridge did-peer.js).", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -8,7 +8,7 @@ |
| 8 | 8 | ".": "./index.js" |
| 9 | 9 | }, |
| 10 | 10 | "scripts": { |
| 11 | "test": "node test/commitops.test.mjs && node test/router.test.mjs && node test/co-owned.test.mjs && node test/coowner-set.test.mjs && node test/evict.test.mjs && node test/fork.test.mjs && node test/rpc-candidates.test.mjs && node test/no-repo.test.mjs && node test/get-repo-since.test.mjs && node test/rpc-timeout.test.mjs", | |
| 11 | "test": "node test/commitops.test.mjs && node test/router.test.mjs && node test/co-owned.test.mjs && node test/coowner-set.test.mjs && node test/evict.test.mjs && node test/fork.test.mjs && node test/rpc-candidates.test.mjs && node test/no-repo.test.mjs && node test/get-repo-since.test.mjs && node test/rpc-timeout.test.mjs && node test/emit-sync.test.mjs", | |
| 12 | 12 | "test:live": "node test/read-live.mjs" |
| 13 | 13 | }, |
| 14 | 14 | "dependencies": { |
| … | … | @@ -0,0 +1,35 @@ |
| 1 | // emit-sync: an OPERATOR re-announce must reach the relay even for a rev that was #synced before. | |
| 2 | // | |
| 3 | // 2026-10-13: bsky's relay held loser-club at a July rev (3mrnruepxss22) newer than the head we serve | |
| 4 | // (3mrfgnyiurk22), so it dropped every commit. The fix is one #sync at our head — and `emit-sync` answered | |
| 5 | // emitted:false, because the relay dedupes frames by key `sync:<did>:<rev>` and a #sync for 3mrfgnyiurk22 | |
| 6 | // had been emitted on 2026-07-26. The lever could never re-announce a head it had announced once, which is | |
| 7 | // precisely the case it exists for. Automatic emits stay deduplicated; an operator request is new each time. | |
| 8 | // Run: node test/emit-sync.test.mjs | |
| 9 | import { DidPeerRouter } from '../index.js' | |
| 10 | ||
| 11 | const assert = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log(' ok -', m) } | |
| 12 | ||
| 13 | // A relay that dedupes by key exactly as FleetRelay.emit does. | |
| 14 | function fakeRelay () { | |
| 15 | const keys = new Set() | |
| 16 | return { keys, async emit (key) { if (keys.has(key)) return false; keys.add(key); return true } } | |
| 17 | } | |
| 18 | const front = { head: { signed: { did: 'did:plc:lc', rev: '3mrfgnyiurk22', version: 3, sig: new Uint8Array(64) } } } | |
| 19 | ||
| 20 | const r = Object.create(DidPeerRouter.prototype) | |
| 21 | r.relay = fakeRelay() | |
| 22 | r.front = async () => front | |
| 23 | ||
| 24 | const auto1 = await r._emitSync('did:plc:lc', front) | |
| 25 | const auto2 = await r._emitSync('did:plc:lc', front) | |
| 26 | assert(auto1.emitted === true, 'the first automatic #sync for a head is emitted') | |
| 27 | assert(auto2.emitted === false, 'a second AUTOMATIC #sync for the same head is deduplicated (re-anchors do not spam the relay)') | |
| 28 | ||
| 29 | const op1 = await r.emitSync('did:plc:lc') | |
| 30 | assert(op1.emitted === true, '⛔ an OPERATOR emit-sync is emitted although that head was #synced before') | |
| 31 | assert(op1.rev === '3mrfgnyiurk22', 'at the served head') | |
| 32 | await new Promise((resolve) => setTimeout(resolve, 2)) | |
| 33 | const op2 = await r.emitSync('did:plc:lc') | |
| 34 | assert(op2.emitted === true, 'and again on the next request — each operator request is its own announcement') | |
| 35 | console.log('emit-sync: all ok') |