codex-overleaf-link 2.3.0 → 2.3.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.
Files changed (50) hide show
  1. package/README.md +23 -23
  2. package/extension/bootstrap/manifest.template.json +1 -1
  3. package/extension/src/background.js +20 -0
  4. package/extension/src/content/assetTransferBroker.js +169 -0
  5. package/extension/src/content/composerPanel.js +0 -2
  6. package/extension/src/content/contentRuntime.js +135 -111
  7. package/extension/src/content/generated/content.bundle.js +1066 -345
  8. package/extension/src/content/generated/content.bundle.meta.json +7 -3
  9. package/extension/src/content/legacyGlobalRegistry.js +3 -0
  10. package/extension/src/content/markdownText.js +12 -10
  11. package/extension/src/content/mathText.js +155 -9
  12. package/extension/src/content/modelPicker.js +2 -2
  13. package/extension/src/content/modelPickerSupport.js +3 -3
  14. package/extension/src/content/nativeCompatibilityController.js +2 -2
  15. package/extension/src/content/otWarmMirror.js +1 -1
  16. package/extension/src/content/pageBridgeClient.js +1 -0
  17. package/extension/src/content/runResultActions.js +114 -0
  18. package/extension/src/content/runTimelineView.js +23 -0
  19. package/extension/src/content/sessionForkController.js +120 -0
  20. package/extension/src/content/writebackController.js +3 -0
  21. package/extension/src/content/writebackOrchestrator.js +5 -50
  22. package/extension/src/page/binaryAssetUploader.js +343 -0
  23. package/extension/src/page/trackedChangesLifecycle.js +12 -4
  24. package/extension/src/page/writebackRouter.js +6 -2
  25. package/extension/src/pageBridge.js +30 -1
  26. package/extension/src/shared/agentTranscript.js +10 -3
  27. package/extension/src/shared/compatibility.js +4 -2
  28. package/extension/src/shared/i18n.js +18 -4
  29. package/extension/src/shared/models.js +13 -2
  30. package/extension/src/shared/pageRpcContract.js +6 -2
  31. package/extension/src/shared/runExecutionSnapshot.js +14 -4
  32. package/extension/src/shared/runExecutionSnapshotCodec.js +5 -2
  33. package/extension/src/shared/scopedPersistenceQueuePolicy.js +20 -1
  34. package/extension/src/shared/scopedPersistenceReceipt.js +60 -0
  35. package/extension/src/shared/scopedPersistenceTransaction.js +50 -47
  36. package/extension/src/shared/sessionState.js +58 -15
  37. package/extension/src/shared/storageDb.js +6 -5
  38. package/extension/src/shared/storageRunActions.js +11 -1
  39. package/extension/src/shared/writebackOperationEvidence.js +16 -6
  40. package/extension/src/shared/writebackSettlement.js +28 -6
  41. package/extension/styles/panel.css +79 -8
  42. package/native-host/src/codexModels.js +6 -2
  43. package/native-host/src/codexPromptAssembly.js +1 -1
  44. package/native-host/src/codexSessionRunner.js +5 -1
  45. package/native-host/src/codexThreadFork.js +103 -0
  46. package/native-host/src/mirrorWorkspace.js +5 -19
  47. package/native-host/src/nativeAssetTransfer.js +183 -0
  48. package/native-host/src/taskRunnerRuntime.js +33 -81
  49. package/native-host/src/updateManager.js +36 -1
  50. package/package.json +1 -1
@@ -22,6 +22,7 @@
22
22
  let projectSnapshotBridge = null;
23
23
  let writebackRouter = null;
24
24
  let writeGuard = null;
25
+ let binaryAssetUploader = null;
25
26
  const saveState = requirePageModule('CodexOverleafSaveState').create({
26
27
  delay,
27
28
  detectEditor,
@@ -66,6 +67,12 @@
66
67
  treeOperations,
67
68
  window
68
69
  });
70
+ binaryAssetUploader = requirePageModule('CodexOverleafBinaryAssetUploader').create({
71
+ document,
72
+ snapshotRouter,
73
+ treeOperations,
74
+ window
75
+ });
69
76
  writeGuard = requirePageModule('CodexOverleafWriteGuard').create({
70
77
  document,
71
78
  treeOperations,
@@ -207,6 +214,10 @@
207
214
  requireEditing: params.requireEditing === true,
208
215
  runProjectId: typeof params.runProjectId === 'string' ? params.runProjectId : ''
209
216
  }),
217
+ binaryUploadBegin: params => binaryAssetUploader.begin(params),
218
+ binaryUploadAppend: params => binaryAssetUploader.append(params),
219
+ binaryUploadCommit: params => binaryAssetUploader.commit(params),
220
+ binaryUploadAbort: params => binaryAssetUploader.abort(params),
210
221
  jumpToPosition,
211
222
  rejectTrackedChanges,
212
223
  acceptTrackedChanges,
@@ -613,7 +624,7 @@
613
624
  return range;
614
625
  }
615
626
 
616
- const focused = editorAdapter.focusActiveEditorRange(range.from, range.to);
627
+ let focused = editorAdapter.focusActiveEditorRange(range.from, range.to);
617
628
  if (!focused.ok) {
618
629
  return {
619
630
  ...focused,
@@ -621,6 +632,24 @@
621
632
  };
622
633
  }
623
634
 
635
+ if (params.line !== undefined) {
636
+ // Overleaf can publish the new file path and text before its unstable
637
+ // store swaps in the final CodeMirror view. Re-resolve the editor after
638
+ // two short settlement windows so a first dispatch to the retiring view
639
+ // cannot leave the visible cursor at its old line. Repeating an already
640
+ // successful selection is idempotent.
641
+ for (let attempt = 0; attempt < 2; attempt += 1) {
642
+ await delay(120);
643
+ if (getActiveFilePath() !== filePath) {
644
+ break;
645
+ }
646
+ const settled = editorAdapter.focusActiveEditorRange(range.from, range.to);
647
+ if (settled.ok) {
648
+ focused = settled;
649
+ }
650
+ }
651
+ }
652
+
624
653
  return {
625
654
  ok: true,
626
655
  method: focused.method,
@@ -727,10 +727,17 @@
727
727
  if (providerError) {
728
728
  return providerError;
729
729
  }
730
- if (/Mode must be "confirm" or "auto"/i.test(text)) {
730
+ if (/suggest_mode_removed|Suggest mode has been removed/i.test(text)) {
731
+ return {
732
+ conclusion: textFor(locale, '建议修改模式已经移除。', 'Suggest mode has been removed.'),
733
+ nextStep: textFor(locale, '请刷新扩展,然后选择“只问不改”或“自动写入”。', 'Reload the extension, then choose Ask or Auto.'),
734
+ failureCode: 'suggest_mode_removed'
735
+ };
736
+ }
737
+ if (/Mode must be "(?:confirm|ask)" or "auto"/i.test(text)) {
731
738
  return {
732
739
  conclusion: textFor(locale, '这轮没有写入:当前是“只问不改”,但这个任务需要写入权限。', 'No files were written: this task needs write access, but the current mode is Ask.'),
733
- nextStep: textFor(locale, '请切换到“建议修改”或“自动写入”后重新运行。', 'Switch to Suggest or Auto and run the task again.'),
740
+ nextStep: textFor(locale, '请切换到“自动写入”后重新运行。', 'Switch to Auto and run the task again.'),
734
741
  failureCode: null // no direct catalog match — preflight / governance
735
742
  };
736
743
  }
@@ -797,7 +804,7 @@
797
804
  if (/checkpoint/i.test(text)) {
798
805
  return {
799
806
  conclusion: textFor(locale, '这轮没有自动写入:Codex 没有拿到可恢复版本。', 'This run did not auto-write: Codex did not get a recoverable version.'),
800
- nextStep: textFor(locale, '请切换到“建议修改”,或确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Switch to Suggest, or confirm Overleaf Reviewing is enabled before using Auto.'),
807
+ nextStep: textFor(locale, '请确认 Overleaf Reviewing 已开启后再用“自动写入”。', 'Confirm that Overleaf Reviewing is enabled before using Auto.'),
801
808
  failureCode: null // no current catalog code; preflight reviewing issue
802
809
  };
803
810
  }
@@ -12,7 +12,7 @@
12
12
  const MIN_NATIVE_VERSION = '1.0.0';
13
13
  const MIN_COMPATIBLE_NATIVE_VERSION = '1.0.0';
14
14
  const MIN_COMPATIBLE_EXTENSION_VERSION = '1.0.0';
15
- const BUILD_TARGET_VERSION = '2.3.0';
15
+ const BUILD_TARGET_VERSION = '2.3.2';
16
16
  const DEFAULT_CHROME_EXTENSION_ID = 'illdpneeeopfffmiepaejglgmhpmdhdc';
17
17
  const REQUIRED_CAPABILITIES = Object.freeze([
18
18
  'bridgePing',
@@ -26,7 +26,9 @@
26
26
  'historyClearPlugin',
27
27
  'localSkills',
28
28
  'mirrorSensitiveScan',
29
- 'providerProfiles'
29
+ 'providerProfiles',
30
+ 'assetTransfer',
31
+ 'threadFork'
30
32
  ]);
31
33
  const UPDATE_AVAILABLE_CAPABILITIES = Object.freeze([
32
34
  'bridgePing',
@@ -94,6 +94,14 @@
94
94
  deleteSessionHistoryFailedToast: 'Deleted this plugin session, but the matching plugin-local Codex thread history was not fully cleared: {message}',
95
95
  deleteSessionNoThreadToast: 'Deleted this plugin session. This old session had no identifiable local Codex thread history, so no local history was cleared.',
96
96
  deleteSessionDoneToast: 'Deleted this plugin session.',
97
+ forkRunTitle: 'Fork a new conversation from this turn',
98
+ forkRunUnavailable: 'This turn cannot be forked because its Codex thread position is unavailable.',
99
+ forkRunFailed: 'Could not fork this conversation.',
100
+ forkRunDone: 'Forked conversation created.',
101
+ forkSessionTitle: '{title} (fork)',
102
+ copyConclusionTitle: 'Copy conclusion',
103
+ copyConclusionDone: 'Conclusion copied',
104
+ copyConclusionFailed: 'Could not copy conclusion',
97
105
  modeSwitchFailedToast: 'Could not switch mode: {message}',
98
106
  diffAccepted: 'Accepted',
99
107
  diffRejected: 'Rejected',
@@ -266,8 +274,6 @@
266
274
  writeMode: 'Write mode',
267
275
  modeAsk: 'Ask',
268
276
  modeAskTitle: 'Read and analyze only. Do not write to Overleaf.',
269
- modeConfirm: 'Suggest',
270
- modeConfirmTitle: 'Show a change plan first, then write after approval.',
271
277
  modeAuto: 'Auto',
272
278
  modeAutoTitle: 'Write directly after authorization. Deletes still require confirmation.',
273
279
  addContext: 'Add @ context',
@@ -403,6 +409,7 @@
403
409
  refreshProbeDone: 'Refreshed: {status}',
404
410
  refreshProbeFailed: 'Refresh failed. Reload Overleaf and try again.',
405
411
  processedFailed: 'Failed {elapsed}',
412
+ processedCancelled: 'Cancelled {elapsed}',
406
413
  processing: 'Running {elapsed}',
407
414
  processed: 'Done {elapsed}',
408
415
  restoredRunStoppedTitle: 'Stopped tracking this run after a page refresh',
@@ -607,6 +614,14 @@
607
614
  deleteSessionHistoryFailedToast: '已删除这个插件会话,但对应的插件本地 Codex 线程历史没有清理完成:{message}',
608
615
  deleteSessionNoThreadToast: '已删除这个插件会话;这个旧会话没有可识别的本地 Codex 线程历史,未清理本地历史。',
609
616
  deleteSessionDoneToast: '已删除这个插件会话。',
617
+ forkRunTitle: '从此轮分叉一个新对话',
618
+ forkRunUnavailable: '该轮缺少可定位的 Codex thread 节点,无法分叉。',
619
+ forkRunFailed: '无法分叉此对话。',
620
+ forkRunDone: '已创建分叉对话。',
621
+ forkSessionTitle: '{title}(分叉)',
622
+ copyConclusionTitle: '复制结论',
623
+ copyConclusionDone: '已复制结论',
624
+ copyConclusionFailed: '无法复制结论',
610
625
  modeSwitchFailedToast: '切换模式失败:{message}',
611
626
  diffAccepted: '已接受',
612
627
  diffRejected: '已拒绝',
@@ -779,8 +794,6 @@
779
794
  writeMode: '写入模式',
780
795
  modeAsk: '只问不改',
781
796
  modeAskTitle: '只读取和分析,不写入 Overleaf',
782
- modeConfirm: '建议修改',
783
- modeConfirmTitle: '先给出修改方案,确认后写入',
784
797
  modeAuto: '自动写入',
785
798
  modeAutoTitle: '授权后直接写入,删除仍需确认',
786
799
  addContext: '添加 @ 上下文',
@@ -916,6 +929,7 @@
916
929
  refreshProbeDone: '已重新检测:{status}',
917
930
  refreshProbeFailed: '检测失败:请刷新 Overleaf 页面后重试',
918
931
  processedFailed: '处理失败 {elapsed}',
932
+ processedCancelled: '已取消 {elapsed}',
919
933
  processing: '处理中 {elapsed}',
920
934
  processed: '已处理 {elapsed}',
921
935
  restoredRunStoppedTitle: '页面刷新后已停止跟踪这轮任务',
@@ -62,13 +62,13 @@
62
62
  const normalized = {
63
63
  id,
64
64
  label,
65
- reasoningEfforts: Array.isArray(model?.reasoningEfforts) ? model.reasoningEfforts.slice() : [],
65
+ reasoningEfforts: normalizeReasoningEfforts(model?.reasoningEfforts),
66
66
  reasoningPresentation: typeof model?.reasoningPresentation === 'string' ? model.reasoningPresentation : '',
67
67
  speedTiers: normalizeSpeedTiers(model?.speedTiers)
68
68
  };
69
69
 
70
70
  if (Object.prototype.hasOwnProperty.call(Object(model), 'defaultReasoningEffort')) {
71
- normalized.defaultReasoningEffort = model.defaultReasoningEffort;
71
+ normalized.defaultReasoningEffort = normalizeReasoningEffort(model.defaultReasoningEffort);
72
72
  }
73
73
  if (Object.prototype.hasOwnProperty.call(Object(model), 'defaultSpeedTier')) {
74
74
  normalized.defaultSpeedTier = model.defaultSpeedTier;
@@ -77,6 +77,17 @@
77
77
  return normalized;
78
78
  }
79
79
 
80
+ function normalizeReasoningEfforts(reasoningEfforts) {
81
+ if (!Array.isArray(reasoningEfforts)) {
82
+ return [];
83
+ }
84
+ return Array.from(new Set(reasoningEfforts.map(normalizeReasoningEffort).filter(Boolean)));
85
+ }
86
+
87
+ function normalizeReasoningEffort(reasoningEffort) {
88
+ return normalizeModelId(reasoningEffort);
89
+ }
90
+
80
91
  function normalizeSpeedTiers(speedTiers) {
81
92
  if (!Array.isArray(speedTiers)) {
82
93
  return ['standard'];
@@ -5,7 +5,7 @@
5
5
  })(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this), function () {
6
6
  'use strict';
7
7
 
8
- var REVISION = '2026-07-26-page-rpc-contract-v13';
8
+ var REVISION = '2026-08-04-page-rpc-contract-v15';
9
9
  var METHODS = freezeCatalog({
10
10
  initializeCapability: entry('bridge.initialize', 'control', 'none', 'default', false, 'none'),
11
11
  probe: entry('bridge.probe', 'read', 'none', 'default', false, 'safe'),
@@ -17,6 +17,10 @@
17
17
  ensureReviewing: entry('reviewing.enable', 'page_state', 'optional', 'default', true, 'idempotent'),
18
18
  ensureEditing: entry('reviewing.disable', 'page_state', 'optional', 'default', true, 'idempotent'),
19
19
  applyOperations: entry('document.write', 'document', 'required', 'writeback', true, 'no_retry'),
20
+ binaryUploadBegin: entry('asset.stage', 'cache', 'required', 'default', false, 'no_retry'),
21
+ binaryUploadAppend: entry('asset.stage', 'cache', 'none', 'writeback', true, 'no_retry'),
22
+ binaryUploadCommit: entry('asset.write', 'document', 'required', 'writeback', true, 'no_retry'),
23
+ binaryUploadAbort: entry('asset.stage', 'cache', 'none', 'default', false, 'idempotent'),
20
24
  jumpToPosition: entry('editor.navigate', 'navigation', 'required', 'default', false, 'idempotent'),
21
25
  rejectTrackedChanges: entry('tracked_changes.reject', 'document', 'required', 'lifecycle', true, 'no_retry'),
22
26
  acceptTrackedChanges: entry('tracked_changes.accept', 'document', 'required', 'lifecycle', true, 'no_retry'),
@@ -67,7 +71,7 @@
67
71
  file_list: 35000,
68
72
  compile: 75000,
69
73
  lifecycle: 120000,
70
- writeback: 30000
74
+ writeback: 45000
71
75
  };
72
76
  var candidate = Number(overrides[timeoutClass]);
73
77
  return Number.isFinite(candidate) && candidate > 0 ? candidate : defaults[timeoutClass];
@@ -28,7 +28,10 @@
28
28
  : rawQueueItem;
29
29
  const persisted = rawQueueItem?.executionSnapshot || rawPayload?.executionSnapshot;
30
30
  if (persisted && typeof persisted === 'object') {
31
- return Codec.normalizeSnapshot(persisted, { source: persisted.source || 'submitted' });
31
+ return Codec.normalizeSnapshot(persisted, {
32
+ source: persisted.source || 'submitted',
33
+ migrateLegacyConfirm: true
34
+ });
32
35
  }
33
36
  const raw = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
34
37
  const fallback = fallbackInput && typeof fallbackInput === 'object' ? fallbackInput : {};
@@ -48,13 +51,17 @@
48
51
  inferred = true;
49
52
  }
50
53
  return Codec.normalizeSnapshot(captured, {
51
- source: inferred ? 'legacy-inferred' : 'legacy-captured'
54
+ source: inferred ? 'legacy-inferred' : 'legacy-captured',
55
+ migrateLegacyConfirm: true
52
56
  });
53
57
  }
54
58
 
55
59
  function resolveForExecution(value = {}, options = {}) {
56
60
  const persisted = value.executionSnapshot || value;
57
- const snapshot = Codec.normalizeSnapshot(persisted, { source: persisted.source || 'submitted' });
61
+ const snapshot = Codec.normalizeSnapshot(persisted, {
62
+ source: persisted.source || 'submitted',
63
+ migrateLegacyConfirm: true
64
+ });
58
65
  if (snapshot.providerId === 'builtin') return Codec.normalizeSnapshot(snapshot, { source: snapshot.source });
59
66
  const catalog = options.catalog || {};
60
67
  const provider = ProviderProfiles?.getProviderById
@@ -80,7 +87,10 @@
80
87
 
81
88
  function snapshotOf(value = {}) {
82
89
  const persisted = value.executionSnapshot || value;
83
- return Codec.normalizeSnapshot(persisted, { source: persisted.source || 'submitted' });
90
+ return Codec.normalizeSnapshot(persisted, {
91
+ source: persisted.source || 'submitted',
92
+ migrateLegacyConfirm: true
93
+ });
84
94
  }
85
95
 
86
96
  function cloneSnapshot(value = {}) {
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  const SCHEMA_VERSION = 1;
11
- const VALID_MODES = new Set(['ask', 'confirm', 'auto']);
11
+ const VALID_MODES = new Set(['ask', 'auto']);
12
12
  const VALID_SOURCES = new Set(['submitted', 'legacy-captured', 'legacy-inferred']);
13
13
  const TUPLE_FIELDS = Object.freeze([
14
14
  'mode', 'providerId', 'providerRevision', 'model', 'reasoningEffort',
@@ -19,12 +19,15 @@
19
19
  function normalizeSnapshot(value = {}, options = {}) {
20
20
  const input = value && typeof value === 'object' ? value : {};
21
21
  assertNoSecretFields(input);
22
+ if (input.mode === 'confirm' && options.migrateLegacyConfirm !== true) {
23
+ throw snapshotError('suggest_mode_removed', 'Suggest mode has been removed. Choose Ask or Auto.');
24
+ }
22
25
  const providerId = normalizeText(input.providerId) || 'builtin';
23
26
  const revision = normalizeRevisionResult(input.providerRevision);
24
27
  if (!revision.ok) throw snapshotError('provider_revision_conflict', revision.error);
25
28
  const snapshot = {
26
29
  schemaVersion: SCHEMA_VERSION,
27
- mode: VALID_MODES.has(input.mode) ? input.mode : 'ask',
30
+ mode: input.mode === 'auto' ? 'auto' : 'ask',
28
31
  providerId,
29
32
  providerRevision: providerId === 'builtin' ? '' : revision.value,
30
33
  model: normalizeText(input.model).slice(0, 160),
@@ -35,8 +35,27 @@
35
35
  }
36
36
 
37
37
  function normalizeMeta(value) {
38
+ var revision = normalizeRevision(value && value.revision);
39
+ var hasDurableRevision = Boolean(value
40
+ && Object.prototype.hasOwnProperty.call(value, 'durableRevision'));
41
+ var durableRevision = hasDurableRevision
42
+ ? Math.min(normalizeRevision(value.durableRevision), revision)
43
+ : revision;
44
+ var pendingValue = value && value.pendingCommit;
45
+ var pendingRevision = normalizeRevision(pendingValue && pendingValue.revision);
46
+ var pendingCommit = pendingValue && typeof pendingValue === 'object'
47
+ && pendingRevision > durableRevision
48
+ && pendingRevision <= revision
49
+ ? {
50
+ revision: pendingRevision,
51
+ writerId: cleanPart(pendingValue.writerId),
52
+ startedAt: cleanPart(pendingValue.startedAt)
53
+ }
54
+ : null;
38
55
  return {
39
- revision: normalizeRevision(value && value.revision),
56
+ revision: revision,
57
+ durableRevision: durableRevision,
58
+ pendingCommit: pendingCommit,
40
59
  queueRevision: normalizeRevision(value && value.queueRevision),
41
60
  writerId: cleanPart(value && value.writerId),
42
61
  updatedAt: cleanPart(value && value.updatedAt),
@@ -0,0 +1,60 @@
1
+ (function (root, factory) {
2
+ if (typeof module === 'object' && module.exports) {
3
+ module.exports = factory();
4
+ } else {
5
+ root.CodexOverleafModuleRegistry.define('ScopedPersistenceReceipt', [], factory);
6
+ }
7
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function () {
8
+ 'use strict';
9
+
10
+ function createFenceMeta(meta, currentMeta, writerId) {
11
+ var startedAt = new Date().toISOString();
12
+ var revision = currentMeta.revision + 1;
13
+ return {
14
+ ...meta,
15
+ revision: revision,
16
+ durableRevision: currentMeta.durableRevision,
17
+ pendingCommit: { revision: revision, writerId: writerId, startedAt: startedAt },
18
+ writerId: writerId,
19
+ updatedAt: startedAt
20
+ };
21
+ }
22
+
23
+ function createCommittedMeta(fenceMeta) {
24
+ return {
25
+ ...fenceMeta,
26
+ durableRevision: fenceMeta.revision,
27
+ pendingCommit: null,
28
+ updatedAt: new Date().toISOString()
29
+ };
30
+ }
31
+
32
+ function createRecoveredMeta(currentMeta, fenceMeta, writerId) {
33
+ var revision = fenceMeta.revision + 1;
34
+ return {
35
+ ...currentMeta,
36
+ revision: revision,
37
+ durableRevision: revision,
38
+ pendingCommit: null,
39
+ queueRevision: fenceMeta.queueRevision + 1,
40
+ writerId: writerId,
41
+ updatedAt: new Date().toISOString()
42
+ };
43
+ }
44
+
45
+ function decorateReceiptError(error, revision) {
46
+ if (error && typeof error === 'object') {
47
+ error.code = error.code || 'persistence_receipt_write_failed';
48
+ error.persistenceActionCommitted = true;
49
+ error.persistenceRevision = revision;
50
+ }
51
+ return error;
52
+ }
53
+
54
+ return Object.freeze({
55
+ createFenceMeta: createFenceMeta,
56
+ createCommittedMeta: createCommittedMeta,
57
+ createRecoveredMeta: createRecoveredMeta,
58
+ decorateReceiptError: decorateReceiptError
59
+ });
60
+ });
@@ -2,15 +2,17 @@
2
2
  if (typeof module === 'object' && module.exports) {
3
3
  module.exports = factory(
4
4
  require('./scopedPersistenceQueuePolicy'),
5
- require('./scopedPersistenceBrowserAdapter')
5
+ require('./scopedPersistenceBrowserAdapter'),
6
+ require('./scopedPersistenceReceipt')
6
7
  );
7
8
  } else {
8
9
  root.CodexOverleafModuleRegistry.define('ScopedPersistenceTransaction', [
9
10
  'ScopedPersistenceQueuePolicy',
10
- 'ScopedPersistenceBrowserAdapter'
11
+ 'ScopedPersistenceBrowserAdapter',
12
+ 'ScopedPersistenceReceipt'
11
13
  ], factory);
12
14
  }
13
- })(typeof globalThis !== 'undefined' ? globalThis : this, function (QueuePolicy, BrowserAdapter) {
15
+ })(typeof globalThis !== 'undefined' ? globalThis : this, function (QueuePolicy, BrowserAdapter, Receipt) {
14
16
  'use strict';
15
17
 
16
18
  var applyQueueMutations = QueuePolicy.applyQueueMutations;
@@ -43,9 +45,9 @@
43
45
  var scope = normalizeScope(inputScope);
44
46
  generation += 1;
45
47
  var tokenGeneration = generation;
46
- active = makeToken(scope, tokenGeneration, 0, false, 0);
48
+ active = tokenFromMeta(scope, tokenGeneration, false, {});
47
49
  var meta = await strictRead(adapter, scope);
48
- var token = makeToken(scope, tokenGeneration, meta.revision, false, meta.queueRevision);
50
+ var token = tokenFromMeta(scope, tokenGeneration, false, meta);
49
51
  if (active && active.generation === tokenGeneration && sameScope(active, token)) active = token;
50
52
  return token;
51
53
  }
@@ -60,10 +62,10 @@
60
62
  });
61
63
  }
62
64
  if (!detached && active && sameScope(active, scope)) {
63
- return makeToken(scope, active.generation, active.revision, false, active.queueRevision);
65
+ return tokenFromMeta(scope, active.generation, false, active);
64
66
  }
65
67
  var meta = await strictRead(adapter, scope);
66
- return makeToken(scope, detached ? 0 : generation, meta.revision, detached, meta.queueRevision);
68
+ return tokenFromMeta(scope, detached ? 0 : generation, detached, meta);
67
69
  }
68
70
 
69
71
  function isCurrent(token) {
@@ -94,13 +96,7 @@
94
96
  if (queueMutations.length
95
97
  && currentMeta.queueRevision !== normalizeRevision(token.queueRevision)) {
96
98
  if (!token.detached && isCurrent(token)) {
97
- active = makeToken(
98
- scope,
99
- token.generation,
100
- currentMeta.revision,
101
- false,
102
- currentMeta.queueRevision
103
- );
99
+ active = tokenFromMeta(scope, token.generation, false, currentMeta);
104
100
  }
105
101
  return {
106
102
  ok: false,
@@ -112,33 +108,26 @@
112
108
  }
113
109
  var projected = applyQueueMutations(currentMeta, queueMutations, writerId);
114
110
  if (!projected.ok) return projected;
115
- var nextMeta = {
111
+ var nextMeta = Receipt.createFenceMeta({
116
112
  ...projected.meta,
117
- revision: currentMeta.revision + 1,
118
- writerId: writerId,
119
- updatedAt: new Date().toISOString(),
120
113
  sessionTombstones: mergeTombstones(
121
114
  projected.meta.sessionTombstones,
122
115
  commitOptions && commitOptions.sessionTombstones,
123
116
  200
124
117
  )
125
- };
118
+ }, currentMeta, writerId);
126
119
  await adapter.write(scope, nextMeta);
127
120
  if (!token.detached && isCurrent(token)) {
128
- active = makeToken(
129
- scope,
130
- token.generation,
131
- nextMeta.revision,
132
- false,
133
- nextMeta.queueRevision
134
- );
121
+ active = tokenFromMeta(scope, token.generation, false, nextMeta);
135
122
  }
136
123
  var value;
137
124
  try {
138
125
  value = await action({
139
126
  scope: scope,
140
127
  baseRevision: currentMeta.revision,
128
+ durableRevision: currentMeta.durableRevision,
141
129
  conflict: conflict,
130
+ previousCommitIncomplete: Boolean(currentMeta.pendingCommit),
142
131
  writerId: writerId,
143
132
  currentMeta: currentMeta,
144
133
  nextMeta: nextMeta
@@ -147,23 +136,11 @@
147
136
  if (queueMutations.some(function (mutation) {
148
137
  return ['remove', 'release', 'restore'].includes(cleanPart(mutation.type));
149
138
  })) {
150
- var recoveredMeta = {
151
- ...currentMeta,
152
- revision: nextMeta.revision + 1,
153
- queueRevision: nextMeta.queueRevision + 1,
154
- writerId: writerId,
155
- updatedAt: new Date().toISOString()
156
- };
139
+ var recoveredMeta = Receipt.createRecoveredMeta(currentMeta, nextMeta, writerId);
157
140
  try {
158
141
  await adapter.write(scope, recoveredMeta);
159
142
  if (!token.detached && isCurrent(token)) {
160
- active = makeToken(
161
- scope,
162
- token.generation,
163
- recoveredMeta.revision,
164
- false,
165
- recoveredMeta.queueRevision
166
- );
143
+ active = tokenFromMeta(scope, token.generation, false, recoveredMeta);
167
144
  }
168
145
  } catch (recoveryError) {
169
146
  if (error && typeof error === 'object') {
@@ -173,13 +150,24 @@
173
150
  }
174
151
  throw error;
175
152
  }
153
+ var committedMeta = Receipt.createCommittedMeta(nextMeta);
154
+ try {
155
+ await adapter.write(scope, committedMeta);
156
+ } catch (error) {
157
+ throw Receipt.decorateReceiptError(error, nextMeta.revision);
158
+ }
159
+ if (!token.detached && isCurrent(token)) {
160
+ active = tokenFromMeta(scope, token.generation, false, committedMeta);
161
+ }
176
162
  return {
177
163
  ok: true,
178
164
  value: value,
179
- revision: nextMeta.revision,
180
- queueRevision: nextMeta.queueRevision,
181
- queueClaims: nextMeta.queueClaims,
165
+ revision: committedMeta.revision,
166
+ durableRevision: committedMeta.durableRevision,
167
+ queueRevision: committedMeta.queueRevision,
168
+ queueClaims: committedMeta.queueClaims,
182
169
  rebased: conflict,
170
+ recoveredIncompleteCommit: Boolean(currentMeta.pendingCommit),
183
171
  superseded: !token.detached && !isCurrent(token)
184
172
  };
185
173
  });
@@ -190,9 +178,7 @@
190
178
  captureCommitView: captureCommitView,
191
179
  commit: commit,
192
180
  getActiveView: function () {
193
- return active
194
- ? makeToken(active, active.generation, active.revision, false, active.queueRevision)
195
- : null;
181
+ return active ? tokenFromMeta(active, active.generation, false, active) : null;
196
182
  },
197
183
  getWriterId: function () { return writerId; },
198
184
  invalidate: function () {
@@ -203,17 +189,34 @@
203
189
  });
204
190
  }
205
191
 
206
- function makeToken(scope, tokenGeneration, revision, detached, queueRevision) {
192
+ function makeToken(
193
+ scope,
194
+ tokenGeneration,
195
+ revision,
196
+ detached,
197
+ queueRevision,
198
+ durableRevision,
199
+ pendingCommit
200
+ ) {
207
201
  return Object.freeze({
208
202
  accountScopeId: scope.accountScopeId,
209
203
  projectId: scope.projectId,
210
204
  generation: normalizeRevision(tokenGeneration),
211
205
  revision: normalizeRevision(revision),
206
+ durableRevision: normalizeRevision(durableRevision),
207
+ pendingCommit: pendingCommit && typeof pendingCommit === 'object'
208
+ ? Object.freeze({ ...pendingCommit })
209
+ : null,
212
210
  queueRevision: normalizeRevision(queueRevision),
213
211
  detached: detached === true
214
212
  });
215
213
  }
216
214
 
215
+ function tokenFromMeta(scope, tokenGeneration, detached, meta) {
216
+ return makeToken(scope, tokenGeneration, meta.revision, detached, meta.queueRevision,
217
+ meta.durableRevision, meta.pendingCommit);
218
+ }
219
+
217
220
  async function strictRead(adapter, scope) {
218
221
  return normalizeMeta(await adapter.read(scope));
219
222
  }