Merge pull request #19817 from overleaf/jpa-types

[overleaf-editor-core] stronger type checking via web

GitOrigin-RevId: 427019f40e2905f2e0ec11dc09f5fccdbb1f905b
This commit is contained in:
Jakob Ackermann 2024-08-07 16:51:44 +02:00 committed by Copybot
parent bd87e1b41b
commit 3836323724
29 changed files with 470 additions and 84 deletions

View file

@ -149,6 +149,7 @@ class Comment {
* @returns {CommentRawData} * @returns {CommentRawData}
*/ */
toRaw() { toRaw() {
/** @type CommentRawData */
const raw = { const raw = {
id: this.id, id: this.id,
ranges: this.ranges.map(range => range.toRaw()), ranges: this.ranges.map(range => range.toRaw()),

View file

@ -13,6 +13,8 @@ const StringFileData = require('./file_data/string_file_data')
* @typedef {import("./blob")} Blob * @typedef {import("./blob")} Blob
* @typedef {import("./types").BlobStore} BlobStore * @typedef {import("./types").BlobStore} BlobStore
* @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore * @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore
* @typedef {import("./types").RawFileData} RawFileData
* @typedef {import("./types").RawFile} RawFile
* @typedef {import("./types").StringFileRawData} StringFileRawData * @typedef {import("./types").StringFileRawData} StringFileRawData
* @typedef {import("./types").CommentRawData} CommentRawData * @typedef {import("./types").CommentRawData} CommentRawData
* @typedef {import("./file_data/comment_list")} CommentList * @typedef {import("./file_data/comment_list")} CommentList
@ -62,9 +64,14 @@ class File {
assert.instance(data, FileData, 'File: bad data') assert.instance(data, FileData, 'File: bad data')
this.data = data this.data = data
this.metadata = {}
this.setMetadata(metadata || {}) this.setMetadata(metadata || {})
} }
/**
* @param {RawFile} raw
* @return {File|null}
*/
static fromRaw(raw) { static fromRaw(raw) {
if (!raw) return null if (!raw) return null
return new File(FileData.fromRaw(raw), raw.metadata) return new File(FileData.fromRaw(raw), raw.metadata)
@ -90,8 +97,8 @@ class File {
} }
/** /**
* @param {number} [byteLength] * @param {number} byteLength
* @param {number} [stringLength] * @param {number?} stringLength
* @param {Object} [metadata] * @param {Object} [metadata]
* @return {File} * @return {File}
*/ */
@ -109,7 +116,11 @@ class File {
return new File(FileData.createLazyFromBlobs(blob, rangesBlob), metadata) return new File(FileData.createLazyFromBlobs(blob, rangesBlob), metadata)
} }
/**
* @returns {RawFile}
*/
toRaw() { toRaw() {
/** @type RawFile */
const rawFileData = this.data.toRaw() const rawFileData = this.data.toRaw()
storeRawMetadata(this.metadata, rawFileData) storeRawMetadata(this.metadata, rawFileData)
return rawFileData return rawFileData
@ -249,15 +260,20 @@ class File {
* the hash. * the hash.
* *
* @param {BlobStore} blobStore * @param {BlobStore} blobStore
* @return {Promise<Object>} a raw HashFile * @return {Promise<RawFile>} a raw HashFile
*/ */
async store(blobStore) { async store(blobStore) {
/** @type RawFile */
const raw = await this.data.store(blobStore) const raw = await this.data.store(blobStore)
storeRawMetadata(this.metadata, raw) storeRawMetadata(this.metadata, raw)
return raw return raw
} }
} }
/**
* @param {Object} metadata
* @param {RawFile} raw
*/
function storeRawMetadata(metadata, raw) { function storeRawMetadata(metadata, raw) {
if (!_.isEmpty(metadata)) { if (!_.isEmpty(metadata)) {
raw.metadata = _.cloneDeep(metadata) raw.metadata = _.cloneDeep(metadata)

View file

@ -5,6 +5,10 @@ const assert = require('check-types').assert
const Blob = require('../blob') const Blob = require('../blob')
const FileData = require('./') const FileData = require('./')
/**
* @typedef {import('../types').RawBinaryFileData} RawBinaryFileData
*/
class BinaryFileData extends FileData { class BinaryFileData extends FileData {
/** /**
* @param {string} hash * @param {string} hash
@ -21,11 +25,18 @@ class BinaryFileData extends FileData {
this.byteLength = byteLength this.byteLength = byteLength
} }
/**
* @param {RawBinaryFileData} raw
* @returns {BinaryFileData}
*/
static fromRaw(raw) { static fromRaw(raw) {
return new BinaryFileData(raw.hash, raw.byteLength) return new BinaryFileData(raw.hash, raw.byteLength)
} }
/** @inheritdoc */ /**
* @inheritdoc
* @returns {RawBinaryFileData}
*/
toRaw() { toRaw() {
return { hash: this.hash, byteLength: this.byteLength } return { hash: this.hash, byteLength: this.byteLength }
} }
@ -60,7 +71,9 @@ class BinaryFileData extends FileData {
return FileData.createHollow(this.byteLength, null) return FileData.createHollow(this.byteLength, null)
} }
/** @inheritdoc */ /** @inheritdoc
* @return {Promise<RawFileData>}
*/
async store() { async store() {
return { hash: this.hash } return { hash: this.hash }
} }

View file

@ -10,6 +10,7 @@ const FileData = require('./')
* @typedef {import('./lazy_string_file_data')} LazyStringFileData * @typedef {import('./lazy_string_file_data')} LazyStringFileData
* @typedef {import('./hollow_string_file_data')} HollowStringFileData * @typedef {import('./hollow_string_file_data')} HollowStringFileData
* @typedef {import('../types').BlobStore} BlobStore * @typedef {import('../types').BlobStore} BlobStore
* @typedef {import('../types').RawHashFileData} RawHashFileData
*/ */
class HashFileData extends FileData { class HashFileData extends FileData {
@ -35,8 +36,7 @@ class HashFileData extends FileData {
/** /**
* *
* @param {{hash: string, rangesHash?: string}} raw * @param {RawHashFileData} raw
* @returns
*/ */
static fromRaw(raw) { static fromRaw(raw) {
return new HashFileData(raw.hash, raw.rangesHash) return new HashFileData(raw.hash, raw.rangesHash)
@ -44,9 +44,10 @@ class HashFileData extends FileData {
/** /**
* @inheritdoc * @inheritdoc
* @returns {{hash: string, rangesHash?: string}} * @returns {RawHashFileData}
*/ */
toRaw() { toRaw() {
/** @type RawHashFileData */
const raw = { hash: this.hash } const raw = { hash: this.hash }
if (this.rangesHash) { if (this.rangesHash) {
raw.rangesHash = this.rangesHash raw.rangesHash = this.rangesHash
@ -97,6 +98,8 @@ class HashFileData extends FileData {
throw new Error('Failed to look up rangesHash in blobStore') throw new Error('Failed to look up rangesHash in blobStore')
} }
if (!blob) throw new Error('blob not found: ' + this.hash) 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) return FileData.createLazyFromBlobs(blob, rangesBlob)
} }
@ -110,14 +113,17 @@ class HashFileData extends FileData {
if (!blob) { if (!blob) {
throw new Error('Failed to look up hash in blobStore') 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()) return FileData.createHollow(blob.getByteLength(), blob.getStringLength())
} }
/** /**
* @inheritdoc * @inheritdoc
* @returns {Promise<{hash: string, rangesHash?: string}>} * @returns {Promise<RawHashFileData>}
*/ */
async store() { async store() {
/** @type RawHashFileData */
const raw = { hash: this.hash } const raw = { hash: this.hash }
if (this.rangesHash) { if (this.rangesHash) {
raw.rangesHash = this.rangesHash raw.rangesHash = this.rangesHash

View file

@ -4,6 +4,10 @@ const assert = require('check-types').assert
const FileData = require('./') const FileData = require('./')
/**
* @typedef {import('../types').RawHollowBinaryFileData} RawHollowBinaryFileData
*/
class HollowBinaryFileData extends FileData { class HollowBinaryFileData extends FileData {
/** /**
* @param {number} byteLength * @param {number} byteLength
@ -16,11 +20,18 @@ class HollowBinaryFileData extends FileData {
this.byteLength = byteLength this.byteLength = byteLength
} }
/**
* @param {RawHollowBinaryFileData} raw
* @returns {HollowBinaryFileData}
*/
static fromRaw(raw) { static fromRaw(raw) {
return new HollowBinaryFileData(raw.byteLength) return new HollowBinaryFileData(raw.byteLength)
} }
/** @inheritdoc */ /**
* @inheritdoc
* @returns {RawHollowBinaryFileData}
*/
toRaw() { toRaw() {
return { byteLength: this.byteLength } return { byteLength: this.byteLength }
} }

View file

@ -8,6 +8,10 @@ const assert = require('check-types').assert
const FileData = require('./') const FileData = require('./')
/**
* @typedef {import('../types').RawHollowStringFileData} RawHollowStringFileData
*/
class HollowStringFileData extends FileData { class HollowStringFileData extends FileData {
/** /**
* @param {number} stringLength * @param {number} stringLength
@ -24,11 +28,18 @@ class HollowStringFileData extends FileData {
this.stringLength = stringLength this.stringLength = stringLength
} }
/**
* @param {RawHollowStringFileData} raw
* @returns {HollowStringFileData}
*/
static fromRaw(raw) { static fromRaw(raw) {
return new HollowStringFileData(raw.stringLength) return new HollowStringFileData(raw.stringLength)
} }
/** @inheritdoc */ /**
* @inheritdoc
* @returns {RawHollowStringFileData}
*/
toRaw() { toRaw() {
return { stringLength: this.stringLength } return { stringLength: this.stringLength }
} }

View file

@ -6,18 +6,10 @@ const assert = require('check-types').assert
const Blob = require('../blob') 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").BlobStore} BlobStore
* @typedef {import("../types").ReadonlyBlobStore} ReadonlyBlobStore * @typedef {import("../types").ReadonlyBlobStore} ReadonlyBlobStore
* @typedef {import("../types").RawFileData} RawFileData
* @typedef {import("../operation/edit_operation")} EditOperation * @typedef {import("../operation/edit_operation")} EditOperation
* @typedef {import("../file_data/comment_list")} CommentList * @typedef {import("../file_data/comment_list")} CommentList
* @typedef {import("../types").CommentRawData} CommentRawData * @typedef {import("../types").CommentRawData} CommentRawData
@ -29,25 +21,37 @@ let StringFileData = null
* should be used only through {@link File}. * should be used only through {@link File}.
*/ */
class FileData { class FileData {
/** @see File.fromRaw */ /** @see File.fromRaw
* @param {RawFileData} raw
*/
static fromRaw(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, 'hash')) {
if (Object.prototype.hasOwnProperty.call(raw, 'byteLength')) if (Object.prototype.hasOwnProperty.call(raw, 'byteLength'))
// @ts-ignore
return BinaryFileData.fromRaw(raw) return BinaryFileData.fromRaw(raw)
if (Object.prototype.hasOwnProperty.call(raw, 'stringLength')) if (Object.prototype.hasOwnProperty.call(raw, 'stringLength'))
// @ts-ignore
return LazyStringFileData.fromRaw(raw) return LazyStringFileData.fromRaw(raw)
// @ts-ignore
return HashFileData.fromRaw(raw) return HashFileData.fromRaw(raw)
} }
if (Object.prototype.hasOwnProperty.call(raw, 'byteLength')) if (Object.prototype.hasOwnProperty.call(raw, 'byteLength'))
// @ts-ignore
return HollowBinaryFileData.fromRaw(raw) return HollowBinaryFileData.fromRaw(raw)
if (Object.prototype.hasOwnProperty.call(raw, 'stringLength')) if (Object.prototype.hasOwnProperty.call(raw, 'stringLength'))
// @ts-ignore
return HollowStringFileData.fromRaw(raw) return HollowStringFileData.fromRaw(raw)
if (Object.prototype.hasOwnProperty.call(raw, 'content')) if (Object.prototype.hasOwnProperty.call(raw, 'content'))
// @ts-ignore
return StringFileData.fromRaw(raw) return StringFileData.fromRaw(raw)
throw new Error('FileData: bad raw object ' + JSON.stringify(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) { static createHollow(byteLength, stringLength) {
if (stringLength == null) { if (stringLength == null) {
return new HollowBinaryFileData(byteLength) return new HollowBinaryFileData(byteLength)
@ -63,15 +67,25 @@ class FileData {
static createLazyFromBlobs(blob, rangesBlob) { static createLazyFromBlobs(blob, rangesBlob) {
assert.instance(blob, Blob, 'FileData: bad blob') assert.instance(blob, Blob, 'FileData: bad blob')
if (blob.getStringLength() == null) { 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( return new LazyStringFileData(
// TODO(das7pad): see call-sites
// @ts-ignore
blob.getHash(), blob.getHash(),
rangesBlob?.getHash(), rangesBlob?.getHash(),
blob.getStringLength() blob.getStringLength()
) )
} }
/**
* @returns {RawFileData}
*/
toRaw() { toRaw() {
throw new Error('FileData: toRaw not implemented') throw new Error('FileData: toRaw not implemented')
} }
@ -184,7 +198,7 @@ class FileData {
* @see File#store * @see File#store
* @function * @function
* @param {BlobStore} blobStore * @param {BlobStore} blobStore
* @return {Promise<Object>} a raw HashFile * @return {Promise<RawFileData>} a raw HashFile
* @abstract * @abstract
*/ */
async store(blobStore) { async store(blobStore) {
@ -216,9 +230,9 @@ class FileData {
module.exports = FileData module.exports = FileData
BinaryFileData = require('./binary_file_data') const BinaryFileData = require('./binary_file_data')
HashFileData = require('./hash_file_data') const HashFileData = require('./hash_file_data')
HollowBinaryFileData = require('./hollow_binary_file_data') const HollowBinaryFileData = require('./hollow_binary_file_data')
HollowStringFileData = require('./hollow_string_file_data') const HollowStringFileData = require('./hollow_string_file_data')
LazyStringFileData = require('./lazy_string_file_data') const LazyStringFileData = require('./lazy_string_file_data')
StringFileData = require('./string_file_data') const StringFileData = require('./string_file_data')

View file

@ -14,6 +14,8 @@ const EditOperationBuilder = require('../operation/edit_operation_builder')
* @typedef {import('../types').BlobStore} BlobStore * @typedef {import('../types').BlobStore} BlobStore
* @typedef {import('../types').ReadonlyBlobStore} ReadonlyBlobStore * @typedef {import('../types').ReadonlyBlobStore} ReadonlyBlobStore
* @typedef {import('../types').RangesBlob} RangesBlob * @typedef {import('../types').RangesBlob} RangesBlob
* @typedef {import('../types').RawFileData} RawFileData
* @typedef {import('../types').RawLazyStringFileData} RawLazyStringFileData
*/ */
class LazyStringFileData extends FileData { class LazyStringFileData extends FileData {
@ -39,6 +41,10 @@ class LazyStringFileData extends FileData {
this.operations = operations || [] this.operations = operations || []
} }
/**
* @param {RawLazyStringFileData} raw
* @returns {LazyStringFileData}
*/
static fromRaw(raw) { static fromRaw(raw) {
return new LazyStringFileData( return new LazyStringFileData(
raw.hash, raw.hash,
@ -48,9 +54,16 @@ class LazyStringFileData extends FileData {
) )
} }
/** @inheritdoc */ /**
* @inheritdoc
* @returns {RawLazyStringFileData}
*/
toRaw() { toRaw() {
const raw = { hash: this.hash, stringLength: this.stringLength } /** @type RawLazyStringFileData */
const raw = {
hash: this.hash,
stringLength: this.stringLength,
}
if (this.rangesHash) { if (this.rangesHash) {
raw.rangesHash = this.rangesHash raw.rangesHash = this.rangesHash
} }
@ -135,18 +148,26 @@ class LazyStringFileData extends FileData {
/** @inheritdoc */ /** @inheritdoc */
async toHollow() { async toHollow() {
// TODO(das7pad): inline 2nd path of FileData.createLazyFromBlobs?
// @ts-ignore
return FileData.createHollow(null, this.stringLength) return FileData.createHollow(null, this.stringLength)
} }
/** @inheritdoc */ /** @inheritdoc
* @param {EditOperation} operation
*/
edit(operation) { edit(operation) {
this.stringLength = operation.applyToLength(this.stringLength) this.stringLength = operation.applyToLength(this.stringLength)
this.operations.push(operation) this.operations.push(operation)
} }
/** @inheritdoc */ /** @inheritdoc
* @param {BlobStore} blobStore
* @return {Promise<RawFileData>}
*/
async store(blobStore) { async store(blobStore) {
if (this.operations.length === 0) { if (this.operations.length === 0) {
/** @type RawFileData */
const raw = { hash: this.hash } const raw = { hash: this.hash }
if (this.rangesHash) { if (this.rangesHash) {
raw.rangesHash = this.rangesHash raw.rangesHash = this.rangesHash
@ -155,6 +176,7 @@ class LazyStringFileData extends FileData {
} }
const eager = await this.toEager(blobStore) const eager = await this.toEager(blobStore)
this.operations.length = 0 this.operations.length = 0
/** @type RawFileData */
return await eager.store(blobStore) return await eager.store(blobStore)
} }
} }

View file

@ -9,6 +9,7 @@ const TrackedChangeList = require('./tracked_change_list')
/** /**
* @typedef {import("../types").StringFileRawData} StringFileRawData * @typedef {import("../types").StringFileRawData} StringFileRawData
* @typedef {import("../types").RawFileData} RawFileData
* @typedef {import("../operation/edit_operation")} EditOperation * @typedef {import("../operation/edit_operation")} EditOperation
* @typedef {import("../types").BlobStore} BlobStore * @typedef {import("../types").BlobStore} BlobStore
* @typedef {import("../types").CommentRawData} CommentRawData * @typedef {import("../types").CommentRawData} CommentRawData
@ -47,6 +48,7 @@ class StringFileData extends FileData {
* @returns {StringFileRawData} * @returns {StringFileRawData}
*/ */
toRaw() { toRaw() {
/** @type StringFileRawData */
const raw = { content: this.content } const raw = { content: this.content }
if (this.comments.length) { if (this.comments.length) {
@ -133,6 +135,7 @@ class StringFileData extends FileData {
/** /**
* @inheritdoc * @inheritdoc
* @param {BlobStore} blobStore * @param {BlobStore} blobStore
* @return {Promise<RawFileData>}
*/ */
async store(blobStore) { async store(blobStore) {
const blob = await blobStore.putString(this.content) const blob = await blobStore.putString(this.content)
@ -143,8 +146,12 @@ class StringFileData extends FileData {
trackedChanges: this.trackedChanges.toRaw(), trackedChanges: this.trackedChanges.toRaw(),
} }
const rangesBlob = await blobStore.putObject(ranges) const rangesBlob = await blobStore.putObject(ranges)
// TODO(das7pad): Provide interface that guarantees hash exists?
// @ts-ignore
return { hash: blob.getHash(), rangesHash: rangesBlob.getHash() } return { hash: blob.getHash(), rangesHash: rangesBlob.getHash() }
} }
// TODO(das7pad): Provide interface that guarantees hash exists?
// @ts-ignore
return { hash: blob.getHash() } return { hash: blob.getHash() }
} }
} }

View file

@ -1,6 +1,7 @@
// @ts-check // @ts-check
/** /**
* @typedef {import("../types").TrackingPropsRawData} TrackingPropsRawData * @typedef {import("../types").TrackingPropsRawData} TrackingPropsRawData
* @typedef {import("../types").TrackingDirective} TrackingDirective
*/ */
class TrackingProps { class TrackingProps {
@ -48,6 +49,10 @@ class TrackingProps {
} }
} }
/**
* @param {TrackingDirective} [other]
* @returns {boolean}
*/
equals(other) { equals(other) {
if (!(other instanceof TrackingProps)) { if (!(other instanceof TrackingProps)) {
return false return false

View file

@ -9,9 +9,18 @@ const pMap = require('p-map')
const File = require('./file') const File = require('./file')
const safePathname = require('./safe_pathname') 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 PathnameError extends OError {}
class NonUniquePathnameError extends PathnameError { class NonUniquePathnameError extends PathnameError {
/**
* @param {string[]} pathnames
*/
constructor(pathnames) { constructor(pathnames) {
super('pathnames are not unique: ' + pathnames, { pathnames }) super('pathnames are not unique: ' + pathnames, { pathnames })
this.pathnames = pathnames this.pathnames = pathnames
@ -19,6 +28,9 @@ class NonUniquePathnameError extends PathnameError {
} }
class BadPathnameError extends PathnameError { class BadPathnameError extends PathnameError {
/**
* @param {string} pathname
*/
constructor(pathname) { constructor(pathname) {
super(pathname + ' is not a valid pathname', { pathname }) super(pathname + ' is not a valid pathname', { pathname })
this.pathname = pathname this.pathname = pathname
@ -26,6 +38,9 @@ class BadPathnameError extends PathnameError {
} }
class PathnameConflictError extends PathnameError { class PathnameConflictError extends PathnameError {
/**
* @param {string} pathname
*/
constructor(pathname) { constructor(pathname) {
super(`pathname '${pathname}' conflicts with another file`, { pathname }) super(`pathname '${pathname}' conflicts with another file`, { pathname })
this.pathname = pathname this.pathname = pathname
@ -33,6 +48,9 @@ class PathnameConflictError extends PathnameError {
} }
class FileNotFoundError extends PathnameError { class FileNotFoundError extends PathnameError {
/**
* @param {string} pathname
*/
constructor(pathname) { constructor(pathname) {
super(`file ${pathname} does not exist`, { pathname }) super(`file ${pathname} does not exist`, { pathname })
this.pathname = pathname this.pathname = pathname
@ -70,13 +88,17 @@ class FileMap {
constructor(files) { constructor(files) {
// create bare object for use as Map // create bare object for use as Map
// http://ryanmorr.com/true-hash-maps-in-javascript/ // http://ryanmorr.com/true-hash-maps-in-javascript/
/** @type {Record<String, File | null>} */ /** @type FileMapData */
this.files = Object.create(null) this.files = Object.create(null)
_.assign(this.files, files) _.assign(this.files, files)
checkPathnamesAreUnique(this.files) checkPathnamesAreUnique(this.files)
checkPathnamesDoNotConflict(this) checkPathnamesDoNotConflict(this)
} }
/**
* @param {RawFileMap} raw
* @returns {FileMap}
*/
static fromRaw(raw) { static fromRaw(raw) {
assert.object(raw, 'bad raw files') assert.object(raw, 'bad raw files')
return new FileMap(_.mapValues(raw, File.fromRaw)) return new FileMap(_.mapValues(raw, File.fromRaw))
@ -85,12 +107,18 @@ class FileMap {
/** /**
* Convert to raw object for serialization. * Convert to raw object for serialization.
* *
* @return {Object} * @return {RawFileMap}
*/ */
toRaw() { toRaw() {
/**
* @param {File} file
* @return {RawFile}
*/
function fileToRaw(file) { function fileToRaw(file) {
return file.toRaw() return file.toRaw()
} }
// TODO(das7pad): refine types to enforce no nulls in FileMapData
// @ts-ignore
return _.mapValues(this.files, fileToRaw) return _.mapValues(this.files, fileToRaw)
} }
@ -103,6 +131,8 @@ class FileMap {
addFile(pathname, file) { addFile(pathname, file) {
checkPathname(pathname) checkPathname(pathname)
assert.object(file, 'bad file') assert.object(file, 'bad file')
// TODO(das7pad): make ignoredPathname argument fully optional
// @ts-ignore
checkNewPathnameDoesNotConflict(this, pathname) checkNewPathnameDoesNotConflict(this, pathname)
addFile(this.files, pathname, file) addFile(this.files, pathname, file)
} }
@ -163,7 +193,7 @@ class FileMap {
*/ */
getFile(pathname) { getFile(pathname) {
const key = findPathnameKey(this.files, 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.) * and MoveFile overwrite existing files.)
* *
* @param {string} pathname * @param {string} pathname
* @param {string} [ignoredPathname] pretend this pathname does not exist * @param {string?} ignoredPathname pretend this pathname does not exist
*/ */
wouldConflict(pathname, ignoredPathname) { wouldConflict(pathname, ignoredPathname) {
checkPathname(pathname) checkPathname(pathname)
@ -224,7 +254,7 @@ class FileMap {
/** /**
* Map the files in this map to new values. * Map the files in this map to new values.
* @template T * @template T
* @param {(file: File | null) => T} iteratee * @param {(file: File | null, path: string) => T} iteratee
* @return {Record<String, T>} * @return {Record<String, T>}
*/ */
map(iteratee) { map(iteratee) {
@ -234,9 +264,10 @@ class FileMap {
/** /**
* Map the files in this map to new values asynchronously, with an optional * Map the files in this map to new values asynchronously, with an optional
* limit on concurrency. * 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] * @param {number} [concurrency]
* @return {Promise<Object>} * @return {Promise<Record<String, T>>}
*/ */
async mapAsync(iteratee, concurrency) { async mapAsync(iteratee, concurrency) {
assert.maybe.number(concurrency, 'bad 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) { function pathnamesEqual(pathname0, pathname1) {
return pathname0 === pathname1 return pathname0 === pathname1
} }
/**
* @param {FileMapData} files
* @returns {boolean}
*/
function pathnamesAreUnique(files) { function pathnamesAreUnique(files) {
const keys = _.keys(files) const keys = _.keys(files)
return _.uniqWith(keys, pathnamesEqual).length === keys.length return _.uniqWith(keys, pathnamesEqual).length === keys.length
} }
/**
* @param {FileMapData} files
*/
function checkPathnamesAreUnique(files) { function checkPathnamesAreUnique(files) {
if (pathnamesAreUnique(files)) return if (pathnamesAreUnique(files)) return
throw new FileMap.NonUniquePathnameError(_.keys(files)) throw new FileMap.NonUniquePathnameError(_.keys(files))
} }
/**
* @param {string} pathname
*/
function checkPathname(pathname) { function checkPathname(pathname) {
assert.nonEmptyString(pathname, 'bad pathname') assert.nonEmptyString(pathname, 'bad pathname')
if (safePathname.isClean(pathname)) return if (safePathname.isClean(pathname)) return
throw new FileMap.BadPathnameError(pathname) throw new FileMap.BadPathnameError(pathname)
} }
/**
* @param {FileMap} fileMap
* @param {string} pathname
* @param {string?} ignoredPathname
*/
function checkNewPathnameDoesNotConflict(fileMap, pathname, ignoredPathname) { function checkNewPathnameDoesNotConflict(fileMap, pathname, ignoredPathname) {
if (fileMap.wouldConflict(pathname, ignoredPathname)) { if (fileMap.wouldConflict(pathname, ignoredPathname)) {
throw new FileMap.PathnameConflictError(pathname) throw new FileMap.PathnameConflictError(pathname)
} }
} }
/**
* @param {FileMap} fileMap
*/
function checkPathnamesDoNotConflict(fileMap) { function checkPathnamesDoNotConflict(fileMap) {
const pathnames = fileMap.getPathnames() const pathnames = fileMap.getPathnames()
// check pathnames for validity first // check pathnames for validity first
@ -299,18 +353,29 @@ function checkPathnamesDoNotConflict(fileMap) {
} }
} }
// /**
// This function is somewhat vestigial: it was used when this map used * This function is somewhat vestigial: it was used when this map used
// case-insensitive pathname comparison. We could probably simplify some of the * 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 * 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. * 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) { function findPathnameKey(files, pathname) {
// we can check for the key without worrying about properties // we can check for the key without worrying about properties
// in the prototype because we are now using a bare object/ // in the prototype because we are now using a bare object/
if (pathname in files) return pathname if (pathname in files) return pathname
} }
/**
* @param {FileMapData} files
* @param {string} pathname
* @param {File?} file
*/
function addFile(files, pathname, file) { function addFile(files, pathname, file) {
const key = findPathnameKey(files, pathname) const key = findPathnameKey(files, pathname)
if (key) delete files[key] if (key) delete files[key]

View file

@ -1,7 +1,12 @@
// @ts-check
'use strict' 'use strict'
const assert = require('check-types').assert const assert = require('check-types').assert
/**
* @typedef {import('./types').RawLabel} RawLabel
*/
/** /**
* @classdesc * @classdesc
* A user-configurable label that can be attached to a specific change. Labels * A user-configurable label that can be attached to a specific change. Labels
@ -13,6 +18,9 @@ class Label {
/** /**
* @constructor * @constructor
* @param {string} text * @param {string} text
* @param {number?} authorId
* @param {Date} timestamp
* @param {number} version
*/ */
constructor(text, authorId, timestamp, version) { constructor(text, authorId, timestamp, version) {
assert.string(text, 'bad text') assert.string(text, 'bad text')
@ -29,7 +37,7 @@ class Label {
/** /**
* Create a Label from its raw form. * Create a Label from its raw form.
* *
* @param {Object} raw * @param {RawLabel} raw
* @return {Label} * @return {Label}
*/ */
static fromRaw(raw) { static fromRaw(raw) {
@ -44,7 +52,7 @@ class Label {
/** /**
* Convert the Label to raw form for transmission. * Convert the Label to raw form for transmission.
* *
* @return {Object} * @return {RawLabel}
*/ */
toRaw() { toRaw() {
return { return {
@ -81,7 +89,7 @@ class Label {
} }
/** /**
* @return {number | undefined} * @return {number}
*/ */
getVersion() { getVersion() {
return this.version return this.version

View file

@ -44,6 +44,7 @@ class AddCommentOperation extends EditOperation {
* @returns {RawAddCommentOperation} * @returns {RawAddCommentOperation}
*/ */
toJSON() { toJSON() {
/** @type RawAddCommentOperation */
const raw = { const raw = {
commentId: this.commentId, commentId: this.commentId,
ranges: this.ranges.map(range => range.toRaw()), ranges: this.ranges.map(range => range.toRaw()),

View file

@ -1,6 +1,10 @@
// @ts-check // @ts-check
'use strict' '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 Operation = require('./')
const EditOperationBuilder = require('./edit_operation_builder') const EditOperationBuilder = require('./edit_operation_builder')
@ -32,7 +36,7 @@ class EditFileOperation extends Operation {
/** /**
* Deserialize an EditFileOperation. * Deserialize an EditFileOperation.
* *
* @param {Object} raw * @param {RawEditFileOperation} raw
* @return {EditFileOperation} * @return {EditFileOperation}
*/ */
static fromRaw(raw) { static fromRaw(raw) {
@ -52,13 +56,18 @@ class EditFileOperation extends Operation {
/** /**
* @inheritdoc * @inheritdoc
* @param {Snapshot} snapshot
*/ */
applyTo(snapshot) { applyTo(snapshot) {
// TODO(das7pad): can we teach typescript our polymorphism?
// @ts-ignore
snapshot.editFile(this.pathname, this.operation) snapshot.editFile(this.pathname, this.operation)
} }
/** /**
* @inheritdoc * @inheritdoc
* @param {Operation} other
* @return {boolean}
*/ */
canBeComposedWithForUndo(other) { canBeComposedWithForUndo(other) {
return ( return (
@ -69,6 +78,8 @@ class EditFileOperation extends Operation {
/** /**
* @inheritdoc * @inheritdoc
* @param {Operation} other
* @return {other is EditFileOperation}
*/ */
canBeComposedWith(other) { canBeComposedWith(other) {
// Ensure that other operation is an edit file operation // Ensure that other operation is an edit file operation
@ -81,6 +92,7 @@ class EditFileOperation extends Operation {
/** /**
* @inheritdoc * @inheritdoc
* @param {EditFileOperation} other
*/ */
compose(other) { compose(other) {
return new EditFileOperation( return new EditFileOperation(

View file

@ -1,5 +1,9 @@
const EditOperation = require('./edit_operation') const EditOperation = require('./edit_operation')
/**
* @typedef {import('../types').RawEditNoOperation} RawEditNoOperation
*/
class EditNoOperation extends EditOperation { class EditNoOperation extends EditOperation {
/** /**
* @inheritdoc * @inheritdoc
@ -9,7 +13,7 @@ class EditNoOperation extends EditOperation {
/** /**
* @inheritdoc * @inheritdoc
* @returns {object} * @returns {RawEditNoOperation}
*/ */
toJSON() { toJSON() {
return { return {

View file

@ -1,6 +1,7 @@
// @ts-check // @ts-check
/** /**
* @typedef {import('../file_data')} FileData * @typedef {import('../file_data')} FileData
* @typedef {import('../types').RawEditOperation} RawEditOperation
*/ */
class EditOperation { class EditOperation {
@ -12,7 +13,7 @@ class EditOperation {
/** /**
* Converts operation into a JSON value. * Converts operation into a JSON value.
* @returns {object} * @returns {RawEditOperation}
*/ */
toJSON() { toJSON() {
throw new Error('Abstract method not implemented') throw new Error('Abstract method not implemented')

View file

@ -142,7 +142,9 @@ class InsertOp extends ScanOp {
return current return current
} }
/** @inheritdoc */ /** @inheritdoc
* @param {ScanOp} other
*/
equals(other) { equals(other) {
if (!(other instanceof InsertOp)) { if (!(other instanceof InsertOp)) {
return false return false
@ -167,6 +169,10 @@ class InsertOp extends ScanOp {
return !other.commentIds return !other.commentIds
} }
/**
* @param {ScanOp} other
* @return {other is InsertOp}
*/
canMergeWith(other) { canMergeWith(other) {
if (!(other instanceof InsertOp)) { if (!(other instanceof InsertOp)) {
return false return false
@ -187,6 +193,9 @@ class InsertOp extends ScanOp {
return !other.commentIds return !other.commentIds
} }
/**
* @param {ScanOp} other
*/
mergeWith(other) { mergeWith(other) {
if (!this.canMergeWith(other)) { if (!this.canMergeWith(other)) {
throw new Error('Cannot merge with incompatible operation') throw new Error('Cannot merge with incompatible operation')
@ -202,6 +211,7 @@ class InsertOp extends ScanOp {
if (!this.tracking && !this.commentIds) { if (!this.tracking && !this.commentIds) {
return this.insertion return this.insertion
} }
/** @type RawInsertOp */
const obj = { i: this.insertion } const obj = { i: this.insertion }
if (this.tracking) { if (this.tracking) {
obj.tracking = this.tracking.toRaw() obj.tracking = this.tracking.toRaw()
@ -274,7 +284,9 @@ class RetainOp extends ScanOp {
return new RetainOp(op.r) return new RetainOp(op.r)
} }
/** @inheritdoc */ /** @inheritdoc
* @param {ScanOp} other
*/
equals(other) { equals(other) {
if (!(other instanceof RetainOp)) { if (!(other instanceof RetainOp)) {
return false return false
@ -288,6 +300,10 @@ class RetainOp extends ScanOp {
return !other.tracking return !other.tracking
} }
/**
* @param {ScanOp} other
* @return {other is RetainOp}
*/
canMergeWith(other) { canMergeWith(other) {
if (!(other instanceof RetainOp)) { if (!(other instanceof RetainOp)) {
return false return false
@ -298,6 +314,9 @@ class RetainOp extends ScanOp {
return !other.tracking return !other.tracking
} }
/**
* @param {ScanOp} other
*/
mergeWith(other) { mergeWith(other) {
if (!this.canMergeWith(other)) { if (!this.canMergeWith(other)) {
throw new Error('Cannot merge with incompatible operation') throw new Error('Cannot merge with incompatible operation')
@ -321,6 +340,9 @@ class RetainOp extends ScanOp {
} }
class RemoveOp extends ScanOp { class RemoveOp extends ScanOp {
/**
* @param {number} length
*/
constructor(length) { constructor(length) {
super() super()
if (length < 0) { if (length < 0) {
@ -352,7 +374,11 @@ class RemoveOp extends ScanOp {
return new RemoveOp(-op) return new RemoveOp(-op)
} }
/** @inheritdoc */ /**
* @inheritdoc
* @param {ScanOp} other
* @return {boolean}
*/
equals(other) { equals(other) {
if (!(other instanceof RemoveOp)) { if (!(other instanceof RemoveOp)) {
return false return false
@ -360,10 +386,17 @@ class RemoveOp extends ScanOp {
return this.length === other.length return this.length === other.length
} }
/**
* @param {ScanOp} other
* @return {other is RemoveOp}
*/
canMergeWith(other) { canMergeWith(other) {
return other instanceof RemoveOp return other instanceof RemoveOp
} }
/**
* @param {ScanOp} other
*/
mergeWith(other) { mergeWith(other) {
if (!this.canMergeWith(other)) { if (!this.canMergeWith(other)) {
throw new Error('Cannot merge with incompatible operation') throw new Error('Cannot merge with incompatible operation')

View file

@ -48,7 +48,7 @@ class SetCommentStateOperation extends EditOperation {
} }
/** /**
* * @param {StringFileData} previousState
* @returns {SetCommentStateOperation | EditNoOperation} * @returns {SetCommentStateOperation | EditNoOperation}
*/ */
invert(previousState) { invert(previousState) {
@ -77,7 +77,7 @@ class SetCommentStateOperation extends EditOperation {
/** /**
* @inheritdoc * @inheritdoc
* @param {EditOperation} other * @param {EditOperation} other
* @returns {EditOperation} * @returns {SetCommentStateOperation | core.DeleteCommentOperation}
*/ */
compose(other) { compose(other) {
if ( if (

View file

@ -35,6 +35,7 @@ const TrackingProps = require('../file_data/tracking_props')
* @typedef {import('../operation/scan_op').ScanOp} ScanOp * @typedef {import('../operation/scan_op').ScanOp} ScanOp
* @typedef {import('../file_data/tracked_change_list')} TrackedChangeList * @typedef {import('../file_data/tracked_change_list')} TrackedChangeList
* @typedef {import('../types').TrackingDirective} TrackingDirective * @typedef {import('../types').TrackingDirective} TrackingDirective
* @typedef {{tracking?: TrackingProps, commentIds?: string[]}} InsertOptions
*/ */
/** /**
@ -69,6 +70,10 @@ class TextOperation extends EditOperation {
this.targetLength = 0 this.targetLength = 0
} }
/**
* @param {TextOperation} other
* @return {boolean}
*/
equals(other) { equals(other) {
if (this.baseLength !== other.baseLength) { if (this.baseLength !== other.baseLength) {
return false return false
@ -129,7 +134,7 @@ class TextOperation extends EditOperation {
/** /**
* Insert a string at the current position. * Insert a string at the current position.
* @param {string | {i: string}} insertValue * @param {string | {i: string}} insertValue
* @param {{tracking?: TrackingProps, commentIds?: string[]}} opts * @param {InsertOptions} opts
* @returns {TextOperation} * @returns {TextOperation}
*/ */
insert(insertValue, opts = {}) { insert(insertValue, opts = {}) {
@ -328,6 +333,8 @@ class TextOperation extends EditOperation {
/** /**
* @inheritdoc * @inheritdoc
* @param {number} length of the original string; non-negative
* @return {number} length of the new string; non-negative
*/ */
applyToLength(length) { applyToLength(length) {
const operation = this const operation = this
@ -573,6 +580,7 @@ class TextOperation extends EditOperation {
op1 = ops1[i1++] op1 = ops1[i1++]
} }
} else if (op1 instanceof InsertOp && op2 instanceof RetainOp) { } else if (op1 instanceof InsertOp && op2 instanceof RetainOp) {
/** @type InsertOptions */
const opts = { const opts = {
commentIds: op1.commentIds, commentIds: op1.commentIds,
} }
@ -807,6 +815,10 @@ function getSimpleOp(operation) {
return null return null
} }
/**
* @param {TextOperation} operation
* @return {number}
*/
function getStartIndex(operation) { function getStartIndex(operation) {
if (operation.ops[0] instanceof RetainOp) { if (operation.ops[0] instanceof RetainOp) {
return operation.ops[0].length return operation.ops[0].length
@ -843,7 +855,10 @@ function calculateTrackingCommentSegments(
const breaks = new Set() const breaks = new Set()
const opStart = cursor const opStart = cursor
const opEnd = cursor + length 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) { function addBreak(rangeBoundary) {
if (rangeBoundary < opStart || rangeBoundary > opEnd) { if (rangeBoundary < opStart || rangeBoundary > opEnd) {
return return

View file

@ -2,6 +2,10 @@
const OError = require('@overleaf/o-error') const OError = require('@overleaf/o-error')
/**
* @typedef {import('./types').RawRange} RawRange
*/
class Range { class Range {
/** /**
* @param {number} pos * @param {number} pos
@ -17,10 +21,16 @@ class Range {
this.length = length this.length = length
} }
/**
* @return {number}
*/
get start() { get start() {
return this.pos return this.pos
} }
/**
* @return {number}
*/
get end() { get end() {
return this.pos + this.length return this.pos + this.length
} }
@ -193,6 +203,10 @@ class Range {
} }
} }
/**
* @param {RawRange} raw
* @return {Range}
*/
static fromRaw(raw) { static fromRaw(raw) {
return new Range(raw.pos, raw.length) return new Range(raw.pos, raw.length)
} }

View file

@ -1,4 +1,4 @@
/** @module */ // @ts-check
'use strict' 'use strict'
const path = require('path-browserify') const path = require('path-browserify')
@ -39,6 +39,7 @@ const MAX_PATH = 1024
/** /**
* Replace invalid characters and filename patterns in a filename with * Replace invalid characters and filename patterns in a filename with
* underscores. * underscores.
* @param {string} filename
*/ */
function cleanPart(filename) { function cleanPart(filename) {
filename = filename.replace(BAD_CHAR_RX, '_') filename = filename.replace(BAD_CHAR_RX, '_')

View file

@ -1,3 +1,4 @@
// @ts-check
'use strict' 'use strict'
const assert = require('check-types').assert const assert = require('check-types').assert
@ -10,9 +11,11 @@ const FILE_LOAD_CONCURRENCY = 50
/** /**
* @typedef {import("./types").BlobStore} BlobStore * @typedef {import("./types").BlobStore} BlobStore
* @typedef {import("./types").RawSnapshot} RawSnapshot
* @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore * @typedef {import("./types").ReadonlyBlobStore} ReadonlyBlobStore
* @typedef {import("./change")} Change * @typedef {import("./change")} Change
* @typedef {import("./operation/text_operation")} TextOperation * @typedef {import("./operation/text_operation")} TextOperation
* @typedef {import("./file")} File
*/ */
class EditMissingFileError extends OError {} class EditMissingFileError extends OError {}
@ -26,6 +29,10 @@ class Snapshot {
static PROJECT_VERSION_RX = new RegExp(Snapshot.PROJECT_VERSION_RX_STRING) static PROJECT_VERSION_RX = new RegExp(Snapshot.PROJECT_VERSION_RX_STRING)
static EditMissingFileError = EditMissingFileError static EditMissingFileError = EditMissingFileError
/**
* @param {RawSnapshot} raw
* @return {Snapshot}
*/
static fromRaw(raw) { static fromRaw(raw) {
assert.object(raw.files, 'bad raw.files') assert.object(raw.files, 'bad raw.files')
return new Snapshot( return new Snapshot(
@ -36,6 +43,7 @@ class Snapshot {
} }
toRaw() { toRaw() {
/** @type RawSnapshot */
const raw = { const raw = {
files: this.fileMap.toRaw(), files: this.fileMap.toRaw(),
} }
@ -64,6 +72,9 @@ class Snapshot {
return this.projectVersion return this.projectVersion
} }
/**
* @param {string} projectVersion
*/
setProjectVersion(projectVersion) { setProjectVersion(projectVersion) {
assert.maybe.match( assert.maybe.match(
projectVersion, projectVersion,
@ -80,6 +91,9 @@ class Snapshot {
return this.v2DocVersions return this.v2DocVersions
} }
/**
* @param {V2DocVersions} v2DocVersions
*/
setV2DocVersions(v2DocVersions) { setV2DocVersions(v2DocVersions) {
assert.maybe.instance( assert.maybe.instance(
v2DocVersions, v2DocVersions,
@ -89,6 +103,9 @@ class Snapshot {
this.v2DocVersions = v2DocVersions this.v2DocVersions = v2DocVersions
} }
/**
* @param {V2DocVersions} v2DocVersions
*/
updateV2DocVersions(v2DocVersions) { updateV2DocVersions(v2DocVersions) {
// merge new v2DocVersions into this.v2DocVersions // merge new v2DocVersions into this.v2DocVersions
v2DocVersions.applyTo(this) v2DocVersions.applyTo(this)
@ -114,6 +131,7 @@ class Snapshot {
/** /**
* Get a File by its pathname. * Get a File by its pathname.
* @see FileMap#getFile * @see FileMap#getFile
* @param {string} pathname
*/ */
getFile(pathname) { getFile(pathname) {
return this.fileMap.getFile(pathname) return this.fileMap.getFile(pathname)
@ -122,6 +140,8 @@ class Snapshot {
/** /**
* Add the given file to the snapshot. * Add the given file to the snapshot.
* @see FileMap#addFile * @see FileMap#addFile
* @param {string} pathname
* @param {File} file
*/ */
addFile(pathname, file) { addFile(pathname, file) {
this.fileMap.addFile(pathname, file) this.fileMap.addFile(pathname, file)
@ -130,6 +150,8 @@ class Snapshot {
/** /**
* Move or remove a file. * Move or remove a file.
* @see FileMap#moveFile * @see FileMap#moveFile
* @param {string} pathname
* @param {string} newPathname
*/ */
moveFile(pathname, newPathname) { moveFile(pathname, newPathname) {
this.fileMap.moveFile(pathname, newPathname) this.fileMap.moveFile(pathname, newPathname)
@ -185,13 +207,18 @@ class Snapshot {
* @param {Set.<String>} blobHashes * @param {Set.<String>} blobHashes
*/ */
findBlobHashes(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 hash = file.getHash()
const rangeHash = file.getRangesHash() const rangeHash = file.getRangesHash()
if (hash) blobHashes.add(hash) if (hash) blobHashes.add(hash)
if (rangeHash) blobHashes.add(rangeHash) 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 * values are the files in the snapshot
*/ */
async loadFiles(kind, blobStore) { async loadFiles(kind, blobStore) {
return await this.fileMap.mapAsync( /**
file => file.load(kind, blobStore), * @param {File} file
FILE_LOAD_CONCURRENCY */
) 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 const rawV2DocVersions = this.v2DocVersions
? this.v2DocVersions.toRaw() ? this.v2DocVersions.toRaw()
: undefined : 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 { return {
files: rawFiles, files: rawFiles,
projectVersion, projectVersion,

View file

@ -17,19 +17,19 @@ export type RangesBlob = {
trackedChanges: TrackedChangeRawData[] trackedChanges: TrackedChangeRawData[]
} }
type Range = { export type RawRange = {
pos: number pos: number
length: number length: number
} }
export type CommentRawData = { export type CommentRawData = {
id: string id: string
ranges: Range[] ranges: RawRange[]
resolved?: boolean resolved?: boolean
} }
export type TrackedChangeRawData = { export type TrackedChangeRawData = {
range: Range range: RawRange
tracking: TrackingPropsRawData tracking: TrackingPropsRawData
} }
@ -51,6 +51,35 @@ export type StringFileRawData = {
trackedChanges?: TrackedChangeRawData[] 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 RawV2DocVersions = Record<string, { pathname: string; v: number }>
export type RawInsertOp = export type RawInsertOp =
@ -78,7 +107,7 @@ export type RawTextOperation = {
export type RawAddCommentOperation = { export type RawAddCommentOperation = {
commentId: string commentId: string
ranges: Range[] ranges: RawRange[]
resolved?: boolean resolved?: boolean
} }
@ -89,14 +118,28 @@ export type RawSetCommentStateOperation = {
resolved: boolean resolved: boolean
} }
export type RawEditNoOperation = {
noOp: true
}
export type RawEditFileOperation = RawEditOperation & { pathname: string }
export type RawEditOperation = export type RawEditOperation =
| RawTextOperation | RawTextOperation
| RawAddCommentOperation | RawAddCommentOperation
| RawDeleteCommentOperation | RawDeleteCommentOperation
| RawSetCommentStateOperation | RawSetCommentStateOperation
| RawEditNoOperation
export type LinkedFileData = { export type LinkedFileData = {
importedAt: string importedAt: string
provider: string provider: string
[other: string]: any [other: string]: any
} }
export type RawLabel = {
text: string
authorId: number | null
timestamp: string
version: number
}

View file

@ -1,9 +1,11 @@
// @ts-check
'use strict' 'use strict'
const _ = require('lodash') const _ = require('lodash')
/** /**
* @typedef {import("./file")} File * @typedef {import("./file")} File
* @typedef {import("./snapshot")} Snapshot
* @typedef {import("./types").RawV2DocVersions} RawV2DocVersions * @typedef {import("./types").RawV2DocVersions} RawV2DocVersions
*/ */
@ -15,13 +17,17 @@ class V2DocVersions {
this.data = data || {} this.data = data || {}
} }
/**
* @param {RawV2DocVersions?} [raw]
* @return {V2DocVersions|undefined}
*/
static fromRaw(raw) { static fromRaw(raw) {
if (!raw) return undefined if (!raw) return undefined
return new V2DocVersions(raw) return new V2DocVersions(raw)
} }
/** /**
* @abstract * @return {?RawV2DocVersions}
*/ */
toRaw() { toRaw() {
if (!this.data) return null if (!this.data) return null
@ -32,12 +38,15 @@ class V2DocVersions {
/** /**
* Clone this object. * Clone this object.
* *
* @return {V2DocVersions} a new object of the same type * @return {V2DocVersions|undefined} a new object of the same type
*/ */
clone() { clone() {
return V2DocVersions.fromRaw(this.toRaw()) return V2DocVersions.fromRaw(this.toRaw())
} }
/**
* @param {Snapshot} snapshot
*/
applyTo(snapshot) { applyTo(snapshot) {
// Only update the snapshot versions if we have new versions // Only update the snapshot versions if we have new versions
if (!_.size(this.data)) return if (!_.size(this.data)) return
@ -54,6 +63,8 @@ class V2DocVersions {
/** /**
* Move or remove a doc. * Move or remove a doc.
* Must be called after FileMap#moveFile, which validates the paths. * Must be called after FileMap#moveFile, which validates the paths.
* @param {string} pathname
* @param {string} newPathname
*/ */
moveFile(pathname, newPathname) { moveFile(pathname, newPathname) {
for (const [id, v] of Object.entries(this.data)) { for (const [id, v] of Object.entries(this.data)) {

View file

@ -5,10 +5,10 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "npm run lint && npm run format && npm run types:check && npm run test:unit", "test": "npm run lint && npm run format && npm run types:check && npm run test:unit",
"format": "prettier --list-different $PWD/'**/*.{js,cjs}'", "format": "prettier --list-different $PWD/'**/*.{js,cjs,ts}'",
"format:fix": "prettier --write $PWD/'**/*.{js,cjs}'", "format:fix": "prettier --write $PWD/'**/*.{js,cjs,ts}'",
"lint": "eslint --ext .js --ext .cjs --max-warnings 0 --format unix .", "lint": "eslint --ext .js --ext .cjs --ext .ts --max-warnings 0 --format unix .",
"lint:fix": "eslint --fix --ext .js --ext .cjs .", "lint:fix": "eslint --fix --ext .js --ext .cjs --ext .ts .",
"test:ci": "npm run test:unit", "test:ci": "npm run test:unit",
"test:unit": "mocha --exit test/**/*.{js,cjs}", "test:unit": "mocha --exit test/**/*.{js,cjs}",
"types:check": "tsc --noEmit" "types:check": "tsc --noEmit"
@ -17,6 +17,8 @@
"license": "Proprietary", "license": "Proprietary",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/check-types": "^7.3.7",
"@types/path-browserify": "^1.0.2",
"chai": "^3.3.0", "chai": "^3.3.0",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"sinon": "^9.2.4", "sinon": "^9.2.4",

30
package-lock.json generated
View file

@ -397,6 +397,8 @@
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/check-types": "^7.3.7",
"@types/path-browserify": "^1.0.2",
"chai": "^3.3.0", "chai": "^3.3.0",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"sinon": "^9.2.4", "sinon": "^9.2.4",
@ -12532,6 +12534,12 @@
"@types/chai": "*" "@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": { "node_modules/@types/connect": {
"version": "3.4.35", "version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -13015,6 +13023,12 @@
"@types/passport": "*" "@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": { "node_modules/@types/pg": {
"version": "8.6.1", "version": "8.6.1",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
@ -44847,6 +44861,7 @@
"mock-fs": "^5.1.2", "mock-fs": "^5.1.2",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nvd3": "^1.8.6", "nvd3": "^1.8.6",
"overleaf-editor-core": "*",
"pdfjs-dist213": "npm:pdfjs-dist@2.13.216", "pdfjs-dist213": "npm:pdfjs-dist@2.13.216",
"pdfjs-dist401": "npm:pdfjs-dist@4.5.136", "pdfjs-dist401": "npm:pdfjs-dist@4.5.136",
"pirates": "^4.0.1", "pirates": "^4.0.1",
@ -53363,6 +53378,7 @@
"nvd3": "^1.8.6", "nvd3": "^1.8.6",
"on-headers": "^1.0.2", "on-headers": "^1.0.2",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"overleaf-editor-core": "*",
"p-limit": "^2.3.0", "p-limit": "^2.3.0",
"p-props": "4.0.0", "p-props": "4.0.0",
"parse-data-url": "^2.0.0", "parse-data-url": "^2.0.0",
@ -57422,6 +57438,12 @@
"@types/chai": "*" "@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": { "@types/connect": {
"version": "3.4.35", "version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@ -57904,6 +57926,12 @@
"@types/passport": "*" "@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": { "@types/pg": {
"version": "8.6.1", "version": "8.6.1",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz",
@ -72798,6 +72826,8 @@
"version": "file:libraries/overleaf-editor-core", "version": "file:libraries/overleaf-editor-core",
"requires": { "requires": {
"@overleaf/o-error": "*", "@overleaf/o-error": "*",
"@types/check-types": "^7.3.7",
"@types/path-browserify": "^1.0.2",
"chai": "^3.3.0", "chai": "^3.3.0",
"check-types": "^5.1.0", "check-types": "^5.1.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",

View file

@ -296,7 +296,7 @@ class BlobStore {
* Read a blob metadata record by hexadecimal hash. * Read a blob metadata record by hexadecimal hash.
* *
* @param {string} hash hexadecimal SHA-1 hash * @param {string} hash hexadecimal SHA-1 hash
* @return {Promise.<core.Blob?>} * @return {Promise<core.Blob | null>}
*/ */
async getBlob(hash) { async getBlob(hash) {
assert.blobHash(hash, 'bad hash') assert.blobHash(hash, 'bad hash')

View file

@ -0,0 +1 @@
import 'overleaf-editor-core'

View file

@ -320,6 +320,7 @@
"mock-fs": "^5.1.2", "mock-fs": "^5.1.2",
"node-fetch": "^2.6.7", "node-fetch": "^2.6.7",
"nvd3": "^1.8.6", "nvd3": "^1.8.6",
"overleaf-editor-core": "*",
"pdfjs-dist213": "npm:pdfjs-dist@2.13.216", "pdfjs-dist213": "npm:pdfjs-dist@2.13.216",
"pdfjs-dist401": "npm:pdfjs-dist@4.5.136", "pdfjs-dist401": "npm:pdfjs-dist@4.5.136",
"pirates": "^4.0.1", "pirates": "^4.0.1",