@luckystack/sync 0.1.8 → 0.2.0

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.
package/dist/client.js CHANGED
@@ -41,7 +41,8 @@ var getStreamCallbacksForRoute = (route) => {
41
41
  var triggerSyncCallbacks = (name, clientOutput, serverOutput) => {
42
42
  const callbacks = syncEvents[name] ?? [];
43
43
  if (callbacks.length === 0) {
44
- if (shouldLogDev()) {
44
+ const streamCallbacks = syncStreamEvents[name] ?? [];
45
+ if (streamCallbacks.length === 0 && shouldLogDev()) {
45
46
  getLogger().warn(`Sync event ${name} has no registered callback on this page`);
46
47
  }
47
48
  return;
@@ -81,6 +82,82 @@ var normalizeSyncError = ({
81
82
  httpStatus: normalized.httpStatus
82
83
  };
83
84
  };
85
+ var prepareSyncRequest = async ({
86
+ name,
87
+ version,
88
+ receiver,
89
+ payloadData
90
+ }) => {
91
+ if (!name || typeof name !== "string") {
92
+ if (shouldLogDev()) {
93
+ getLogger().error("Invalid name for syncRequest");
94
+ }
95
+ if (shouldNotifyDev()) {
96
+ notify.error({ key: "sync.invalidName" });
97
+ }
98
+ return { ok: false, error: normalizeSyncError({
99
+ response: { status: "error", errorCode: "sync.invalidName" },
100
+ fallbackErrorCode: "sync.invalidName"
101
+ }) };
102
+ }
103
+ const parsedRoute = parseServiceRouteName(name);
104
+ if (parsedRoute.status === "error") {
105
+ if (shouldLogDev()) {
106
+ getLogger().error(`[syncRequest] Invalid service route name '${name}'`, void 0, { reason: parsedRoute.reason });
107
+ }
108
+ if (shouldNotifyDev()) {
109
+ notify.error({ key: "routing.invalidServiceRouteName" });
110
+ }
111
+ return { ok: false, error: normalizeSyncError({
112
+ response: {
113
+ status: "error",
114
+ errorCode: "routing.invalidServiceRouteName",
115
+ errorParams: [{ key: "name", value: name }]
116
+ },
117
+ fallbackErrorCode: "routing.invalidServiceRouteName"
118
+ }) };
119
+ }
120
+ const sanitizedName = parsedRoute.normalizedRouteName;
121
+ const data = payloadData && typeof payloadData === "object" ? payloadData : {};
122
+ if (!version || typeof version !== "string") {
123
+ if (shouldLogDev()) {
124
+ getLogger().error("Invalid version for syncRequest");
125
+ }
126
+ if (shouldNotifyDev()) {
127
+ notify.error({ key: "sync.invalidVersion" });
128
+ }
129
+ return { ok: false, error: normalizeSyncError({
130
+ response: { status: "error", errorCode: "sync.invalidVersion" },
131
+ fallbackErrorCode: "sync.invalidVersion"
132
+ }) };
133
+ }
134
+ const normalizedReceiver = typeof receiver === "string" ? receiver.trim() : "";
135
+ if (!normalizedReceiver) {
136
+ if (shouldLogDev()) {
137
+ getLogger().error("You need to provide a receiver for syncRequest, this can be either 'all' to trigger all sockets which we do not recommend or it can be any value such as a code e.g 'Ag2cg4'. this works together with the joinRoom and leaveRoom function");
138
+ }
139
+ if (shouldNotifyDev()) {
140
+ notify.error({ key: "sync.missingReceiver" });
141
+ }
142
+ return { ok: false, error: normalizeSyncError({
143
+ response: { status: "error", errorCode: "sync.missingReceiver" },
144
+ fallbackErrorCode: "sync.missingReceiver"
145
+ }) };
146
+ }
147
+ if (!await waitForSocket()) {
148
+ return { ok: false, error: normalizeSyncError({
149
+ response: { status: "error", errorCode: "sync.ioUnavailable" },
150
+ fallbackErrorCode: "sync.ioUnavailable"
151
+ }) };
152
+ }
153
+ if (!socket) {
154
+ return { ok: false, error: normalizeSyncError({
155
+ response: { status: "error", errorCode: "sync.ioUnavailable" },
156
+ fallbackErrorCode: "sync.ioUnavailable"
157
+ }) };
158
+ }
159
+ return { ok: true, sanitizedName, version, data, normalizedReceiver };
160
+ };
84
161
  var syncRequestInternal = (params) => {
85
162
  const runtimeParams = params;
86
163
  const { name, version, receiver, ignoreSelf, onStream, offlineDropPolicy, signal: externalSignal } = runtimeParams;
@@ -94,85 +171,26 @@ var syncRequestInternal = (params) => {
94
171
  }));
95
172
  return;
96
173
  }
97
- if (!name || typeof name !== "string") {
98
- if (shouldLogDev()) {
99
- getLogger().error("Invalid name for syncRequest");
100
- }
101
- if (shouldNotifyDev()) {
102
- notify.error({ key: "sync.invalidName" });
103
- }
104
- resolve(normalizeSyncError({
105
- response: { status: "error", errorCode: "sync.invalidName" },
106
- fallbackErrorCode: "sync.invalidName"
107
- }));
108
- return;
109
- }
110
- const parsedRoute = parseServiceRouteName(name);
111
- if (parsedRoute.status === "error") {
112
- if (shouldLogDev()) {
113
- getLogger().error(`[syncRequest] Invalid service route name '${name}'`, void 0, { reason: parsedRoute.reason });
114
- }
115
- if (shouldNotifyDev()) {
116
- notify.error({ key: "routing.invalidServiceRouteName" });
117
- }
118
- resolve(normalizeSyncError({
119
- response: {
120
- status: "error",
121
- errorCode: "routing.invalidServiceRouteName",
122
- errorParams: [{ key: "name", value: name }]
123
- },
124
- fallbackErrorCode: "routing.invalidServiceRouteName"
125
- }));
126
- return;
127
- }
128
- const sanitizedName = parsedRoute.normalizedRouteName;
129
- const data = payloadData && typeof payloadData === "object" ? payloadData : {};
130
- if (!version || typeof version !== "string") {
131
- if (shouldLogDev()) {
132
- getLogger().error("Invalid version for syncRequest");
133
- }
134
- if (shouldNotifyDev()) {
135
- notify.error({ key: "sync.invalidVersion" });
136
- }
137
- resolve(normalizeSyncError({
138
- response: { status: "error", errorCode: "sync.invalidVersion" },
139
- fallbackErrorCode: "sync.invalidVersion"
140
- }));
141
- return;
142
- }
143
- const normalizedReceiver = typeof receiver === "string" ? receiver.trim() : "";
144
- if (!normalizedReceiver) {
145
- if (shouldLogDev()) {
146
- getLogger().error("You need to provide a receiver for syncRequest, this can be either 'all' to trigger all sockets which we do not recommend or it can be any value such as a code e.g 'Ag2cg4'. this works together with the joinRoom and leaveRoom function");
147
- }
148
- if (shouldNotifyDev()) {
149
- notify.error({ key: "sync.missingReceiver" });
150
- }
151
- resolve(normalizeSyncError({
152
- response: { status: "error", errorCode: "sync.missingReceiver" },
153
- fallbackErrorCode: "sync.missingReceiver"
154
- }));
155
- return;
156
- }
157
- if (!await waitForSocket()) {
158
- resolve(normalizeSyncError({
159
- response: { status: "error", errorCode: "sync.ioUnavailable" },
160
- fallbackErrorCode: "sync.ioUnavailable"
161
- }));
174
+ const prepared = await prepareSyncRequest({ name, version, receiver, payloadData });
175
+ if (!prepared.ok) {
176
+ resolve(prepared.error);
162
177
  return;
163
178
  }
164
- if (!socket) {
179
+ const { sanitizedName, version: resolvedVersion, data, normalizedReceiver } = prepared;
180
+ const activeSocket = socket;
181
+ if (!activeSocket) {
165
182
  resolve(normalizeSyncError({
166
183
  response: { status: "error", errorCode: "sync.ioUnavailable" },
167
184
  fallbackErrorCode: "sync.ioUnavailable"
168
185
  }));
169
186
  return;
170
187
  }
171
- const fullName = `sync/${sanitizedName}/${version}`;
188
+ const fullName = `sync/${sanitizedName}/${resolvedVersion}`;
172
189
  let queueId = null;
173
190
  const runRequest = (socketInstance) => {
174
191
  if (!canSendNow(socketInstance)) {
175
192
  queueId ??= createQueueId();
193
+ let suppressOnDrop = true;
176
194
  const enqueued = enqueueSyncRequest({
177
195
  id: queueId,
178
196
  key: fullName,
@@ -180,8 +198,23 @@ var syncRequestInternal = (params) => {
180
198
  runRequest(s);
181
199
  },
182
200
  createdAt: Date.now(),
183
- dropPolicy: offlineDropPolicy
201
+ dropPolicy: offlineDropPolicy,
202
+ //? SYNC-09 — settle the awaiting caller when this QUEUED request is
203
+ //? later EVICTED (drop-oldest by a future enqueue / age expiry)
204
+ //? instead of ever running. Without it the promise would hang
205
+ //? forever, since `run` is the only thing that could otherwise
206
+ //? resolve it. The default `offlineQueue.dropPolicy` is
207
+ //? `'drop-oldest'`, so async eviction is the NORMAL overflow path
208
+ //? for high-churn syncs (cursor moves), not an edge case.
209
+ onDrop: (reason) => {
210
+ if (suppressOnDrop) return;
211
+ resolve(normalizeSyncError({
212
+ response: { status: "error", errorCode: "offline.dropped", errorParams: [{ key: "reason", value: reason }] },
213
+ fallbackErrorCode: "offline.dropped"
214
+ }));
215
+ }
184
216
  });
217
+ suppressOnDrop = false;
185
218
  if (!enqueued) {
186
219
  resolve(normalizeSyncError({
187
220
  response: { status: "error", errorCode: "offline.queueFull" },
@@ -192,30 +225,46 @@ var syncRequestInternal = (params) => {
192
225
  }
193
226
  const tempIndex = incrementResponseIndex();
194
227
  let cleanupProgressListener = null;
228
+ let responseTimeout = null;
229
+ const clearResponseTimeout = () => {
230
+ if (responseTimeout) {
231
+ clearTimeout(responseTimeout);
232
+ responseTimeout = null;
233
+ }
234
+ };
195
235
  if (shouldLogDev()) {
196
236
  getLogger().debug(`Client Sync Request(${String(tempIndex)})`, { syncName: sanitizedName, data, receiver: normalizedReceiver, ignoreSelf });
197
237
  }
198
- if (typeof onStream === "function") {
199
- const progressEventName = buildSyncProgressEventName(tempIndex);
200
- const progressListener = (streamPayload) => {
201
- if (shouldLogStream()) {
202
- getLogger().debug(`Server Sync Stream(${String(tempIndex)})`, { syncName: sanitizedName, streamPayload });
203
- }
204
- onStream(streamPayload);
205
- };
206
- socketInstance.on(progressEventName, progressListener);
207
- cleanupProgressListener = () => {
208
- socketInstance.off(progressEventName, progressListener);
209
- };
210
- }
211
- const syncCb = `${sanitizedName}/${version}`;
238
+ let serverCancelId = null;
239
+ const progressEventName = buildSyncProgressEventName(tempIndex);
240
+ const progressListener = (streamPayload) => {
241
+ if (typeof streamPayload.__cancelId === "string") {
242
+ serverCancelId = streamPayload.__cancelId;
243
+ return;
244
+ }
245
+ if (typeof onStream !== "function") return;
246
+ if (shouldLogStream()) {
247
+ getLogger().debug(`Server Sync Stream(${String(tempIndex)})`, { syncName: sanitizedName, streamPayload });
248
+ }
249
+ onStream(streamPayload);
250
+ };
251
+ socketInstance.on(progressEventName, progressListener);
252
+ cleanupProgressListener = () => {
253
+ socketInstance.off(progressEventName, progressListener);
254
+ };
255
+ const syncCb = `${sanitizedName}/${resolvedVersion}`;
212
256
  socketInstance.emit(socketEventNames.sync, { name: fullName, data, cb: syncCb, receiver: normalizedReceiver, responseIndex: tempIndex, ignoreSelf });
257
+ const responseEventName = buildSyncResponseEventName(tempIndex);
258
+ let cleanupResponseListener = null;
213
259
  let cleanupExternalAbort = null;
214
260
  if (externalSignal) {
215
261
  const externalAbortHandler = () => {
216
- socketInstance.emit(socketEventNames.syncCancel, { cb: syncCb });
262
+ clearResponseTimeout();
263
+ socketInstance.emit(socketEventNames.syncCancel, { cb: serverCancelId ?? syncCb });
217
264
  cleanupProgressListener?.();
218
265
  cleanupProgressListener = null;
266
+ cleanupResponseListener?.();
267
+ cleanupResponseListener = null;
219
268
  cleanupExternalAbort?.();
220
269
  cleanupExternalAbort = null;
221
270
  resolve(normalizeSyncError({
@@ -228,8 +277,30 @@ var syncRequestInternal = (params) => {
228
277
  externalSignal.removeEventListener("abort", externalAbortHandler);
229
278
  };
230
279
  }
231
- socketInstance.once(buildSyncResponseEventName(tempIndex), (responseData) => {
280
+ const effectiveTimeoutMs = getProjectConfig().sync.requestTimeoutMs;
281
+ if (typeof effectiveTimeoutMs === "number" && effectiveTimeoutMs > 0) {
282
+ responseTimeout = setTimeout(() => {
283
+ responseTimeout = null;
284
+ cleanupProgressListener?.();
285
+ cleanupProgressListener = null;
286
+ cleanupResponseListener?.();
287
+ cleanupResponseListener = null;
288
+ cleanupExternalAbort?.();
289
+ cleanupExternalAbort = null;
290
+ if (shouldLogDev()) {
291
+ getLogger().error(`Sync ${sanitizedName} timed out`, void 0, { responseIndex: tempIndex });
292
+ }
293
+ resolve(normalizeSyncError({
294
+ response: { status: "error", errorCode: "sync.requestTimeout", httpStatus: 504 },
295
+ fallbackErrorCode: "sync.requestTimeout"
296
+ }));
297
+ }, effectiveTimeoutMs);
298
+ }
299
+ const responseListener = (responseData) => {
300
+ clearResponseTimeout();
232
301
  cleanupProgressListener?.();
302
+ cleanupProgressListener = null;
303
+ cleanupResponseListener = null;
233
304
  cleanupExternalAbort?.();
234
305
  cleanupExternalAbort = null;
235
306
  if (responseData.status === "error") {
@@ -265,9 +336,13 @@ var syncRequestInternal = (params) => {
265
336
  message: typeof responseData.message === "string" && responseData.message.trim().length > 0 ? responseData.message : `sync ${sanitizedName} success`,
266
337
  result
267
338
  });
268
- });
339
+ };
340
+ socketInstance.once(responseEventName, responseListener);
341
+ cleanupResponseListener = () => {
342
+ socketInstance.off(responseEventName, responseListener);
343
+ };
269
344
  };
270
- runRequest(socket);
345
+ runRequest(activeSocket);
271
346
  })();
272
347
  });
273
348
  };
@@ -442,6 +517,78 @@ var useSyncEventTrigger = () => {
442
517
  }, []);
443
518
  return { triggerSyncEvent, triggerSyncStreamEvent };
444
519
  };
520
+ var normalizeSyncRouteKey = (value) => {
521
+ const sanitized = value.replaceAll(/^\/+|\/+$/g, "");
522
+ if (sanitized.length === 0) return "";
523
+ if (sanitized.startsWith("sync/")) return sanitized;
524
+ return `sync/${sanitized}`;
525
+ };
526
+ var getSyncRouteKeys = ({ fullName, cb }) => {
527
+ const keys = /* @__PURE__ */ new Set();
528
+ if (typeof fullName === "string") {
529
+ const normalized = normalizeSyncRouteKey(fullName);
530
+ if (normalized) keys.add(normalized);
531
+ }
532
+ if (typeof cb === "string") {
533
+ const normalized = normalizeSyncRouteKey(cb);
534
+ if (normalized) keys.add(normalized);
535
+ }
536
+ return [...keys];
537
+ };
538
+ var attachedSyncSockets = /* @__PURE__ */ new WeakSet();
539
+ var attachSyncReceiver = (socketInstance) => {
540
+ if (attachedSyncSockets.has(socketInstance)) return;
541
+ attachedSyncSockets.add(socketInstance);
542
+ socketInstance.on(socketEventNames.sync, (payload) => {
543
+ const { cb, clientOutput, serverOutput, message, status, fullName, errorCode, errorParams } = payload;
544
+ if (shouldLogDev()) {
545
+ getLogger().debug("Server Sync Response", { payload });
546
+ }
547
+ const routeKeys = getSyncRouteKeys({ fullName, cb });
548
+ if (status === "stream") {
549
+ if (routeKeys.length === 0) return;
550
+ const streamPayload = { ...payload };
551
+ delete streamPayload.status;
552
+ delete streamPayload.fullName;
553
+ delete streamPayload.cb;
554
+ delete streamPayload.errorCode;
555
+ delete streamPayload.errorParams;
556
+ delete streamPayload.httpStatus;
557
+ delete streamPayload.message;
558
+ if (shouldLogStream()) {
559
+ getLogger().debug("Server Sync Stream", { routeKeys, streamPayload });
560
+ }
561
+ for (const routeKey of routeKeys) {
562
+ triggerSyncStreamCallbacks(routeKey, streamPayload);
563
+ }
564
+ return;
565
+ }
566
+ if (status === "error") {
567
+ if (errorCode === "sync.ignore" || message === "sync.ignore") return;
568
+ if (shouldNotifyDev()) {
569
+ if (errorCode) {
570
+ notify.error({ key: errorCode, params: errorParams });
571
+ } else if (message) {
572
+ notify.error({ key: message });
573
+ }
574
+ }
575
+ return;
576
+ }
577
+ if (routeKeys.length === 0) {
578
+ const errorMessage = `Sync response is missing fullName for cb '${cb ?? "unknown"}'.`;
579
+ if (shouldLogDev()) {
580
+ getLogger().error(errorMessage);
581
+ }
582
+ if (shouldNotifyDev()) {
583
+ notify.error({ key: "sync.invalidRequestFormat" });
584
+ }
585
+ return;
586
+ }
587
+ for (const routeKey of routeKeys) {
588
+ triggerSyncCallbacks(routeKey, clientOutput, serverOutput);
589
+ }
590
+ });
591
+ };
445
592
  var buildConnectHandler = ({ setSocketStatus }) => () => {
446
593
  if (shouldLogSocketStatus()) getLogger().info("Connected to server");
447
594
  setSocketStatus((prev) => ({ ...prev, self: { ...prev.self, status: "CONNECTED" } }));
@@ -529,6 +676,7 @@ var initSyncRequest = async ({
529
676
  socket.on(socketEventNames.connectError, connectError);
530
677
  };
531
678
  export {
679
+ attachSyncReceiver,
532
680
  initSyncRequest,
533
681
  syncRequest,
534
682
  useSyncEventTrigger,