Skip to content

Commit 76f9bb1

Browse files
committed
fix(Contact): composite-id-aware modifiedBy / access.id equality (Plan 58 Phase 4f)
Plan 66 introduces composite access ids on the wire — `<base>:<serial>` for any access that has been updated at least once. Events carry whatever serial was active at write time, so an event's `modifiedBy` may be bare while the current access.id is composite (or vice-versa). String equality breaks silently across the boundary. `Contact.eventIsFromContact()` now extracts `base` via the new pryv lib's `parseAccessRef()` helper before comparing. Three sites canonicalised: - `event.modifiedBy === access.id` (current access) - `collectorPrevIds.includes(event.modifiedBy)` (clientData.hdsCollectorClient chain) - `bridgePrevIds.includes(event.modifiedBy)` (clientData.previousAccessIds chain) The `refBase()` helper is defensive: if `parseAccessRef` is unavailable (older pryv lib) or parsing throws, it returns the input unchanged — keeps pre-Plan-66 callers working unchanged. [CTAQ3] regression test covers: bare event modifiedBy matching composite access.id; composite event modifiedBy at lower serial matching head; different base → no match. 489/489 tests pass. Lint clean.
1 parent b355608 commit 76f9bb1

2 files changed

Lines changed: 55 additions & 5 deletions

File tree

tests/contact.test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,21 @@ describe('[CTCT] Contact class', function () {
302302
assert.equal(c.eventIsFromContact({ modifiedBy: 'a2-older' }), true);
303303
assert.equal(c.eventIsFromContact({ modifiedBy: 'unknown' }), false);
304304
});
305+
306+
it('[CTAQ3] should match across Plan 66 composite ids (base-only equality)', () => {
307+
const c = new Contact('u', 'U');
308+
// Updated access serialises as composite `<base>:<serial>` on the wire
309+
c.addAccessObject({ id: 'a1:2' });
310+
// Event written when the access was at serial 0 (bare cuid)
311+
assert.equal(c.eventIsFromContact({ modifiedBy: 'a1' }), true);
312+
// Event written when the access was at serial 1 (composite, lower serial)
313+
assert.equal(c.eventIsFromContact({ modifiedBy: 'a1:1' }), true);
314+
// Event written at current serial
315+
assert.equal(c.eventIsFromContact({ modifiedBy: 'a1:2' }), true);
316+
// Different base → no match
317+
assert.equal(c.eventIsFromContact({ modifiedBy: 'a2' }), false);
318+
assert.equal(c.eventIsFromContact({ modifiedBy: 'a2:2' }), false);
319+
});
305320
});
306321

307322
// ---- sourceFromAccess (static) ---- //

ts/appTemplates/Contact.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,27 @@ import { HDSModelAppStreams } from '../HDSModel/HDSModel-AppStreams.ts';
66
import { getStreamIdAndChildrenIds } from '../toolkit/StreamsTools.ts';
77
import { pryv } from '../patchedPryv.ts';
88

9+
/**
10+
* Plan 66 composite-id base extractor. Refs serialise as either bare cuid
11+
* (`"abc123"`) for never-updated accesses or composite (`"abc123:3"`) for
12+
* updated heads / historical writes. Equality must be on the *base* — an
13+
* event's `modifiedBy` may carry a serial different from the current
14+
* access head, but it still attributes to the same access chain.
15+
*
16+
* Returns the input unchanged when `parseAccessRef` is unavailable
17+
* (older pryv lib), letting callers keep working under pre-Plan-66 servers.
18+
*/
19+
function refBase (ref: string | null | undefined): string | null {
20+
if (ref == null) return null;
21+
const parse = (pryv as any).utils?.parseAccessRef;
22+
if (typeof parse !== 'function') return ref;
23+
try {
24+
return parse(ref).base;
25+
} catch {
26+
return null;
27+
}
28+
}
29+
930
/** Doctor-side: a form invite pair (which collector + which invite) */
1031
export interface ContactInvite {
1132
collector: Collector;
@@ -141,14 +162,28 @@ export class Contact {
141162

142163
/** Check if an event was created/modified by this contact (including replaced accesses) */
143164
eventIsFromContact (event: pryv.Event): boolean {
165+
const eventBase = refBase(event.modifiedBy);
166+
if (eventBase == null) return false;
144167
for (const access of this.accessObjects) {
145-
if (access.id && event.modifiedBy === access.id) return true;
146-
// Check previous access IDs from replaced accesses (collector pattern)
168+
// Plan 66: access.id may be composite (`<base>:<serial>`) on updated accesses.
169+
// An event's modifiedBy carries the serial active at write time, possibly
170+
// different from the current head. Compare on base only.
171+
if (refBase(access.id) === eventBase) return true;
172+
// Check previous access IDs from replaced accesses (collector pattern, legacy delete+create).
173+
// The historical chain stays in clientData even after Plan 58 switches to in-place update.
147174
const collectorPrevIds = access.clientData?.hdsCollectorClient?.previousAccessIds;
148-
if (Array.isArray(collectorPrevIds) && collectorPrevIds.includes(event.modifiedBy)) return true;
149-
// Check previous access IDs from bridge access recreate pattern
175+
if (Array.isArray(collectorPrevIds)) {
176+
for (const prev of collectorPrevIds) {
177+
if (refBase(prev) === eventBase) return true;
178+
}
179+
}
180+
// Check previous access IDs from bridge access recreate pattern (legacy)
150181
const bridgePrevIds = access.clientData?.previousAccessIds;
151-
if (Array.isArray(bridgePrevIds) && bridgePrevIds.includes(event.modifiedBy)) return true;
182+
if (Array.isArray(bridgePrevIds)) {
183+
for (const prev of bridgePrevIds) {
184+
if (refBase(prev) === eventBase) return true;
185+
}
186+
}
152187
}
153188
return false;
154189
}

0 commit comments

Comments
 (0)