openvibers/openvibe.sdk Commits

openvibe-sdk 0.37.2: realtime callbacks get the event's opaque cursor (#31)

OP OpenVibers committed 10 Oct 2026 · 522c46b216 · 8 files, +27 −16

Plan T7 (retire the numeric `seq` once every consumer reads by opaque cursor). A realtime subscriber could resume from Events' cursor (the SSE id) through `lastEventId`, but its callback was only handed the numeric `seq`, so it had nothing opaque to save after handling an event. Now each callback gets `{ seq, cursor }`, where `cursor` is the SSE id as Events sent it (null when no id was sent). The header documents saving it and passing it back as `lastEventId`.

0.37.2, additive. `realtime.test.js` checks that the cursor reaches each callback (and the replayed event is still delivered once). `ov test`: 30 files reached, green; version, changelog, STATUS and browser bundle all updated.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 43f672a..9c9b407 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -3,6 +3,13 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
release may change an API and says so here.
++## 0.37.2 (2026-10-10)
++
++- `openvibe-sdk/realtime`: each event's callback gets its opaque `cursor` (the SSE id Events sent) beside `seq`:
++ `subscribe(topics, (event, { seq, cursor }) => …)`. Save the cursor after handling the event and pass it back as
++ `lastEventId` to resume, so a subscriber never needs the numeric seq (plan T7). `cursor` is null when Events sent no
++ id. Additive.
++
## 0.37.1 (2026-10-10)
- `openvibe-sdk/testing`'s mock Events answers a pull with `latest_cursor` (the head as an opaque cursor), as
ddiff --git a/STATUS.json b/STATUS.json
iindex e83b26a..e7f4d5b 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,7 +3,7 @@
"stage": "alpha",
"code": true,
"wave": "W2",
-- "version": "0.37.1",
++ "version": "0.37.2",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex 71dfc2e..8f12e84 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1,4 +1,4 @@
--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.37.1, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.37.2, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// (auth, registry, modules, realtime, media, community, bot, search, chat, ai, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.37.1';
++const SDK_VERSION = '0.37.2';
module.exports = {
createClient,
@@@ -955,16 +955,17 @@ function (module, exports) {
/**
* openvibe-sdk/realtime: the browser realtime plane inside OpenVibe.Events (ADR-005), over SSE.
*
-- * const sub = subscribe(['live.stream.*'], (event, { seq }) => { … }, {
++ * const sub = subscribe(['live.stream.*'], (event, { seq, cursor }) => { … save cursor … }, {
* client, // or url: 'https://openvibe.events'
-- * lastEventId: savedSeq, // resume after a reload
++ * lastEventId: savedCursor, // resume after a reload (the `cursor` the callback was given)
* onGap: (gap) => refetchState(),
* });
* sub.close();
*
* Transport: EventSource when the environment has one and no Bearer token is needed (cookies ride
* along with withCredentials); otherwise fetch with a streamed body (Node, or a token). Either
-- * way it resumes from the last seq it saw (Last-Event-ID), skips anything it already delivered,
++ * way it resumes from the last position it saw (Last-Event-ID: Events' opaque cursor, the `cursor` each
++ * callback gets; an older Events sends a bare seq), skips anything it already delivered,
* and reports an `event: gap` (events missed beyond retention or replay limits) through onGap.
* Browser-safe.
*/
@@@ -1015,7 +1016,7 @@ function subscribe(topics, onEvent, opts = {}) {
state.lastSeq = seq;
}
if (id !== undefined && id !== '') state.lastId = String(id);
-- try { onEvent(msg && msg.event, { seq }); } catch (err) { report(err); }
++ try { onEvent(msg && msg.event, { seq, cursor: id !== undefined && id !== '' ? String(id) : null }); } catch (err) { report(err); }
}
async function streamUrl() {
ddiff --git a/package-lock.json b/package-lock.json
iindex 1effa93..87f2a38 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.37.1",
++ "version": "0.37.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.37.1",
++ "version": "0.37.2",
"license": "MIT",
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
ddiff --git a/package.json b/package.json
iindex 33682d0..1bb3e7a 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.37.1",
++ "version": "0.37.2",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"types": "types/index.d.ts",
ddiff --git a/src/core/index.js b/src/core/index.js
iindex 8760337..194ed60 100644
---- a/src/core/index.js
++++ b/src/core/index.js
@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
--const SDK_VERSION = '0.37.1';
++const SDK_VERSION = '0.37.2';
module.exports = {
createClient,
ddiff --git a/src/realtime.js b/src/realtime.js
iindex fc2e462..9f56a1a 100644
---- a/src/realtime.js
++++ b/src/realtime.js
@@@ -2,16 +2,17 @@
/**
* openvibe-sdk/realtime: the browser realtime plane inside OpenVibe.Events (ADR-005), over SSE.
*
-- * const sub = subscribe(['live.stream.*'], (event, { seq }) => { … }, {
++ * const sub = subscribe(['live.stream.*'], (event, { seq, cursor }) => { … save cursor … }, {
* client, // or url: 'https://openvibe.events'
-- * lastEventId: savedSeq, // resume after a reload
++ * lastEventId: savedCursor, // resume after a reload (the `cursor` the callback was given)
* onGap: (gap) => refetchState(),
* });
* sub.close();
*
* Transport: EventSource when the environment has one and no Bearer token is needed (cookies ride
* along with withCredentials); otherwise fetch with a streamed body (Node, or a token). Either
-- * way it resumes from the last seq it saw (Last-Event-ID), skips anything it already delivered,
++ * way it resumes from the last position it saw (Last-Event-ID: Events' opaque cursor, the `cursor` each
++ * callback gets; an older Events sends a bare seq), skips anything it already delivered,
* and reports an `event: gap` (events missed beyond retention or replay limits) through onGap.
* Browser-safe.
*/
@@@ -62,7 +63,7 @@ function subscribe(topics, onEvent, opts = {}) {
state.lastSeq = seq;
}
if (id !== undefined && id !== '') state.lastId = String(id);
-- try { onEvent(msg && msg.event, { seq }); } catch (err) { report(err); }
++ try { onEvent(msg && msg.event, { seq, cursor: id !== undefined && id !== '' ? String(id) : null }); } catch (err) { report(err); }
}
async function streamUrl() {
ddiff --git a/test/realtime.test.js b/test/realtime.test.js
iindex c33c5b4..b42737e 100644
---- a/test/realtime.test.js
++++ b/test/realtime.test.js
@@@ -75,9 +75,11 @@ run([
else { res.write(cframe(2)); res.write(cframe(3)); }
});
const got = [];
-- const sub = subscribe('live.*', (event, { seq }) => got.push([seq, event.event_id]), { url: srv.url, token: 't', reconnectDelayMs: 10 });
++ const cursors = [];
++ const sub = subscribe('live.*', (event, { seq, cursor }) => { got.push([seq, event.event_id]); cursors.push(cursor); }, { url: srv.url, token: 't', reconnectDelayMs: 10 });
await waitFor(() => got.length === 3);
assert.deepEqual(got, [[1, 'e1'], [2, 'e2'], [3, 'e3']], 'the replayed event is not delivered twice');
++ assert.deepEqual(cursors, [cur(1), cur(2), cur(3)], 'each callback gets its event\'s opaque cursor, to save and resume from');
assert.equal(seen[1], cur(2), 'the reconnect resumes from the cursor');
assert.equal(sub.lastEventId, cur(3));
sub.close();