@rimori/client 2.5.29-next.0 → 2.5.29-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { EventBusMessage } from '../fromRimori/EventBus';
1
+ import { EventBusHandler, EventBusMessage } from '../fromRimori/EventBus';
2
2
  export type AccomplishmentMessage = EventBusMessage<MicroAccomplishmentPayload>;
3
3
  export declare const skillCategories: readonly ["reading", "listening", "speaking", "writing", "learning", "community"];
4
4
  interface BaseAccomplishmentPayload {
@@ -23,7 +23,8 @@ export interface MacroAccomplishmentPayload extends BaseAccomplishmentPayload {
23
23
  export type AccomplishmentPayload = MicroAccomplishmentPayload | MacroAccomplishmentPayload;
24
24
  export declare class AccomplishmentController {
25
25
  private pluginId;
26
- constructor(pluginId: string);
26
+ private eventBus;
27
+ constructor(pluginId: string, eventBus?: EventBusHandler);
27
28
  emitAccomplishment(payload: Omit<AccomplishmentPayload, 'type'>): void;
28
29
  private validateAccomplishment;
29
30
  private sanitizeAccomplishment;
@@ -1,8 +1,9 @@
1
1
  import { EventBus } from '../fromRimori/EventBus';
2
2
  export const skillCategories = ['reading', 'listening', 'speaking', 'writing', 'learning', 'community'];
3
3
  export class AccomplishmentController {
4
- constructor(pluginId) {
4
+ constructor(pluginId, eventBus) {
5
5
  this.pluginId = pluginId;
6
+ this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
6
7
  }
7
8
  emitAccomplishment(payload) {
8
9
  const accomplishmentPayload = Object.assign(Object.assign({}, payload), { type: 'durationMinutes' in payload ? 'macro' : 'micro' });
@@ -11,7 +12,7 @@ export class AccomplishmentController {
11
12
  }
12
13
  const sanitizedPayload = this.sanitizeAccomplishment(accomplishmentPayload);
13
14
  const topic = 'global.accomplishment.trigger' + (accomplishmentPayload.type === 'macro' ? 'Macro' : 'Micro');
14
- EventBus.emit(this.pluginId, topic, sanitizedPayload);
15
+ this.eventBus.emit(this.pluginId, topic, sanitizedPayload);
15
16
  }
16
17
  validateAccomplishment(payload) {
17
18
  if (!skillCategories.includes(payload.skillCategory)) {
@@ -85,7 +86,7 @@ export class AccomplishmentController {
85
86
  else if (topicLength !== 3) {
86
87
  throw new Error('Invalid accomplishment topic pattern. The pattern must be plugin.skillCategory.accomplishmentKeyword or an * as wildcard for any plugin, skill category or accomplishment keyword');
87
88
  }
88
- EventBus.on(['global.accomplishment.triggerMicro', 'global.accomplishment.triggerMacro'], (event) => {
89
+ this.eventBus.on(['global.accomplishment.triggerMicro', 'global.accomplishment.triggerMacro'], (event) => {
89
90
  const { plugin, skillCategory, accomplishmentKeyword } = this.getDecoupledTopic(accomplishmentTopic);
90
91
  if (plugin !== '*' && event.sender !== plugin)
91
92
  return;
@@ -79,12 +79,13 @@ export class Translator {
79
79
  });
80
80
  }
81
81
  getTranslationUrl(language) {
82
+ const baseUrl = this.translationUrl || window.location.origin;
82
83
  // For localhost development, use local- prefix for non-English languages
83
- if (window.location.hostname === 'localhost') {
84
+ if (window.location.hostname === 'localhost' || new URL(baseUrl).hostname === 'localhost') {
84
85
  const filename = language !== 'en' ? `local-${language}` : language;
85
- return `${window.location.origin}/locales/${filename}.json`;
86
+ return `${baseUrl}/locales/${filename}.json`;
86
87
  }
87
- return `${this.translationUrl}/locales/${language}.json`;
88
+ return `${baseUrl}/locales/${language}.json`;
88
89
  }
89
90
  usePlugin(plugin) {
90
91
  if (!this.i18n) {
@@ -29,6 +29,11 @@ export declare class EventBusHandler {
29
29
  private generatedIds;
30
30
  private cleanupInterval;
31
31
  private constructor();
32
+ /**
33
+ * Creates a new non-singleton EventBusHandler instance.
34
+ * Used in federation mode where each plugin needs its own isolated EventBus.
35
+ */
36
+ static create(name?: string): EventBusHandler;
32
37
  static getInstance(name?: string): EventBusHandler;
33
38
  /**
34
39
  * Starts the interval to cleanup the generated ids.
@@ -109,6 +114,10 @@ export declare class EventBusHandler {
109
114
  */
110
115
  private validateTopic;
111
116
  private logIfDebug;
117
+ /**
118
+ * Destroys this EventBus instance, cleaning up all listeners and intervals.
119
+ */
120
+ destroy(): void;
112
121
  private logAndThrowError;
113
122
  }
114
123
  export declare const EventBus: EventBusHandler;
@@ -15,9 +15,18 @@ export class EventBusHandler {
15
15
  this.evName = '';
16
16
  this.generatedIds = new Map(); // Map<id, timestamp>
17
17
  this.cleanupInterval = null;
18
- //private constructor
19
18
  this.startIdCleanup();
20
19
  }
20
+ /**
21
+ * Creates a new non-singleton EventBusHandler instance.
22
+ * Used in federation mode where each plugin needs its own isolated EventBus.
23
+ */
24
+ static create(name) {
25
+ const instance = new EventBusHandler();
26
+ if (name)
27
+ instance.evName = name;
28
+ return instance;
29
+ }
21
30
  static getInstance(name) {
22
31
  if (!EventBusHandler.instance) {
23
32
  EventBusHandler.instance = new EventBusHandler();
@@ -307,6 +316,18 @@ export class EventBusHandler {
307
316
  console.debug(`[${this.evName}] ` + args[0], ...args.slice(1));
308
317
  }
309
318
  }
319
+ /**
320
+ * Destroys this EventBus instance, cleaning up all listeners and intervals.
321
+ */
322
+ destroy() {
323
+ this.listeners.clear();
324
+ this.responseResolvers.clear();
325
+ if (this.cleanupInterval) {
326
+ clearInterval(this.cleanupInterval);
327
+ this.cleanupInterval = null;
328
+ }
329
+ this.generatedIds.clear();
330
+ }
310
331
  logAndThrowError(throwError, ...args) {
311
332
  const message = `[${this.evName}] ` + args[0];
312
333
  console.error(message, ...args.slice(1));
@@ -86,6 +86,8 @@ export interface RimoriPluginConfig<T extends object = object> {
86
86
  sidebar: (SidebarPage & T)[];
87
87
  /** Optional path to the plugin's settings/configuration page */
88
88
  settings?: string;
89
+ /** When true, rimori-main loads this plugin via Module Federation instead of an iframe. */
90
+ federated?: boolean;
89
91
  /** Optional array of event topics the plugin pages can listen to for cross-plugin communication */
90
92
  topics?: string[];
91
93
  };
@@ -67,8 +67,9 @@ export declare class RimoriCommunicationHandler {
67
67
  /**
68
68
  * Handles updates to RimoriInfo from rimori-main.
69
69
  * Updates the cached info and Supabase client, then notifies all registered callbacks.
70
+ * Public so that federated mode can call it when the update event arrives via the plugin's isolated EventBus.
70
71
  */
71
- private handleRimoriInfoUpdate;
72
+ handleRimoriInfoUpdate(newInfo: RimoriInfo): void;
72
73
  /**
73
74
  * Registers a callback to be called when RimoriInfo is updated.
74
75
  * @param callback - Function to call with the new RimoriInfo
@@ -77,7 +77,7 @@ export class RimoriCommunicationHandler {
77
77
  // Listen for updates from rimori-main (data changes, token refresh, etc.)
78
78
  // Topic format: {pluginId}.supabase.triggerUpdate
79
79
  EventBus.on(`${this.pluginId}.supabase.triggerUpdate`, (ev) => {
80
- // console.log('[RimoriCommunicationHandler] Received update from rimori-main', ev.data);
80
+ // console.log('[RimoriCommunicationHandler] Received triggerUpdate via MessageChannel for', this.pluginId);
81
81
  this.handleRimoriInfoUpdate(ev.data);
82
82
  });
83
83
  // Mark MessageChannel as ready and process pending requests
@@ -205,12 +205,14 @@ export class RimoriCommunicationHandler {
205
205
  /**
206
206
  * Handles updates to RimoriInfo from rimori-main.
207
207
  * Updates the cached info and Supabase client, then notifies all registered callbacks.
208
+ * Public so that federated mode can call it when the update event arrives via the plugin's isolated EventBus.
208
209
  */
209
210
  handleRimoriInfoUpdate(newInfo) {
210
211
  if (JSON.stringify(this.rimoriInfo) === JSON.stringify(newInfo)) {
211
- // console.log('[RimoriCommunicationHandler] RimoriInfo update is the same as the cached info, skipping update');
212
+ // console.log('[RimoriCommunicationHandler] RimoriInfo update identical to cached info, skipping', this.pluginId);
212
213
  return;
213
214
  }
215
+ // console.log('[RimoriCommunicationHandler] Applying RimoriInfo update for', this.pluginId, '| ttsEnabled:', newInfo.ttsEnabled);
214
216
  // Update cached rimoriInfo
215
217
  this.rimoriInfo = newInfo;
216
218
  // Update Supabase client with new token
@@ -1,10 +1,12 @@
1
1
  import { SharedContentController } from './module/SharedContentController';
2
+ import { RimoriInfo } from './CommunicationHandler';
2
3
  import { PluginModule } from './module/PluginModule';
3
4
  import { DbModule } from './module/DbModule';
4
5
  import { EventModule } from './module/EventModule';
5
6
  import { AIModule } from './module/AIModule';
6
7
  import { ExerciseModule } from './module/ExerciseModule';
7
8
  import { StorageModule } from './module/StorageModule';
9
+ import { EventBusHandler } from '../fromRimori/EventBus';
8
10
  export declare class RimoriClient {
9
11
  private static instance;
10
12
  sharedContent: SharedContentController;
@@ -16,7 +18,15 @@ export declare class RimoriClient {
16
18
  /** Upload and manage images stored in Supabase via the backend. */
17
19
  storage: StorageModule;
18
20
  private rimoriInfo;
21
+ /** The EventBus instance used by this client. In federation mode this is a per-plugin instance. */
22
+ eventBus: EventBusHandler;
19
23
  private constructor();
24
+ /**
25
+ * Creates a RimoriClient with pre-existing RimoriInfo (federation mode).
26
+ * Uses a fresh per-plugin EventBus instance instead of the global singleton.
27
+ * Creates the Supabase PostgrestClient internally from the info.
28
+ */
29
+ static createWithInfo(info: RimoriInfo): RimoriClient;
20
30
  static getInstance(pluginId?: string): Promise<RimoriClient>;
21
31
  navigation: {
22
32
  toDashboard: () => void;
@@ -16,9 +16,10 @@ import { EventModule } from './module/EventModule';
16
16
  import { AIModule } from './module/AIModule';
17
17
  import { ExerciseModule } from './module/ExerciseModule';
18
18
  import { StorageModule } from './module/StorageModule';
19
- import { EventBus } from '../fromRimori/EventBus';
19
+ import { PostgrestClient } from '@supabase/postgrest-js';
20
+ import { EventBus, EventBusHandler } from '../fromRimori/EventBus';
20
21
  export class RimoriClient {
21
- constructor(controller, supabase, info) {
22
+ constructor(controller, supabase, info, eventBus) {
22
23
  this.navigation = {
23
24
  toDashboard: () => {
24
25
  this.event.emit('global.navigation.triggerToDashboard');
@@ -30,12 +31,13 @@ export class RimoriClient {
30
31
  }),
31
32
  };
32
33
  this.rimoriInfo = info;
34
+ this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
33
35
  this.sharedContent = new SharedContentController(supabase, this);
34
36
  this.ai = new AIModule(info.backendUrl, () => this.rimoriInfo.token, info.pluginId);
35
37
  this.ai.setOnRateLimited((exercisesRemaining) => {
36
- EventBus.emit(info.pluginId, 'global.quota.triggerExceeded', { exercises_remaining: exercisesRemaining });
38
+ this.eventBus.emit(info.pluginId, 'global.quota.triggerExceeded', { exercises_remaining: exercisesRemaining });
37
39
  });
38
- this.event = new EventModule(info.pluginId, info.backendUrl, () => this.rimoriInfo.token, this.ai);
40
+ this.event = new EventModule(info.pluginId, info.backendUrl, () => this.rimoriInfo.token, this.ai, this.eventBus);
39
41
  this.db = new DbModule(supabase, controller, info);
40
42
  this.plugin = new PluginModule(supabase, controller, info, this.ai);
41
43
  this.exercise = new ExerciseModule(supabase, controller, info, this.event);
@@ -48,6 +50,32 @@ export class RimoriClient {
48
50
  Logger.getInstance(this);
49
51
  }
50
52
  }
53
+ /**
54
+ * Creates a RimoriClient with pre-existing RimoriInfo (federation mode).
55
+ * Uses a fresh per-plugin EventBus instance instead of the global singleton.
56
+ * Creates the Supabase PostgrestClient internally from the info.
57
+ */
58
+ static createWithInfo(info) {
59
+ const eventBus = EventBusHandler.create('Plugin EventBus ' + info.pluginId);
60
+ const controller = new RimoriCommunicationHandler(info.pluginId, true);
61
+ const supabase = new PostgrestClient(`${info.url}/rest/v1`, {
62
+ schema: info.dbSchema,
63
+ headers: {
64
+ apikey: info.key,
65
+ Authorization: `Bearer ${info.token}`,
66
+ },
67
+ });
68
+ const client = new RimoriClient(controller, supabase, info, eventBus);
69
+ // In federated mode, CommunicationHandler skips MessageChannel setup so it never
70
+ // subscribes to triggerUpdate events. PluginEventBridge forwards host-bus events to
71
+ // this plugin's isolated eventBus, so we listen here and hand off to the controller.
72
+ eventBus.on(`${info.pluginId}.supabase.triggerUpdate`, (ev) => {
73
+ var _a;
74
+ console.log('[RimoriClient] Federated triggerUpdate received for', info.pluginId, '| ttsEnabled:', (_a = ev.data) === null || _a === void 0 ? void 0 : _a.ttsEnabled);
75
+ controller.handleRimoriInfoUpdate(ev.data);
76
+ });
77
+ return client;
78
+ }
51
79
  static getInstance(pluginId) {
52
80
  return __awaiter(this, void 0, void 0, function* () {
53
81
  if (!RimoriClient.instance) {
@@ -1,6 +1,6 @@
1
1
  import { MainPanelAction, SidebarAction } from '../../fromRimori/PluginTypes';
2
2
  import { AccomplishmentPayload } from '../../controller/AccomplishmentController';
3
- import { EventBusMessage, EventHandler, EventPayload, EventListener } from '../../fromRimori/EventBus';
3
+ import { EventBusHandler, EventBusMessage, EventHandler, EventPayload, EventListener } from '../../fromRimori/EventBus';
4
4
  import { AIModule } from './AIModule';
5
5
  /**
6
6
  * Event module for plugin event bus operations.
@@ -12,7 +12,8 @@ export declare class EventModule {
12
12
  private aiModule;
13
13
  private backendUrl;
14
14
  private getToken;
15
- constructor(pluginId: string, backendUrl: string, getToken: () => string, aiModule: AIModule);
15
+ private eventBus;
16
+ constructor(pluginId: string, backendUrl: string, getToken: () => string, aiModule: AIModule, eventBus?: EventBusHandler);
16
17
  getGlobalEventTopic(preliminaryTopic: string): string;
17
18
  /**
18
19
  * Emit an event to Rimori or a plugin.
@@ -14,12 +14,13 @@ import { EventBus } from '../../fromRimori/EventBus';
14
14
  * Provides methods for emitting, listening to, and responding to events.
15
15
  */
16
16
  export class EventModule {
17
- constructor(pluginId, backendUrl, getToken, aiModule) {
17
+ constructor(pluginId, backendUrl, getToken, aiModule, eventBus) {
18
18
  this.pluginId = pluginId;
19
19
  this.backendUrl = backendUrl;
20
20
  this.getToken = getToken;
21
21
  this.aiModule = aiModule;
22
- this.accomplishmentController = new AccomplishmentController(pluginId);
22
+ this.eventBus = eventBus !== null && eventBus !== void 0 ? eventBus : EventBus;
23
+ this.accomplishmentController = new AccomplishmentController(pluginId, this.eventBus);
23
24
  }
24
25
  getGlobalEventTopic(preliminaryTopic) {
25
26
  var _a;
@@ -54,7 +55,7 @@ export class EventModule {
54
55
  */
55
56
  emit(topic, data, eventId) {
56
57
  const globalTopic = this.getGlobalEventTopic(topic);
57
- EventBus.emit(this.pluginId, globalTopic, data, eventId);
58
+ this.eventBus.emit(this.pluginId, globalTopic, data, eventId);
58
59
  }
59
60
  /**
60
61
  * Request an event.
@@ -68,7 +69,7 @@ export class EventModule {
68
69
  const globalTopic = this.getGlobalEventTopic(topic);
69
70
  yield this.aiModule.session.ensure();
70
71
  const sessionToken = (_a = this.aiModule.session.get()) !== null && _a !== void 0 ? _a : undefined;
71
- return EventBus.request(this.pluginId, globalTopic, data, sessionToken);
72
+ return this.eventBus.request(this.pluginId, globalTopic, data, sessionToken);
72
73
  });
73
74
  }
74
75
  /**
@@ -79,7 +80,7 @@ export class EventModule {
79
80
  */
80
81
  on(topic, callback) {
81
82
  const topics = Array.isArray(topic) ? topic : [topic];
82
- return EventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), (event) => {
83
+ return this.eventBus.on(topics.map((t) => this.getGlobalEventTopic(t)), (event) => {
83
84
  if (event.ai_session_token && !this.aiModule.session.get()) {
84
85
  this.aiModule.session.set(event.ai_session_token);
85
86
  }
@@ -92,7 +93,7 @@ export class EventModule {
92
93
  * @param callback The callback to call when the event is emitted.
93
94
  */
94
95
  once(topic, callback) {
95
- EventBus.once(this.getGlobalEventTopic(topic), callback);
96
+ this.eventBus.once(this.getGlobalEventTopic(topic), callback);
96
97
  }
97
98
  /**
98
99
  * Respond to an event.
@@ -124,7 +125,7 @@ export class EventModule {
124
125
  }
125
126
  });
126
127
  }
127
- EventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), wrappedData);
128
+ this.eventBus.respond(this.pluginId, topics.map((t) => this.getGlobalEventTopic(t)), wrappedData);
128
129
  }
129
130
  /**
130
131
  * Emit an accomplishment.
@@ -219,15 +220,21 @@ export class EventModule {
219
220
  */
220
221
  onSidePanelAction(callback, actionsToListen = []) {
221
222
  const listeningActions = Array.isArray(actionsToListen) ? actionsToListen : [actionsToListen];
222
- // this needs to be a emit and on because the main panel action is triggered by the user and not by the plugin
223
- this.emit('action.requestSidebar');
224
- return this.on('action.requestSidebar', ({ data }) => {
225
- // console.log("eventHandler .onSidePanelAction", data);
226
- // console.log('Received action for sidebar ' + data.action);
227
- // console.log('Listening to actions', listeningActions);
223
+ // Register the listener BEFORE emitting the request, so the synchronous response
224
+ // from the bridge/responder is captured (emit → bridge outbound → host respond → bridge inbound is synchronous).
225
+ console.log('[EventModule] onSidePanelAction: setting up listener for', this.pluginId, 'listening to:', listeningActions);
226
+ const listener = this.on('action.requestSidebar', ({ data }) => {
227
+ console.log('[EventModule] onSidePanelAction: received event', { data, listeningActions });
228
228
  if (listeningActions.length === 0 || listeningActions.includes(data.action)) {
229
+ console.log('[EventModule] onSidePanelAction: action matched, calling callback');
229
230
  callback(data);
230
231
  }
232
+ else {
233
+ console.log('[EventModule] onSidePanelAction: action NOT matched. Got:', data.action, 'expected:', listeningActions);
234
+ }
231
235
  });
236
+ console.log('[EventModule] onSidePanelAction: emitting action.requestSidebar for', this.pluginId);
237
+ this.emit('action.requestSidebar');
238
+ return listener;
232
239
  }
233
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.29-next.0",
3
+ "version": "2.5.29-next.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "scripts": {
33
33
  "build": "tsc",
34
34
  "dev": "tsc -w --preserveWatchOutput",
35
- "lint": "eslint . --fix",
35
+ "lint": "npx eslint . --fix",
36
36
  "format": "prettier --write ."
37
37
  },
38
38
  "dependencies": {
@@ -41,10 +41,8 @@
41
41
  "i18next": "^25.10.10"
42
42
  },
43
43
  "devDependencies": {
44
- "@eslint/js": "^9.37.0",
45
44
  "@types/node": "^25.0.1",
46
45
  "eslint-config-prettier": "^10.1.8",
47
- "eslint-plugin-prettier": "^5.5.4",
48
46
  "html2canvas": "^1.4.1",
49
47
  "globals": "^16.4.0",
50
48
  "prettier": "^3.6.2",