mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #19817 from overleaf/jpa-types
[overleaf-editor-core] stronger type checking via web GitOrigin-RevId: 427019f40e2905f2e0ec11dc09f5fccdbb1f905b
This commit is contained in:
parent
bd87e1b41b
commit
3836323724
29 changed files with 470 additions and 84 deletions
|
@ -149,6 +149,7 @@ class Comment {
|
|||
* @returns {CommentRawData}
|
||||
*/
|
||||
toRaw() {
|
||||
/** @type CommentRawData */
|
||||
const raw = {
|
||||
id: this.id,
|
||||
ranges: this.ranges.map(range => range.toRaw()),
|
||||
|
|
|
@ -13,6 +13,8 @@ const StringFileData = require('./file_data/string_file_data')
|
|||
* @typedef {import("./blob")} Blob
|
||||
* @typedef {import("./types").BlobStore} BlobStore
|
||||
* @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore
|
||||
* @typedef {import("./types").RawFileData} RawFileData
|
||||
* @typedef {import("./types").RawFile} RawFile
|
||||
* @typedef {import("./types").StringFileRawData} StringFileRawData
|
||||
* @typedef {import("./types").CommentRawData} CommentRawData
|
||||
* @typedef {import("./file_data/comment_list")} CommentList
|
||||
|
@ -62,9 +64,14 @@ class File {
|
|||
assert.instance(data, FileData, 'File: bad data')
|
||||
|
||||
this.data = data
|
||||
this.metadata = {}
|
||||
this.setMetadata(metadata || {})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawFile} raw
|
||||
* @return {File|null}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
if (!raw) return null
|
||||
return new File(FileData.fromRaw(raw), raw.metadata)
|
||||
|
@ -90,8 +97,8 @@ class File {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {number} [byteLength]
|
||||
* @param {number} [stringLength]
|
||||
* @param {number} byteLength
|
||||
* @param {number?} stringLength
|
||||
* @param {Object} [metadata]
|
||||
* @return {File}
|
||||
*/
|
||||
|
@ -109,7 +116,11 @@ class File {
|
|||
return new File(FileData.createLazyFromBlobs(blob, rangesBlob), metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {RawFile}
|
||||
*/
|
||||
toRaw() {
|
||||
/** @type RawFile */
|
||||
const rawFileData = this.data.toRaw()
|
||||
storeRawMetadata(this.metadata, rawFileData)
|
||||
return rawFileData
|
||||
|
@ -249,15 +260,20 @@ class File {
|
|||
* the hash.
|
||||
*
|
||||
* @param {BlobStore} blobStore
|
||||
* @return {Promise<Object>} a raw HashFile
|
||||
* @return {Promise<RawFile>} a raw HashFile
|
||||
*/
|
||||
async store(blobStore) {
|
||||
/** @type RawFile */
|
||||
const raw = await this.data.store(blobStore)
|
||||
storeRawMetadata(this.metadata, raw)
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} metadata
|
||||
* @param {RawFile} raw
|
||||
*/
|
||||
function storeRawMetadata(metadata, raw) {
|
||||
if (!_.isEmpty(metadata)) {
|
||||
raw.metadata = _.cloneDeep(metadata)
|
||||
|
|
|
@ -5,6 +5,10 @@ const assert = require('check-types').assert
|
|||
const Blob = require('../blob')
|
||||
const FileData = require('./')
|
||||
|
||||
/**
|
||||
* @typedef {import('../types').RawBinaryFileData} RawBinaryFileData
|
||||
*/
|
||||
|
||||
class BinaryFileData extends FileData {
|
||||
/**
|
||||
* @param {string} hash
|
||||
|
@ -21,11 +25,18 @@ class BinaryFileData extends FileData {
|
|||
this.byteLength = byteLength
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawBinaryFileData} raw
|
||||
* @returns {BinaryFileData}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new BinaryFileData(raw.hash, raw.byteLength)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {RawBinaryFileData}
|
||||
*/
|
||||
toRaw() {
|
||||
return { hash: this.hash, byteLength: this.byteLength }
|
||||
}
|
||||
|
@ -60,7 +71,9 @@ class BinaryFileData extends FileData {
|
|||
return FileData.createHollow(this.byteLength, null)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @inheritdoc
|
||||
* @return {Promise<RawFileData>}
|
||||
*/
|
||||
async store() {
|
||||
return { hash: this.hash }
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const FileData = require('./')
|
|||
* @typedef {import('./lazy_string_file_data')} LazyStringFileData
|
||||
* @typedef {import('./hollow_string_file_data')} HollowStringFileData
|
||||
* @typedef {import('../types').BlobStore} BlobStore
|
||||
* @typedef {import('../types').RawHashFileData} RawHashFileData
|
||||
*/
|
||||
|
||||
class HashFileData extends FileData {
|
||||
|
@ -35,8 +36,7 @@ class HashFileData extends FileData {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param {{hash: string, rangesHash?: string}} raw
|
||||
* @returns
|
||||
* @param {RawHashFileData} raw
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new HashFileData(raw.hash, raw.rangesHash)
|
||||
|
@ -44,9 +44,10 @@ class HashFileData extends FileData {
|
|||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {{hash: string, rangesHash?: string}}
|
||||
* @returns {RawHashFileData}
|
||||
*/
|
||||
toRaw() {
|
||||
/** @type RawHashFileData */
|
||||
const raw = { hash: this.hash }
|
||||
if (this.rangesHash) {
|
||||
raw.rangesHash = this.rangesHash
|
||||
|
@ -97,6 +98,8 @@ class HashFileData extends FileData {
|
|||
throw new Error('Failed to look up rangesHash in blobStore')
|
||||
}
|
||||
if (!blob) throw new Error('blob not found: ' + this.hash)
|
||||
// TODO(das7pad): inline 2nd path of FileData.createLazyFromBlobs?
|
||||
// @ts-ignore
|
||||
return FileData.createLazyFromBlobs(blob, rangesBlob)
|
||||
}
|
||||
|
||||
|
@ -110,14 +113,17 @@ class HashFileData extends FileData {
|
|||
if (!blob) {
|
||||
throw new Error('Failed to look up hash in blobStore')
|
||||
}
|
||||
// TODO(das7pad): inline 2nd path of FileData.createHollow?
|
||||
// @ts-ignore
|
||||
return FileData.createHollow(blob.getByteLength(), blob.getStringLength())
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {Promise<{hash: string, rangesHash?: string}>}
|
||||
* @returns {Promise<RawHashFileData>}
|
||||
*/
|
||||
async store() {
|
||||
/** @type RawHashFileData */
|
||||
const raw = { hash: this.hash }
|
||||
if (this.rangesHash) {
|
||||
raw.rangesHash = this.rangesHash
|
||||
|
|
|
@ -4,6 +4,10 @@ const assert = require('check-types').assert
|
|||
|
||||
const FileData = require('./')
|
||||
|
||||
/**
|
||||
* @typedef {import('../types').RawHollowBinaryFileData} RawHollowBinaryFileData
|
||||
*/
|
||||
|
||||
class HollowBinaryFileData extends FileData {
|
||||
/**
|
||||
* @param {number} byteLength
|
||||
|
@ -16,11 +20,18 @@ class HollowBinaryFileData extends FileData {
|
|||
this.byteLength = byteLength
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawHollowBinaryFileData} raw
|
||||
* @returns {HollowBinaryFileData}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new HollowBinaryFileData(raw.byteLength)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {RawHollowBinaryFileData}
|
||||
*/
|
||||
toRaw() {
|
||||
return { byteLength: this.byteLength }
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@ const assert = require('check-types').assert
|
|||
|
||||
const FileData = require('./')
|
||||
|
||||
/**
|
||||
* @typedef {import('../types').RawHollowStringFileData} RawHollowStringFileData
|
||||
*/
|
||||
|
||||
class HollowStringFileData extends FileData {
|
||||
/**
|
||||
* @param {number} stringLength
|
||||
|
@ -24,11 +28,18 @@ class HollowStringFileData extends FileData {
|
|||
this.stringLength = stringLength
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawHollowStringFileData} raw
|
||||
* @returns {HollowStringFileData}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new HollowStringFileData(raw.stringLength)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {RawHollowStringFileData}
|
||||
*/
|
||||
toRaw() {
|
||||
return { stringLength: this.stringLength }
|
||||
}
|
||||
|
|
|
@ -6,18 +6,10 @@ const assert = require('check-types').assert
|
|||
|
||||
const Blob = require('../blob')
|
||||
|
||||
// Dependencies are loaded at the bottom of the file to mitigate circular
|
||||
// dependency
|
||||
let BinaryFileData = null
|
||||
let HashFileData = null
|
||||
let HollowBinaryFileData = null
|
||||
let HollowStringFileData = null
|
||||
let LazyStringFileData = null
|
||||
let StringFileData = null
|
||||
|
||||
/**
|
||||
* @typedef {import("../types").BlobStore} BlobStore
|
||||
* @typedef {import("../types").ReadonlyBlobStore} ReadonlyBlobStore
|
||||
* @typedef {import("../types").RawFileData} RawFileData
|
||||
* @typedef {import("../operation/edit_operation")} EditOperation
|
||||
* @typedef {import("../file_data/comment_list")} CommentList
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
|
@ -29,25 +21,37 @@ let StringFileData = null
|
|||
* should be used only through {@link File}.
|
||||
*/
|
||||
class FileData {
|
||||
/** @see File.fromRaw */
|
||||
/** @see File.fromRaw
|
||||
* @param {RawFileData} raw
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
// TODO(das7pad): can we teach typescript to understand our polymorphism?
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'hash')) {
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'byteLength'))
|
||||
// @ts-ignore
|
||||
return BinaryFileData.fromRaw(raw)
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'stringLength'))
|
||||
// @ts-ignore
|
||||
return LazyStringFileData.fromRaw(raw)
|
||||
// @ts-ignore
|
||||
return HashFileData.fromRaw(raw)
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'byteLength'))
|
||||
// @ts-ignore
|
||||
return HollowBinaryFileData.fromRaw(raw)
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'stringLength'))
|
||||
// @ts-ignore
|
||||
return HollowStringFileData.fromRaw(raw)
|
||||
if (Object.prototype.hasOwnProperty.call(raw, 'content'))
|
||||
// @ts-ignore
|
||||
return StringFileData.fromRaw(raw)
|
||||
throw new Error('FileData: bad raw object ' + JSON.stringify(raw))
|
||||
}
|
||||
|
||||
/** @see File.createHollow */
|
||||
/** @see File.createHollow
|
||||
* @param {number} byteLength
|
||||
* @param {number|null} stringLength
|
||||
*/
|
||||
static createHollow(byteLength, stringLength) {
|
||||
if (stringLength == null) {
|
||||
return new HollowBinaryFileData(byteLength)
|
||||
|
@ -63,15 +67,25 @@ class FileData {
|
|||
static createLazyFromBlobs(blob, rangesBlob) {
|
||||
assert.instance(blob, Blob, 'FileData: bad blob')
|
||||
if (blob.getStringLength() == null) {
|
||||
return new BinaryFileData(blob.getHash(), blob.getByteLength())
|
||||
return new BinaryFileData(
|
||||
// TODO(das7pad): see call-sites
|
||||
// @ts-ignore
|
||||
blob.getHash(),
|
||||
blob.getByteLength()
|
||||
)
|
||||
}
|
||||
return new LazyStringFileData(
|
||||
// TODO(das7pad): see call-sites
|
||||
// @ts-ignore
|
||||
blob.getHash(),
|
||||
rangesBlob?.getHash(),
|
||||
blob.getStringLength()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {RawFileData}
|
||||
*/
|
||||
toRaw() {
|
||||
throw new Error('FileData: toRaw not implemented')
|
||||
}
|
||||
|
@ -184,7 +198,7 @@ class FileData {
|
|||
* @see File#store
|
||||
* @function
|
||||
* @param {BlobStore} blobStore
|
||||
* @return {Promise<Object>} a raw HashFile
|
||||
* @return {Promise<RawFileData>} a raw HashFile
|
||||
* @abstract
|
||||
*/
|
||||
async store(blobStore) {
|
||||
|
@ -216,9 +230,9 @@ class FileData {
|
|||
|
||||
module.exports = FileData
|
||||
|
||||
BinaryFileData = require('./binary_file_data')
|
||||
HashFileData = require('./hash_file_data')
|
||||
HollowBinaryFileData = require('./hollow_binary_file_data')
|
||||
HollowStringFileData = require('./hollow_string_file_data')
|
||||
LazyStringFileData = require('./lazy_string_file_data')
|
||||
StringFileData = require('./string_file_data')
|
||||
const BinaryFileData = require('./binary_file_data')
|
||||
const HashFileData = require('./hash_file_data')
|
||||
const HollowBinaryFileData = require('./hollow_binary_file_data')
|
||||
const HollowStringFileData = require('./hollow_string_file_data')
|
||||
const LazyStringFileData = require('./lazy_string_file_data')
|
||||
const StringFileData = require('./string_file_data')
|
||||
|
|
|
@ -14,6 +14,8 @@ const EditOperationBuilder = require('../operation/edit_operation_builder')
|
|||
* @typedef {import('../types').BlobStore} BlobStore
|
||||
* @typedef {import('../types').ReadonlyBlobStore} ReadonlyBlobStore
|
||||
* @typedef {import('../types').RangesBlob} RangesBlob
|
||||
* @typedef {import('../types').RawFileData} RawFileData
|
||||
* @typedef {import('../types').RawLazyStringFileData} RawLazyStringFileData
|
||||
*/
|
||||
|
||||
class LazyStringFileData extends FileData {
|
||||
|
@ -39,6 +41,10 @@ class LazyStringFileData extends FileData {
|
|||
this.operations = operations || []
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawLazyStringFileData} raw
|
||||
* @returns {LazyStringFileData}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new LazyStringFileData(
|
||||
raw.hash,
|
||||
|
@ -48,9 +54,16 @@ class LazyStringFileData extends FileData {
|
|||
)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {RawLazyStringFileData}
|
||||
*/
|
||||
toRaw() {
|
||||
const raw = { hash: this.hash, stringLength: this.stringLength }
|
||||
/** @type RawLazyStringFileData */
|
||||
const raw = {
|
||||
hash: this.hash,
|
||||
stringLength: this.stringLength,
|
||||
}
|
||||
if (this.rangesHash) {
|
||||
raw.rangesHash = this.rangesHash
|
||||
}
|
||||
|
@ -135,18 +148,26 @@ class LazyStringFileData extends FileData {
|
|||
|
||||
/** @inheritdoc */
|
||||
async toHollow() {
|
||||
// TODO(das7pad): inline 2nd path of FileData.createLazyFromBlobs?
|
||||
// @ts-ignore
|
||||
return FileData.createHollow(null, this.stringLength)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @inheritdoc
|
||||
* @param {EditOperation} operation
|
||||
*/
|
||||
edit(operation) {
|
||||
this.stringLength = operation.applyToLength(this.stringLength)
|
||||
this.operations.push(operation)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @inheritdoc
|
||||
* @param {BlobStore} blobStore
|
||||
* @return {Promise<RawFileData>}
|
||||
*/
|
||||
async store(blobStore) {
|
||||
if (this.operations.length === 0) {
|
||||
/** @type RawFileData */
|
||||
const raw = { hash: this.hash }
|
||||
if (this.rangesHash) {
|
||||
raw.rangesHash = this.rangesHash
|
||||
|
@ -155,6 +176,7 @@ class LazyStringFileData extends FileData {
|
|||
}
|
||||
const eager = await this.toEager(blobStore)
|
||||
this.operations.length = 0
|
||||
/** @type RawFileData */
|
||||
return await eager.store(blobStore)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ const TrackedChangeList = require('./tracked_change_list')
|
|||
|
||||
/**
|
||||
* @typedef {import("../types").StringFileRawData} StringFileRawData
|
||||
* @typedef {import("../types").RawFileData} RawFileData
|
||||
* @typedef {import("../operation/edit_operation")} EditOperation
|
||||
* @typedef {import("../types").BlobStore} BlobStore
|
||||
* @typedef {import("../types").CommentRawData} CommentRawData
|
||||
|
@ -47,6 +48,7 @@ class StringFileData extends FileData {
|
|||
* @returns {StringFileRawData}
|
||||
*/
|
||||
toRaw() {
|
||||
/** @type StringFileRawData */
|
||||
const raw = { content: this.content }
|
||||
|
||||
if (this.comments.length) {
|
||||
|
@ -133,6 +135,7 @@ class StringFileData extends FileData {
|
|||
/**
|
||||
* @inheritdoc
|
||||
* @param {BlobStore} blobStore
|
||||
* @return {Promise<RawFileData>}
|
||||
*/
|
||||
async store(blobStore) {
|
||||
const blob = await blobStore.putString(this.content)
|
||||
|
@ -143,8 +146,12 @@ class StringFileData extends FileData {
|
|||
trackedChanges: this.trackedChanges.toRaw(),
|
||||
}
|
||||
const rangesBlob = await blobStore.putObject(ranges)
|
||||
// TODO(das7pad): Provide interface that guarantees hash exists?
|
||||
// @ts-ignore
|
||||
return { hash: blob.getHash(), rangesHash: rangesBlob.getHash() }
|
||||
}
|
||||
// TODO(das7pad): Provide interface that guarantees hash exists?
|
||||
// @ts-ignore
|
||||
return { hash: blob.getHash() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @ts-check
|
||||
/**
|
||||
* @typedef {import("../types").TrackingPropsRawData} TrackingPropsRawData
|
||||
* @typedef {import("../types").TrackingDirective} TrackingDirective
|
||||
*/
|
||||
|
||||
class TrackingProps {
|
||||
|
@ -48,6 +49,10 @@ class TrackingProps {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TrackingDirective} [other]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (!(other instanceof TrackingProps)) {
|
||||
return false
|
||||
|
|
|
@ -9,9 +9,18 @@ const pMap = require('p-map')
|
|||
const File = require('./file')
|
||||
const safePathname = require('./safe_pathname')
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').RawFile} RawFile
|
||||
* @typedef {import('./types').RawFileMap} RawFileMap
|
||||
* @typedef {Record<String, File | null>} FileMapData
|
||||
*/
|
||||
|
||||
class PathnameError extends OError {}
|
||||
|
||||
class NonUniquePathnameError extends PathnameError {
|
||||
/**
|
||||
* @param {string[]} pathnames
|
||||
*/
|
||||
constructor(pathnames) {
|
||||
super('pathnames are not unique: ' + pathnames, { pathnames })
|
||||
this.pathnames = pathnames
|
||||
|
@ -19,6 +28,9 @@ class NonUniquePathnameError extends PathnameError {
|
|||
}
|
||||
|
||||
class BadPathnameError extends PathnameError {
|
||||
/**
|
||||
* @param {string} pathname
|
||||
*/
|
||||
constructor(pathname) {
|
||||
super(pathname + ' is not a valid pathname', { pathname })
|
||||
this.pathname = pathname
|
||||
|
@ -26,6 +38,9 @@ class BadPathnameError extends PathnameError {
|
|||
}
|
||||
|
||||
class PathnameConflictError extends PathnameError {
|
||||
/**
|
||||
* @param {string} pathname
|
||||
*/
|
||||
constructor(pathname) {
|
||||
super(`pathname '${pathname}' conflicts with another file`, { pathname })
|
||||
this.pathname = pathname
|
||||
|
@ -33,6 +48,9 @@ class PathnameConflictError extends PathnameError {
|
|||
}
|
||||
|
||||
class FileNotFoundError extends PathnameError {
|
||||
/**
|
||||
* @param {string} pathname
|
||||
*/
|
||||
constructor(pathname) {
|
||||
super(`file ${pathname} does not exist`, { pathname })
|
||||
this.pathname = pathname
|
||||
|
@ -70,13 +88,17 @@ class FileMap {
|
|||
constructor(files) {
|
||||
// create bare object for use as Map
|
||||
// http://ryanmorr.com/true-hash-maps-in-javascript/
|
||||
/** @type {Record<String, File | null>} */
|
||||
/** @type FileMapData */
|
||||
this.files = Object.create(null)
|
||||
_.assign(this.files, files)
|
||||
checkPathnamesAreUnique(this.files)
|
||||
checkPathnamesDoNotConflict(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawFileMap} raw
|
||||
* @returns {FileMap}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
assert.object(raw, 'bad raw files')
|
||||
return new FileMap(_.mapValues(raw, File.fromRaw))
|
||||
|
@ -85,12 +107,18 @@ class FileMap {
|
|||
/**
|
||||
* Convert to raw object for serialization.
|
||||
*
|
||||
* @return {Object}
|
||||
* @return {RawFileMap}
|
||||
*/
|
||||
toRaw() {
|
||||
/**
|
||||
* @param {File} file
|
||||
* @return {RawFile}
|
||||
*/
|
||||
function fileToRaw(file) {
|
||||
return file.toRaw()
|
||||
}
|
||||
// TODO(das7pad): refine types to enforce no nulls in FileMapData
|
||||
// @ts-ignore
|
||||
return _.mapValues(this.files, fileToRaw)
|
||||
}
|
||||
|
||||
|
@ -103,6 +131,8 @@ class FileMap {
|
|||
addFile(pathname, file) {
|
||||
checkPathname(pathname)
|
||||
assert.object(file, 'bad file')
|
||||
// TODO(das7pad): make ignoredPathname argument fully optional
|
||||
// @ts-ignore
|
||||
checkNewPathnameDoesNotConflict(this, pathname)
|
||||
addFile(this.files, pathname, file)
|
||||
}
|
||||
|
@ -163,7 +193,7 @@ class FileMap {
|
|||
*/
|
||||
getFile(pathname) {
|
||||
const key = findPathnameKey(this.files, pathname)
|
||||
return key && this.files[key]
|
||||
if (key) return this.files[key]
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -177,7 +207,7 @@ class FileMap {
|
|||
* and MoveFile overwrite existing files.)
|
||||
*
|
||||
* @param {string} pathname
|
||||
* @param {string} [ignoredPathname] pretend this pathname does not exist
|
||||
* @param {string?} ignoredPathname pretend this pathname does not exist
|
||||
*/
|
||||
wouldConflict(pathname, ignoredPathname) {
|
||||
checkPathname(pathname)
|
||||
|
@ -224,7 +254,7 @@ class FileMap {
|
|||
/**
|
||||
* Map the files in this map to new values.
|
||||
* @template T
|
||||
* @param {(file: File | null) => T} iteratee
|
||||
* @param {(file: File | null, path: string) => T} iteratee
|
||||
* @return {Record<String, T>}
|
||||
*/
|
||||
map(iteratee) {
|
||||
|
@ -234,9 +264,10 @@ class FileMap {
|
|||
/**
|
||||
* Map the files in this map to new values asynchronously, with an optional
|
||||
* limit on concurrency.
|
||||
* @param {function} iteratee like for _.mapValues
|
||||
* @template T
|
||||
* @param {(file: File | null | undefined, path: string, pathnames: string[]) => T} iteratee
|
||||
* @param {number} [concurrency]
|
||||
* @return {Promise<Object>}
|
||||
* @return {Promise<Record<String, T>>}
|
||||
*/
|
||||
async mapAsync(iteratee, concurrency) {
|
||||
assert.maybe.number(concurrency, 'bad concurrency')
|
||||
|
@ -253,32 +284,55 @@ class FileMap {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathname0
|
||||
* @param {string?} pathname1
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function pathnamesEqual(pathname0, pathname1) {
|
||||
return pathname0 === pathname1
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileMapData} files
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function pathnamesAreUnique(files) {
|
||||
const keys = _.keys(files)
|
||||
return _.uniqWith(keys, pathnamesEqual).length === keys.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileMapData} files
|
||||
*/
|
||||
function checkPathnamesAreUnique(files) {
|
||||
if (pathnamesAreUnique(files)) return
|
||||
throw new FileMap.NonUniquePathnameError(_.keys(files))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pathname
|
||||
*/
|
||||
function checkPathname(pathname) {
|
||||
assert.nonEmptyString(pathname, 'bad pathname')
|
||||
if (safePathname.isClean(pathname)) return
|
||||
throw new FileMap.BadPathnameError(pathname)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileMap} fileMap
|
||||
* @param {string} pathname
|
||||
* @param {string?} ignoredPathname
|
||||
*/
|
||||
function checkNewPathnameDoesNotConflict(fileMap, pathname, ignoredPathname) {
|
||||
if (fileMap.wouldConflict(pathname, ignoredPathname)) {
|
||||
throw new FileMap.PathnameConflictError(pathname)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileMap} fileMap
|
||||
*/
|
||||
function checkPathnamesDoNotConflict(fileMap) {
|
||||
const pathnames = fileMap.getPathnames()
|
||||
// check pathnames for validity first
|
||||
|
@ -299,18 +353,29 @@ function checkPathnamesDoNotConflict(fileMap) {
|
|||
}
|
||||
}
|
||||
|
||||
//
|
||||
// This function is somewhat vestigial: it was used when this map used
|
||||
// case-insensitive pathname comparison. We could probably simplify some of the
|
||||
// logic in the callers, but in the hope that we will one day return to
|
||||
// case-insensitive semantics, we've just left things as-is for now.
|
||||
//
|
||||
/**
|
||||
* This function is somewhat vestigial: it was used when this map used
|
||||
* case-insensitive pathname comparison. We could probably simplify some of the
|
||||
* logic in the callers, but in the hope that we will one day return to
|
||||
* case-insensitive semantics, we've just left things as-is for now.
|
||||
*
|
||||
* TODO(das7pad): In a followup, inline this function and make types stricter.
|
||||
*
|
||||
* @param {FileMapData} files
|
||||
* @param {string} pathname
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function findPathnameKey(files, pathname) {
|
||||
// we can check for the key without worrying about properties
|
||||
// in the prototype because we are now using a bare object/
|
||||
if (pathname in files) return pathname
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileMapData} files
|
||||
* @param {string} pathname
|
||||
* @param {File?} file
|
||||
*/
|
||||
function addFile(files, pathname, file) {
|
||||
const key = findPathnameKey(files, pathname)
|
||||
if (key) delete files[key]
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
// @ts-check
|
||||
'use strict'
|
||||
|
||||
const assert = require('check-types').assert
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').RawLabel} RawLabel
|
||||
*/
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* A user-configurable label that can be attached to a specific change. Labels
|
||||
|
@ -13,6 +18,9 @@ class Label {
|
|||
/**
|
||||
* @constructor
|
||||
* @param {string} text
|
||||
* @param {number?} authorId
|
||||
* @param {Date} timestamp
|
||||
* @param {number} version
|
||||
*/
|
||||
constructor(text, authorId, timestamp, version) {
|
||||
assert.string(text, 'bad text')
|
||||
|
@ -29,7 +37,7 @@ class Label {
|
|||
/**
|
||||
* Create a Label from its raw form.
|
||||
*
|
||||
* @param {Object} raw
|
||||
* @param {RawLabel} raw
|
||||
* @return {Label}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
|
@ -44,7 +52,7 @@ class Label {
|
|||
/**
|
||||
* Convert the Label to raw form for transmission.
|
||||
*
|
||||
* @return {Object}
|
||||
* @return {RawLabel}
|
||||
*/
|
||||
toRaw() {
|
||||
return {
|
||||
|
@ -81,7 +89,7 @@ class Label {
|
|||
}
|
||||
|
||||
/**
|
||||
* @return {number | undefined}
|
||||
* @return {number}
|
||||
*/
|
||||
getVersion() {
|
||||
return this.version
|
||||
|
|
|
@ -44,6 +44,7 @@ class AddCommentOperation extends EditOperation {
|
|||
* @returns {RawAddCommentOperation}
|
||||
*/
|
||||
toJSON() {
|
||||
/** @type RawAddCommentOperation */
|
||||
const raw = {
|
||||
commentId: this.commentId,
|
||||
ranges: this.ranges.map(range => range.toRaw()),
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
// @ts-check
|
||||
'use strict'
|
||||
/** @typedef {import('./edit_operation')} EditOperation */
|
||||
/**
|
||||
* @typedef {import('./edit_operation')} EditOperation
|
||||
* @typedef {import('../types').RawEditFileOperation} RawEditFileOperation
|
||||
* @typedef {import("../snapshot")} Snapshot
|
||||
*/
|
||||
|
||||
const Operation = require('./')
|
||||
const EditOperationBuilder = require('./edit_operation_builder')
|
||||
|
@ -32,7 +36,7 @@ class EditFileOperation extends Operation {
|
|||
/**
|
||||
* Deserialize an EditFileOperation.
|
||||
*
|
||||
* @param {Object} raw
|
||||
* @param {RawEditFileOperation} raw
|
||||
* @return {EditFileOperation}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
|
@ -52,13 +56,18 @@ class EditFileOperation extends Operation {
|
|||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param {Snapshot} snapshot
|
||||
*/
|
||||
applyTo(snapshot) {
|
||||
// TODO(das7pad): can we teach typescript our polymorphism?
|
||||
// @ts-ignore
|
||||
snapshot.editFile(this.pathname, this.operation)
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param {Operation} other
|
||||
* @return {boolean}
|
||||
*/
|
||||
canBeComposedWithForUndo(other) {
|
||||
return (
|
||||
|
@ -69,6 +78,8 @@ class EditFileOperation extends Operation {
|
|||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param {Operation} other
|
||||
* @return {other is EditFileOperation}
|
||||
*/
|
||||
canBeComposedWith(other) {
|
||||
// Ensure that other operation is an edit file operation
|
||||
|
@ -81,6 +92,7 @@ class EditFileOperation extends Operation {
|
|||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param {EditFileOperation} other
|
||||
*/
|
||||
compose(other) {
|
||||
return new EditFileOperation(
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
const EditOperation = require('./edit_operation')
|
||||
|
||||
/**
|
||||
* @typedef {import('../types').RawEditNoOperation} RawEditNoOperation
|
||||
*/
|
||||
|
||||
class EditNoOperation extends EditOperation {
|
||||
/**
|
||||
* @inheritdoc
|
||||
|
@ -9,7 +13,7 @@ class EditNoOperation extends EditOperation {
|
|||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @returns {object}
|
||||
* @returns {RawEditNoOperation}
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @ts-check
|
||||
/**
|
||||
* @typedef {import('../file_data')} FileData
|
||||
* @typedef {import('../types').RawEditOperation} RawEditOperation
|
||||
*/
|
||||
|
||||
class EditOperation {
|
||||
|
@ -12,7 +13,7 @@ class EditOperation {
|
|||
|
||||
/**
|
||||
* Converts operation into a JSON value.
|
||||
* @returns {object}
|
||||
* @returns {RawEditOperation}
|
||||
*/
|
||||
toJSON() {
|
||||
throw new Error('Abstract method not implemented')
|
||||
|
|
|
@ -142,7 +142,9 @@ class InsertOp extends ScanOp {
|
|||
return current
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @inheritdoc
|
||||
* @param {ScanOp} other
|
||||
*/
|
||||
equals(other) {
|
||||
if (!(other instanceof InsertOp)) {
|
||||
return false
|
||||
|
@ -167,6 +169,10 @@ class InsertOp extends ScanOp {
|
|||
return !other.commentIds
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScanOp} other
|
||||
* @return {other is InsertOp}
|
||||
*/
|
||||
canMergeWith(other) {
|
||||
if (!(other instanceof InsertOp)) {
|
||||
return false
|
||||
|
@ -187,6 +193,9 @@ class InsertOp extends ScanOp {
|
|||
return !other.commentIds
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScanOp} other
|
||||
*/
|
||||
mergeWith(other) {
|
||||
if (!this.canMergeWith(other)) {
|
||||
throw new Error('Cannot merge with incompatible operation')
|
||||
|
@ -202,6 +211,7 @@ class InsertOp extends ScanOp {
|
|||
if (!this.tracking && !this.commentIds) {
|
||||
return this.insertion
|
||||
}
|
||||
/** @type RawInsertOp */
|
||||
const obj = { i: this.insertion }
|
||||
if (this.tracking) {
|
||||
obj.tracking = this.tracking.toRaw()
|
||||
|
@ -274,7 +284,9 @@ class RetainOp extends ScanOp {
|
|||
return new RetainOp(op.r)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @inheritdoc
|
||||
* @param {ScanOp} other
|
||||
*/
|
||||
equals(other) {
|
||||
if (!(other instanceof RetainOp)) {
|
||||
return false
|
||||
|
@ -288,6 +300,10 @@ class RetainOp extends ScanOp {
|
|||
return !other.tracking
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScanOp} other
|
||||
* @return {other is RetainOp}
|
||||
*/
|
||||
canMergeWith(other) {
|
||||
if (!(other instanceof RetainOp)) {
|
||||
return false
|
||||
|
@ -298,6 +314,9 @@ class RetainOp extends ScanOp {
|
|||
return !other.tracking
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScanOp} other
|
||||
*/
|
||||
mergeWith(other) {
|
||||
if (!this.canMergeWith(other)) {
|
||||
throw new Error('Cannot merge with incompatible operation')
|
||||
|
@ -321,6 +340,9 @@ class RetainOp extends ScanOp {
|
|||
}
|
||||
|
||||
class RemoveOp extends ScanOp {
|
||||
/**
|
||||
* @param {number} length
|
||||
*/
|
||||
constructor(length) {
|
||||
super()
|
||||
if (length < 0) {
|
||||
|
@ -352,7 +374,11 @@ class RemoveOp extends ScanOp {
|
|||
return new RemoveOp(-op)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param {ScanOp} other
|
||||
* @return {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (!(other instanceof RemoveOp)) {
|
||||
return false
|
||||
|
@ -360,10 +386,17 @@ class RemoveOp extends ScanOp {
|
|||
return this.length === other.length
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScanOp} other
|
||||
* @return {other is RemoveOp}
|
||||
*/
|
||||
canMergeWith(other) {
|
||||
return other instanceof RemoveOp
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ScanOp} other
|
||||
*/
|
||||
mergeWith(other) {
|
||||
if (!this.canMergeWith(other)) {
|
||||
throw new Error('Cannot merge with incompatible operation')
|
||||
|
|
|
@ -48,7 +48,7 @@ class SetCommentStateOperation extends EditOperation {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {StringFileData} previousState
|
||||
* @returns {SetCommentStateOperation | EditNoOperation}
|
||||
*/
|
||||
invert(previousState) {
|
||||
|
@ -77,7 +77,7 @@ class SetCommentStateOperation extends EditOperation {
|
|||
/**
|
||||
* @inheritdoc
|
||||
* @param {EditOperation} other
|
||||
* @returns {EditOperation}
|
||||
* @returns {SetCommentStateOperation | core.DeleteCommentOperation}
|
||||
*/
|
||||
compose(other) {
|
||||
if (
|
||||
|
|
|
@ -35,6 +35,7 @@ const TrackingProps = require('../file_data/tracking_props')
|
|||
* @typedef {import('../operation/scan_op').ScanOp} ScanOp
|
||||
* @typedef {import('../file_data/tracked_change_list')} TrackedChangeList
|
||||
* @typedef {import('../types').TrackingDirective} TrackingDirective
|
||||
* @typedef {{tracking?: TrackingProps, commentIds?: string[]}} InsertOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -69,6 +70,10 @@ class TextOperation extends EditOperation {
|
|||
this.targetLength = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextOperation} other
|
||||
* @return {boolean}
|
||||
*/
|
||||
equals(other) {
|
||||
if (this.baseLength !== other.baseLength) {
|
||||
return false
|
||||
|
@ -129,7 +134,7 @@ class TextOperation extends EditOperation {
|
|||
/**
|
||||
* Insert a string at the current position.
|
||||
* @param {string | {i: string}} insertValue
|
||||
* @param {{tracking?: TrackingProps, commentIds?: string[]}} opts
|
||||
* @param {InsertOptions} opts
|
||||
* @returns {TextOperation}
|
||||
*/
|
||||
insert(insertValue, opts = {}) {
|
||||
|
@ -328,6 +333,8 @@ class TextOperation extends EditOperation {
|
|||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @param {number} length of the original string; non-negative
|
||||
* @return {number} length of the new string; non-negative
|
||||
*/
|
||||
applyToLength(length) {
|
||||
const operation = this
|
||||
|
@ -573,6 +580,7 @@ class TextOperation extends EditOperation {
|
|||
op1 = ops1[i1++]
|
||||
}
|
||||
} else if (op1 instanceof InsertOp && op2 instanceof RetainOp) {
|
||||
/** @type InsertOptions */
|
||||
const opts = {
|
||||
commentIds: op1.commentIds,
|
||||
}
|
||||
|
@ -807,6 +815,10 @@ function getSimpleOp(operation) {
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TextOperation} operation
|
||||
* @return {number}
|
||||
*/
|
||||
function getStartIndex(operation) {
|
||||
if (operation.ops[0] instanceof RetainOp) {
|
||||
return operation.ops[0].length
|
||||
|
@ -843,7 +855,10 @@ function calculateTrackingCommentSegments(
|
|||
const breaks = new Set()
|
||||
const opStart = cursor
|
||||
const opEnd = cursor + length
|
||||
// Utility function to limit breaks to the boundary set by the operation range
|
||||
/**
|
||||
* Utility function to limit breaks to the boundary set by the operation range
|
||||
* @param {number} rangeBoundary
|
||||
*/
|
||||
function addBreak(rangeBoundary) {
|
||||
if (rangeBoundary < opStart || rangeBoundary > opEnd) {
|
||||
return
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
const OError = require('@overleaf/o-error')
|
||||
|
||||
/**
|
||||
* @typedef {import('./types').RawRange} RawRange
|
||||
*/
|
||||
|
||||
class Range {
|
||||
/**
|
||||
* @param {number} pos
|
||||
|
@ -17,10 +21,16 @@ class Range {
|
|||
this.length = length
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
get start() {
|
||||
return this.pos
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {number}
|
||||
*/
|
||||
get end() {
|
||||
return this.pos + this.length
|
||||
}
|
||||
|
@ -193,6 +203,10 @@ class Range {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawRange} raw
|
||||
* @return {Range}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
return new Range(raw.pos, raw.length)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/** @module */
|
||||
// @ts-check
|
||||
'use strict'
|
||||
|
||||
const path = require('path-browserify')
|
||||
|
@ -39,6 +39,7 @@ const MAX_PATH = 1024
|
|||
/**
|
||||
* Replace invalid characters and filename patterns in a filename with
|
||||
* underscores.
|
||||
* @param {string} filename
|
||||
*/
|
||||
function cleanPart(filename) {
|
||||
filename = filename.replace(BAD_CHAR_RX, '_')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @ts-check
|
||||
'use strict'
|
||||
|
||||
const assert = require('check-types').assert
|
||||
|
@ -10,9 +11,11 @@ const FILE_LOAD_CONCURRENCY = 50
|
|||
|
||||
/**
|
||||
* @typedef {import("./types").BlobStore} BlobStore
|
||||
* @typedef {import("./types").RawSnapshot} RawSnapshot
|
||||
* @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore
|
||||
* @typedef {import("./change")} Change
|
||||
* @typedef {import("./operation/text_operation")} TextOperation
|
||||
* @typedef {import("./file")} File
|
||||
*/
|
||||
|
||||
class EditMissingFileError extends OError {}
|
||||
|
@ -26,6 +29,10 @@ class Snapshot {
|
|||
static PROJECT_VERSION_RX = new RegExp(Snapshot.PROJECT_VERSION_RX_STRING)
|
||||
static EditMissingFileError = EditMissingFileError
|
||||
|
||||
/**
|
||||
* @param {RawSnapshot} raw
|
||||
* @return {Snapshot}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
assert.object(raw.files, 'bad raw.files')
|
||||
return new Snapshot(
|
||||
|
@ -36,6 +43,7 @@ class Snapshot {
|
|||
}
|
||||
|
||||
toRaw() {
|
||||
/** @type RawSnapshot */
|
||||
const raw = {
|
||||
files: this.fileMap.toRaw(),
|
||||
}
|
||||
|
@ -64,6 +72,9 @@ class Snapshot {
|
|||
return this.projectVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} projectVersion
|
||||
*/
|
||||
setProjectVersion(projectVersion) {
|
||||
assert.maybe.match(
|
||||
projectVersion,
|
||||
|
@ -80,6 +91,9 @@ class Snapshot {
|
|||
return this.v2DocVersions
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {V2DocVersions} v2DocVersions
|
||||
*/
|
||||
setV2DocVersions(v2DocVersions) {
|
||||
assert.maybe.instance(
|
||||
v2DocVersions,
|
||||
|
@ -89,6 +103,9 @@ class Snapshot {
|
|||
this.v2DocVersions = v2DocVersions
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {V2DocVersions} v2DocVersions
|
||||
*/
|
||||
updateV2DocVersions(v2DocVersions) {
|
||||
// merge new v2DocVersions into this.v2DocVersions
|
||||
v2DocVersions.applyTo(this)
|
||||
|
@ -114,6 +131,7 @@ class Snapshot {
|
|||
/**
|
||||
* Get a File by its pathname.
|
||||
* @see FileMap#getFile
|
||||
* @param {string} pathname
|
||||
*/
|
||||
getFile(pathname) {
|
||||
return this.fileMap.getFile(pathname)
|
||||
|
@ -122,6 +140,8 @@ class Snapshot {
|
|||
/**
|
||||
* Add the given file to the snapshot.
|
||||
* @see FileMap#addFile
|
||||
* @param {string} pathname
|
||||
* @param {File} file
|
||||
*/
|
||||
addFile(pathname, file) {
|
||||
this.fileMap.addFile(pathname, file)
|
||||
|
@ -130,6 +150,8 @@ class Snapshot {
|
|||
/**
|
||||
* Move or remove a file.
|
||||
* @see FileMap#moveFile
|
||||
* @param {string} pathname
|
||||
* @param {string} newPathname
|
||||
*/
|
||||
moveFile(pathname, newPathname) {
|
||||
this.fileMap.moveFile(pathname, newPathname)
|
||||
|
@ -185,13 +207,18 @@ class Snapshot {
|
|||
* @param {Set.<String>} blobHashes
|
||||
*/
|
||||
findBlobHashes(blobHashes) {
|
||||
// eslint-disable-next-line array-callback-return
|
||||
this.fileMap.map(file => {
|
||||
/**
|
||||
* @param {File} file
|
||||
*/
|
||||
function find(file) {
|
||||
const hash = file.getHash()
|
||||
const rangeHash = file.getRangesHash()
|
||||
if (hash) blobHashes.add(hash)
|
||||
if (rangeHash) blobHashes.add(rangeHash)
|
||||
})
|
||||
}
|
||||
// TODO(das7pad): refine types to enforce no nulls in FileMapData
|
||||
// @ts-ignore
|
||||
this.fileMap.map(find)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,10 +230,15 @@ class Snapshot {
|
|||
* values are the files in the snapshot
|
||||
*/
|
||||
async loadFiles(kind, blobStore) {
|
||||
return await this.fileMap.mapAsync(
|
||||
file => file.load(kind, blobStore),
|
||||
FILE_LOAD_CONCURRENCY
|
||||
)
|
||||
/**
|
||||
* @param {File} file
|
||||
*/
|
||||
function load(file) {
|
||||
return file.load(kind, blobStore)
|
||||
}
|
||||
// TODO(das7pad): refine types to enforce no nulls in FileMapData
|
||||
// @ts-ignore
|
||||
return await this.fileMap.mapAsync(load, FILE_LOAD_CONCURRENCY)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -224,10 +256,16 @@ class Snapshot {
|
|||
const rawV2DocVersions = this.v2DocVersions
|
||||
? this.v2DocVersions.toRaw()
|
||||
: undefined
|
||||
const rawFiles = await this.fileMap.mapAsync(
|
||||
file => file.store(blobStore),
|
||||
concurrency
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {File} file
|
||||
*/
|
||||
function store(file) {
|
||||
return file.store(blobStore)
|
||||
}
|
||||
// TODO(das7pad): refine types to enforce no nulls in FileMapData
|
||||
// @ts-ignore
|
||||
const rawFiles = await this.fileMap.mapAsync(store, concurrency)
|
||||
return {
|
||||
files: rawFiles,
|
||||
projectVersion,
|
||||
|
|
|
@ -17,19 +17,19 @@ export type RangesBlob = {
|
|||
trackedChanges: TrackedChangeRawData[]
|
||||
}
|
||||
|
||||
type Range = {
|
||||
export type RawRange = {
|
||||
pos: number
|
||||
length: number
|
||||
}
|
||||
|
||||
export type CommentRawData = {
|
||||
id: string
|
||||
ranges: Range[]
|
||||
ranges: RawRange[]
|
||||
resolved?: boolean
|
||||
}
|
||||
|
||||
export type TrackedChangeRawData = {
|
||||
range: Range
|
||||
range: RawRange
|
||||
tracking: TrackingPropsRawData
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,35 @@ export type StringFileRawData = {
|
|||
trackedChanges?: TrackedChangeRawData[]
|
||||
}
|
||||
|
||||
export type RawSnapshot = {
|
||||
files: RawFileMap
|
||||
projectVersion?: string
|
||||
v2DocVersions?: RawV2DocVersions | null
|
||||
}
|
||||
|
||||
export type RawFileMap = Record<string, RawFile>
|
||||
|
||||
export type RawFile = { metadata?: Object } & RawFileData
|
||||
|
||||
export type RawFileData =
|
||||
| RawBinaryFileData
|
||||
| RawHashFileData
|
||||
| RawHollowBinaryFileData
|
||||
| RawHollowStringFileData
|
||||
| RawLazyStringFileData
|
||||
| StringFileRawData
|
||||
|
||||
export type RawHashFileData = { hash: string; rangesHash?: string }
|
||||
export type RawBinaryFileData = { hash: string; byteLength: number }
|
||||
export type RawLazyStringFileData = {
|
||||
hash: string
|
||||
stringLength: number
|
||||
rangesHash?: string
|
||||
operations?: RawEditOperation[]
|
||||
}
|
||||
export type RawHollowBinaryFileData = { byteLength: number }
|
||||
export type RawHollowStringFileData = { stringLength: number }
|
||||
|
||||
export type RawV2DocVersions = Record<string, { pathname: string; v: number }>
|
||||
|
||||
export type RawInsertOp =
|
||||
|
@ -78,7 +107,7 @@ export type RawTextOperation = {
|
|||
|
||||
export type RawAddCommentOperation = {
|
||||
commentId: string
|
||||
ranges: Range[]
|
||||
ranges: RawRange[]
|
||||
resolved?: boolean
|
||||
}
|
||||
|
||||
|
@ -89,14 +118,28 @@ export type RawSetCommentStateOperation = {
|
|||
resolved: boolean
|
||||
}
|
||||
|
||||
export type RawEditNoOperation = {
|
||||
noOp: true
|
||||
}
|
||||
|
||||
export type RawEditFileOperation = RawEditOperation & { pathname: string }
|
||||
|
||||
export type RawEditOperation =
|
||||
| RawTextOperation
|
||||
| RawAddCommentOperation
|
||||
| RawDeleteCommentOperation
|
||||
| RawSetCommentStateOperation
|
||||
| RawEditNoOperation
|
||||
|
||||
export type LinkedFileData = {
|
||||
importedAt: string
|
||||
provider: string
|
||||
[other: string]: any
|
||||
}
|
||||
|
||||
export type RawLabel = {
|
||||
text: string
|
||||
authorId: number | null
|
||||
timestamp: string
|
||||
version: number
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
// @ts-check
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
/**
|
||||
* @typedef {import("./file")} File
|
||||
* @typedef {import("./snapshot")} Snapshot
|
||||
* @typedef {import("./types").RawV2DocVersions} RawV2DocVersions
|
||||
*/
|
||||
|
||||
|
@ -15,13 +17,17 @@ class V2DocVersions {
|
|||
this.data = data || {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {RawV2DocVersions?} [raw]
|
||||
* @return {V2DocVersions|undefined}
|
||||
*/
|
||||
static fromRaw(raw) {
|
||||
if (!raw) return undefined
|
||||
return new V2DocVersions(raw)
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {?RawV2DocVersions}
|
||||
*/
|
||||
toRaw() {
|
||||
if (!this.data) return null
|
||||
|
@ -32,12 +38,15 @@ class V2DocVersions {
|
|||
/**
|
||||
* Clone this object.
|
||||
*
|
||||
* @return {V2DocVersions} a new object of the same type
|
||||
* @return {V2DocVersions|undefined} a new object of the same type
|
||||
*/
|
||||
clone() {
|
||||
return V2DocVersions.fromRaw(this.toRaw())
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Snapshot} snapshot
|
||||
*/
|
||||
applyTo(snapshot) {
|
||||
// Only update the snapshot versions if we have new versions
|
||||
if (!_.size(this.data)) return
|
||||
|
@ -54,6 +63,8 @@ class V2DocVersions {
|
|||
/**
|
||||
* Move or remove a doc.
|
||||
* Must be called after FileMap#moveFile, which validates the paths.
|
||||
* @param {string} pathname
|
||||
* @param {string} newPathname
|
||||
*/
|
||||
moveFile(pathname, newPathname) {
|
||||
for (const [id, v] of Object.entries(this.data)) {
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
|
||||
"format": "prettier --list-different $PWD/'**/*.{js,cjs}'",
|
||||
"format:fix": "prettier --write $PWD/'**/*.{js,cjs}'",
|
||||
"lint": "eslint --ext .js --ext .cjs --max-warnings 0 --format unix .",
|
||||
"lint:fix": "eslint --fix --ext .js --ext .cjs .",
|
||||
"format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
|
||||
"format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
|
||||
"lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
|
||||
"lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
|
||||
"test:ci": "npm run test:unit",
|
||||
"test:unit": "mocha --exit test/**/*.{js,cjs}",
|
||||
"types:check": "tsc --noEmit"
|
||||
|
@ -17,6 +17,8 @@
|
|||
"license": "Proprietary",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/check-types": "^7.3.7",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"chai": "^3.3.0",
|
||||
"mocha": "^10.2.0",
|
||||
"sinon": "^9.2.4",
|
||||
|
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -397,6 +397,8 @@
|
|||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/check-types": "^7.3.7",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"chai": "^3.3.0",
|
||||
"mocha": "^10.2.0",
|
||||
"sinon": "^9.2.4",
|
||||
|
@ -12532,6 +12534,12 @@
|
|||
"@types/chai": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/check-types": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/check-types/-/check-types-7.3.7.tgz",
|
||||
"integrity": "sha512-ZNAGaVc/joAV3lAuRwPdsQY/caU1RvKoa+U7i/TkYIlOStdYq4vyArFnA1zItfEDkHpXNWApWIqqbp5fsHAiRg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
|
@ -13015,6 +13023,12 @@
|
|||
"@types/passport": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/path-browserify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/path-browserify/-/path-browserify-1.0.2.tgz",
|
||||
"integrity": "sha512-ZkC5IUqqIFPXx3ASTTybTzmQdwHwe2C0u3eL75ldQ6T9E9IWFJodn6hIfbZGab73DfyiHN4Xw15gNxUq2FbvBA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
|
||||
|
@ -44847,6 +44861,7 @@
|
|||
"mock-fs": "^5.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nvd3": "^1.8.6",
|
||||
"overleaf-editor-core": "*",
|
||||
"pdfjs-dist213": "npm:pdfjs-dist@2.13.216",
|
||||
"pdfjs-dist401": "npm:pdfjs-dist@4.5.136",
|
||||
"pirates": "^4.0.1",
|
||||
|
@ -53363,6 +53378,7 @@
|
|||
"nvd3": "^1.8.6",
|
||||
"on-headers": "^1.0.2",
|
||||
"otplib": "^12.0.1",
|
||||
"overleaf-editor-core": "*",
|
||||
"p-limit": "^2.3.0",
|
||||
"p-props": "4.0.0",
|
||||
"parse-data-url": "^2.0.0",
|
||||
|
@ -57422,6 +57438,12 @@
|
|||
"@types/chai": "*"
|
||||
}
|
||||
},
|
||||
"@types/check-types": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/check-types/-/check-types-7.3.7.tgz",
|
||||
"integrity": "sha512-ZNAGaVc/joAV3lAuRwPdsQY/caU1RvKoa+U7i/TkYIlOStdYq4vyArFnA1zItfEDkHpXNWApWIqqbp5fsHAiRg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
|
@ -57904,6 +57926,12 @@
|
|||
"@types/passport": "*"
|
||||
}
|
||||
},
|
||||
"@types/path-browserify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/path-browserify/-/path-browserify-1.0.2.tgz",
|
||||
"integrity": "sha512-ZkC5IUqqIFPXx3ASTTybTzmQdwHwe2C0u3eL75ldQ6T9E9IWFJodn6hIfbZGab73DfyiHN4Xw15gNxUq2FbvBA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/pg": {
|
||||
"version": "8.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
|
||||
|
@ -72798,6 +72826,8 @@
|
|||
"version": "file:libraries/overleaf-editor-core",
|
||||
"requires": {
|
||||
"@overleaf/o-error": "*",
|
||||
"@types/check-types": "^7.3.7",
|
||||
"@types/path-browserify": "^1.0.2",
|
||||
"chai": "^3.3.0",
|
||||
"check-types": "^5.1.0",
|
||||
"lodash": "^4.17.19",
|
||||
|
|
|
@ -296,7 +296,7 @@ class BlobStore {
|
|||
* Read a blob metadata record by hexadecimal hash.
|
||||
*
|
||||
* @param {string} hash hexadecimal SHA-1 hash
|
||||
* @return {Promise.<core.Blob?>}
|
||||
* @return {Promise<core.Blob | null>}
|
||||
*/
|
||||
async getBlob(hash) {
|
||||
assert.blobHash(hash, 'bad hash')
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
import 'overleaf-editor-core'
|
|
@ -320,6 +320,7 @@
|
|||
"mock-fs": "^5.1.2",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nvd3": "^1.8.6",
|
||||
"overleaf-editor-core": "*",
|
||||
"pdfjs-dist213": "npm:pdfjs-dist@2.13.216",
|
||||
"pdfjs-dist401": "npm:pdfjs-dist@4.5.136",
|
||||
"pirates": "^4.0.1",
|
||||
|
|
Loading…
Reference in a new issue