leafrelay 0.2.2 → 0.2.3

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.
@@ -152,6 +152,7 @@ var OverleafRealtimeSocket = class {
152
152
  cookie;
153
153
  socket;
154
154
  connected = false;
155
+ projectJoined = false;
155
156
  explicitlyDisconnected = false;
156
157
  reconnectEnabled;
157
158
  reconnectAttempts = 0;
@@ -160,7 +161,6 @@ var OverleafRealtimeSocket = class {
160
161
  closeTimeoutMs = 6e4;
161
162
  acknowledgementId = 0;
162
163
  acknowledgements = /* @__PURE__ */ new Map();
163
- sendBuffer = [];
164
164
  io = {
165
165
  reconnect: (enabled) => {
166
166
  this.reconnectEnabled = enabled;
@@ -192,15 +192,24 @@ var OverleafRealtimeSocket = class {
192
192
  if (id && callback) {
193
193
  this.acknowledgements.set(id, callback);
194
194
  }
195
- this.send(encodePacket({
195
+ const packet = encodePacket({
196
196
  type: "event",
197
197
  id,
198
198
  ack: callback ? "data" : void 0,
199
199
  name: event,
200
200
  args
201
- }));
201
+ });
202
+ if (!this.sendApplicationEvent(packet) && callback) {
203
+ if (id) {
204
+ this.acknowledgements.delete(id);
205
+ }
206
+ queueMicrotask(() => callback(new Error("The Overleaf project session is not connected.")));
207
+ }
202
208
  return this;
203
209
  }
210
+ markProjectJoined() {
211
+ this.projectJoined = true;
212
+ }
204
213
  disconnect() {
205
214
  this.explicitlyDisconnected = true;
206
215
  this.reconnectEnabled = false;
@@ -215,6 +224,7 @@ var OverleafRealtimeSocket = class {
215
224
  }
216
225
  async connect() {
217
226
  try {
227
+ this.projectJoined = false;
218
228
  const handshakeUrl = new URL("/socket.io/1/", this.endpoint.origin);
219
229
  for (const [name, value] of this.endpoint.searchParams) {
220
230
  handshakeUrl.searchParams.set(name, value);
@@ -243,7 +253,7 @@ var OverleafRealtimeSocket = class {
243
253
  this.socket.addEventListener("open", () => this.events.emit("connecting", "websocket"));
244
254
  this.socket.addEventListener("message", (event) => void this.receive(event.data));
245
255
  this.socket.addEventListener("error", (error2) => this.events.emit("error", error2));
246
- this.socket.addEventListener("close", () => this.handleClose());
256
+ this.socket.addEventListener("close", (event) => this.handleClose(event));
247
257
  } catch (error2) {
248
258
  this.events.emit("connect_failed", error2);
249
259
  this.events.emit("error", error2);
@@ -263,7 +273,6 @@ var OverleafRealtimeSocket = class {
263
273
  const wasReconnecting = this.reconnectAttempts > 0;
264
274
  this.connected = true;
265
275
  this.events.emit("connect");
266
- this.flush();
267
276
  if (wasReconnecting) {
268
277
  this.events.emit("reconnect", "websocket", this.reconnectAttempts);
269
278
  this.reconnectAttempts = 0;
@@ -309,16 +318,16 @@ var OverleafRealtimeSocket = class {
309
318
  }
310
319
  }
311
320
  send(packet) {
312
- if (!this.connected || this.socket?.readyState !== WebSocket.OPEN) {
313
- this.sendBuffer.push(packet);
314
- return;
321
+ if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
322
+ this.socket.send(packet);
315
323
  }
316
- this.socket.send(packet);
317
324
  }
318
- flush() {
319
- while (this.sendBuffer.length > 0 && this.socket?.readyState === WebSocket.OPEN) {
320
- this.socket.send(this.sendBuffer.shift());
325
+ sendApplicationEvent(packet) {
326
+ if (!this.connected || !this.projectJoined || this.socket?.readyState !== WebSocket.OPEN) {
327
+ return false;
321
328
  }
329
+ this.socket.send(packet);
330
+ return true;
322
331
  }
323
332
  resetInactivityTimer() {
324
333
  if (this.inactivityTimer) {
@@ -326,20 +335,25 @@ var OverleafRealtimeSocket = class {
326
335
  }
327
336
  this.inactivityTimer = setTimeout(() => this.closeSocket("timeout"), this.closeTimeoutMs);
328
337
  }
329
- handleClose() {
338
+ handleClose(event) {
330
339
  const wasConnected = this.connected;
331
340
  this.connected = false;
341
+ this.projectJoined = false;
332
342
  if (this.inactivityTimer) {
333
343
  clearTimeout(this.inactivityTimer);
334
344
  }
335
345
  if (wasConnected) {
336
- this.events.emit("disconnect", "transport close");
346
+ const detail = event.reason ? `: ${event.reason}` : "";
347
+ const reason = `transport close (code ${event.code})${detail}`;
348
+ this.failAcknowledgements(reason);
349
+ this.events.emit("disconnect", reason);
337
350
  }
338
351
  this.scheduleReconnect();
339
352
  }
340
353
  closeSocket(reason) {
341
354
  const wasConnected = this.connected;
342
355
  this.connected = false;
356
+ this.projectJoined = false;
343
357
  if (this.inactivityTimer) {
344
358
  clearTimeout(this.inactivityTimer);
345
359
  }
@@ -347,12 +361,20 @@ var OverleafRealtimeSocket = class {
347
361
  this.socket = void 0;
348
362
  socket?.close();
349
363
  if (wasConnected) {
364
+ this.failAcknowledgements(reason);
350
365
  this.events.emit("disconnect", reason);
351
366
  }
352
367
  if (!this.explicitlyDisconnected) {
353
368
  this.scheduleReconnect();
354
369
  }
355
370
  }
371
+ failAcknowledgements(reason) {
372
+ const error2 = new Error(`Overleaf realtime connection closed: ${reason}`);
373
+ for (const callback of this.acknowledgements.values()) {
374
+ callback(error2);
375
+ }
376
+ this.acknowledgements.clear();
377
+ }
356
378
  scheduleReconnect() {
357
379
  if (!this.reconnectEnabled || this.explicitlyDisconnected || this.reconnectTimer) {
358
380
  return;
@@ -1188,6 +1210,8 @@ var SocketIOAPI = class _SocketIOAPI {
1188
1210
  static alternativeSocketFactory;
1189
1211
  scheme;
1190
1212
  record;
1213
+ joinedProject;
1214
+ pendingProjectJoin;
1191
1215
  _handlers = [];
1192
1216
  connectedPublicId;
1193
1217
  socket;
@@ -1224,6 +1248,8 @@ var SocketIOAPI = class _SocketIOAPI {
1224
1248
  break;
1225
1249
  case "realtime":
1226
1250
  this.record = void 0;
1251
+ this.joinedProject = void 0;
1252
+ this.resetProjectJoin(new Error("The realtime connection was replaced."));
1227
1253
  const query = `?projectId=${this.projectId}&t=${Date.now()}`;
1228
1254
  this.socket = this.api._initSocketV0(this.identity, query);
1229
1255
  break;
@@ -1266,6 +1292,11 @@ var SocketIOAPI = class _SocketIOAPI {
1266
1292
  });
1267
1293
  this.socket.on("disconnect", (reason) => {
1268
1294
  log("SocketIOAPI: disconnect event", { scheme: this.scheme, projectId: this.projectId, reason });
1295
+ if (this.scheme === "realtime") {
1296
+ this.joinedProject = void 0;
1297
+ this.connectedPublicId = void 0;
1298
+ this.resetProjectJoin(new Error(`Overleaf realtime connection closed: ${String(reason ?? "unknown")}`));
1299
+ }
1269
1300
  });
1270
1301
  this.socket.on("reconnecting", (delay, attempt) => {
1271
1302
  log("SocketIOAPI: reconnecting", { scheme: this.scheme, projectId: this.projectId, delay, attempt });
@@ -1290,22 +1321,50 @@ var SocketIOAPI = class _SocketIOAPI {
1290
1321
  error(`Overleaf connection error: ${message}`);
1291
1322
  });
1292
1323
  if (this.scheme === "realtime") {
1293
- this.record = new Promise((resolve6, reject) => {
1294
- this.socket.on("joinProjectResponse", (res) => {
1295
- const publicId = res.publicId;
1296
- const project = res.project;
1297
- this.connectedPublicId = publicId;
1298
- for (const handler of this._handlers) {
1299
- handler.onConnectionAccepted?.(publicId);
1300
- }
1301
- resolve6(project);
1302
- });
1303
- this.socket.on("connectionRejected", (err) => {
1304
- reject(err?.message || err);
1305
- });
1324
+ this.pendingProjectJoin ??= this.createProjectJoin();
1325
+ this.socket.on("joinProjectResponse", (res) => {
1326
+ const publicId = res.publicId;
1327
+ const project = res.project;
1328
+ this.socket.markProjectJoined?.();
1329
+ log("SocketIOAPI: project joined", { scheme: this.scheme, projectId: this.projectId, publicId });
1330
+ this.connectedPublicId = publicId;
1331
+ this.joinedProject = project;
1332
+ this.pendingProjectJoin?.resolve(project);
1333
+ for (const handler of this._handlers) {
1334
+ handler.onProjectJoined?.(project);
1335
+ handler.onConnectionAccepted?.(publicId);
1336
+ }
1337
+ });
1338
+ this.socket.on("connectionRejected", (err) => {
1339
+ this.pendingProjectJoin?.reject(err?.message || err);
1306
1340
  });
1307
1341
  }
1308
1342
  }
1343
+ createProjectJoin() {
1344
+ let resolve6;
1345
+ let reject;
1346
+ const promise = new Promise((resolveProject, rejectProject) => {
1347
+ resolve6 = resolveProject;
1348
+ reject = rejectProject;
1349
+ });
1350
+ void promise.catch(() => void 0);
1351
+ return { promise, resolve: resolve6, reject };
1352
+ }
1353
+ resetProjectJoin(reason) {
1354
+ if (reason !== void 0) {
1355
+ this.pendingProjectJoin?.reject(reason);
1356
+ }
1357
+ this.pendingProjectJoin = this.createProjectJoin();
1358
+ }
1359
+ waitForRealtimeProject() {
1360
+ return this.joinedProject ? Promise.resolve(this.joinedProject) : this.pendingProjectJoin.promise;
1361
+ }
1362
+ async emitProjectEvent(event, ...args) {
1363
+ if (this.scheme === "realtime") {
1364
+ await this.waitForRealtimeProject();
1365
+ }
1366
+ return this.emit(event, ...args);
1367
+ }
1309
1368
  disconnect() {
1310
1369
  this.socket.disconnect();
1311
1370
  }
@@ -1364,10 +1423,12 @@ var SocketIOAPI = class _SocketIOAPI {
1364
1423
  });
1365
1424
  break;
1366
1425
  case handlers.onDisconnected:
1367
- this.socket.on("disconnect", () => {
1368
- handler();
1426
+ this.socket.on("disconnect", (reason) => {
1427
+ handler(reason);
1369
1428
  });
1370
1429
  break;
1430
+ case handlers.onProjectJoined:
1431
+ break;
1371
1432
  case handlers.onConnectionAccepted:
1372
1433
  this.socket.on("connectionAccepted", (_, publicId) => {
1373
1434
  handler(publicId);
@@ -1437,7 +1498,7 @@ var SocketIOAPI = class _SocketIOAPI {
1437
1498
  timeoutPromise
1438
1499
  ]);
1439
1500
  case "realtime":
1440
- return Promise.race([this.record, timeoutPromise]);
1501
+ return Promise.race([this.waitForRealtimeProject(), timeoutPromise]);
1441
1502
  }
1442
1503
  }
1443
1504
  /**
@@ -1446,7 +1507,7 @@ var SocketIOAPI = class _SocketIOAPI {
1446
1507
  * @returns {Promise}
1447
1508
  */
1448
1509
  async joinDoc(docId) {
1449
- return this.emit("joinDoc", docId, { encodeRanges: true }).then((returns) => {
1510
+ return this.emitProjectEvent("joinDoc", docId, { encodeRanges: true }).then((returns) => {
1450
1511
  const [docLinesAscii, version, updates, ranges] = returns;
1451
1512
  const docLines = docLinesAscii.map((line) => decodePackedUtf8(line));
1452
1513
  return { docLines, version, updates, ranges };
@@ -1458,7 +1519,7 @@ var SocketIOAPI = class _SocketIOAPI {
1458
1519
  * @returns {Promise}
1459
1520
  */
1460
1521
  async leaveDoc(docId) {
1461
- return this.emit("leaveDoc", docId).then(() => {
1522
+ return this.emitProjectEvent("leaveDoc", docId).then(() => {
1462
1523
  return;
1463
1524
  });
1464
1525
  }
@@ -1470,7 +1531,7 @@ var SocketIOAPI = class _SocketIOAPI {
1470
1531
  */
1471
1532
  async applyOtUpdate(docId, update) {
1472
1533
  try {
1473
- await this.emit("applyOtUpdate", docId, update);
1534
+ await this.emitProjectEvent("applyOtUpdate", docId, update);
1474
1535
  } catch (error2) {
1475
1536
  let detail;
1476
1537
  if (error2 instanceof Error) {
@@ -1492,7 +1553,7 @@ var SocketIOAPI = class _SocketIOAPI {
1492
1553
  * @returns {Promise}
1493
1554
  */
1494
1555
  async getConnectedUsers() {
1495
- return this.emit("clientTracking.getConnectedUsers").then((returns) => {
1556
+ return this.emitProjectEvent("clientTracking.getConnectedUsers").then((returns) => {
1496
1557
  const [connectedUsers] = returns;
1497
1558
  return connectedUsers;
1498
1559
  });
@@ -1503,7 +1564,7 @@ var SocketIOAPI = class _SocketIOAPI {
1503
1564
  * @returns {Promise}
1504
1565
  */
1505
1566
  async updatePosition(doc_id, row, column) {
1506
- return this.emit("clientTracking.updatePosition", { row, column, doc_id }).then(() => {
1567
+ return this.emitProjectEvent("clientTracking.updatePosition", { row, column, doc_id }).then(() => {
1507
1568
  return;
1508
1569
  });
1509
1570
  }
@@ -1768,6 +1829,7 @@ var RemoteProject = class {
1768
1829
  entries = /* @__PURE__ */ new Map();
1769
1830
  idToPath = /* @__PURE__ */ new Map();
1770
1831
  changeHandler;
1832
+ reconnectHandler;
1771
1833
  externalTransport;
1772
1834
  async connect() {
1773
1835
  if (this.externalTransport) {
@@ -1793,6 +1855,9 @@ var RemoteProject = class {
1793
1855
  onChange(handler) {
1794
1856
  this.changeHandler = handler;
1795
1857
  }
1858
+ onReconnect(handler) {
1859
+ this.reconnectHandler = handler;
1860
+ }
1796
1861
  listEntries() {
1797
1862
  return [...this.entries.values()];
1798
1863
  }
@@ -2011,6 +2076,11 @@ var RemoteProject = class {
2011
2076
  }
2012
2077
  registerEvents() {
2013
2078
  this.socket.updateEventHandlers({
2079
+ onProjectJoined: (project) => {
2080
+ this.project = project;
2081
+ this.reindex();
2082
+ this.reconnectHandler?.();
2083
+ },
2014
2084
  onFileCreated: (parentId, type, entity) => {
2015
2085
  const parent = this.folderById(parentId);
2016
2086
  if (!parent || type === "outputs") {
@@ -2211,6 +2281,11 @@ var SyncEngine = class {
2211
2281
  }
2212
2282
  this.schedule(change.path, "remote");
2213
2283
  });
2284
+ this.options.remote.onReconnect?.(() => {
2285
+ this.queue = this.queue.then(() => this.reconcileAfterReconnect()).catch((error2) => {
2286
+ this.options.log(`Failed to reconcile after reconnecting: ${error2 instanceof Error ? error2.message : String(error2)}`);
2287
+ });
2288
+ });
2214
2289
  this.options.log(`Synchronization active at Overleaf version ${this.state.remoteVersion}.`);
2215
2290
  }
2216
2291
  async stop() {
@@ -2324,6 +2399,15 @@ var SyncEngine = class {
2324
2399
  this.options.log(`Synchronization remains active; ${this.conflicts.size} conflicting path(s) are paused.`);
2325
2400
  }
2326
2401
  }
2402
+ async reconcileAfterReconnect() {
2403
+ const currentVersion = await this.options.remote.getCurrentVersion();
2404
+ await this.startupReconcile(
2405
+ currentVersion,
2406
+ await this.options.local.listFiles(),
2407
+ this.remoteFilePaths()
2408
+ );
2409
+ this.options.log(`Realtime connection restored at Overleaf version ${currentVersion}.`);
2410
+ }
2327
2411
  schedule(path, source) {
2328
2412
  const normalized = normalizePath2(path);
2329
2413
  if (this.shouldIgnore(normalized) || this.conflicts.has(normalized)) {
@@ -2534,4 +2618,4 @@ export {
2534
2618
  RemoteProject,
2535
2619
  startServe
2536
2620
  };
2537
- //# sourceMappingURL=chunk-Q3R5N4YE.js.map
2621
+ //# sourceMappingURL=chunk-3RL67IG5.js.map