mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
* [editor-core] share the pieces the editor, real-time and history-v1 all need Project-level history-OT has the browser, real-time and history-v1 all reason about the same changes, so the rules they reason with move into the shared library rather than being reimplemented on each side. - `origin/editor_origin.js` plus `EDITOR_ORIGIN_KIND` and its schema entry: the origin an editor client's change carries, holding the editorId that scopes a change's timestamp. - `change_identity.js`: how a submission is recognised when it comes back, so a resend of a change that already landed is deduplicated rather than applied twice. - `rebase.js`: transforming a change onto concurrent ones, in the same order on both sides, which is what lets the server acknowledge a change with only the version it landed at. - `text_file_defaults.js`: the doc-vs-file extension lists, now read from here by web's settings instead of being spelled out again. - `blob_string_length.js`: the string length of a blob, used by history-v1's blob store and by web when it writes an uploaded file into history. - `file_tree_diff.js`: file-tree operations to a diff, used by project-history's FileTreeDiffGenerator. - `Blob.EMPTY_HASH`, so callers can answer for the empty blob without storing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [project-history] build the file-tree diff with the shared helper FileTreeDiffGenerator's hand-rolled walk over add/edit/move/delete operations becomes a call to `buildFileTreeDiff` from overleaf-editor-core, so the editor and project-history read a file-tree change the same way. Adds the unit tests the decaffeinated original never had. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [web] read the editable-file lists from overleaf-editor-core web's settings spelled out the text extensions and editable filenames that decide whether a file becomes a doc, and every other service that has to make that call carried its own copy. The lists now live in overleaf-editor-core and web builds settings.textExtensions and settings.editableFilenames from them, keeping the ADDITIONAL_TEXT_EXTENSIONS override for Server CE/Pro on top. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * [history-v1] read a blob's string length from overleaf-editor-core Two copies of getStringLengthOfFile lived here, the one in recover_zip.js carrying a TODO asking for exactly this. Both now import the shared implementation, which the blob store keeps re-exporting for backupArchiver. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> GitOrigin-RevId: 31948332f416f56848e3b0dfb5142a4c9f93f508
109 lines
3.8 KiB
JavaScript
109 lines
3.8 KiB
JavaScript
// @ts-check
|
|
|
|
'use strict'
|
|
|
|
const { EDITOR_ORIGIN_KIND } = require('./origin')
|
|
|
|
/**
|
|
* @import { RawChange } from "./types"
|
|
*/
|
|
|
|
/**
|
|
* Identifying a change an editor client submitted.
|
|
*
|
|
* history-v1 uses this to recognise a change a client is resending, so as not to
|
|
* apply the same operations twice; the client uses it to recognise its own change
|
|
* coming back on the change stream, so as not to apply it a second time locally.
|
|
* Both sides have to agree exactly — if one of them compares a field the other
|
|
* does not, they disagree about what is a duplicate, and a change is either
|
|
* applied twice or discarded. Neither reports an error. So the key lives here
|
|
* rather than being spelled out in each service.
|
|
*
|
|
* The fields are the ones a rebase leaves alone: transforming a change rewrites
|
|
* its operations, so those cannot identify it, while the origin, the author and
|
|
* the timestamp travel through history untouched.
|
|
*
|
|
* Each rules out a different false match:
|
|
*
|
|
* - the origin kind keeps other writers out, so a Dropbox sync or a resync is
|
|
* never mistaken for a client's change.
|
|
* - `editorId` separates editors that share an author. Nothing coordinates
|
|
* timestamps between two tabs of one user, or between anonymous editors, who
|
|
* share an empty author — without it, two clients colliding on a millisecond
|
|
* look identical.
|
|
* - the author makes a forged `editorId` unusable: it arrives from the client, but
|
|
* a change belonging to another account can never match.
|
|
* - the timestamp separates successive changes from one editor, which the client
|
|
* keeps distinct and stable across resends.
|
|
*
|
|
* @typedef {{editorId: string, author: (string|null), timestamp: number}} EditorChangeIdentity
|
|
*/
|
|
|
|
/**
|
|
* The identity of a raw change, or null if it is not an identifiable editor
|
|
* change — another writer's, or one whose `editorId` was dropped when its chunk
|
|
* was written.
|
|
*
|
|
* The timestamp is compared as a point in time rather than as a string, so that
|
|
* two spellings of the same instant cannot read as different changes.
|
|
*
|
|
* @param {RawChange} [raw]
|
|
* @return {EditorChangeIdentity | null}
|
|
*/
|
|
function editorChangeIdentity(raw) {
|
|
const origin = raw?.origin
|
|
if (!origin || origin.kind !== EDITOR_ORIGIN_KIND) return null
|
|
|
|
// The origin union's catch-all variant is `{kind: string}`, which also admits
|
|
// this kind, so checking the kind cannot narrow to the variant carrying the id.
|
|
const { editorId } = /** @type {{editorId?: string}} */ (origin)
|
|
if (!editorId) return null
|
|
|
|
// real-time stamps exactly one author on every change it forwards, so anything
|
|
// else did not come from this path.
|
|
const authors = raw.v2Authors
|
|
if (!Array.isArray(authors) || authors.length !== 1) return null
|
|
|
|
const timestamp = new Date(raw.timestamp).getTime()
|
|
if (Number.isNaN(timestamp)) return null
|
|
|
|
return { editorId, author: authors[0] ?? null, timestamp }
|
|
}
|
|
|
|
/**
|
|
* Build an identity from a client's own record of a change it submitted, for
|
|
* comparing against what comes back from history.
|
|
*
|
|
* @param {Object} params
|
|
* @param {string} params.editorId
|
|
* @param {string | null} params.author
|
|
* @param {Date} params.timestamp
|
|
* @return {EditorChangeIdentity}
|
|
*/
|
|
function editorChangeIdentityOf({ editorId, author, timestamp }) {
|
|
return { editorId, author: author ?? null, timestamp: timestamp.getTime() }
|
|
}
|
|
|
|
/**
|
|
* Whether two identities name the same change. A null identity matches nothing,
|
|
* including another null.
|
|
*
|
|
* @param {EditorChangeIdentity | null} [a]
|
|
* @param {EditorChangeIdentity | null} [b]
|
|
* @return {boolean}
|
|
*/
|
|
function isSameEditorChange(a, b) {
|
|
if (!a || !b) return false
|
|
return (
|
|
a.editorId === b.editorId &&
|
|
a.author === b.author &&
|
|
a.timestamp === b.timestamp
|
|
)
|
|
}
|
|
|
|
module.exports = {
|
|
editorChangeIdentity,
|
|
editorChangeIdentityOf,
|
|
isSameEditorChange,
|
|
}
|