Skip to content
Open
13 changes: 13 additions & 0 deletions src/accessDeep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ const getNthKey = (value: Map<any, any> | Set<any>, n: number): any => {
};

function validatePath(path: (string | number)[]) {
let suspicious = false;
for (let i = 0; i < path.length; i++) {
const key = path[i];
if (key === '__proto__' || key === 'prototype' || key === 'constructor') {
suspicious = true;
break;
}
}

if (!suspicious) {
return;
}

if (includes(path, '__proto__')) {
throw new Error('__proto__ is not allowed as a property');
}
Expand Down
18 changes: 14 additions & 4 deletions src/custom-transformer-registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { JSONValue } from './types.js';
import { find } from './util.js';

export interface CustomTransfomer<I, O extends JSONValue> {
name: string;
Expand All @@ -10,15 +9,26 @@ export interface CustomTransfomer<I, O extends JSONValue> {

export class CustomTransformerRegistry {
private transfomers: Record<string, CustomTransfomer<any, any>> = {};
private ordered: CustomTransfomer<any, any>[] = [];

register<I, O extends JSONValue>(transformer: CustomTransfomer<I, O>) {
this.transfomers[transformer.name] = transformer;
this.ordered = Object.values(this.transfomers);
}

get size(): number {
return this.ordered.length;
}

findApplicable<T>(v: T) {
return find(this.transfomers, transformer =>
transformer.isApplicable(v)
) as CustomTransfomer<T, JSONValue> | undefined;
const transformers = this.ordered;
for (let i = 0; i < transformers.length; i++) {
if (transformers[i].isApplicable(v)) {
return transformers[i] as CustomTransfomer<T, JSONValue>;
}
}

return undefined;
}

findByName(name: string) {
Expand Down
71 changes: 71 additions & 0 deletions src/date.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { parseISOString, toISOString } from './date.js';

import { expect, test } from 'vitest';

const sameTime = (a: Date, b: Date) =>
Object.is(a.getTime(), b.getTime());

test('toISOString matches Date#toISOString', () => {
const times = [
0,
-1,
Date.UTC(999, 11, 31, 23, 59, 59, 999),
Date.UTC(1000, 0, 1),
Date.UTC(2024, 1, 29, 12, 5, 9, 7),
Date.UTC(9999, 11, 31, 23, 59, 59, 999),
Date.UTC(10000, 0, 1),
-62198755200000,
8.64e15,
-8.64e15,
];
for (let i = 0; i < 20000; i++) {
times.push(Math.floor((Math.random() - 0.4) * 4e14));
}

for (const time of times) {
const date = new Date(time);
expect(toISOString(date)).toBe(date.toISOString());
}
});

test('parseISOString matches new Date(string)', () => {
const inputs = [
'2024-02-29T12:00:00.000Z',
'2023-02-29T12:00:00.000Z',
'2023-04-31T00:00:00.000Z',
'2023-00-10T00:00:00.000Z',
'2023-13-10T00:00:00.000Z',
'2023-01-00T00:00:00.000Z',
'2023-01-32T00:00:00.000Z',
'2023-01-01T24:00:00.000Z',
'2023-01-01T23:60:00.000Z',
'2023-01-01T23:59:60.000Z',
'0099-01-01T00:00:00.000Z',
'0999-12-31T23:59:59.999Z',
'1000-01-01T00:00:00.000Z',
'9999-12-31T23:59:59.999Z',
'+010000-01-01T00:00:00.000Z',
'-000001-01-01T00:00:00.000Z',
'2023-01-01T00:00:00.000+01:00',
'2023-01-01 00:00:00.000Z',
'2023-01-0aT00:00:00.000Z',
'2023-01-01T00:00:00.0-0Z',
'not a date',
'',
];
for (let i = 0; i < 20000; i++) {
inputs.push(new Date(Math.floor((Math.random() - 0.4) * 4e14)).toISOString());
}

const chars = '0123456789-T:.Z+ x';
for (let i = 0; i < 20000; i++) {
const base = inputs[Math.floor(Math.random() * inputs.length)].split('');
base[Math.floor(Math.random() * base.length)] =
chars[Math.floor(Math.random() * chars.length)];
inputs.push(base.join(''));
}

for (const input of inputs) {
expect(sameTime(parseISOString(input), new Date(input))).toBe(true);
}
});
87 changes: 87 additions & 0 deletions src/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const pad2 = (n: number) => (n < 10 ? '0' + n : '' + n);
const pad3 = (n: number) => (n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n);

// Same output as Date#toISOString, which is several times slower in V8.
export function toISOString(date: Date): string {
const year = date.getUTCFullYear();
if (year < 1000 || year > 9999) {
return date.toISOString();
}

return (
year +
'-' +
pad2(date.getUTCMonth() + 1) +
'-' +
pad2(date.getUTCDate()) +
'T' +
pad2(date.getUTCHours()) +
':' +
pad2(date.getUTCMinutes()) +
':' +
pad2(date.getUTCSeconds()) +
'.' +
pad3(date.getUTCMilliseconds()) +
'Z'
);
}

const digits = (s: string, from: number, count: number): number => {
let n = 0;
for (let i = from; i < from + count; i++) {
const d = s.charCodeAt(i) - 48;
if (d < 0 || d > 9) {
return -1;
}

n = n * 10 + d;
}

return n;
};

// Same result as `new Date(value)`. Only the exact toISOString shape with in-range fields takes the
// fast path; everything else goes to the native parser.
export function parseISOString(value: string): Date {
if (
typeof value === 'string' &&
value.length === 24 &&
value.charCodeAt(4) === 45 &&
value.charCodeAt(7) === 45 &&
value.charCodeAt(10) === 84 &&
value.charCodeAt(13) === 58 &&
value.charCodeAt(16) === 58 &&
value.charCodeAt(19) === 46 &&
value.charCodeAt(23) === 90
) {
const year = digits(value, 0, 4);
const month = digits(value, 5, 2);
const day = digits(value, 8, 2);
const hours = digits(value, 11, 2);
const minutes = digits(value, 14, 2);
const seconds = digits(value, 17, 2);
const ms = digits(value, 20, 3);
if (
year >= 1000 &&
month >= 1 &&
month <= 12 &&
day >= 1 &&
hours >= 0 &&
hours <= 23 &&
minutes >= 0 &&
minutes <= 59 &&
seconds >= 0 &&
seconds <= 59 &&
ms >= 0
) {
const date = new Date(
Date.UTC(year, month - 1, day, hours, minutes, seconds, ms)
);
if (date.getUTCDate() === day) {
return date;
}
}
}

return new Date(value);
}
9 changes: 4 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
applyReferentialEqualityAnnotations,
applyValueAnnotations,
generateReferentialEqualityAnnotations,
walker,
walkRoot,
} from './plainer.js';
import { copy } from 'copy-anything';
import { copy } from './util.js';

export default class SuperJSON {
/**
Expand All @@ -31,8 +31,7 @@ export default class SuperJSON {
}

serialize(object: SuperJSONValue): SuperJSONResult {
const identities = new Map<any, any[][]>();
const output = walker(object, identities, this, this.dedupe);
const output = walkRoot(object, this, this.dedupe);
const res: SuperJSONResult = {
json: output.transformedValue,
};
Expand All @@ -45,7 +44,7 @@ export default class SuperJSON {
}

const equalityAnnotations = generateReferentialEqualityAnnotations(
identities,
output.identities,
this.dedupe
);
if (equalityAnnotations) {
Expand Down
39 changes: 26 additions & 13 deletions src/is.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
const getType = (payload: any): string =>
Object.prototype.toString.call(payload).slice(8, -1);

export const isUndefined = (payload: any): payload is undefined =>
typeof payload === 'undefined';

Expand All @@ -11,9 +8,9 @@ export const isPlainObject = (
): payload is { [key: string]: any } => {
if (typeof payload !== 'object' || payload === null) return false;
if (payload === Object.prototype) return false;
if (Object.getPrototypeOf(payload) === null) return true;
const prototype = Object.getPrototypeOf(payload);

return Object.getPrototypeOf(payload) === Object.prototype;
return prototype === null || prototype === Object.prototype;
};

export const isEmptyObject = (payload: any): payload is {} =>
Expand All @@ -40,8 +37,14 @@ export const isMap = (payload: any): payload is Map<any, any> =>
export const isSet = (payload: any): payload is Set<any> =>
payload instanceof Set;

// Object.prototype.toString reports an object as "Symbol" only through its @@toStringTag.
const hasSymbolTag = (payload: any): boolean =>
((typeof payload === 'object' && payload !== null) ||
typeof payload === 'function') &&
payload[Symbol.toStringTag] === 'Symbol';

export const isSymbol = (payload: any): payload is symbol =>
getType(payload) === 'Symbol';
typeof payload === 'symbol' || hasSymbolTag(payload);

export const isDate = (payload: any): payload is Date =>
payload instanceof Date && !isNaN(payload.valueOf());
Expand All @@ -54,13 +57,23 @@ export const isNaNValue = (payload: any): payload is typeof NaN =>

export const isPrimitive = (
payload: any
): payload is boolean | null | undefined | number | string | symbol =>
isBoolean(payload) ||
isNull(payload) ||
isUndefined(payload) ||
isNumber(payload) ||
isString(payload) ||
isSymbol(payload);
): payload is boolean | null | undefined | number | string | symbol => {
switch (typeof payload) {
case 'boolean':
case 'string':
case 'undefined':
case 'symbol':
return true;
case 'number':
return !isNaN(payload);
case 'object':
return payload === null || hasSymbolTag(payload);
case 'function':
return hasSymbolTag(payload);
default:
return false;
}
};

export const isBigint = (payload: any): payload is bigint =>
typeof payload === 'bigint';
Expand Down
24 changes: 23 additions & 1 deletion src/pathstringifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,37 @@ export type StringifiedPath = string;
type Path = string[];

export const escapeKey = (key: string) =>
key.replace(/\\/g, '\\\\').replace(/\./g, '\\.');
key.indexOf('\\') === -1 && key.indexOf('.') === -1
? key
: key.replace(/\\/g, '\\\\').replace(/\./g, '\\.');

export const stringifyPath = (path: Path): StringifiedPath =>
path
.map(String)
.map(escapeKey)
.join('.');

// V8's String.prototype.split is several times slower than this on short keys.
const splitOnDots = (string: string): string[] => {
const result: string[] = [];
let start = 0;
for (;;) {
const dot = string.indexOf('.', start);
if (dot === -1) {
result.push(start === 0 ? string : string.slice(start));
return result;
}

result.push(string.slice(start, dot));
start = dot + 1;
}
};

export const parsePath = (string: StringifiedPath, legacyPaths: boolean) => {
if (typeof string === 'string' && string.indexOf('\\') === -1) {
return splitOnDots(string);
}

const result: string[] = [];

let segment = '';
Expand Down
Loading