From c1594c1687f782eda35c8e2bc601701cac66260f Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Mon, 18 Sep 2023 15:38:43 +0500 Subject: [PATCH] core: convert everything else to typescript --- packages/core/package-lock.json | 7 + packages/core/package.json | 1 + .../core/src/database/cached-collection.ts | 5 +- packages/core/src/{logger.js => logger.ts} | 76 ++-- .../core/src/utils/{map.js => buffer-pool.ts} | 21 +- .../src/utils/{constants.js => constants.ts} | 21 +- .../src/utils/{filename.js => filename.ts} | 32 +- .../src/utils/{hostname.js => hostname.ts} | 15 +- packages/core/src/utils/{http.js => http.ts} | 184 +++++----- packages/core/src/utils/id.ts | 6 +- packages/core/src/utils/object-id.js | 336 ------------------ packages/core/src/utils/object-id.ts | 51 +++ 12 files changed, 232 insertions(+), 523 deletions(-) rename packages/core/src/{logger.js => logger.ts} (72%) rename packages/core/src/utils/{map.js => buffer-pool.ts} (70%) rename packages/core/src/utils/{constants.js => constants.ts} (77%) rename packages/core/src/utils/{filename.js => filename.ts} (81%) rename packages/core/src/utils/{hostname.js => hostname.ts} (79%) rename packages/core/src/utils/{http.js => http.ts} (59%) delete mode 100644 packages/core/src/utils/object-id.js create mode 100644 packages/core/src/utils/object-id.ts diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index f68318fbe..d1f3a642b 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -33,6 +33,7 @@ "@types/event-source-polyfill": "^1.0.1", "@types/html-to-text": "^9.0.0", "@types/katex": "^0.16.2", + "@types/mime-db": "^1.43.1", "@types/prismjs": "^1.26.0", "@types/spark-md5": "^3.0.2", "@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6", @@ -1688,6 +1689,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mime-db": { + "version": "1.43.5", + "resolved": "https://registry.npmjs.org/@types/mime-db/-/mime-db-1.43.5.tgz", + "integrity": "sha512-/bfTiIUTNPUBnwnYvUxXAre5MhD88jgagLEQiQtIASjU+bwxd8kS/ASDA4a8ufd8m0Lheu6eeMJHEUpLHoJ28A==", + "dev": true + }, "node_modules/@types/node": { "version": "18.11.9", "dev": true, diff --git a/packages/core/package.json b/packages/core/package.json index 81fbb5896..03fd7bcb6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,6 +12,7 @@ "@types/event-source-polyfill": "^1.0.1", "@types/html-to-text": "^9.0.0", "@types/katex": "^0.16.2", + "@types/mime-db": "^1.43.1", "@types/prismjs": "^1.26.0", "@types/spark-md5": "^3.0.2", "@types/streetwriters__showdown": "npm:@types/showdown@^2.0.6", diff --git a/packages/core/src/database/cached-collection.ts b/packages/core/src/database/cached-collection.ts index 410a6202e..7e6ba538f 100644 --- a/packages/core/src/database/cached-collection.ts +++ b/packages/core/src/database/cached-collection.ts @@ -18,7 +18,6 @@ along with this program. If not, see . */ import { IndexedCollection } from "./indexed-collection"; -import MapStub from "../utils/map"; import { CollectionType, Collections, @@ -51,9 +50,7 @@ export class CachedCollection< const data = await this.collection.indexer.readMulti( this.collection.indexer.indices ); - if ("dispose" in this.cache && typeof this.cache.dispose === "function") - this.cache.dispose(); - this.cache = new MapStub.Map(data); + this.cache = new Map(data); } async add(item: MaybeDeletedItem) { diff --git a/packages/core/src/logger.js b/packages/core/src/logger.ts similarity index 72% rename from packages/core/src/logger.js rename to packages/core/src/logger.ts index e0fc6b047..31588b4fc 100644 --- a/packages/core/src/logger.js +++ b/packages/core/src/logger.ts @@ -18,13 +18,17 @@ along with this program. If not, see . */ import { + ILogReporter, LogLevel, + LogMessage, Logger, NoopLogger, combineReporters, consoleReporter, - format + format, + ILogger } from "@notesnook/logger"; +import { IStorage } from "./interfaces"; const WEEK = 86400000 * 7; @@ -35,32 +39,21 @@ const WEEK = 86400000 * 7; // 4. Implement functions for log retrieval & filtering class DatabaseLogReporter { - /** - * - * @param {import("./database/crypto").default} storage - */ - constructor(storage) { + writer: DatabaseLogWriter; + constructor(storage: IStorage) { this.writer = new DatabaseLogWriter(storage); } - /** - * - * @param {import("@notesnook/logger").LogMessage} log - */ - write(log) { + write(log: LogMessage) { this.writer.push(log); } } class DatabaseLogWriter { - /** - * - * @param {import("./database/crypto").default} storage - */ - constructor(storage) { - this.storage = storage; - this.queue = new Map(); - this.hasCleared = false; + private queue: Map = new Map(); + private hasCleared = false; + + constructor(private readonly storage: IStorage) { setInterval(() => { setTimeout(() => { if (!this.hasCleared) { @@ -72,7 +65,7 @@ class DatabaseLogWriter { }, 10000); } - push(message) { + push(message: LogMessage) { const key = new Date(message.timestamp).toLocaleDateString(); this.queue.set(`${key}:${message.timestamp}`, message); } @@ -100,33 +93,27 @@ class DatabaseLogWriter { } class DatabaseLogManager { - /** - * - * @param {import("./database/crypto").default} storage - */ - constructor(storage) { - this.storage = storage; - } + constructor(private readonly storage: IStorage) {} async get() { const logKeys = await this.storage.getAllKeys(); - const logs = await this.storage.readMulti(logKeys); - const logGroups = {}; + const logEntries = await this.storage.readMulti(logKeys); + const logs: Record = {}; - for (const [key, log] of logs) { - const keyParts = key.split(":"); + for (const [logKey, log] of logEntries) { + const keyParts = logKey.split(":"); if (keyParts.length === 1) continue; - const groupKey = keyParts[0]; - if (!logGroups[groupKey]) logGroups[groupKey] = []; - logGroups[groupKey].push(log); + const key = keyParts[0]; + if (!logs[key]) logs[key] = []; + logs[key].push(log); } - return Object.keys(logGroups) + return Object.keys(logs) .sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) .map((key) => ({ key, - logs: logGroups[key]?.sort((a, b) => a.timestamp - b.timestamp) + logs: logs[key]?.sort((a, b) => a.timestamp - b.timestamp) })); } @@ -135,7 +122,7 @@ class DatabaseLogManager { await this.storage.removeMulti(logKeys); } - async delete(key) { + async delete(key: string) { const logKeys = await this.storage.getAllKeys(); const keysToRemove = []; for (const logKey of logKeys) { @@ -148,9 +135,9 @@ class DatabaseLogManager { } } -function initialize(storage, disableConsoleLogs) { +function initialize(storage: IStorage, disableConsoleLogs?: boolean) { if (storage) { - let reporters = [new DatabaseLogReporter(storage)]; + const reporters: ILogReporter[] = [new DatabaseLogReporter(storage)]; if (process.env.NODE_ENV !== "production" && !disableConsoleLogs) reporters.push(consoleReporter); logger = new Logger({ @@ -161,14 +148,7 @@ function initialize(storage, disableConsoleLogs) { } } -/** - * @type {import("@notesnook/logger").ILogger} - */ -var logger = new NoopLogger(); - -/** - * @type {DatabaseLogManager | undefined} - */ -var logManager; +let logger: ILogger = new NoopLogger(); +let logManager: DatabaseLogManager | undefined = undefined; export { LogLevel, format, initialize, logManager, logger }; diff --git a/packages/core/src/utils/map.js b/packages/core/src/utils/buffer-pool.ts similarity index 70% rename from packages/core/src/utils/map.js rename to packages/core/src/utils/buffer-pool.ts index f93568f86..dfae03a3e 100644 --- a/packages/core/src/utils/map.js +++ b/packages/core/src/utils/buffer-pool.ts @@ -17,15 +17,20 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -var map = Map; -class MapStub { - override(replacement) { - map = replacement; +export class BufferPool { + private freeBuffers: Buffer[] = []; + constructor(private readonly size: number) {} + + alloc() { + return this.freeBuffers.pop() || this.allocNew(); } - get Map() { - return map; + private allocNew() { + return Buffer.alloc(this.size); + } + + free(buf: Buffer) { + this.freeBuffers.push(buf); + return true; } } - -export default new MapStub(); diff --git a/packages/core/src/utils/constants.js b/packages/core/src/utils/constants.ts similarity index 77% rename from packages/core/src/utils/constants.js rename to packages/core/src/utils/constants.ts index adbfe225e..1533c1aea 100644 --- a/packages/core/src/utils/constants.js +++ b/packages/core/src/utils/constants.ts @@ -45,14 +45,15 @@ export const hosts = { export default hosts; -export const getServerNameFromHost = (host) => { - const names = { - [extractHostname(hosts.API_HOST)]: "Notesnook Sync Server", - [extractHostname(hosts.AUTH_HOST)]: "Authentication Server", - [extractHostname(hosts.SSE_HOST)]: "Eventing Server", - [extractHostname(hosts.SUBSCRIPTIONS_HOST)]: - "Subscriptions Management Server", - [extractHostname(hosts.ISSUES_HOST)]: "Bug Reporting Server" - }; - return names[host]; +const HOSTNAMES = { + [extractHostname(hosts.API_HOST)]: "Notesnook Sync Server", + [extractHostname(hosts.AUTH_HOST)]: "Authentication Server", + [extractHostname(hosts.SSE_HOST)]: "Eventing Server", + [extractHostname(hosts.SUBSCRIPTIONS_HOST)]: + "Subscriptions Management Server", + [extractHostname(hosts.ISSUES_HOST)]: "Bug Reporting Server" +}; + +export const getServerNameFromHost = (host: string) => { + return HOSTNAMES[host]; }; diff --git a/packages/core/src/utils/filename.js b/packages/core/src/utils/filename.ts similarity index 81% rename from packages/core/src/utils/filename.js rename to packages/core/src/utils/filename.ts index a16a7cf82..2b416b308 100644 --- a/packages/core/src/utils/filename.js +++ b/packages/core/src/utils/filename.ts @@ -16,22 +16,13 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ + import db from "mime-db"; -// type MimeTypeInfo = { -// source: string; -// extensions?: string[]; -// charset?: string; -// compressible?: boolean; -// }; - -/** - * - * @param {string} filename - * @param {string | undefined} mime - * @returns {string} - */ -export function getFileNameWithExtension(filename, mime) { +export function getFileNameWithExtension( + filename: string, + mime: string | undefined +): string { if (!mime || mime === "application/octet-stream") return filename; const mimeData = db[mime]; if (!mimeData || !mimeData.extensions || mimeData.extensions.length === 0) @@ -59,24 +50,23 @@ export const DocumentMimeTypes = [ "application/vnd.oasis.opendocument.presentation" ]; -export const WebClipMimeType = "application/vnd.notesnook.web-clip"; - -export function isDocument(mime) { +export function isDocument(mime: string) { return DocumentMimeTypes.some((a) => a.startsWith(mime)); } -export function isWebClip(mime) { +export const WebClipMimeType = "application/vnd.notesnook.web-clip"; +export function isWebClip(mime: string) { return mime === WebClipMimeType; } -export function isImage(mime) { +export function isImage(mime: string) { return mime.startsWith("image/"); } -export function isVideo(mime) { +export function isVideo(mime: string) { return mime.startsWith("video/"); } -export function isAudio(mime) { +export function isAudio(mime: string) { return mime.startsWith("audio/"); } diff --git a/packages/core/src/utils/hostname.js b/packages/core/src/utils/hostname.ts similarity index 79% rename from packages/core/src/utils/hostname.js rename to packages/core/src/utils/hostname.ts index bbc1dd2b1..6b6711732 100644 --- a/packages/core/src/utils/hostname.js +++ b/packages/core/src/utils/hostname.ts @@ -17,20 +17,13 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -export function extractHostname(url) { - var hostname; +export function extractHostname(url: string) { //find & remove protocol (http, ftp, etc.) and get hostname - - if (url.indexOf("//") > -1) { - hostname = url.split("/")[2]; - } else { - hostname = url.split("/")[0]; - } + const hostname: string = + url.indexOf("//") > -1 ? url.split("/")[2] : url.split("/")[0]; //find & remove port number // hostname = hostname.split(":")[0]; //find & remove "?" - hostname = hostname.split("?")[0]; - - return hostname; + return hostname.split("?")[0]; } diff --git a/packages/core/src/utils/http.js b/packages/core/src/utils/http.ts similarity index 59% rename from packages/core/src/utils/http.js rename to packages/core/src/utils/http.ts index 28497facf..ce256919c 100644 --- a/packages/core/src/utils/http.js +++ b/packages/core/src/utils/http.ts @@ -22,28 +22,43 @@ import { logger } from "../logger"; import { getServerNameFromHost } from "./constants"; import { extractHostname } from "./hostname"; -function get(url, token) { - return request(url, token, "GET"); +type ContentType = "application/json" | "application/x-www-form-urlencoded"; +type RequestBody = Record | null; +type JsonRequestBody = Record | null; +function get(url: string, token?: string) { + return request(url, "GET", token); } -function deleteRequest(url, token) { - return request(url, token, "DELETE"); +function deleteRequest(url: string, token?: string) { + return request(url, "DELETE", token); } -function patch(url, data, token) { - return bodyRequest(url, data, token, "PATCH"); +function patch(url: string, data: RequestBody, token?: string) { + return bodyRequest(url, transformFormData(data), token, "PATCH"); } -patch.json = function (url, data, token) { - return bodyRequest(url, data, token, "PATCH", "application/json"); +patch.json = function (url: string, data: JsonRequestBody, token?: string) { + return bodyRequest( + url, + transformJson(data), + token, + "PATCH", + "application/json" + ); }; -function post(url, data, token) { - return bodyRequest(url, data, token, "POST"); +function post(url: string, data: RequestBody, token?: string) { + return bodyRequest(url, transformFormData(data), token, "POST"); } -post.json = function (url, data, token) { - return bodyRequest(url, data, token, "POST", "application/json"); +post.json = function (url: string, data: JsonRequestBody, token?: string) { + return bodyRequest( + url, + transformJson(data), + token, + "POST", + "application/json" + ); }; export default { @@ -53,84 +68,40 @@ export default { patch }; -function transformer(data, type) { - if (!data) return; - if (type === "application/json") return JSON.stringify(data); - else { - return Object.entries(data) - .map(([key, value]) => - value ? `${encodeURIComponent(key)}=${encodeURIComponent(value)}` : "" - ) - .join("&"); - } -} - -/** - * - * @param {Response} response - * @returns - */ -async function handleResponse(response) { - try { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - const json = await response.json(); - if (response.ok) { - return json; - } - throw new RequestError(errorTransformer(json)); - } else { - if (response.status === 429) - throw new Error("You are being rate limited."); - - if (response.ok) return await response.text(); - else if (response.status === 401) { - EV.publish(EVENTS.userUnauthorized, response.url); - throw new Error("Unauthorized."); - } else - throw new Error( - `Request failed with status code: ${response.status} ${response.statusText}.` - ); - } - } catch (e) { - logger.error(e, "Error while sending request:", { url: response.url }); - throw e; - } -} - -async function request(url, token, method) { +async function request(url: string, method: "GET" | "DELETE", token?: string) { return handleResponse( await fetchWrapped(url, { method, - headers: getAuthorizationHeader(token) + headers: getHeaders(token) }) ); } async function bodyRequest( - url, - data, - token, - method, - contentType = "application/x-www-form-urlencoded" + url: string, + data: string | undefined, + token: string | undefined, + method: "POST" | "PATCH" | "PUT", + contentType: ContentType = "application/x-www-form-urlencoded" ) { return handleResponse( await fetchWrapped(url, { method, - body: transformer(data, contentType), + body: data, headers: { - ...getAuthorizationHeader(token), + ...getHeaders(token), "Content-Type": contentType } }) ); } -function getAuthorizationHeader(token) { - return token ? { Authorization: "Bearer " + token } : {}; -} - -export function errorTransformer(errorJson) { +export function errorTransformer(errorJson: { + error?: string; + errors?: string[]; + error_description?: string; + data?: string; +}) { let errorMessage = "Unknown error."; let errorCode = "unknown"; @@ -156,8 +127,8 @@ export function errorTransformer(errorJson) { } } default: - errorMessage = error_description || error || "An unknown error occurred."; - errorCode = error; + errorMessage = error_description || error || errorMessage; + errorCode = error || errorCode; break; } @@ -168,12 +139,7 @@ export function errorTransformer(errorJson) { }; } -/** - * - * @param {RequestInfo} input - * @param {RequestInit} init - */ -async function fetchWrapped(input, init) { +async function fetchWrapped(input: string, init: RequestInit) { try { const response = await fetch(input, init); return response; @@ -182,17 +148,73 @@ async function fetchWrapped(input, init) { const serverName = getServerNameFromHost(host); if (serverName) throw new Error( - `${serverName} is not responding. Please check your internet connection. If the problem persists, feel free email us at support@streetwriters.co. (Reference error: ${e.message})` + `${serverName} is not responding. Please check your internet connection. If the problem persists, feel free email us at support@streetwriters.co. (Reference error: ${ + (e as Error).message + })` ); throw e; } } +async function handleResponse(response: Response) { + try { + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + const json = await response.json(); + if (response.ok) { + return json; + } + throw new RequestError(errorTransformer(json)); + } else { + if (response.status === 429) + throw new Error("You are being rate limited."); + + if (response.ok) return await response.text(); + else if (response.status === 401) { + EV.publish(EVENTS.userUnauthorized, response.url); + throw new Error("Unauthorized."); + } else + throw new Error( + `Request failed with status code: ${response.status} ${response.statusText}.` + ); + } + } catch (e) { + logger.error(e as Error, "Error while sending request:", { + url: response.url + }); + throw e; + } +} + export class RequestError extends Error { - constructor(error) { + code: string; + data: unknown; + constructor(error: { code: string; data: unknown; description: string }) { super(error.description); this.code = error.code; this.data = error.data; } } + +function getHeaders(token?: string | null) { + return token ? { Authorization: "Bearer " + token } : undefined; +} + +function transformJson(data: JsonRequestBody) { + return JSON.stringify(data); +} + +function transformFormData(data: RequestBody) { + if (data) { + return Object.entries(data) + .map(([key, value]) => + value + ? `${encodeURIComponent(key)}=${ + value ? encodeURIComponent(value) : "" + }` + : "" + ) + .join("&"); + } +} diff --git a/packages/core/src/utils/id.ts b/packages/core/src/utils/id.ts index daece54b6..761210a14 100644 --- a/packages/core/src/utils/id.ts +++ b/packages/core/src/utils/id.ts @@ -18,12 +18,10 @@ along with this program. If not, see . */ import SparkMD5 from "spark-md5"; -import ObjectID from "./object-id"; +import { createObjectId } from "./object-id"; export function getId(time?: number) { - if (time) - return new ObjectID(new ObjectID().generate(time / 1000)).toHexString(); - return new ObjectID().toHexString(); + return createObjectId(time); } export function makeId(text: string) { diff --git a/packages/core/src/utils/object-id.js b/packages/core/src/utils/object-id.js deleted file mode 100644 index 3a3a6c0bc..000000000 --- a/packages/core/src/utils/object-id.js +++ /dev/null @@ -1,336 +0,0 @@ -/* -This file is part of the Notesnook project (https://notesnook.com/) - -Copyright (C) 2023 Streetwriters (Private) Limited - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -// Copied from https://github.com/williamkapke/bson-objectid - -import { randomInt } from "./random"; - -var MACHINE_ID = randomInt(); -var index = randomInt(); -var pid = - (typeof process === "undefined" || typeof process.pid !== "number" - ? randomInt() - : process.pid) % 0xffff; - -/** - * Determine if an object is Buffer - * - * Author: Feross Aboukhadijeh - * License: MIT - * - */ -var isBuffer = function (obj) { - return !!( - obj != null && - obj.constructor && - typeof obj.constructor.isBuffer === "function" && - obj.constructor.isBuffer(obj) - ); -}; - -// Precomputed hex table enables speedy hex string conversion -var hexTable = []; -for (var i = 0; i < 256; i++) { - hexTable[i] = (i <= 15 ? "0" : "") + i.toString(16); -} - -// Regular expression that checks for hex value -var checkForHexRegExp = new RegExp("^[0-9a-fA-F]{24}$"); - -// Lookup tables -var decodeLookup = []; -i = 0; -while (i < 10) decodeLookup[0x30 + i] = i++; -while (i < 16) decodeLookup[0x41 - 10 + i] = decodeLookup[0x61 - 10 + i] = i++; - -/** - * Create a new immutable ObjectID instance - * - * @class Represents the BSON ObjectID type - * @param {String|Number} id Can be a 24 byte hex string, 12 byte binary string or a Number. - * @return {Object} instance of ObjectID. - */ -class ObjectID { - constructor(id) { - if (!(this instanceof ObjectID)) return new ObjectID(id); - if (id && (id instanceof ObjectID || id._bsontype === "ObjectID")) - return id; - - this._bsontype = "ObjectID"; - - // The most common usecase (blank id, new objectId instance) - if (id == null || typeof id === "number") { - // Generate a new id - this.id = this.generate(id); - // Return the object - return; - } - - // Check if the passed in id is valid - var valid = ObjectID.isValid(id); - - // Throw an error if it's not a valid setup - if (!valid && id != null) { - throw new Error( - "Argument passed in must be a single String of 12 bytes or a string of 24 hex characters" - ); - } else if (valid && typeof id === "string" && id.length === 24) { - return ObjectID.createFromHexString(id); - } else if (id != null && id.length === 12) { - // assume 12 byte string - this.id = id; - } else if (id != null && typeof id.toHexString === "function") { - // Duck-typing to support ObjectId from different npm packages - return id; - } else { - throw new Error( - "Argument passed in must be a single String of 12 bytes or a string of 24 hex characters" - ); - } - } - /** - * Creates an ObjectID from a second based number, with the rest of the ObjectID zeroed out. Used for comparisons or sorting the ObjectID. - * - * @param {Number} time an integer number representing a number of seconds. - * @return {ObjectID} return the created ObjectID - * @api public - */ - static createFromTime(time) { - time = parseInt(time, 10) % 0xffffffff; - return new ObjectID(hex(8, time) + "0000000000000000"); - } - /** - * Creates an ObjectID from a hex string representation of an ObjectID. - * - * @param {String} hexString create a ObjectID from a passed in 24 byte hexstring. - * @return {ObjectID} return the created ObjectID - * @api public - */ - static createFromHexString(hexString) { - // Throw an error if it's not a valid setup - if ( - typeof hexString === "undefined" || - (hexString != null && hexString.length !== 24) - ) { - throw new Error( - "Argument passed in must be a single String of 12 bytes or a string of 24 hex characters" - ); - } - - // Calculate lengths - var data = ""; - var i = 0; - - while (i < 24) { - data += String.fromCharCode( - (decodeLookup[hexString.charCodeAt(i++)] << 4) | - decodeLookup[hexString.charCodeAt(i++)] - ); - } - - return new ObjectID(data); - } - /** - * Checks if a value is a valid bson ObjectId - * - * @param {String} objectid Can be a 24 byte hex string or an instance of ObjectID. - * @return {Boolean} return true if the value is a valid bson ObjectID, return false otherwise. - * @api public - * - * THE NATIVE DOCUMENTATION ISN'T CLEAR ON THIS GUY! - * http://mongodb.github.io/node-mongodb-native/api-bson-generated/objectid.html#objectid-isvalid - */ - static isValid(id) { - if (id == null) return false; - - if (typeof id === "number") { - return true; - } - - if (typeof id === "string") { - return ( - id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id)) - ); - } - - if (id instanceof ObjectID) { - return true; - } - - if (isBuffer(id)) { - return true; - } - - // Duck-Typing detection of ObjectId like objects - if ( - typeof id.toHexString === "function" && - (id.id instanceof Buffer || typeof id.id === "string") - ) { - return ( - id.id.length === 12 || - (id.id.length === 24 && checkForHexRegExp.test(id.id)) - ); - } - - return false; - } - /** - * Return the ObjectID id as a 24 byte hex string representation - * - * @return {String} return the 24 byte hex string representation. - * @api public - */ - toHexString() { - if (!this.id || !this.id.length) { - throw new Error( - "invalid ObjectId, ObjectId.id must be either a string or a Buffer, but is [" + - JSON.stringify(this.id) + - "]" - ); - } - - if (this.id.length === 24) { - return this.id; - } - - if (isBuffer(this.id)) { - return this.id.toString("hex"); - } - - var hexString = ""; - for (var i = 0; i < this.id.length; i++) { - hexString += hexTable[this.id.charCodeAt(i)]; - } - - return hexString; - } - /** - * Compares the equality of this ObjectID with `otherID`. - * - * @param {Object} otherId ObjectID instance to compare against. - * @return {Boolean} the result of comparing two ObjectID's - * @api public - */ - equals(otherId) { - if (otherId instanceof ObjectID) { - return this.toString() === otherId.toString(); - } else if ( - typeof otherId === "string" && - ObjectID.isValid(otherId) && - otherId.length === 12 && - isBuffer(this.id) - ) { - return otherId === this.id.toString("binary"); - } else if ( - typeof otherId === "string" && - ObjectID.isValid(otherId) && - otherId.length === 24 - ) { - return otherId.toLowerCase() === this.toHexString(); - } else if ( - typeof otherId === "string" && - ObjectID.isValid(otherId) && - otherId.length === 12 - ) { - return otherId === this.id; - } else if ( - otherId != null && - (otherId instanceof ObjectID || otherId.toHexString) - ) { - return otherId.toHexString() === this.toHexString(); - } else { - return false; - } - } - /** - * Returns the generation date (accurate up to the second) that this ID was generated. - * - * @return {Date} the generation date - * @api public - */ - getTimestamp() { - var timestamp = new Date(); - var time; - if (isBuffer(this.id)) { - time = - this.id[3] | - (this.id[2] << 8) | - (this.id[1] << 16) | - (this.id[0] << 24); - } else { - time = - this.id.charCodeAt(3) | - (this.id.charCodeAt(2) << 8) | - (this.id.charCodeAt(1) << 16) | - (this.id.charCodeAt(0) << 24); - } - timestamp.setTime(Math.floor(time) * 1000); - return timestamp; - } - /** - * Generate a 12 byte id buffer used in ObjectID's - * - * @method - * @param {number} [time] optional parameter allowing to pass in a second based timestamp. - * @return {string} return the 12 byte id buffer string. - */ - generate(time) { - if ("number" !== typeof time) { - time = ~~(Date.now() / 1000); - } - - //keep it in the ring! - time = parseInt(time, 10) % 0xffffffff; - - var inc = next(); - - return String.fromCharCode( - (time >> 24) & 0xff, - (time >> 16) & 0xff, - (time >> 8) & 0xff, - time & 0xff, - (MACHINE_ID >> 16) & 0xff, - (MACHINE_ID >> 8) & 0xff, - MACHINE_ID & 0xff, - (pid >> 8) & 0xff, - pid & 0xff, - (inc >> 16) & 0xff, - (inc >> 8) & 0xff, - inc & 0xff - ); - } - - toJSON() { - return this.toHexString(); - } - toString() { - return this.toHexString(); - } -} - -export default ObjectID; - -function next() { - return (index = (index + 1) % 0xffffff); -} - -function hex(length, n) { - n = n.toString(16); - return n.length === length ? n : "00000000".substring(n.length, length) + n; -} diff --git a/packages/core/src/utils/object-id.ts b/packages/core/src/utils/object-id.ts new file mode 100644 index 000000000..a9ebe4df9 --- /dev/null +++ b/packages/core/src/utils/object-id.ts @@ -0,0 +1,51 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { BufferPool } from "./buffer-pool"; +import { randomBytes, randomInt } from "./random"; + +const PROCESS_UNIQUE = randomBytes(5); +let index = ~~(randomInt() * 0xffffff); + +const objectIdPool = new BufferPool(12); +export function createObjectId(date = Date.now()): string { + const buffer = objectIdPool.alloc(); + index = (index + 1) % 0xffffff; + + const time = ~~(date / 1000); + + // 4-byte timestamp + new DataView(buffer.buffer, 0, 4).setUint32(0, time); + + // 5-byte process unique + buffer[4] = PROCESS_UNIQUE[0]; + buffer[5] = PROCESS_UNIQUE[1]; + buffer[6] = PROCESS_UNIQUE[2]; + buffer[7] = PROCESS_UNIQUE[3]; + buffer[8] = PROCESS_UNIQUE[4]; + + // 3-byte counter + buffer[11] = index & 0xff; + buffer[10] = (index >> 8) & 0xff; + buffer[9] = (index >> 16) & 0xff; + + const objectId = buffer.toString("hex"); + objectIdPool.free(buffer); + return objectId; +}