Skip to content

Commit 968e7aa

Browse files
committed
feat: accessInfo cache + monitor.onAccessUpdated + socket auto-bust (Plan 66)
Follow-up to Phase H. Wires the server's Plan 66 `accessUpdated` fine-grained event into a useful SDK-side cache + helper. connection.accessInfo now memoizes - First call fetches from `/access-info` and caches the result in `this._accessInfoCache`. Subsequent calls return the cached object reference in O(1). - New optional arg `forceRefresh` (default `false`); pass `true` to bypass + refresh the cache. A failed fetch leaves any prior cached value intact. - Cached object reference is stable so consumers can do reference equality across calls. connection.socket auto-busts the cache - When the server emits `accessUpdated` after `connection.socket.open()`, SocketIO automatically calls `this.connection.accessInfo(true)` so the cache reflects the new permissions / serial on the next read. Best-effort — a failed refresh is swallowed and leaves the cache alone. - `'accessUpdated'` is added to the SocketIO EVENTS allow-list so `socket.on('accessUpdated', handler)` is now a valid subscription. monitor.onAccessUpdated(handler) helper - Convenience on the Monitor instance that subscribes to the same `accessUpdated` event, re-emitted by `Monitor.UpdateMethod.Socket` on the Monitor itself. Requires the Socket update method (the event is server-pushed via socket.io; polling-based update methods never fire it). - Returns `this` for chaining. Type definitions - `Connection.accessInfo(forceRefresh?)` updated in components/pryv /src/index.d.ts. `updateAccess` + `getAccessWithHistory` also added. - `Monitor.onAccessUpdated(handler)` declared in components/pryv-monitor/src/index.d.ts. Tests - 2 new Connection.test.js cases — `[CAIC]` memoize-returns-same-ref and `[CAID]` forceRefresh-replaces-cache. Both passing. - Full pryv test suite: 173 passing, 0 failing, 2 pending (matches prior baseline; no regression).
1 parent a7bd786 commit 968e7aa

8 files changed

Lines changed: 101 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Plan 66 (open-pryv.io ≥ 2.0.0-pre.X): access versioning.
1212
- `pryv.StaleAccessIdError` — typed error (extends `PryvError`) surfaced when the server responds with `409 stale-resource` on `accesses.update` / `accesses.delete`. Carries `{ provided, currentSerial }` in `.data` so callers can refetch + retry.
1313
- `connection.updateAccess(id, changes)` — convenience wrapper around `accesses.update`. Automatically translates the server's 409 response into `StaleAccessIdError`.
1414
- `connection.getAccessWithHistory(id)` — convenience wrapper around `accesses.getOne?includeHistory=true`. Returns `{ access, current?, history?: [...] }`.
15+
- `connection.accessInfo(forceRefresh)` — now memoized. First call fetches from the server and caches; subsequent calls return the cached copy in O(1). Pass `forceRefresh: true` to bypass the cache and re-fetch. Cached object reference is stable across calls (safe to compare).
16+
- `connection.socket.open()` — when the server emits `accessUpdated` (Plan 66 fine-grained event), the SocketIO layer automatically invokes `connection.accessInfo(true)` so the cache reflects the new permissions / serial on the next read. Best-effort: a failed refresh leaves any prior cached value intact.
17+
- `@pryv/socket.io`: `'accessUpdated'` is now an allowed event for `socket.on(...)`. Payload: `{ type: 'access-updated', accessId: '<base>:<serial>', serial }`.
18+
- `@pryv/monitor`: `monitor.onAccessUpdated(handler)` — register a callback for the server-pushed `accessUpdated` event. Requires `Monitor.UpdateMethod.Socket` (the event is server-pushed via socket.io; polling-based update methods won't fire it). The handler receives the structured payload above.
1519

1620
### Notes
1721
- Wire-format compatibility: every `access.id` / `access.createdBy` / `access.modifiedBy` returned by a Plan-66-capable server is parseable with `parseAccessRef`. Older servers still return bare cuids — `parseAccessRef` returns `{ base, serial: null }` for those, so callers can use the helper unconditionally.

components/pryv-monitor/src/Monitor.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,30 @@ class Monitor extends EventEmitter {
163163
return this.states.started;
164164
}
165165

166+
/**
167+
* Plan 66: register a handler for the server's fine-grained
168+
* `accessUpdated` event. Fires whenever a Pryv.io ≥ 2.0.0-pre.X server
169+
* emits the event after a successful `accesses.update` — payload is
170+
* `{ type: 'access-updated', accessId: '<base>:<serial>', serial }`.
171+
*
172+
* Requires `Monitor.UpdateMethod.Socket` to be active (the event
173+
* is server-pushed via `@pryv/socket.io`). With other update methods
174+
* (e.g. `EventsTimer` polling), the handler will never fire —
175+
* polling-based consumers should call `connection.accessInfo(true)`
176+
* themselves when they need to refresh the cache.
177+
*
178+
* The underlying `connection.socket` already busts the
179+
* `connection.accessInfo` cache when this event fires, so the next
180+
* `connection.accessInfo()` call returns the freshly-fetched copy.
181+
*
182+
* @param {Function} handler - called with `(payload)` on each event
183+
* @returns {Monitor} this (chainable)
184+
*/
185+
onAccessUpdated (handler) {
186+
this.on('accessUpdated', handler);
187+
return this;
188+
}
189+
166190
/**
167191
* @private
168192
* Called by UpdateMethod to share cross references

components/pryv-monitor/src/UpdateMethod/Socket.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ class Socket extends UpdateMethod {
2323
this.socket = await this.monitor.connection.socket.open();
2424
this.socket.on('eventsChanged', () => { this.monitor.updateEvents(); });
2525
this.socket.on('streamsChanged', () => { this.monitor.updateStreams(); });
26+
// Plan 66: re-emit the server's fine-grained `accessUpdated` event
27+
// on the Monitor itself so consumers can subscribe via
28+
// `monitor.onAccessUpdated(handler)`. The underlying SocketIO has
29+
// already busted the connection's accessInfo cache; the payload
30+
// carries the new composite accessId + serial for fine-grained
31+
// reactions.
32+
this.socket.on('accessUpdated', (payload) => {
33+
this.monitor.emit('accessUpdated', payload);
34+
});
2635
this.socket.on('error', (error) => {
2736
this.monitor.emit(Changes.ERROR, error);
2837
// If the underlying socket.io-client transport has been torn down

components/pryv-monitor/src/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ declare module 'pryv' {
9696
*/
9797
stop(): Monitor;
9898

99+
/**
100+
* Plan 66: register a handler for the server's fine-grained
101+
* `accessUpdated` event. Requires `Monitor.UpdateMethod.Socket`.
102+
* The underlying `connection.socket` busts the
103+
* `connection.accessInfo` cache before this fires.
104+
*/
105+
onAccessUpdated(handler: (payload: { type: 'access-updated', accessId: string, serial: number }) => void): Monitor;
106+
99107
/**
100108
* True when the monitor has been started and not yet stopped.
101109
*/

components/pryv-socket.io/src/SocketIO.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
const io = require('socket.io-client');
66
const { EventEmitter } = require('events');
77

8-
const EVENTS = ['eventsChanged', 'streamsChanged', 'accessesChanged', 'disconnect', 'error'];
8+
const EVENTS = ['eventsChanged', 'streamsChanged', 'accessesChanged', 'accessUpdated', 'disconnect', 'error'];
99

1010
/**
1111
* Socket.IO transport for a Connection.
@@ -84,6 +84,14 @@ class SocketIO extends EventEmitter {
8484
this._io.on('connect', () => {
8585
this.connecting = false;
8686
registerListeners(this);
87+
// Plan 66: when the server emits `accessUpdated` (fine-grained
88+
// event fired after every successful `accesses.update`), bust
89+
// the connection's `accessInfo` cache so the next read picks
90+
// up the new permissions / serial. Best-effort: a failed
91+
// refresh leaves the previous cached value intact.
92+
this._io.on('accessUpdated', () => {
93+
this.connection.accessInfo(true).catch(() => { /* swallow */ });
94+
});
8795
resolve(this);
8896
});
8997
})
@@ -105,7 +113,7 @@ class SocketIO extends EventEmitter {
105113

106114
/**
107115
* Add listener for Socket.IO events
108-
* @param {('eventsChanged'|'streamsChanged'|'accessesChanged'|'disconnect'|'error')} eventName - The event to listen for
116+
* @param {('eventsChanged'|'streamsChanged'|'accessesChanged'|'accessUpdated'|'disconnect'|'error')} eventName - The event to listen for
109117
* @param {Function} listener - The callback function
110118
* @returns {SocketIO} this
111119
*/

components/pryv/src/Connection.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,22 @@ class Connection {
7272

7373
/**
7474
* Get access info for this connection.
75-
* It's async as it is fetched from the API.
75+
*
76+
* Memoized per-Connection: the first call fetches from the server and
77+
* caches the result; subsequent calls return the cached copy in O(1).
78+
* Pass `forceRefresh: true` to invalidate the cache and fetch a fresh
79+
* copy from the server — used internally by `connection.socket` to
80+
* react to Plan 66 `accessUpdated` server-push events. A failed
81+
* server fetch leaves any prior cached value intact.
82+
*
83+
* @param {boolean} [forceRefresh=false] - bypass + refresh the cache
7684
* @returns {Promise<AccessInfo>} Promise resolving to the access info
7785
*/
78-
async accessInfo () {
79-
return this.get('access-info', null);
86+
async accessInfo (forceRefresh = false) {
87+
if (!forceRefresh && this._accessInfoCache != null) return this._accessInfoCache;
88+
const fresh = await this.get('access-info', null);
89+
this._accessInfoCache = fresh;
90+
return fresh;
8091
}
8192

8293
/**

components/pryv/src/index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,12 @@ declare module 'pryv' {
723723
fields: string[],
724724
points: Array<Array<number | string>>,
725725
): Promise<HFSeriesAddResult>;
726-
accessInfo(): Promise<AccessInfo>;
726+
/** Memoized; pass `forceRefresh: true` to bypass + refresh the cache. */
727+
accessInfo(forceRefresh?: boolean): Promise<AccessInfo>;
728+
/** Plan 66: update an access by composite id. */
729+
updateAccess(id: string, changes: object): Promise<object>;
730+
/** Plan 66: fetch an access including its full version history. */
731+
getAccessWithHistory(id: string): Promise<{ access: object, current?: string, history?: object[] }>;
727732
revoke(throwOnFail?: boolean, usingConnection?: Connection): Promise<any>;
728733
readonly deltaTime: number;
729734
readonly apiEndpoint: string;

components/pryv/test/Connection.test.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,5 +499,31 @@ describe('[CONX] Connection', () => {
499499
expect(accessInfoUser.token).to.exist;
500500
expect(newUser.access.token).to.equal(accessInfoUser.token);
501501
});
502+
503+
// Plan 66 — accessInfo caching + forceRefresh.
504+
505+
it('[CAIC] accessInfo() memoizes — second call returns the same object reference', async () => {
506+
const regexAPIandToken = /(.+):\/\/(.+)/gm;
507+
const res = regexAPIandToken.exec(testData.apiEndpoint);
508+
const apiEndpointWithToken = res[1] + '://' + newUser.access.token + '@' + res[2];
509+
const cachingConn = new pryv.Connection(apiEndpointWithToken);
510+
const first = await cachingConn.accessInfo();
511+
const second = await cachingConn.accessInfo();
512+
expect(second).to.equal(first); // same reference — served from cache
513+
});
514+
515+
it('[CAID] accessInfo(true) forces a refresh and replaces the cached object', async () => {
516+
const regexAPIandToken = /(.+):\/\/(.+)/gm;
517+
const res = regexAPIandToken.exec(testData.apiEndpoint);
518+
const apiEndpointWithToken = res[1] + '://' + newUser.access.token + '@' + res[2];
519+
const cachingConn = new pryv.Connection(apiEndpointWithToken);
520+
const first = await cachingConn.accessInfo();
521+
const refreshed = await cachingConn.accessInfo(true);
522+
expect(refreshed).to.not.equal(first); // distinct object — re-fetched
523+
expect(refreshed.token).to.equal(first.token); // same content
524+
// Next non-forced call returns the refreshed copy from cache.
525+
const cached = await cachingConn.accessInfo();
526+
expect(cached).to.equal(refreshed);
527+
});
502528
});
503529
});

0 commit comments

Comments
 (0)