fimo 0.10.0 → 0.10.1-staging.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.
@@ -7,15 +7,25 @@ import { getFimoDefaultLocale, getFimoLocales } from '../../../runtime/i18n/loca
7
7
  import { getConfigServer } from '../../../runtime/shared/fimo-config.server.js';
8
8
  const VIRTUAL_ID = 'virtual:translations';
9
9
  const RESOLVED_ID = '\0' + VIRTUAL_ID;
10
+ const EVENT_RECONNECT_DELAY_MS = 2000;
11
+ const FALLBACK_POLL_INTERVAL_MS = 30_000;
12
+ const LABEL_FETCH_TIMEOUT_MS = 30_000;
10
13
  export default function translationsPlugin() {
11
14
  let rootDir = process.cwd();
12
- let bundle = { dictionaries: { en: {} }, defaultLocale: 'en', locales: ['en'], updatedAt: null };
15
+ let bundle = {
16
+ dictionaries: { en: {} },
17
+ defaultLocale: 'en',
18
+ locales: ['en'],
19
+ updatedAt: null,
20
+ };
13
21
  let command = 'build';
14
22
  let devServer = null;
15
23
  let eventSockets = [];
16
24
  let reconnectTimer;
17
25
  let refreshTimer;
18
26
  let pollTimer;
27
+ let eventConnectionAttempt = 0;
28
+ let closed = false;
19
29
  const refreshLabels = async () => {
20
30
  bundle = await loadLabelBundle(rootDir);
21
31
  return bundle;
@@ -51,13 +61,91 @@ export default function translationsPlugin() {
51
61
  });
52
62
  };
53
63
  const scheduleRefresh = () => {
64
+ if (closed) {
65
+ return;
66
+ }
54
67
  if (refreshTimer) {
55
68
  clearTimeout(refreshTimer);
56
69
  }
57
70
  refreshTimer = setTimeout(() => {
71
+ refreshTimer = undefined;
72
+ if (closed) {
73
+ return;
74
+ }
58
75
  void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
59
76
  }, 150);
60
77
  };
78
+ const startFallbackPolling = () => {
79
+ if (closed || pollTimer) {
80
+ return;
81
+ }
82
+ pollTimer = setInterval(() => {
83
+ void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
84
+ }, FALLBACK_POLL_INTERVAL_MS);
85
+ };
86
+ const stopFallbackPolling = () => {
87
+ if (!pollTimer) {
88
+ return;
89
+ }
90
+ clearInterval(pollTimer);
91
+ pollTimer = undefined;
92
+ };
93
+ const closeEventSockets = () => {
94
+ closeSockets(eventSockets.splice(0));
95
+ };
96
+ const scheduleReconnect = (delayMs = EVENT_RECONNECT_DELAY_MS) => {
97
+ startFallbackPolling();
98
+ if (closed || reconnectTimer) {
99
+ return;
100
+ }
101
+ reconnectTimer = setTimeout(() => {
102
+ reconnectTimer = undefined;
103
+ closeEventSockets();
104
+ void connectEvents();
105
+ }, delayMs);
106
+ };
107
+ const connectEvents = async () => {
108
+ if (closed) {
109
+ return;
110
+ }
111
+ const attempt = ++eventConnectionAttempt;
112
+ const sockets = [];
113
+ eventSockets = sockets;
114
+ try {
115
+ await connectLabelEvents(rootDir, scheduleRefresh, sockets, {
116
+ isActive: () => !closed && attempt === eventConnectionAttempt,
117
+ onConnected: () => {
118
+ if (closed || attempt !== eventConnectionAttempt) {
119
+ return;
120
+ }
121
+ if (reconnectTimer) {
122
+ clearTimeout(reconnectTimer);
123
+ reconnectTimer = undefined;
124
+ }
125
+ stopFallbackPolling();
126
+ },
127
+ onDisconnected: () => {
128
+ if (closed || attempt !== eventConnectionAttempt) {
129
+ return;
130
+ }
131
+ scheduleReconnect();
132
+ },
133
+ });
134
+ if (closed || attempt !== eventConnectionAttempt) {
135
+ closeSockets(sockets.splice(0));
136
+ return;
137
+ }
138
+ if (sockets.length === 0) {
139
+ scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
140
+ }
141
+ }
142
+ catch {
143
+ if (!closed && attempt === eventConnectionAttempt) {
144
+ closeEventSockets();
145
+ scheduleReconnect(FALLBACK_POLL_INTERVAL_MS);
146
+ }
147
+ }
148
+ };
61
149
  return {
62
150
  name: 'fimo-virtual-translations',
63
151
  enforce: 'pre',
@@ -76,16 +164,11 @@ export default function translationsPlugin() {
76
164
  },
77
165
  configureServer(server) {
78
166
  devServer = server;
167
+ closed = false;
79
168
  server.watcher.add([join(rootDir, 'fimo-config.json'), join(rootDir, '.env'), join(rootDir, '.env.local')]);
80
169
  void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
81
- connectLabelEvents(rootDir, scheduleRefresh)
82
- .then((sockets) => {
83
- eventSockets = sockets;
84
- })
85
- .catch(() => undefined);
86
- pollTimer = setInterval(() => {
87
- void refreshLabelsAndInvalidateIfChanged().catch(() => undefined);
88
- }, 2000);
170
+ startFallbackPolling();
171
+ void connectEvents();
89
172
  },
90
173
  resolveId(id) {
91
174
  if (id === VIRTUAL_ID) {
@@ -128,84 +211,119 @@ export default function translationsPlugin() {
128
211
  return [];
129
212
  },
130
213
  closeBundle() {
131
- for (const socket of eventSockets) {
132
- socket.close();
133
- }
134
- eventSockets = [];
214
+ closed = true;
215
+ eventConnectionAttempt += 1;
216
+ closeEventSockets();
217
+ devServer = null;
135
218
  if (reconnectTimer) {
136
219
  clearTimeout(reconnectTimer);
220
+ reconnectTimer = undefined;
137
221
  }
138
222
  if (refreshTimer) {
139
223
  clearTimeout(refreshTimer);
224
+ refreshTimer = undefined;
140
225
  }
141
- if (pollTimer) {
142
- clearInterval(pollTimer);
143
- }
226
+ stopFallbackPolling();
144
227
  },
145
228
  };
146
- async function connectLabelEvents(root, onLabelsChanged) {
229
+ async function connectLabelEvents(root, onLabelsChanged, sockets, callbacks) {
147
230
  const env = await readProjectEnv(root);
231
+ if (!callbacks.isActive()) {
232
+ return;
233
+ }
148
234
  const apiUrl = env.VITE_API_URL;
149
235
  if (!apiUrl) {
150
- return [];
236
+ return;
151
237
  }
152
238
  const runtimeConfig = await loadRuntimeConfig(apiUrl);
239
+ if (!callbacks.isActive()) {
240
+ return;
241
+ }
153
242
  const eventsUrl = runtimeConfig.eventsServerUrl ?? undefined;
154
243
  if (!eventsUrl) {
155
- return [];
244
+ return;
156
245
  }
157
246
  const settings = await readProjectSettings(root);
247
+ if (!callbacks.isActive()) {
248
+ return;
249
+ }
158
250
  if (!settings.projectId) {
159
- return [];
251
+ return;
160
252
  }
161
253
  const WebSocketCtor = globalThis.WebSocket;
162
254
  if (!WebSocketCtor) {
163
- return [];
255
+ return;
164
256
  }
165
- const sockets = [];
166
- for (const room of deriveEventsRooms(settings.projectId, apiUrl)) {
167
- const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
168
- const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
169
- wsUrl.searchParams.set('authToken', token);
170
- const socket = new WebSocketCtor(wsUrl.toString());
171
- socket.onmessage = (event) => {
172
- const raw = typeof event.data === 'string' ? event.data : '';
173
- if (!raw) {
257
+ const rooms = deriveEventsRooms(settings.projectId, apiUrl);
258
+ const openSockets = new Set();
259
+ try {
260
+ for (const room of rooms) {
261
+ const token = await generateOneTimeToken(settings.apiUrl ?? process.env.FIMO_API_URL ?? 'http://localhost:3000');
262
+ if (!callbacks.isActive()) {
263
+ closeSockets(sockets.splice(0));
174
264
  return;
175
265
  }
176
- try {
177
- const message = JSON.parse(raw);
178
- if (message.type === 'content:changed') {
179
- emitContentUpdate(parseContentChangePayload(message.payload));
266
+ const wsUrl = new URL(`/parties/events/${room}`, eventsUrl.replace(/^http/, 'ws'));
267
+ wsUrl.searchParams.set('authToken', token);
268
+ const socket = new WebSocketCtor(wsUrl.toString());
269
+ socket.onopen = () => {
270
+ if (!callbacks.isActive()) {
180
271
  return;
181
272
  }
182
- if (message.type !== 'labels:changed') {
273
+ openSockets.add(socket);
274
+ if (openSockets.size === rooms.length) {
275
+ callbacks.onConnected();
276
+ }
277
+ };
278
+ socket.onmessage = (event) => {
279
+ if (!callbacks.isActive()) {
183
280
  return;
184
281
  }
185
- const changedLocales = message.payload?.changes
186
- ?.map((change) => change.locale)
187
- .filter((locale) => typeof locale === 'string') ?? [];
188
- if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
189
- onLabelsChanged();
282
+ const raw = typeof event.data === 'string' ? event.data : '';
283
+ if (!raw) {
284
+ return;
190
285
  }
191
- }
192
- catch {
193
- return;
194
- }
195
- };
196
- socket.onclose = () => {
197
- reconnectTimer = setTimeout(() => {
198
- void connectLabelEvents(root, onLabelsChanged)
199
- .then((nextSockets) => {
200
- eventSockets = nextSockets;
201
- })
202
- .catch(() => undefined);
203
- }, 2000);
204
- };
205
- socket.onerror = () => undefined;
206
- sockets.push(socket);
286
+ try {
287
+ const message = JSON.parse(raw);
288
+ if (message.type === 'content:changed') {
289
+ emitContentUpdate(parseContentChangePayload(message.payload));
290
+ return;
291
+ }
292
+ if (message.type !== 'labels:changed') {
293
+ return;
294
+ }
295
+ const changedLocales = message.payload?.changes
296
+ ?.map((change) => change.locale)
297
+ .filter((locale) => typeof locale === 'string') ?? [];
298
+ if (changedLocales.length === 0 || changedLocales.some((locale) => bundle.locales.includes(locale))) {
299
+ onLabelsChanged();
300
+ }
301
+ }
302
+ catch {
303
+ return;
304
+ }
305
+ };
306
+ socket.onclose = () => {
307
+ openSockets.delete(socket);
308
+ callbacks.onDisconnected();
309
+ };
310
+ socket.onerror = callbacks.onDisconnected;
311
+ sockets.push(socket);
312
+ }
207
313
  }
208
- return sockets;
314
+ catch (error) {
315
+ closeSockets(sockets.splice(0));
316
+ throw error;
317
+ }
318
+ }
319
+ }
320
+ function closeSockets(sockets) {
321
+ for (const socket of sockets) {
322
+ socket.onopen = null;
323
+ socket.onmessage = null;
324
+ socket.onclose = null;
325
+ socket.onerror = null;
326
+ socket.close();
209
327
  }
210
328
  }
211
329
  function parseContentChangePayload(payload) {
@@ -306,7 +424,7 @@ export async function loadLabelBundle(rootDir) {
306
424
  async function fetchLabelDictionary(apiUrl, locale) {
307
425
  const url = new URL('/labels', apiUrl);
308
426
  url.searchParams.set('locale', locale);
309
- const response = await fetchWithTimeout(url, 4000);
427
+ const response = await fetchWithTimeout(url, LABEL_FETCH_TIMEOUT_MS);
310
428
  if (!response.ok) {
311
429
  return { locale, labels: {}, updatedAt: null };
312
430
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-07-27T11:53:13.436Z",
4
+ "bundledAt": "2026-07-28T15:18:08.648Z",
5
5
  "cliVersion": "0.10.0",
6
6
  "external": [
7
7
  "oxc-parser",