@makerbi/remodex 1.5.4 → 1.5.8

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.
@@ -5,6 +5,8 @@
5
5
  // Depends on: global fetch
6
6
 
7
7
  const DEFAULT_PUSH_SERVICE_TIMEOUT_MS = 10_000;
8
+ const DEFAULT_PUSH_SERVICE_RETRY_LIMIT = 2;
9
+ const DEFAULT_PUSH_SERVICE_RETRY_BASE_DELAY_MS = 500;
8
10
 
9
11
  function createPushNotificationServiceClient({
10
12
  baseUrl = "",
@@ -13,8 +15,16 @@ function createPushNotificationServiceClient({
13
15
  fetchImpl = globalThis.fetch,
14
16
  logPrefix = "[remodex]",
15
17
  requestTimeoutMs = DEFAULT_PUSH_SERVICE_TIMEOUT_MS,
18
+ retryLimit = DEFAULT_PUSH_SERVICE_RETRY_LIMIT,
19
+ retryBaseDelayMs = DEFAULT_PUSH_SERVICE_RETRY_BASE_DELAY_MS,
20
+ sleepImpl = sleep,
16
21
  } = {}) {
17
22
  const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
23
+ const safeRetryLimit = normalizeNonNegativeInteger(retryLimit, DEFAULT_PUSH_SERVICE_RETRY_LIMIT);
24
+ const safeRetryBaseDelayMs = normalizeNonNegativeInteger(
25
+ retryBaseDelayMs,
26
+ DEFAULT_PUSH_SERVICE_RETRY_BASE_DELAY_MS
27
+ );
18
28
 
19
29
  async function registerDevice({
20
30
  deviceToken,
@@ -55,48 +65,70 @@ function createPushNotificationServiceClient({
55
65
  return { ok: false, skipped: true };
56
66
  }
57
67
 
58
- const controller = typeof AbortController === "function" && requestTimeoutMs > 0
59
- ? new AbortController()
60
- : null;
61
- const timeoutID = controller
62
- ? setTimeout(() => {
63
- controller.abort(createTimeoutAbortError(requestTimeoutMs));
64
- }, requestTimeoutMs)
65
- : null;
66
-
67
- let response;
68
- try {
69
- response = await fetchImpl(`${normalizedBaseUrl}${pathname}`, {
70
- method: "POST",
71
- headers: {
72
- "content-type": "application/json",
73
- },
74
- body: JSON.stringify(payload),
75
- signal: controller?.signal,
76
- });
77
- } catch (error) {
78
- if (isAbortError(error)) {
79
- const timeoutError = new Error(`Push service request timed out after ${requestTimeoutMs}ms`);
80
- timeoutError.code = "push_request_timeout";
81
- throw timeoutError;
68
+ const bodyJSON = JSON.stringify(payload);
69
+ let lastError = null;
70
+ for (let attempt = 0; attempt <= safeRetryLimit; attempt += 1) {
71
+ if (attempt > 0) {
72
+ const delayMs = safeRetryBaseDelayMs * Math.pow(2, attempt - 1);
73
+ if (delayMs > 0) {
74
+ await sleepImpl(delayMs);
75
+ }
82
76
  }
83
- throw error;
84
- } finally {
85
- if (timeoutID) {
86
- clearTimeout(timeoutID);
77
+
78
+ const controller = typeof AbortController === "function" && requestTimeoutMs > 0
79
+ ? new AbortController()
80
+ : null;
81
+ const timeoutID = controller
82
+ ? setTimeout(() => {
83
+ controller.abort(createTimeoutAbortError(requestTimeoutMs));
84
+ }, requestTimeoutMs)
85
+ : null;
86
+
87
+ let response;
88
+ try {
89
+ response = await fetchImpl(`${normalizedBaseUrl}${pathname}`, {
90
+ method: "POST",
91
+ headers: {
92
+ "content-type": "application/json",
93
+ },
94
+ body: bodyJSON,
95
+ signal: controller?.signal,
96
+ });
97
+ } catch (error) {
98
+ lastError = error;
99
+ if (isAbortError(error)) {
100
+ continue;
101
+ }
102
+ if (isRetryableNetworkError(error)) {
103
+ continue;
104
+ }
105
+ throw error;
106
+ } finally {
107
+ if (timeoutID) {
108
+ clearTimeout(timeoutID);
109
+ }
110
+ }
111
+
112
+ const responseText = await response.text();
113
+ const parsed = safeParseJSON(responseText);
114
+ if (!response.ok) {
115
+ const message = parsed?.error || parsed?.message || responseText || `HTTP ${response.status}`;
116
+ const error = new Error(message);
117
+ error.status = response.status;
118
+ if (response.status >= 500 && attempt < safeRetryLimit) {
119
+ lastError = error;
120
+ continue;
121
+ }
122
+ throw error;
87
123
  }
88
- }
89
124
 
90
- const responseText = await response.text();
91
- const parsed = safeParseJSON(responseText);
92
- if (!response.ok) {
93
- const message = parsed?.error || parsed?.message || responseText || `HTTP ${response.status}`;
94
- const error = new Error(message);
95
- error.status = response.status;
96
- throw error;
125
+ return parsed ?? { ok: true };
97
126
  }
98
127
 
99
- return parsed ?? { ok: true };
128
+ if (lastError) {
129
+ throw lastError;
130
+ }
131
+ return { ok: false };
100
132
  }
101
133
 
102
134
  return {
@@ -127,6 +159,7 @@ function normalizeBaseUrl(value) {
127
159
  function createTimeoutAbortError(timeoutMs) {
128
160
  const error = new Error(`Push service request timed out after ${timeoutMs}ms`);
129
161
  error.name = "AbortError";
162
+ error.code = "push_request_timeout";
130
163
  return error;
131
164
  }
132
165
 
@@ -134,6 +167,21 @@ function isAbortError(error) {
134
167
  return error?.name === "AbortError" || error?.code === "ABORT_ERR";
135
168
  }
136
169
 
170
+ function isRetryableNetworkError(error) {
171
+ const code = error?.code ?? "";
172
+ return code === "ECONNRESET" || code === "ECONNREFUSED" || code === "ETIMEDOUT"
173
+ || code === "ENETUNREACH" || code === "EHOSTUNREACH" || code === "ENOTFOUND"
174
+ || error?.message?.includes("fetch failed");
175
+ }
176
+
177
+ function normalizeNonNegativeInteger(value, fallback) {
178
+ return Number.isInteger(value) && value >= 0 ? value : fallback;
179
+ }
180
+
181
+ function sleep(delayMs) {
182
+ return new Promise((resolve) => setTimeout(resolve, delayMs));
183
+ }
184
+
137
185
  function safeParseJSON(value) {
138
186
  if (!value || typeof value !== "string") {
139
187
  return null;
@@ -9,6 +9,9 @@ const {
9
9
  } = require("./push-notification-completion-dedupe");
10
10
 
11
11
  const DEFAULT_PREVIEW_MAX_CHARS = 160;
12
+ const MAX_THREAD_TITLE_ENTRIES = 200;
13
+ const MAX_TURN_STATE_ENTRIES = 500;
14
+ const MAX_THREAD_ID_BY_TURN_ENTRIES = 500;
12
15
 
13
16
  function createPushNotificationTracker({
14
17
  sessionId,
@@ -73,6 +76,10 @@ function createPushNotificationTracker({
73
76
  // Remembers thread/turn linkage before the terminal event arrives on a different payload shape.
74
77
  function rememberMessageContext({ threadId, turnId, params, eventObject }) {
75
78
  if (threadId && turnId) {
79
+ if (!threadIdByTurnId.has(turnId) && threadIdByTurnId.size >= MAX_THREAD_ID_BY_TURN_ENTRIES) {
80
+ const oldest = threadIdByTurnId.keys().next().value;
81
+ threadIdByTurnId.delete(oldest);
82
+ }
76
83
  threadIdByTurnId.set(turnId, threadId);
77
84
  ensureTurnState(threadId, turnId);
78
85
  }
@@ -83,6 +90,10 @@ function createPushNotificationTracker({
83
90
 
84
91
  const nextTitle = extractThreadTitle(params, eventObject);
85
92
  if (nextTitle) {
93
+ if (!threadTitleById.has(threadId) && threadTitleById.size >= MAX_THREAD_TITLE_ENTRIES) {
94
+ const oldest = threadTitleById.keys().next().value;
95
+ threadTitleById.delete(oldest);
96
+ }
86
97
  threadTitleById.set(threadId, nextTitle);
87
98
  }
88
99
  }
@@ -225,6 +236,10 @@ function createPushNotificationTracker({
225
236
  function ensureTurnState(threadId, turnId) {
226
237
  const key = turnStateKey(threadId, turnId);
227
238
  if (!turnStateByKey.has(key)) {
239
+ if (turnStateByKey.size >= MAX_TURN_STATE_ENTRIES) {
240
+ const oldest = turnStateByKey.keys().next().value;
241
+ turnStateByKey.delete(oldest);
242
+ }
228
243
  turnStateByKey.set(key, {
229
244
  latestAssistantPreview: "",
230
245
  latestFailurePreview: "",