Skip to content

Commit 3dc2eed

Browse files
committed
refactor(CollectorClient): acceptUpdate uses accesses.update (Plan 58 Phase 4c)
Replaces the delete+create pattern (90 lines, token rotation, race window between delete and create, requester must re-store apiEndpoint) with a single `connection.updateAccess(id, { permissions })` call. Behavior change visible to consumers: - Access id, token, apiEndpoint preserved across update. Wire id becomes composite `<base>:<serial>` per Plan 66. - `response/collector-v1` `update-accept` event no longer carries an `apiEndpoint` field. The doctor's stored apiEndpoint stays valid. Q2 (locked): lockstep upgrade — no producer-writes-both compat window. clientData handling (Q6): - Update payload does NOT include clientData. Server-side `clientData` merge (verified empirically on Plan 66 demo) preserves the existing `hdsCollectorClient` object including any legacy `previousAccessIds` chain from the delete+create era. - `this.eventData` is unchanged at update time so the snapshot in `clientData.hdsCollectorClient.eventData` stays correct. StaleAccessIdError: - 1-retry budget. If another writer updates the access between the pendingUpdate load and now (rare, but possible if a doctor issues two update requests back to back), refetch the access by name and retry. Two consecutive stale errors propagate. Missing test coverage: there's no integration test for acceptUpdate in the existing suite (apptemplates.test.js covers invite create/revoke but not the update flow). The Phase 4g historical-data audit on demo accounts will catch live regressions. 488/488 unit tests pass. Lint clean.
1 parent 9431d3c commit 3dc2eed

1 file changed

Lines changed: 37 additions & 37 deletions

File tree

ts/appTemplates/CollectorClient.ts

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -511,12 +511,26 @@ export class CollectorClient {
511511
}
512512

513513
/**
514-
* Accept a pending update request: delete old access, create new one with updated permissions,
515-
* notify requester via inbox with new apiEndpoint.
514+
* Accept a pending update request: update the access in place via accesses.update
515+
* (Plan 66), notify requester via inbox.
516+
*
517+
* The access id, token and apiEndpoint are preserved across the update; only
518+
* the wire id becomes composite (`<base>:<serial>`). The requester does not
519+
* need to re-store an apiEndpoint.
520+
*
521+
* `clientData` is intentionally NOT sent: the server merges (verified empirically
522+
* on Plan 66 demo) so existing keys including legacy `hdsCollectorClient.previousAccessIds`
523+
* chains stay intact. The current `hdsCollectorClient.eventData` snapshot remains
524+
* valid because `this.eventData` is unchanged at update time.
525+
*
526+
* `StaleAccessIdError` handling: if another writer updated the access between
527+
* the load of `pendingUpdate` and now, refetch the access by name and retry
528+
* once. Two consecutive stale errors propagate.
516529
*/
517530
async acceptUpdate (): Promise<{ accessData: any, requesterEvent: any } | null> {
518531
if (!this.pendingUpdate) throw new HDSLibError('No pending update to accept');
519532
if (this.status !== CollectorClient.STATUSES.active) throw new HDSLibError('Can only accept updates on active CollectorClients');
533+
if (!this.accessData?.id) throw new HDSLibError('No access to update', this.accessData);
520534

521535
const update = this.pendingUpdate.content;
522536

@@ -527,7 +541,7 @@ export class CollectorClient {
527541
});
528542

529543
// Handle chat feature if requested
530-
const responseContent: { apiEndpoint?: string, chat?: any } = {};
544+
const responseContent: { chat?: any } = {};
531545
if (update.features?.chat && !this.hasChatFeature) {
532546
const chatStreamMain = `chat-${this.requesterUsername}`;
533547
const chatStreamIncoming = `chat-${this.requesterUsername}-in`;
@@ -559,44 +573,30 @@ export class CollectorClient {
559573
);
560574
}
561575

562-
// Collect previous access IDs for event attribution (modifiedBy tracking)
563-
const previousAccessIds: string[] = [];
564-
if (this.accessData) {
565-
if (this.accessData.id) previousAccessIds.push(this.accessData.id);
566-
// Chain: carry forward any IDs from the old access's clientData
567-
const oldPrevIds = this.accessData.clientData?.hdsCollectorClient?.previousAccessIds;
568-
if (Array.isArray(oldPrevIds)) {
569-
for (const id of oldPrevIds) {
570-
if (!previousAccessIds.includes(id)) previousAccessIds.push(id);
576+
// Update access in place. 1-retry on StaleAccessIdError.
577+
let attempt = 0;
578+
while (true) {
579+
try {
580+
const updated = await (this.app.connection as any).updateAccess(this.accessData.id, {
581+
permissions: cleanedPermissions
582+
});
583+
this.accessData = updated;
584+
break;
585+
} catch (e: any) {
586+
if (e instanceof (pryv as any).StaleAccessIdError && attempt === 0) {
587+
attempt++;
588+
// Refetch the access by name (composite serial may have advanced)
589+
const accesses = await this.app.connection.apiOne('accesses.get', {}, 'accesses');
590+
const fresh = accesses.find((a: any) => a.name === this.key);
591+
if (!fresh) throw new HDSLibError('Access disappeared during update', accesses);
592+
this.accessData = fresh;
593+
continue;
571594
}
595+
throw e;
572596
}
573597
}
574598

575-
// Delete old access
576-
if (this.accessData && !this.accessData.deleted) {
577-
await this.app.connection.apiOne('accesses.delete', { id: this.accessData.id }, 'accessDeletion');
578-
}
579-
580-
// Create new access with updated permissions
581-
const accessCreateData = {
582-
name: this.key,
583-
type: 'shared',
584-
permissions: cleanedPermissions,
585-
clientData: {
586-
hdsCollectorClient: {
587-
version: 0,
588-
eventData: this.eventData,
589-
previousAccessIds
590-
}
591-
}
592-
};
593-
const accessData = await this.app.connection.apiOne('accesses.create', accessCreateData, 'access');
594-
this.accessData = accessData;
595-
if (!this.accessData?.apiEndpoint) throw new HDSLibError('Failed creating updated access', accessData);
596-
597-
responseContent.apiEndpoint = this.accessData.apiEndpoint;
598-
599-
// Notify requester via inbox
599+
// Notify requester via inbox. No `apiEndpoint` field — token is preserved.
600600
const requesterEvent = await this.#updateRequester('update-accept', responseContent);
601601
if (requesterEvent != null) {
602602
this.pendingUpdate = null;

0 commit comments

Comments
 (0)