openvibers/openvibe.examples Commits

Examples: read Events by opaque cursor everywhere; openvibe-sdk 0.37.2; the structure test reads the pin (#11)

OP OpenVibers committed 10 Oct 2026 · 60ec75e67f · 18 files, +87 −65

* Examples: read Events by opaque cursor everywhere; openvibe-sdk 0.37.2; the structure test reads the pin

Plan T7: OpenVibe.Events retires its numeric `seq` once every consumer reads by opaque cursor (ADR-042 decision 7). This is the public sample code developers copy, so it should show the cursor way. This replaces the closed Examples#10.

- **event-subscriber:**
  - Every saved position is Events' opaque cursor, kept as a string. The cursor store refuses numbers.
  - Pull iterates `after=<saved cursor>`, saves each event's `cursor` and the page's `next_cursor` (in `onPage`, as before).
  - Realtime saves each event's `cursor`: its SSE id, which openvibe-sdk 0.37.2 hands to the callback.
  - New `startAtHead()` starts pulling from now (the head's `latest_cursor`). README updated.
- **scripts/tools-job-proof.js:** notes the head as `latest_cursor` and pages by `next_cursor` until a short page; no numeric `latest_seq`.
- **scripts/developer-path.js:** starts the subscriber at the head before publishing, instead of `seq - 1`. The export check pulls with no `after_seq`.
- **Pins:** openvibe-sdk v0.37.2 in the root and all nine examples (`latest_cursor` in the mock platform, Events#45/SDK#30; the realtime cursor, SDK#31). README and STATUS name it.
- **The structure test reads the expected pin from the root package.json** instead of a hardcoded tag. A hardcoded tag is why the pin bot's PR failed CI: it moved every package.json and not the test's constant.

Tests: structure, tools-job-proof (the mock's Events through the cursor path), developer-path (start at head → publish → found by cursor), create-app and public-surface all pass (`ov test` 5/5). No `after_seq`, `afterSeq`, `next_after_seq` or `latest_seq` remains in code or docs.

* event-subscriber smoke: saved positions are opaque cursors (the realtime one is the SSE id as sent)

Changes

ddiff --git a/README.md b/README.md
iindex 55eb417..55bcd1c 100644
---- a/README.md
++++ b/README.md
@@@ -9,7 +9,7 @@ of them (Media, Events pull, a Tools job) against the real platform with your ow
is not part of CI or `npm test`, and it was not run against production as part of this release.
`npm run developer-path` is the Wave 20 exit check (account to Media and Events to revoked
credentials); CI runs it against the mock platform, and a person runs it against production.
--**Built on:** [openvibe-sdk v0.35.2](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.35.2), and
++**Built on:** [openvibe-sdk v0.37.2](https://github.com/OpenVibers/OpenVibe.SDK/tree/v0.37.2), and
openvibe-contracts v0.127.0 for the mod manifest.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.1
and §18.9; roadmap Wave 20, §30 (public SDK surface), ADR-014 (developer projects).
@@@ -265,7 +265,7 @@ Repository checks (`test/`):
value in a `.env.example`.
- `structure.test.js` checks the nine examples are complete and copyable: README with "What it
proves" and "Run it against the real platform", a `.env.example` that names every variable the
-- code reads, MIT license, `openvibe-sdk` pinned to the v0.35.2 tag, no `file:` links; and that
++ code reads, MIT license, `openvibe-sdk` pinned to one tag (the root package.json's, v0.37.2 now), no `file:` links; and that
`npm run e2e` is outside CI and `npm test` and refuses to start without its variables; and that
CI runs the developer path against the mock platform only.
- `developer-path.test.js` runs the developer path against the mock platform and checks the
@@@ -298,7 +298,7 @@ test/public-surface.test.js, test/structure.test.js
## Depends on
--- OpenVibe.SDK (v0.35.2), OpenVibe.Contracts (v0.113.0, mod manifest only)
++- OpenVibe.SDK (v0.37.2), OpenVibe.Contracts (v0.113.0, mod manifest only)
- at run time against the real platform: Network (tokens, registry, developer projects), Events,
Media, Tools, Chat (on openvibe.live)
@@@ -349,5 +349,5 @@ own app.
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
<!-- versions:start -->
--- openvibe-sdk: v0.35.2
++- openvibe-sdk: v0.37.2
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex ed207b8..0f65b8d 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W20",
"version": "0.3.0",
-- "sdk": "openvibe-sdk v0.35.2",
++ "sdk": "openvibe-sdk v0.37.2",
"contracts": "openvibe-contracts v0.126.0 (mod-manifest example)",
"deployed": false,
"deployment": "none: a repository with CI, not a runtime and not a published package",
ddiff --git a/examples/browser-app/package.json b/examples/browser-app/package.json
iindex 00f8ddf..52dc455 100644
---- a/examples/browser-app/package.json
++++ b/examples/browser-app/package.json
@@@ -13,6 +13,6 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/examples/event-subscriber/README.md b/examples/event-subscriber/README.md
iindex b20d751..c4e4416 100644
---- a/examples/event-subscriber/README.md
++++ b/examples/event-subscriber/README.md
@@@ -21,17 +21,19 @@ OV_EVENTS_MODE=realtime OV_TOPICS=chat.message.created node subscriber.js # pu
answered as a duplicate, so retries are safe). Another project's `app.*` topic is refused by the
SDK before anything is sent (and by Events with `403 events.topic_not_allowed`).
`OV_PLATFORM_TOPICS` adds public first-party topics (`live.stream.*`) to a pull.
--- **Pull** (`openvibe-sdk/events` `iterate()`): pages from the saved cursor to the head. The
-- cursor is saved after each handled event, and moved past non-matching events (the page's
-- `next_after_seq`) in `onPage`, which the SDK calls only after every item of that page was
-- handled. A handler that throws leaves the cursor on the last event that succeeded, so the failed
++- **Pull** (`openvibe-sdk/events` `iterate()`): pages from the saved cursor to the head. Every
++ position is Events' opaque cursor (ADR-042): kept as a string, handed back as `after`, never
++ computed with. The cursor is saved after each handled event, and moved past non-matching events
++ (the page's `next_cursor`) in `onPage`, which the SDK calls only after every item of that page was
++ handled. `startAtHead()` starts from now instead (the head's `latest_cursor`), skipping history. A handler that throws leaves the cursor on the last event that succeeded, so the failed
event is retried next time.
- **Gaps**: when Events can no longer supply part of the range (retention pruned it: sandbox app
events are kept 7 days; or a restored cursor is ahead of the stream), `onResync(gap)` is called.
Nothing can replay the missing events, so reload whatever state you derive from them.
- **Realtime** (`openvibe-sdk/realtime` `subscribe()`): SSE from `/realtime/stream`,
-- anonymously, for public first-party events. It resumes from the saved cursor with
-- `Last-Event-ID`, and nothing is delivered twice. The app token is never sent on it.
++ anonymously, for public first-party events. Each callback gets the event's `cursor` (its SSE id,
++ openvibe-sdk 0.37.2), saved after the handler finished; it resumes from it with `Last-Event-ID`,
++ and nothing is delivered twice. The app token is never sent on it.
- The cursor file is replaced atomically (write + rename).
## Files
ddiff --git a/examples/event-subscriber/package.json b/examples/event-subscriber/package.json
iindex 1f5b4bb..ff068b5 100644
---- a/examples/event-subscriber/package.json
++++ b/examples/event-subscriber/package.json
@@@ -13,6 +13,6 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/examples/event-subscriber/subscriber.js b/examples/event-subscriber/subscriber.js
iindex 5424c98..38292fe 100644
---- a/examples/event-subscriber/subscriber.js
++++ b/examples/event-subscriber/subscriber.js
@@@ -18,8 +18,9 @@
*
* The cursor is saved only AFTER an event was handled, so a crash replays at most the event in
* hand (make your handler idempotent on event_id). In pull mode the cursor also moves past events
-- * that did not match the topic, to the page's next_after_seq: iterate() calls onPage only after
-- * every item of that page was handled, so saving it there is crash-safe.
++ * that did not match the topic, to the page's next_cursor: iterate() calls onPage only after
++ * every item of that page was handled, so saving it there is crash-safe. Every position saved is
++ * Events' opaque cursor (ADR-042): keep it as a string and hand it back; never compute with it.
*
* A gap means Events can no longer give you part of the range (retention pruned it: sandbox app
* events are kept 7 days; or the cursor is ahead of the stream after a restore). Nothing can replay
@@@ -58,15 +59,16 @@ function loadConfig(env = process.env) {
};
}
--/** A tiny durable cursor: { realtime: seq, pull: seq } in a JSON file, replaced atomically. */
++/** A tiny durable cursor: { realtime: <cursor>, pull: <cursor> } in a JSON file, replaced atomically. */
function createCursorStore(file) {
let state = {};
try { state = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { state = {}; }
++ const usable = (v) => typeof v === 'string' && v.length > 0;
return {
-- get: (name) => (Number.isFinite(state[name]) ? state[name] : null),
-- set(name, seq) {
-- if (!Number.isFinite(seq) || seq === state[name]) return;
-- state = { ...state, [name]: seq };
++ get: (name) => (usable(state[name]) ? state[name] : null),
++ set(name, cursor) {
++ if (!usable(cursor) || cursor === state[name]) return;
++ state = { ...state, [name]: cursor };
fs.mkdirSync(path.dirname(file), { recursive: true });
const tmp = `${file}.${process.pid}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(state));
@@@ -123,15 +125,15 @@ function createSubscriber(config, { onEvent, onResync = () => {}, fetch, log = c
const iterator = ev.iterate({
topic: config.topics.length ? config.topics : '*',
platformTopics: config.platformTopics,
-- afterSeq: cursor.get('pull') || 0,
++ ...(cursor.get('pull') ? { after: cursor.get('pull') } : {}),
limit,
onGap: (g) => gap(g, 'pull'),
// After every item of the page was handled: also moves past events of other topics.
-- onPage: (page) => cursor.set('pull', page.next_after_seq),
++ onPage: (page) => cursor.set('pull', page.next_cursor),
});
-- for await (const { seq, event } of iterator) {
++ for await (const { seq, cursor: at, event } of iterator) {
await onEvent(event, { seq, via: 'pull' });
-- cursor.set('pull', seq);
++ cursor.set('pull', at);
handled++;
stats.handled++;
}
@@@ -155,11 +157,11 @@ function createSubscriber(config, { onEvent, onResync = () => {}, fetch, log = c
// ── realtime ────────────────────────────────────────────
function startRealtime() {
let chain = Promise.resolve();
-- sub = subscribe(config.topics, (event, { seq }) => {
-- // Handle strictly in order; save the cursor only after the handler finished.
++ sub = subscribe(config.topics, (event, { seq, cursor: at }) => {
++ // Handle strictly in order; save the cursor (the event's SSE id) only after the handler finished.
chain = chain.then(async () => {
await onEvent(event, { seq, via: 'realtime' });
-- cursor.set('realtime', seq);
++ cursor.set('realtime', at);
stats.handled++;
}).catch((err) => log.error(`[events] handler failed at seq ${seq}: ${err.message}`));
}, {
@@@ -168,17 +170,26 @@ function createSubscriber(config, { onEvent, onResync = () => {}, fetch, log = c
transport: 'fetch',
lastEventId: cursor.get('realtime'),
onGap: (g) => { chain = chain.then(() => gap(g, 'realtime')); },
-- onOpen: () => log.log(`[events] realtime connected (${config.topics.join(', ')}) from seq ${sub ? sub.lastEventId : cursor.get('realtime')}`),
++ onOpen: () => log.log(`[events] realtime connected (${config.topics.join(', ')}) from ${(sub ? sub.lastEventId : cursor.get('realtime')) || 'now'}`),
onError: (err) => log.error(`[events] realtime: ${isOpenVibeError(err) ? err.code : err.message}`),
});
return sub;
}
++ /** Start pulling from now: the pull cursor becomes Events' head (latest_cursor), so history is skipped. → the cursor */
++ async function startAtHead() {
++ const ev = await eventsForApp();
++ const page = await ev.pull({ topic: config.topics.length ? config.topics : '*', platformTopics: config.platformTopics, limit: 1 });
++ if (page.latest_cursor) cursor.set('pull', page.latest_cursor);
++ return page.latest_cursor || null;
++ }
++
return {
client,
stats,
cursor,
pullOnce,
++ startAtHead,
resolveTopics,
start() {
stopped = false;
ddiff --git a/examples/event-subscriber/test/smoke.test.js b/examples/event-subscriber/test/smoke.test.js
iindex f144764..67748ec 100644
---- a/examples/event-subscriber/test/smoke.test.js
++++ b/examples/event-subscriber/test/smoke.test.js
@@@ -66,6 +66,8 @@ const firstParty = (visibility = 'public', type = 'media.object.uploaded') => ({
const b = await pub(env, { name: 'order.paid' });
// ── pull: own topic by default, durable cursor ──────────
++ // Saved positions are Events' opaque cursors; the mock makes them as Events does (c1.<epoch>.<base64url(seq)>).
++ const cur = (seq) => `c1.0.${Buffer.from(String(seq), 'utf8').toString('base64url')}`;
const pullEnv = { ...env, OV_CURSOR_PATH: path.join(dir, 'pull.json') };
let seen = [];
let s = createSubscriber(loadConfig(pullEnv), { fetch, log: quiet, onEvent: (e, { seq }) => { seen.push(seq); } });
@@@ -73,7 +75,7 @@ const firstParty = (visibility = 'public', type = 'media.object.uploaded') => ({
assert.equal(await s.pullOnce({ limit: 2 }), 2);
assert.deepEqual(seen, [a.seq, b.seq], 'own project only: not the other project, not the forged one');
const head = platform.state.events.at(-1).seq;
-- assert.equal(createCursorStore(pullEnv.OV_CURSOR_PATH).get('pull'), head, 'cursor saved at the head');
++ assert.equal(createCursorStore(pullEnv.OV_CURSOR_PATH).get('pull'), cur(head), 'cursor saved at the head, as an opaque cursor');
// A new process resumes from the cursor: only what is new.
const c = await pub(env, { name: 'order.shipped' });
@@@ -91,14 +93,14 @@ const firstParty = (visibility = 'public', type = 'media.object.uploaded') => ({
onEvent: (ev, { seq }) => { if (seq === e.seq) throw new Error('boom'); seen.push(seq); },
});
await assert.rejects(s.pullOnce(), /boom/);
-- assert.equal(createCursorStore(pullEnv.OV_CURSOR_PATH).get('pull'), d.seq);
++ assert.equal(createCursorStore(pullEnv.OV_CURSOR_PATH).get('pull'), cur(d.seq));
s = createSubscriber(loadConfig(pullEnv), { fetch, log: quiet, onEvent: (ev, { seq }) => { seen.push(seq); } });
await s.pullOnce();
assert.deepEqual(seen, [d.seq, e.seq], 'the failed event is retried, nothing is skipped');
// A gap: retention pruned the range the cursor still needs. onResync is told, reading continues.
const gapEnv = { ...env, OV_CURSOR_PATH: path.join(dir, 'gap.json') };
-- createCursorStore(gapEnv.OV_CURSOR_PATH).set('pull', a.seq);
++ createCursorStore(gapEnv.OV_CURSOR_PATH).set('pull', cur(a.seq));
platform.pruneEvents(c.seq);
const f = await pub(env);
const resyncs = [];
@@@ -143,7 +145,8 @@ const firstParty = (visibility = 'public', type = 'media.object.uploaded') => ({
const p2 = platform.publishEvent(firstParty('public', 'media.object.deleted'));
await waitFor(() => live.length === 2, 'two public events');
assert.deepEqual(live.map((x) => x.seq), [p1.seq, p2.seq]);
-- await waitFor(() => createCursorStore(rtEnv.OV_CURSOR_PATH).get('realtime') === p2.seq, 'the realtime cursor');
++ // The realtime position is the SSE id as sent: an opaque cursor from Events, a bare seq from this mock.
++ await waitFor(() => createCursorStore(rtEnv.OV_CURSOR_PATH).get('realtime') === String(p2.seq), 'the realtime cursor');
// The connection drops; an event published meanwhile arrives once after the reconnect.
platform.dropRealtime();
@@@ -165,7 +168,7 @@ const firstParty = (visibility = 'public', type = 'media.object.uploaded') => ({
// A cursor ahead of the stream (restored from another environment): a gap, then live events.
const aheadPath = path.join(dir, 'ahead.json');
-- createCursorStore(aheadPath).set('realtime', 9999);
++ createCursorStore(aheadPath).set('realtime', '9999');
const rtGaps = [];
const rt3 = createSubscriber(loadConfig({ ...rtEnv, OV_CURSOR_PATH: aheadPath }), { fetch, log: quiet, onEvent() {}, onResync: (g, { via }) => rtGaps.push({ ...g, via }) });
rt3.start();
ddiff --git a/examples/media-uploader/package.json b/examples/media-uploader/package.json
iindex ecb4487..a71d1fa 100644
---- a/examples/media-uploader/package.json
++++ b/examples/media-uploader/package.json
@@@ -13,6 +13,6 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/examples/mod-manifest/package.json b/examples/mod-manifest/package.json
iindex d5d7d90..1d024e3 100644
---- a/examples/mod-manifest/package.json
++++ b/examples/mod-manifest/package.json
@@@ -13,7 +13,7 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.127.0"
}
}
ddiff --git a/examples/node-server-app/package.json b/examples/node-server-app/package.json
iindex f2cf46b..e5005db 100644
---- a/examples/node-server-app/package.json
++++ b/examples/node-server-app/package.json
@@@ -13,6 +13,6 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/examples/oauth-app/package.json b/examples/oauth-app/package.json
iindex 7ef3a1e..538b95b 100644
---- a/examples/oauth-app/package.json
++++ b/examples/oauth-app/package.json
@@@ -13,6 +13,6 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/examples/tool-job/package.json b/examples/tool-job/package.json
iindex 19d6d11..18c1a82 100644
---- a/examples/tool-job/package.json
++++ b/examples/tool-job/package.json
@@@ -13,6 +13,6 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/examples/webhook-consumer/package.json b/examples/webhook-consumer/package.json
iindex 50ae525..17e4837 100644
---- a/examples/webhook-consumer/package.json
++++ b/examples/webhook-consumer/package.json
@@@ -13,7 +13,7 @@
"node": ">=22"
},
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2",
"better-sqlite3": "^11.10.0"
}
}
ddiff --git a/package-lock.json b/package-lock.json
iindex ee5ed70..c7dbf85 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -15,7 +15,7 @@
"create-openvibe-app": "scripts/create-openvibe-app.js"
},
"devDependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -26,7 +26,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -48,7 +48,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -59,7 +59,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -71,7 +71,7 @@
"license": "MIT",
"dependencies": {
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.127.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -82,7 +82,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -93,7 +93,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -104,7 +104,7 @@
"version": "0.2.0",
"license": "MIT",
"dependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -116,7 +116,7 @@
"license": "MIT",
"dependencies": {
"better-sqlite3": "^11.10.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
},
"engines": {
"node": ">=22"
@@@ -472,9 +472,9 @@
"link": true
},
"node_modules/openvibe-sdk": {
-- "version": "0.35.2",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2",
-- "integrity": "sha512-HudKZJK3lZ9i0+yRdMrz9pJmIrYNTN8AFqbHFCz5jDCF68CfZwUJQyhE61pHODzGRyTvUi4DtAzkd0WEzIWx/g==",
++ "version": "0.37.2",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2",
++ "integrity": "sha512-7kZbDVi35cXAC3J6zFffz/NvcKRVFxI4SQnrl0m45HEo4Tig6cB25K/lYlPzCGzNaVsA3K8MjbOkGRGd9lvF9A==",
"license": "MIT",
"engines": {
"node": ">=20"
ddiff --git a/package.json b/package.json
iindex 5805942..87acf66 100644
---- a/package.json
++++ b/package.json
@@@ -23,6 +23,6 @@
"node": ">=22"
},
"devDependencies": {
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.37.2"
}
}
ddiff --git a/scripts/developer-path.js b/scripts/developer-path.js
iindex 9c778d8..e22e3aa 100644
---- a/scripts/developer-path.js
++++ b/scripts/developer-path.js
@@@ -209,15 +209,16 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, services = PRODU
});
await step('events', async () => {
-- const pub = await publishAppEvent(publishConfig(creds()), { name: 'developer_path.ran', subject: { type: 'check', id: 'developer-path' }, payload: { at: now().toISOString() } }, { fetch: fetchImpl });
-- say(`published ${pub.event_type} ${pub.event_id} -> seq ${pub.seq}`);
-- st.eventId = pub.event_id;
const cursorPath = path.join(dir, 'cursor.json');
-- createCursorStore(cursorPath).set('pull', Math.max(0, pub.seq - 1));
const seen = [];
const s = createSubscriber(subscriberConfig({ ...creds(), OV_EVENTS_MODE: 'pull', OV_CURSOR_PATH: cursorPath }), {
fetch: fetchImpl, log: { log: () => {}, error: (m) => say(m) }, onEvent: (event, { seq }) => { seen.push({ seq, id: event.event_id }); },
});
++ // Start the subscriber at the head (an opaque cursor), then publish: the pull finds what came after it.
++ await s.startAtHead();
++ const pub = await publishAppEvent(publishConfig(creds()), { name: 'developer_path.ran', subject: { type: 'check', id: 'developer-path' }, payload: { at: now().toISOString() } }, { fetch: fetchImpl });
++ say(`published ${pub.event_type} ${pub.event_id}`);
++ st.eventId = pub.event_id;
const topics = await s.resolveTopics();
if (topics[0] !== `app.${projectKey(st.projectId)}.*`) throw new Error(`unexpected topic ${topics[0]}`);
const n = await s.pullOnce();
@@@ -249,7 +250,7 @@ async function runDeveloperPath({ network = PRODUCTION_NETWORK, services = PRODU
if (write.status !== 403) throw new Error(`a write with the export token answered ${write.status}, not 403`);
say('a write with it is refused (403)');
const et = await mint('openvibe.events');
-- const pull = await fetchImpl(`${events.replace(/\/+$/, '')}/api/v1/events?topic=${encodeURIComponent(`app.${projectKey(st.projectId)}.*`)}&after_seq=0&limit=100`, { headers: auth(et) });
++ const pull = await fetchImpl(`${events.replace(/\/+$/, '')}/api/v1/events?topic=${encodeURIComponent(`app.${projectKey(st.projectId)}.*`)}&limit=100`, { headers: auth(et) });
const got = pull.status === 200 ? ((await pull.json()).events || []) : [];
if (!got.some((e) => (e.event || e).event_id === st.eventId)) throw new Error(`Events did not return the event published in step 7 with the export token (${pull.status})`);
say(`Events returns the project's event ${st.eventId} with the other`);
ddiff --git a/scripts/tools-job-proof.js b/scripts/tools-job-proof.js
iindex ee05ae6..5b6301c 100644
---- a/scripts/tools-job-proof.js
++++ b/scripts/tools-job-proof.js
@@@ -107,9 +107,10 @@ async function runToolsJobProof({ network = PRODUCTION_NETWORK, toolsUrl, client
});
await step('cursor', async () => {
-- const page = await st.events.pull({ topic: 'tools.job.*', afterSeq: Number.MAX_SAFE_INTEGER, limit: 1 });
-- st.cursor = Number(page.latest_seq) || 0;
-- say(`Events head: seq ${st.cursor}`);
++ const page = await st.events.pull({ topic: 'tools.job.*', limit: 1 });
++ st.cursor = page.latest_cursor;
++ if (!st.cursor) throw new Error('Events answered no latest_cursor (Events#45)');
++ say('Events head noted (an opaque cursor)');
});
await step('submit', async () => {
@@@ -162,13 +163,15 @@ async function runToolsJobProof({ network = PRODUCTION_NETWORK, toolsUrl, client
const deadline = Date.now() + eventsWaitMs;
for (;;) {
let page;
++ let moved;
do {
-- page = await st.events.pull({ topic: 'tools.job.*', afterSeq: after, limit: 200 });
++ page = await st.events.pull({ topic: 'tools.job.*', after, limit: 200 });
for (const { seq, event } of page.events || []) {
if (event && event.subject && event.subject.id === st.jobId && EVENT_TYPES.includes(event.event_type)) found.set(event.event_type, seq);
}
-- after = page.next_after_seq != null ? page.next_after_seq : after;
-- } while ((page.events || []).length && after < page.latest_seq);
++ moved = Boolean(page.next_cursor) && page.next_cursor !== after;
++ if (moved) after = page.next_cursor;
++ } while (moved && (page.events || []).length === 200);
if (EVENT_TYPES.every((t) => found.has(t))) break;
if (Date.now() >= deadline) throw new Error(`not in the Events store after ${eventsWaitMs / 1000} s: ${EVENT_TYPES.filter((t) => !found.has(t)).join(', ')}`);
await sleep(pollMs);
ddiff --git a/test/structure.test.js b/test/structure.test.js
iindex f878024..7bc1bab 100644
---- a/test/structure.test.js
++++ b/test/structure.test.js
@@@ -12,7 +12,9 @@ const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const EXPECTED = ['browser-app', 'chat-bot', 'event-subscriber', 'media-uploader', 'mod-manifest', 'node-server-app', 'oauth-app', 'tool-job', 'webhook-consumer'];
--const SDK = 'https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.2';
++// The pin every example must share: the repository's own (the pin bot moves them all at once).
++const ROOT_PKG = require(path.join(ROOT, 'package.json'));
++const SDK = (ROOT_PKG.dependencies || {})['openvibe-sdk'] || (ROOT_PKG.devDependencies || {})['openvibe-sdk'];
const dirs = fs.readdirSync(path.join(ROOT, 'examples')).filter((d) => fs.statSync(path.join(ROOT, 'examples', d)).isDirectory()).sort();
assert.deepEqual(dirs, EXPECTED, 'the nine charter examples');
@@@ -38,7 +40,7 @@ for (const name of EXPECTED) {
assert.equal(pkg.scripts.test, 'node test/smoke.test.js', `${name}: npm test runs the smoke test`);
for (const [dep, spec] of Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })) {
assert.ok(!/^(file|link):/.test(spec), `${name}: ${dep} is not a local link`);
-- if (dep === 'openvibe-sdk') assert.equal(spec, SDK, `${name}: openvibe-sdk pinned to v0.35.0`);
++ if (dep === 'openvibe-sdk') assert.equal(spec, SDK, `${name}: openvibe-sdk pinned as the repository is (${SDK.split('/').pop()})`);
}
const readme = fs.readFileSync(path.join(dir, 'README.md'), 'utf8');