mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +01:00
core: convert everything else to typescript
This commit is contained in:
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<T>) {
|
||||
|
||||
@@ -18,13 +18,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<string, LogMessage> = 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<LogMessage>(logKeys);
|
||||
const logs: Record<string, LogMessage[]> = {};
|
||||
|
||||
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 };
|
||||
@@ -17,15 +17,20 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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();
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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/");
|
||||
}
|
||||
@@ -17,20 +17,13 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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];
|
||||
}
|
||||
@@ -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<string, string | number | boolean | undefined> | null;
|
||||
type JsonRequestBody = Record<string, unknown> | 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("&");
|
||||
}
|
||||
}
|
||||
@@ -18,12 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// 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 <feross@feross.org> <http://feross.org>
|
||||
* 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;
|
||||
}
|
||||
51
packages/core/src/utils/object-id.ts
Normal file
51
packages/core/src/utils/object-id.ts
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user