ep_data_tables 0.0.98 → 0.0.99

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ep_data_tables",
3
- "version": "0.0.98",
3
+ "version": "0.0.99",
4
4
  "description": "BETA - etherpad tables plugin, compatible with other character/line based styling and other features",
5
5
  "author": {
6
6
  "name": "DCastelone",
@@ -2601,12 +2601,18 @@ exports.aceInitialized = (h, ctx) => {
2601
2601
  let originalTableLine = null; // Track where the table actually is
2602
2602
 
2603
2603
  // Recover cursor if browser moved it to wrong line between compositions
2604
+ // CONSERVATIVE: More validation, shorter timeout, always clear after check
2604
2605
  let usedCursorRecovery = false;
2605
2606
  if (desktopComposition && desktopComposition._expectedCursor) {
2606
2607
  const stored = desktopComposition._expectedCursor;
2607
2608
  const timeSinceStored = Date.now() - stored.timestamp;
2608
2609
 
2609
- if (timeSinceStored < 1000) {
2610
+ // Reduced timeout from 1000ms to 500ms for rapid typing safety
2611
+ const isFresh = timeSinceStored < 500;
2612
+ const sameTable = stored.tblId && tableMetadata && stored.tblId === tableMetadata.tblId;
2613
+ const reasonableLineDelta = typeof stored.lineNum === 'number' && Math.abs(lineNum - stored.lineNum) <= 1;
2614
+
2615
+ if (isFresh && sameTable && reasonableLineDelta) {
2610
2616
  const currentLine = lineNum;
2611
2617
  const expectedLine = stored.lineNum;
2612
2618
  const expectedCell = stored.cellIndex;
@@ -2615,26 +2621,35 @@ exports.aceInitialized = (h, ctx) => {
2615
2621
  const seemsWrong = lineChanged && (currentLine === expectedLine + 1);
2616
2622
 
2617
2623
  if (seemsWrong) {
2618
- const storedMeta = getTableMetadataForLine(expectedLine);
2619
- if (storedMeta && storedMeta.tblId === stored.tblId) {
2620
- lineNum = expectedLine;
2621
- cellIndex = expectedCell;
2622
- tableMetadata = storedMeta;
2623
- originalTableLine = expectedLine;
2624
- usedCursorRecovery = true;
2624
+ // Additional validation: verify stored position is still valid
2625
+ try {
2626
+ const storedEntry = rep0.lines.atIndex(expectedLine);
2627
+ const storedText = storedEntry?.text || '';
2628
+ const storedCells = storedText.split(DELIMITER);
2625
2629
 
2626
- try {
2627
- ed.ace_callWithAce((aceInstance) => {
2628
- aceInstance.ace_performSelectionChange([expectedLine, stored.col], [expectedLine, stored.col], false);
2629
- }, 'compositionstart-cursor-recovery', true);
2630
- } catch (_) {}
2631
- }
2630
+ // Only recover if cell structure matches expectations
2631
+ if (expectedCell >= 0 && expectedCell < storedCells.length) {
2632
+ const storedMeta = getTableMetadataForLine(expectedLine);
2633
+ if (storedMeta && storedMeta.tblId === stored.tblId) {
2634
+ lineNum = expectedLine;
2635
+ cellIndex = expectedCell;
2636
+ tableMetadata = storedMeta;
2637
+ originalTableLine = expectedLine;
2638
+ usedCursorRecovery = true;
2639
+
2640
+ try {
2641
+ ed.ace_callWithAce((aceInstance) => {
2642
+ aceInstance.ace_performSelectionChange([expectedLine, stored.col], [expectedLine, stored.col], false);
2643
+ }, 'compositionstart-cursor-recovery', true);
2644
+ } catch (_) {}
2645
+ }
2646
+ }
2647
+ } catch (_) { /* validation failed, don't recover */ }
2632
2648
  }
2633
2649
  }
2634
2650
 
2635
- if (timeSinceStored > 1000 || usedCursorRecovery) {
2636
- delete desktopComposition._expectedCursor;
2637
- }
2651
+ // ALWAYS clear after checking to prevent stale reuse
2652
+ delete desktopComposition._expectedCursor;
2638
2653
  }
2639
2654
 
2640
2655
  try {
@@ -4081,12 +4096,29 @@ exports.aceInitialized = (h, ctx) => {
4081
4096
  const dataPreview = typeof nativeEvt?.data === 'string' ? nativeEvt.data : '';
4082
4097
  logCompositionEvent('compositionend-desktop-fired', evt, { data: dataPreview });
4083
4098
 
4084
- // Capture desktopComposition state before async ops
4085
- // which would overwrite desktopComposition and cause us to use the wrong state.
4086
- const capturedComposition = desktopComposition ? { ...desktopComposition } : null;
4087
-
4088
- // Prevent the immediate post-composition input commit from running; we pipeline instead
4089
- suppressNextInputCommit = true;
4099
+ // CRITICAL: Capture composition state with DEEP COPY before async deferral.
4100
+ // This prevents race conditions where a new compositionstart overwrites state
4101
+ // before this callback executes. Deep copy all arrays to prevent shared references.
4102
+ const capturedComposition = desktopComposition
4103
+ ? {
4104
+ active: desktopComposition.active,
4105
+ start: desktopComposition.start ? desktopComposition.start.slice() : null,
4106
+ end: desktopComposition.end ? desktopComposition.end.slice() : null,
4107
+ lineNum: desktopComposition.lineNum,
4108
+ cellIndex: desktopComposition.cellIndex,
4109
+ tblId: desktopComposition.tblId,
4110
+ snapshot: desktopComposition.snapshot ? desktopComposition.snapshot.slice() : null,
4111
+ snapshotMeta: desktopComposition.snapshotMeta ? { ...desktopComposition.snapshotMeta } : null,
4112
+ originalTableLine: desktopComposition.originalTableLine,
4113
+ usedCursorRecovery: desktopComposition.usedCursorRecovery,
4114
+ }
4115
+ : null;
4116
+
4117
+ // Capture the commit string synchronously too (before any async deferral)
4118
+ const commitStrRawSync = typeof nativeEvt?.data === 'string' ? nativeEvt.data : '';
4119
+
4120
+ // Prevent the immediate post-composition input commit from running; we pipeline instead
4121
+ suppressNextInputCommit = true;
4090
4122
  requestAnimationFrame(() => {
4091
4123
  try {
4092
4124
  ed.ace_callWithAce((aceInstance) => {
@@ -4106,12 +4138,12 @@ exports.aceInitialized = (h, ctx) => {
4106
4138
  return;
4107
4139
  }
4108
4140
 
4109
- // Pipeline: Apply committed IME string synchronously to the target cell
4110
- const commitStrRaw = typeof nativeEvt?.data === 'string' ? nativeEvt.data : '';
4111
- const commitStr = sanitizeCellContent(commitStrRaw || '');
4141
+ // Pipeline: Apply committed IME string to the target cell
4142
+ // Use the synchronously captured commit string (commitStrRawSync) to avoid race conditions
4143
+ const commitStr = sanitizeCellContent(commitStrRawSync || '');
4112
4144
  // Only suppress the next beforeinput commit if the IME provided a non-empty commit string.
4113
4145
  // Normalize using the same soft-whitespace rules the editor uses.
4114
- const willCommit = typeof commitStrRaw === 'string' && normalizeSoftWhitespace(commitStrRaw).trim().length > 0;
4146
+ const willCommit = typeof commitStrRawSync === 'string' && normalizeSoftWhitespace(commitStrRawSync).trim().length > 0;
4115
4147
  if (willCommit) suppressNextBeforeinputCommitOnce = true;
4116
4148
  const repNow = aceInstance.ace_getRep();
4117
4149
  const caret = repNow && repNow.selStart;
@@ -4239,15 +4271,44 @@ exports.aceInitialized = (h, ctx) => {
4239
4271
  }
4240
4272
  const cellsNow = (entry.text || '').split(DELIMITER);
4241
4273
  while (cellsNow.length < metadata.cols) cellsNow.push(' ');
4242
- // Prefer the cell index captured at compositionstart; otherwise compute using RAW mapping.
4243
- let idx = (capturedComposition && capturedComposition.cellIndex >= 0)
4244
- ? capturedComposition.cellIndex
4245
- : (() => {
4246
- const selCol = (capturedComposition && capturedComposition.start) ? capturedComposition.start[1] : (caret ? caret[1] : 0);
4247
- const rawMap = computeTargetCellIndexFromRaw(entry, selCol);
4248
- return rawMap.index;
4249
- })();
4250
- if (idx < 0) idx = Math.min(metadata.cols - 1, 0);
4274
+
4275
+ // DEFENSIVE CELL INDEX RESOLUTION:
4276
+ // 1. Prefer the cell index captured at compositionstart
4277
+ // 2. Fall back to live DOM selection (authoritative for current cursor)
4278
+ // 3. Fall back to raw line text mapping using current caret
4279
+ // 4. Final fallback: first cell (ensures we never lose text)
4280
+ let idx = -1;
4281
+
4282
+ // Strategy 1: Use captured cell index from compositionstart
4283
+ if (capturedComposition && capturedComposition.cellIndex >= 0) {
4284
+ idx = capturedComposition.cellIndex;
4285
+ }
4286
+
4287
+ // Strategy 2: Use live DOM selection as authoritative source
4288
+ if (idx < 0) {
4289
+ try {
4290
+ const liveDomTarget = getDomCellTargetFromSelection();
4291
+ if (liveDomTarget && typeof liveDomTarget.idx === 'number' && liveDomTarget.idx >= 0) {
4292
+ idx = liveDomTarget.idx;
4293
+ }
4294
+ } catch (_) { /* ignore DOM errors */ }
4295
+ }
4296
+
4297
+ // Strategy 3: Compute from raw line text using captured or current caret
4298
+ if (idx < 0) {
4299
+ try {
4300
+ const selCol = (capturedComposition && capturedComposition.start)
4301
+ ? capturedComposition.start[1]
4302
+ : (caret ? caret[1] : 0);
4303
+ const rawMap = computeTargetCellIndexFromRaw(entry, selCol);
4304
+ if (rawMap.index >= 0) idx = rawMap.index;
4305
+ } catch (_) { /* ignore */ }
4306
+ }
4307
+
4308
+ // Strategy 4: Final fallback - first cell (ensures no data loss)
4309
+ if (idx < 0) idx = 0;
4310
+ // Clamp to valid range
4311
+ if (idx >= metadata.cols) idx = metadata.cols - 1;
4251
4312
 
4252
4313
  // Compute relative selection in cell
4253
4314
  let baseOffset = 0;
@@ -4314,7 +4375,21 @@ exports.aceInitialized = (h, ctx) => {
4314
4375
  }
4315
4376
 
4316
4377
  // Post-composition orphan detection and repair using snapshot
4378
+ // Capture timestamp for staleness check
4379
+ const orphanRepairScheduledAt = Date.now();
4317
4380
  setTimeout(() => {
4381
+ // GUARD: Skip if a new composition started since we scheduled this
4382
+ // This prevents concurrent document modifications during rapid typing
4383
+ if (__epDT_compositionActive) {
4384
+ console.debug('[ep_data_tables:compositionend-orphan-repair] skipped - new composition active');
4385
+ return;
4386
+ }
4387
+ // Also skip if another composition just ended (within 30ms)
4388
+ if (Date.now() - __epDT_lastCompositionEndTime < 30 && __epDT_lastCompositionEndTime > orphanRepairScheduledAt) {
4389
+ console.debug('[ep_data_tables:compositionend-orphan-repair] skipped - very recent composition end');
4390
+ return;
4391
+ }
4392
+
4318
4393
  // Pre-check editor state
4319
4394
  // (keyToNodeMap.get(...) is undefined error)
4320
4395
  try {
@@ -4686,7 +4761,10 @@ exports.aceInitialized = (h, ctx) => {
4686
4761
  }
4687
4762
  }, 50); // Small delay to let Etherpad process the cell edit first
4688
4763
 
4764
+ // Reset composition state but PRESERVE _expectedCursor for next compositionstart
4765
+ const preservedCursor = desktopComposition._expectedCursor;
4689
4766
  desktopComposition = { active: false, start: null, end: null, lineNum: null, cellIndex: -1, snapshot: null, snapshotMeta: null };
4767
+ if (preservedCursor) desktopComposition._expectedCursor = preservedCursor;
4690
4768
  }, 'tableDesktopCompositionEnd');
4691
4769
  } catch (compositionErr) {
4692
4770
  console.error(`${compLogPrefix} ERROR during desktop composition repair:`, compositionErr);