openvibers/openvibe.sdk Commits
SDK 0.49.1: the outbox rejects Events' 409 events.loop_detected instead of retrying it forever (#54)
Changes
@@@ -3,6 +3,12 @@All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minorrelease may change an API and says so here.++## 0.49.1 (2026-10-16)++++- Outbox: Events' `409 events.loop_detected` is a permanent refusal: the row is marked rejected instead of being++ retried with backoff forever (Search retried 18 such events up to 35 times each after a Reviews loop was fixed).++ Every other 409 is still retried.++## 0.49.0 (2026-10-15)- `openvibe-sdk/download`: `createDownloadClient({ key })` for OpenVibe.Download (the owner's "file storage and backup"):@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.49.0",++ "version": "0.49.1","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",@@@ -1,4 +1,4 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.49.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.49.1, 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.49.0';++const SDK_VERSION = '0.49.1';module.exports = {createClient,@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.49.0",++ "version": "0.49.1","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.49.0",++ "version": "0.49.1","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.49.0",++ "version": "0.49.1","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",@@@ -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.49.0';++const SDK_VERSION = '0.49.1';module.exports = {createClient,@@@ -17,7 +17,9 @@** Delivery is at least once: a crash between publish and mark-sent republishes the same event_id,* which OpenVibe.Events answers as a duplicate. A 4xx other than 401/408/409/425/429 is permanent-- * (the row is marked rejected and never retried); anything else is retried with backoff.++ * (the row is marked rejected and never retried), and so is Events' 409 events.loop_detected (a trace that already++ * carries too many hops of that type is refused for good: retrying it only repeats the refusal); anything else is++ * retried with backoff.*/const TRACEPARENT_RE = /^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/;@@@ -44,6 +46,7 @@ function serialFlush(pass) {function isPermanent(err) {const s = err && err.status;if (err && typeof err.url === 'string' && /\/oauth\/token(\?|$)/.test(err.url)) return false;++ if (s === 409 && err.code === 'events.loop_detected') return true;return Number.isInteger(s) && s >= 400 && s < 500 && !RETRYABLE_4XX.has(s);}@@@ -93,6 +93,22 @@ function cases(label, open) {assert.equal(await outbox.pending(), 1);} finally { await done(); }}],++ [`${label}: Events' loop refusal (409 events.loop_detected) rejects the row; any other 409 is retried`, async () => {++ const { db, done } = await open();++ try {++ const { events } = eventsClient();++ // Like Events: a batch holding the looped event is refused as a loop, so the relay tries each row alone.++ const looped = { prepare: events.prepare, publish: (env) => Promise.reject(Object.assign(new Error('loop'), { status: 409, code: (Array.isArray(env) ? env : [env]).some((e) => e.subject.id === 'loop') ? 'events.loop_detected' : 'events.conflict' })) };++ const outbox = createPgOutbox(db, { events: looped });++ await db.tx(async (t) => {++ await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'loop' } });++ await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'busy' } });++ });++ assert.deepEqual(await outbox.flush(), { sent: 0, failed: 1, rejected: 1 });++ assert.equal(await outbox.rejected(), 1, 'the looped event is not retried');++ assert.equal(await outbox.pending(), 1, 'the other conflict waits for its retry');++ } finally { await done(); }++ }],[`${label}: a flush during a pass also publishes what was committed after that pass claimed`, async () => {const { db, done } = await open();try {
@@ -3,6 +3,12 @@
@@ -3,6 +3,12 @@
All notable changes to `openvibe-sdk`. The package follows semver; while it is `0.x`, a minor
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.
release may change an API and says so here.
+## 0.49.1 (2026-10-16)
+
+- Outbox: Events' `409 events.loop_detected` is a permanent refusal: the row is marked rejected instead of being
+ retried with backoff forever (Search retried 18 such events up to 35 times each after a Reviews loop was fixed).
+ Every other 409 is still retried.
+
## 0.49.0 (2026-10-15)
## 0.49.0 (2026-10-15)
- `openvibe-sdk/download`: `createDownloadClient({ key })` for OpenVibe.Download (the owner's "file storage and backup"):
- `openvibe-sdk/download`: `createDownloadClient({ key })` for OpenVibe.Download (the owner's "file storage and backup"):
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.49.0",
+ "version": "0.49.1",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.49.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.49.1, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// 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.
// (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 */
/* eslint-disable */
@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);
@@ -42,7 +42,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.49.0';
+const SDK_VERSION = '0.49.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.49.0",
+ "version": "0.49.1",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.49.0",
+ "version": "0.49.1",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.49.0",
+ "version": "0.49.1",
"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.",
"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",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.49.0';
+const SDK_VERSION = '0.49.1';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -17,7 +17,9 @@
@@ -17,7 +17,9 @@
*
*
* Delivery is at least once: a crash between publish and mark-sent republishes the same event_id,
* Delivery is at least once: a crash between publish and mark-sent republishes the same event_id,
* which OpenVibe.Events answers as a duplicate. A 4xx other than 401/408/409/425/429 is permanent
* which OpenVibe.Events answers as a duplicate. A 4xx other than 401/408/409/425/429 is permanent
- * (the row is marked rejected and never retried); anything else is retried with backoff.
+ * (the row is marked rejected and never retried), and so is Events' 409 events.loop_detected (a trace that already
+ * carries too many hops of that type is refused for good: retrying it only repeats the refusal); anything else is
+ * retried with backoff.
*/
*/
const TRACEPARENT_RE = /^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/;
const TRACEPARENT_RE = /^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/;
@@ -44,6 +46,7 @@ function serialFlush(pass) {
@@ -44,6 +46,7 @@ function serialFlush(pass) {
function isPermanent(err) {
function isPermanent(err) {
const s = err && err.status;
const s = err && err.status;
if (err && typeof err.url === 'string' && /\/oauth\/token(\?|$)/.test(err.url)) return false;
if (err && typeof err.url === 'string' && /\/oauth\/token(\?|$)/.test(err.url)) return false;
+ if (s === 409 && err.code === 'events.loop_detected') return true;
return Number.isInteger(s) && s >= 400 && s < 500 && !RETRYABLE_4XX.has(s);
return Number.isInteger(s) && s >= 400 && s < 500 && !RETRYABLE_4XX.has(s);
}
}
@@ -93,6 +93,22 @@ function cases(label, open) {
@@ -93,6 +93,22 @@ function cases(label, open) {
assert.equal(await outbox.pending(), 1);
assert.equal(await outbox.pending(), 1);
} finally { await done(); }
} finally { await done(); }
}],
}],
+ [`${label}: Events' loop refusal (409 events.loop_detected) rejects the row; any other 409 is retried`, async () => {
+ const { db, done } = await open();
+ try {
+ const { events } = eventsClient();
+ // Like Events: a batch holding the looped event is refused as a loop, so the relay tries each row alone.
+ const looped = { prepare: events.prepare, publish: (env) => Promise.reject(Object.assign(new Error('loop'), { status: 409, code: (Array.isArray(env) ? env : [env]).some((e) => e.subject.id === 'loop') ? 'events.loop_detected' : 'events.conflict' })) };
+ const outbox = createPgOutbox(db, { events: looped });
+ await db.tx(async (t) => {
+ await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'loop' } });
+ await outbox.enqueue(t, { event_type: 'live.stream.started', actor, subject: { type: 'stream', id: 'busy' } });
+ });
+ assert.deepEqual(await outbox.flush(), { sent: 0, failed: 1, rejected: 1 });
+ assert.equal(await outbox.rejected(), 1, 'the looped event is not retried');
+ assert.equal(await outbox.pending(), 1, 'the other conflict waits for its retry');
+ } finally { await done(); }
+ }],
[`${label}: a flush during a pass also publishes what was committed after that pass claimed`, async () => {
[`${label}: a flush during a pass also publishes what was committed after that pass claimed`, async () => {
const { db, done } = await open();
const { db, done } = await open();
try {
try {