mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge pull request #14327 from meteor/oxc/phase-2
[phase2] Applying linter, formatter and new tests for a few pkgs
This commit is contained in:
13
.fmtignore
13
.fmtignore
@@ -35,3 +35,16 @@ packages/*
|
||||
!packages/non-core/bundle-visualizer/
|
||||
!packages/non-core/mongo-decimal/
|
||||
!packages/non-core/xmlbuilder/
|
||||
!packages/base64/
|
||||
!packages/binary-heap/
|
||||
!packages/diff-sequence/
|
||||
!packages/callback-hook/
|
||||
!packages/ejson/
|
||||
!packages/id-map/
|
||||
!packages/ordered-dict/
|
||||
!packages/rate-limit/
|
||||
!packages/retry/
|
||||
!packages/logging/
|
||||
!packages/session/
|
||||
!packages/test-in-console/
|
||||
!packages/webapp-hashing/
|
||||
|
||||
@@ -3,3 +3,9 @@
|
||||
# auto-format and lint fixes 2026-03-27
|
||||
1bfad0bcbb9c495172d1475f1c6d54df8d34ceae
|
||||
|
||||
# oxlint phase-2 lint fixes 2026-04-09
|
||||
5f50d759d3bb08cebada41481d9b38b015c3fca7
|
||||
568eace1b0726b3e2fb823292e30b0e2f0efb4ca
|
||||
163f18d512d4320d57c4ddf3e14cc83607000cfa
|
||||
de68f14b9c8e0916a2ac0b239a79d16c5e2adef9
|
||||
|
||||
|
||||
@@ -32,3 +32,16 @@ packages/*
|
||||
!packages/non-core/bundle-visualizer/
|
||||
!packages/non-core/mongo-decimal/
|
||||
!packages/non-core/xmlbuilder/
|
||||
!packages/base64/
|
||||
!packages/binary-heap/
|
||||
!packages/diff-sequence/
|
||||
!packages/callback-hook/
|
||||
!packages/ejson/
|
||||
!packages/id-map/
|
||||
!packages/ordered-dict/
|
||||
!packages/rate-limit/
|
||||
!packages/retry/
|
||||
!packages/logging/
|
||||
!packages/session/
|
||||
!packages/test-in-console/
|
||||
!packages/webapp-hashing/
|
||||
|
||||
@@ -4,22 +4,21 @@ const BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234
|
||||
|
||||
const BASE_64_VALS = Object.create(null);
|
||||
|
||||
const getChar = val => BASE_64_CHARS.charAt(val);
|
||||
const getVal = ch => ch === '=' ? -1 : BASE_64_VALS[ch];
|
||||
const getChar = (val) => BASE_64_CHARS.charAt(val);
|
||||
const getVal = (ch) => (ch === "=" ? -1 : BASE_64_VALS[ch]);
|
||||
|
||||
for (let i = 0; i < BASE_64_CHARS.length; i++) {
|
||||
BASE_64_VALS[getChar(i)] = i;
|
||||
};
|
||||
}
|
||||
|
||||
const encode = array => {
|
||||
const encode = (array) => {
|
||||
if (typeof array === "string") {
|
||||
const str = array;
|
||||
array = newBinary(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const ch = str.charCodeAt(i);
|
||||
if (ch > 0xFF) {
|
||||
throw new Error(
|
||||
"Not ascii. Base64.encode can only take ascii strings.");
|
||||
if (ch > 0xff) {
|
||||
throw new Error("Not ascii. Base64.encode can only take ascii strings.");
|
||||
}
|
||||
|
||||
array[i] = ch;
|
||||
@@ -35,16 +34,16 @@ const encode = array => {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
switch (i % 3) {
|
||||
case 0:
|
||||
a = (array[i] >> 2) & 0x3F;
|
||||
a = (array[i] >> 2) & 0x3f;
|
||||
b = (array[i] & 0x03) << 4;
|
||||
break;
|
||||
case 1:
|
||||
b = b | (array[i] >> 4) & 0xF;
|
||||
c = (array[i] & 0xF) << 2;
|
||||
b = b | ((array[i] >> 4) & 0xf);
|
||||
c = (array[i] & 0xf) << 2;
|
||||
break;
|
||||
case 2:
|
||||
c = c | (array[i] >> 6) & 0x03;
|
||||
d = array[i] & 0x3F;
|
||||
c = c | ((array[i] >> 6) & 0x03);
|
||||
d = array[i] & 0x3f;
|
||||
answer.push(getChar(a));
|
||||
answer.push(getChar(b));
|
||||
answer.push(getChar(c));
|
||||
@@ -61,28 +60,26 @@ const encode = array => {
|
||||
answer.push(getChar(a));
|
||||
answer.push(getChar(b));
|
||||
if (c == null) {
|
||||
answer.push('=');
|
||||
answer.push("=");
|
||||
} else {
|
||||
answer.push(getChar(c));
|
||||
}
|
||||
|
||||
if (d == null) {
|
||||
answer.push('=');
|
||||
answer.push("=");
|
||||
}
|
||||
}
|
||||
|
||||
return answer.join("");
|
||||
};
|
||||
|
||||
|
||||
|
||||
// XXX This is a weird place for this to live, but it's used both by
|
||||
// this package and 'ejson', and we can't put it in 'ejson' without
|
||||
// introducing a circular dependency. It should probably be in its own
|
||||
// package or as a helper in a package that both 'base64' and 'ejson'
|
||||
// use.
|
||||
const newBinary = len => {
|
||||
if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') {
|
||||
const newBinary = (len) => {
|
||||
if (typeof Uint8Array === "undefined" || typeof ArrayBuffer === "undefined") {
|
||||
const ret = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
ret.push(0);
|
||||
@@ -94,11 +91,11 @@ const newBinary = len => {
|
||||
return new Uint8Array(new ArrayBuffer(len));
|
||||
};
|
||||
|
||||
const decode = str => {
|
||||
const decode = (str) => {
|
||||
let len = Math.floor((str.length * 3) / 4);
|
||||
if (str.charAt(str.length - 1) == '=') {
|
||||
if (str.charAt(str.length - 1) == "=") {
|
||||
len--;
|
||||
if (str.charAt(str.length - 2) == '=') {
|
||||
if (str.charAt(str.length - 2) == "=") {
|
||||
len--;
|
||||
}
|
||||
}
|
||||
@@ -117,19 +114,19 @@ const decode = str => {
|
||||
switch (i % 4) {
|
||||
case 0:
|
||||
if (v < 0) {
|
||||
throw new Error('invalid base64 string');
|
||||
throw new Error("invalid base64 string");
|
||||
}
|
||||
|
||||
one = v << 2;
|
||||
break;
|
||||
case 1:
|
||||
if (v < 0) {
|
||||
throw new Error('invalid base64 string');
|
||||
throw new Error("invalid base64 string");
|
||||
}
|
||||
|
||||
one = one | (v >> 4);
|
||||
arr[j++] = one;
|
||||
two = (v & 0x0F) << 4;
|
||||
two = (v & 0x0f) << 4;
|
||||
break;
|
||||
case 2:
|
||||
if (v >= 0) {
|
||||
|
||||
@@ -1,58 +1,232 @@
|
||||
import { Base64 } from './base64.js';
|
||||
import { Base64 } from "./base64.js";
|
||||
|
||||
const asciiToArray = str => {
|
||||
const asciiToArray = (str) => {
|
||||
const arr = Base64.newBinary(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str.charCodeAt(i);
|
||||
if (c > 0xFF) {
|
||||
if (c > 0xff) {
|
||||
throw new Error("Not ascii");
|
||||
}
|
||||
|
||||
arr[i] = c;
|
||||
}
|
||||
|
||||
|
||||
return arr;
|
||||
};
|
||||
|
||||
const arrayToAscii = arr => arr
|
||||
.reduce(
|
||||
(prev, charCode) => prev.push(String.fromCharCode(charCode)) && prev, []
|
||||
).join('');
|
||||
const arrayToAscii = (arr) =>
|
||||
arr.reduce((prev, charCode) => prev.push(String.fromCharCode(charCode)) && prev, []).join("");
|
||||
|
||||
Tinytest.add("base64 - testing the test", test => {
|
||||
test.equal(arrayToAscii(asciiToArray("The quick brown fox jumps over the lazy dog")),
|
||||
"The quick brown fox jumps over the lazy dog");
|
||||
Tinytest.add("base64 - testing the test", (test) => {
|
||||
test.equal(
|
||||
arrayToAscii(asciiToArray("The quick brown fox jumps over the lazy dog")),
|
||||
"The quick brown fox jumps over the lazy dog",
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - empty", test => {
|
||||
Tinytest.add("base64 - empty", (test) => {
|
||||
test.equal(Base64.encode(EJSON.newBinary(0)), "");
|
||||
test.equal(Base64.decode(""), EJSON.newBinary(0));
|
||||
});
|
||||
|
||||
|
||||
Tinytest.add("base64 - wikipedia examples", test => {
|
||||
Tinytest.add("base64 - wikipedia examples", (test) => {
|
||||
const tests = [
|
||||
{txt: "pleasure.", res: "cGxlYXN1cmUu"},
|
||||
{txt: "leasure.", res: "bGVhc3VyZS4="},
|
||||
{txt: "easure.", res: "ZWFzdXJlLg=="},
|
||||
{txt: "asure.", res: "YXN1cmUu"},
|
||||
{txt: "sure.", res: "c3VyZS4="}
|
||||
{ txt: "pleasure.", res: "cGxlYXN1cmUu" },
|
||||
{ txt: "leasure.", res: "bGVhc3VyZS4=" },
|
||||
{ txt: "easure.", res: "ZWFzdXJlLg==" },
|
||||
{ txt: "asure.", res: "YXN1cmUu" },
|
||||
{ txt: "sure.", res: "c3VyZS4=" },
|
||||
];
|
||||
tests.forEach(t => {
|
||||
tests.forEach((t) => {
|
||||
test.equal(Base64.encode(asciiToArray(t.txt)), t.res);
|
||||
test.equal(arrayToAscii(Base64.decode(t.res)), t.txt);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - non-text examples", test => {
|
||||
Tinytest.add("base64 - non-text examples", (test) => {
|
||||
const tests = [
|
||||
{array: [0, 0, 0], b64: "AAAA"},
|
||||
{array: [0, 0, 1], b64: "AAAB"}
|
||||
{ array: [0, 0, 0], b64: "AAAA" },
|
||||
{ array: [0, 0, 1], b64: "AAAB" },
|
||||
];
|
||||
tests.forEach(t => {
|
||||
tests.forEach((t) => {
|
||||
test.equal(Base64.encode(t.array), t.b64);
|
||||
const expectedAsBinary = EJSON.newBinary(t.array.length);
|
||||
t.array.forEach((val, i) => expectedAsBinary[i] = val);
|
||||
t.array.forEach((val, i) => (expectedAsBinary[i] = val));
|
||||
test.equal(Base64.decode(t.b64), expectedAsBinary);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - RFC 4648 test vectors", (test) => {
|
||||
const vectors = [
|
||||
{ txt: "", res: "" },
|
||||
{ txt: "f", res: "Zg==" },
|
||||
{ txt: "fo", res: "Zm8=" },
|
||||
{ txt: "foo", res: "Zm9v" },
|
||||
{ txt: "foob", res: "Zm9vYg==" },
|
||||
{ txt: "fooba", res: "Zm9vYmE=" },
|
||||
{ txt: "foobar", res: "Zm9vYmFy" },
|
||||
];
|
||||
vectors.forEach((v) => {
|
||||
test.equal(Base64.encode(asciiToArray(v.txt)), v.res);
|
||||
test.equal(arrayToAscii(Base64.decode(v.res)), v.txt);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - round-trip for all byte values 0-255", (test) => {
|
||||
const original = Base64.newBinary(256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
original[i] = i;
|
||||
}
|
||||
|
||||
const decoded = Base64.decode(Base64.encode(original));
|
||||
test.equal(decoded.length, 256);
|
||||
for (let i = 0; i < 256; i++) {
|
||||
test.equal(decoded[i], i, `byte at index ${i} should round-trip`);
|
||||
}
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - round-trip for various lengths", (test) => {
|
||||
const lengths = [0, 1, 2, 3, 4, 5, 6, 7, 8, 15, 16, 17, 31, 32, 33];
|
||||
lengths.forEach((n) => {
|
||||
const original = Base64.newBinary(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
original[i] = i % 256;
|
||||
}
|
||||
|
||||
const decoded = Base64.decode(Base64.encode(original));
|
||||
test.equal(decoded.length, n, `length ${n} should round-trip`);
|
||||
for (let i = 0; i < n; i++) {
|
||||
test.equal(decoded[i], i % 256, `byte at index ${i} (length ${n})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - encoded output length is ceil(n/3)*4", (test) => {
|
||||
const sizes = [0, 1, 2, 3, 4, 5, 10, 100, 999, 1000];
|
||||
sizes.forEach((n) => {
|
||||
const arr = Base64.newBinary(n);
|
||||
for (let i = 0; i < n; i++) {
|
||||
arr[i] = i % 256;
|
||||
}
|
||||
|
||||
const expected = Math.ceil(n / 3) * 4;
|
||||
test.equal(
|
||||
Base64.encode(arr).length,
|
||||
expected,
|
||||
`encode(length=${n}) should be ${expected} chars`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - padding characters are correct", (test) => {
|
||||
// 1 byte -> "==" padding
|
||||
const oneByte = Base64.encode([0x41]);
|
||||
test.equal(oneByte.length, 4);
|
||||
test.equal(oneByte.slice(-2), "==", "1-byte input should end with ==");
|
||||
test.isFalse(oneByte.slice(-3) === "===", "should not end with ===");
|
||||
|
||||
// 2 bytes -> "=" padding (single)
|
||||
const twoBytes = Base64.encode([0x41, 0x42]);
|
||||
test.equal(twoBytes.length, 4);
|
||||
test.equal(twoBytes.slice(-1), "=", "2-byte input should end with single =");
|
||||
test.isFalse(twoBytes.slice(-2) === "==", "2-byte input should not end with ==");
|
||||
|
||||
// 3 bytes -> no padding
|
||||
const threeBytes = Base64.encode([0x41, 0x42, 0x43]);
|
||||
test.equal(threeBytes.length, 4);
|
||||
test.equal(threeBytes.indexOf("="), -1, "3-byte input should contain no =");
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - large input round-trip", (test) => {
|
||||
const size = 10000;
|
||||
const original = Base64.newBinary(size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
original[i] = (i * 31) % 256;
|
||||
}
|
||||
|
||||
const encoded = Base64.encode(original);
|
||||
test.equal(encoded.length, Math.ceil(size / 3) * 4);
|
||||
|
||||
const decoded = Base64.decode(encoded);
|
||||
test.equal(decoded.length, size);
|
||||
for (let i = 0; i < size; i++) {
|
||||
if (decoded[i] !== (i * 31) % 256) {
|
||||
test.fail({
|
||||
type: "large-input-mismatch",
|
||||
message: `byte at index ${i}: expected ${(i * 31) % 256}, got ${decoded[i]}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
test.ok();
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - encode rejects characters above 0xff", (test) => {
|
||||
// \u0100 is the first code point > 0xff and must be rejected.
|
||||
test.throws(() => Base64.encode("\u0100"), /Not ascii/);
|
||||
// \uffff is well above the limit and must also be rejected.
|
||||
test.throws(() => Base64.encode("\uffff"), /Not ascii/);
|
||||
// A string containing a mix of valid and invalid chars must still throw.
|
||||
test.throws(() => Base64.encode("abc\u0100"), /Not ascii/);
|
||||
|
||||
// Positive controls: the boundary is > 0xff, not > 0x7f.
|
||||
// \u00ff is the last allowed code point and must NOT throw.
|
||||
const resultFf = Base64.encode("\u00ff");
|
||||
test.equal(typeof resultFf, "string");
|
||||
test.equal(resultFf.length, 4, "single byte encodes to 4 base64 chars");
|
||||
// \u0080 (first non-7-bit-ASCII byte) must also be accepted.
|
||||
const result80 = Base64.encode("\u0080");
|
||||
test.equal(typeof result80, "string");
|
||||
test.equal(result80.length, 4);
|
||||
});
|
||||
|
||||
// Documents the ACTUAL error-handling behavior of Base64.decode:
|
||||
// the implementation only throws when "=" appears at position 0 or 1
|
||||
// (mod 4) of a 4-char group. Characters outside the base64 alphabet
|
||||
// (e.g. "!", "?") return undefined from the internal lookup and are
|
||||
// silently treated as zero — they do NOT throw. This test pins down
|
||||
// the real behavior so future refactors cannot regress it unnoticed.
|
||||
Tinytest.add("base64 - decode rejects '=' in leading positions", (test) => {
|
||||
// "=" at position 0 of a group.
|
||||
test.throws(() => Base64.decode("===="), /invalid base64 string/);
|
||||
|
||||
// "=" at position 1 of a group.
|
||||
test.throws(() => Base64.decode("A==="), /invalid base64 string/);
|
||||
|
||||
// "=" at positions 0/1 of a *later* 4-char group.
|
||||
test.throws(() => Base64.decode("AAAA===="), /invalid base64 string/);
|
||||
test.throws(() => Base64.decode("AAAAA==="), /invalid base64 string/);
|
||||
});
|
||||
|
||||
Tinytest.add("base64 - newBinary contract", (test) => {
|
||||
[0, 1, 100].forEach((n) => {
|
||||
test.equal(Base64.newBinary(n).length, n, `newBinary(${n}).length`);
|
||||
});
|
||||
|
||||
// Zero-initialized.
|
||||
const five = Base64.newBinary(5);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
test.equal(five[i], 0, `newBinary(5)[${i}] should start at 0`);
|
||||
}
|
||||
|
||||
// Indexed writes are readable back.
|
||||
const three = Base64.newBinary(3);
|
||||
three[0] = 42;
|
||||
three[1] = 7;
|
||||
three[2] = 255;
|
||||
test.equal(three[0], 42);
|
||||
test.equal(three[1], 7);
|
||||
test.equal(three[2], 255);
|
||||
|
||||
// Interoperates with encode/decode.
|
||||
const buf = Base64.newBinary(3);
|
||||
buf[0] = 0x41;
|
||||
buf[1] = 0x42;
|
||||
buf[2] = 0x43;
|
||||
test.equal(Base64.encode(buf), "QUJD");
|
||||
const decoded = Base64.decode("QUJD");
|
||||
test.equal(decoded.length, 3);
|
||||
test.equal(decoded[0], 0x41);
|
||||
test.equal(decoded[1], 0x42);
|
||||
test.equal(decoded[2], 0x43);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
Package.describe({
|
||||
summary: "Base64 encoding and decoding",
|
||||
version: '1.0.13',
|
||||
version: "1.0.13",
|
||||
});
|
||||
|
||||
Package.onUse(api => {
|
||||
api.export('Base64');
|
||||
api.use('ecmascript');
|
||||
api.mainModule('base64.js');
|
||||
Package.onUse((api) => {
|
||||
api.export("Base64");
|
||||
api.use("ecmascript");
|
||||
api.mainModule("base64.js");
|
||||
});
|
||||
|
||||
Package.onTest(api => {
|
||||
api.use(['ecmascript', 'tinytest', 'ejson']);
|
||||
api.addFiles('base64_test.js', ['client', 'server']);
|
||||
Package.onTest((api) => {
|
||||
api.use(["ecmascript", "tinytest", "ejson"]);
|
||||
api.addFiles("base64_test.js", ["client", "server"]);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MaxHeap } from './max-heap.js';
|
||||
import { MinMaxHeap } from './min-max-heap.js';
|
||||
import { MaxHeap } from "./max-heap.js";
|
||||
import { MinHeap } from "./min-heap.js";
|
||||
import { MinMaxHeap } from "./min-max-heap.js";
|
||||
|
||||
// Based on underscore implementation (Fisher-Yates shuffle)
|
||||
const shuffle = arr => {
|
||||
const shuffle = (arr) => {
|
||||
let j = 0;
|
||||
let temp = null;
|
||||
|
||||
@@ -33,7 +34,7 @@ const range = (start, stop, step = 1) => {
|
||||
return range;
|
||||
};
|
||||
|
||||
Tinytest.add("binary-heap - simple max-heap tests", test => {
|
||||
Tinytest.add("binary-heap - simple max-heap tests", (test) => {
|
||||
const h = new MaxHeap((a, b) => a - b);
|
||||
h.set("a", 1);
|
||||
h.set("b", 233);
|
||||
@@ -63,7 +64,7 @@ Tinytest.add("binary-heap - simple max-heap tests", test => {
|
||||
test.equal(h.maxElementId(), "a");
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - big test for max-heap", test => {
|
||||
Tinytest.add("binary-heap - big test for max-heap", (test) => {
|
||||
const positiveNumbers = shuffle(range(1, 41));
|
||||
const negativeNumbers = shuffle(range(-1, -41, -1));
|
||||
const allNumbers = [...negativeNumbers, ...positiveNumbers];
|
||||
@@ -71,7 +72,7 @@ Tinytest.add("binary-heap - big test for max-heap", test => {
|
||||
const heap = new MaxHeap((a, b) => a - b);
|
||||
const output = [];
|
||||
|
||||
allNumbers.forEach(n => heap.set(n, n));
|
||||
allNumbers.forEach((n) => heap.set(n, n));
|
||||
|
||||
allNumbers.forEach(() => {
|
||||
const maxId = heap.maxElementId();
|
||||
@@ -84,7 +85,7 @@ Tinytest.add("binary-heap - big test for max-heap", test => {
|
||||
test.equal(output, allNumbers);
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - min-max heap tests", test => {
|
||||
Tinytest.add("binary-heap - min-max heap tests", (test) => {
|
||||
const h = new MinMaxHeap((a, b) => a - b);
|
||||
h.set("a", 1);
|
||||
h.set("b", 233);
|
||||
@@ -116,7 +117,7 @@ Tinytest.add("binary-heap - min-max heap tests", test => {
|
||||
test.equal(h.minElementId(), "a");
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - big test for min-max-heap", test => {
|
||||
Tinytest.add("binary-heap - big test for min-max-heap", (test) => {
|
||||
const N = 500;
|
||||
const positiveNumbers = shuffle(range(1, N + 1));
|
||||
const negativeNumbers = shuffle(range(-1, -N - 1, -1));
|
||||
@@ -126,7 +127,7 @@ Tinytest.add("binary-heap - big test for min-max-heap", test => {
|
||||
let output = [];
|
||||
|
||||
const initialSets = [...allNumbers];
|
||||
allNumbers.forEach(n => {
|
||||
allNumbers.forEach((n) => {
|
||||
heap.set(n, n);
|
||||
heap._selfCheck();
|
||||
heap._minHeap._selfCheck();
|
||||
@@ -135,7 +136,7 @@ Tinytest.add("binary-heap - big test for min-max-heap", test => {
|
||||
shuffle(allNumbers);
|
||||
const secondarySets = [...allNumbers];
|
||||
|
||||
allNumbers.forEach(n => {
|
||||
allNumbers.forEach((n) => {
|
||||
heap.set(-n, n);
|
||||
heap._selfCheck();
|
||||
heap._minHeap._selfCheck();
|
||||
@@ -145,19 +146,20 @@ Tinytest.add("binary-heap - big test for min-max-heap", test => {
|
||||
const minId = heap.minElementId();
|
||||
output.push(heap.get(minId));
|
||||
heap.remove(minId);
|
||||
heap._selfCheck(); heap._minHeap._selfCheck();
|
||||
heap._selfCheck();
|
||||
heap._minHeap._selfCheck();
|
||||
});
|
||||
|
||||
test.equal(heap.size(), 0);
|
||||
|
||||
allNumbers.sort((a, b) => a - b);
|
||||
|
||||
const initialTestText = `initial sets: ${initialSets.toString()}` +
|
||||
`; secondary sets: ${secondarySets.toString()}`;
|
||||
const initialTestText =
|
||||
`initial sets: ${initialSets.toString()}` + `; secondary sets: ${secondarySets.toString()}`;
|
||||
test.equal(output, allNumbers, initialTestText);
|
||||
|
||||
initialSets.forEach(n => heap.set(n, n));
|
||||
secondarySets.forEach(n => heap.set(-n, n));
|
||||
initialSets.forEach((n) => heap.set(n, n));
|
||||
secondarySets.forEach((n) => heap.set(-n, n));
|
||||
|
||||
allNumbers.sort((a, b) => b - a);
|
||||
output = [];
|
||||
@@ -165,8 +167,237 @@ Tinytest.add("binary-heap - big test for min-max-heap", test => {
|
||||
const maxId = heap.maxElementId();
|
||||
output.push(heap.get(maxId));
|
||||
heap.remove(maxId);
|
||||
heap._selfCheck(); heap._minHeap._selfCheck();
|
||||
heap._selfCheck();
|
||||
heap._minHeap._selfCheck();
|
||||
});
|
||||
|
||||
test.equal(output, allNumbers, initialTestText);
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - constructor throws on non-function comparator", (test) => {
|
||||
test.throws(() => new MaxHeap(null), /comparator is invalid/);
|
||||
test.throws(() => new MaxHeap("not a fn"), /comparator is invalid/);
|
||||
test.throws(() => new MaxHeap(), /comparator is invalid/);
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MaxHeap built from initData preserves elements", (test) => {
|
||||
const heap = new MaxHeap((a, b) => a - b, {
|
||||
initData: [
|
||||
{ id: "a", value: 5 },
|
||||
{ id: "b", value: 10 },
|
||||
{ id: "c", value: 1 },
|
||||
{ id: "d", value: 7 },
|
||||
{ id: "e", value: 3 },
|
||||
],
|
||||
});
|
||||
|
||||
test.equal(heap.size(), 5);
|
||||
test.equal(heap.maxElementId(), "b");
|
||||
test.equal(heap.get("a"), 5);
|
||||
test.equal(heap.get("d"), 7);
|
||||
|
||||
// Drain should yield descending order.
|
||||
const drained = [];
|
||||
while (!heap.empty()) {
|
||||
const id = heap.maxElementId();
|
||||
drained.push(heap.get(id));
|
||||
heap.remove(id);
|
||||
}
|
||||
test.equal(drained, [10, 7, 5, 3, 1]);
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MaxHeap.empty and forEach", (test) => {
|
||||
const heap = new MaxHeap((a, b) => a - b);
|
||||
test.isTrue(heap.empty());
|
||||
|
||||
heap.set("a", 1);
|
||||
heap.set("b", 2);
|
||||
heap.set("c", 3);
|
||||
test.isFalse(heap.empty());
|
||||
|
||||
const collected = Object.create(null);
|
||||
heap.forEach((value, id) => {
|
||||
collected[id] = value;
|
||||
});
|
||||
test.equal(collected, { a: 1, b: 2, c: 3 });
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MaxHeap.set updates existing value and rebalances", (test) => {
|
||||
const heap = new MaxHeap((a, b) => a - b);
|
||||
heap.set("x", 1);
|
||||
heap.set("y", 5);
|
||||
heap.set("z", 3);
|
||||
test.equal(heap.maxElementId(), "y");
|
||||
|
||||
// Bubble up: x goes to top.
|
||||
heap.set("x", 100);
|
||||
test.equal(heap.maxElementId(), "x");
|
||||
test.equal(heap.get("x"), 100);
|
||||
|
||||
// Bubble down: x drops below y and z.
|
||||
heap.set("x", -100);
|
||||
test.equal(heap.maxElementId(), "y");
|
||||
test.equal(heap.get("x"), -100);
|
||||
heap._selfCheck();
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MaxHeap.set with same value is a no-op", (test) => {
|
||||
const heap = new MaxHeap((a, b) => a - b);
|
||||
heap.set("a", 10);
|
||||
heap.set("b", 20);
|
||||
heap.set("c", 15);
|
||||
const before = heap.maxElementId();
|
||||
|
||||
heap.set("b", 20); // same id, same value
|
||||
test.equal(heap.maxElementId(), before);
|
||||
test.equal(heap.size(), 3);
|
||||
test.equal(heap.get("b"), 20);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// KNOWN BUG — pinned by the two tests below.
|
||||
// =============================================================================
|
||||
//
|
||||
// BUG:
|
||||
// MaxHeap.clone() and MinMaxHeap.clone() are broken. Both return an empty
|
||||
// heap regardless of the original's contents, AND they mutate the original
|
||||
// heap's internal `_heap` array as a side effect (attaching an `IdMap`
|
||||
// property to it).
|
||||
//
|
||||
// WHERE:
|
||||
// packages/binary-heap/max-heap.js, MaxHeap.clone():
|
||||
//
|
||||
// clone() {
|
||||
// const clone = new MaxHeap(this._comparator, this._heap);
|
||||
// return clone;
|
||||
// }
|
||||
//
|
||||
// packages/binary-heap/min-max-heap.js, MinMaxHeap.clone(): same shape.
|
||||
//
|
||||
// WHY IT IS BROKEN:
|
||||
// The MaxHeap constructor signature is `constructor(comparator, options)`,
|
||||
// where `options` is `{ initData, IdMap }`. clone() passes `this._heap` — a
|
||||
// plain array of `{ id, value }` records — as `options`. Inside the
|
||||
// constructor:
|
||||
// 1. `options.IdMap = IdMap` ← mutates the original _heap array,
|
||||
// attaching an `IdMap` property.
|
||||
// 2. `Array.isArray(options.initData)` is false (arrays have no
|
||||
// `initData` property), so `_initFromData` is never called.
|
||||
// The returned clone therefore has `_heap === []` and an empty `_heapIdx`.
|
||||
//
|
||||
// CORRECT FIX (to be applied in a separate PR):
|
||||
// Wrap `this._heap` in the expected options object:
|
||||
//
|
||||
// clone() {
|
||||
// return new MaxHeap(this._comparator, { initData: this._heap });
|
||||
// }
|
||||
//
|
||||
// clone() { // MinMaxHeap
|
||||
// return new MinMaxHeap(this._comparator, { initData: this._heap });
|
||||
// }
|
||||
//
|
||||
// `_initFromData` already expects records in the exact `{ id, value }`
|
||||
// shape that `_heap` stores, so the fix is a one-line wrap in each file.
|
||||
//
|
||||
// WHEN THE FIX LANDS:
|
||||
// Delete the two `*.clone (BROKEN)` tests below and replace them with the
|
||||
// "produces independent copy" tests that were originally written:
|
||||
// they should assert size, maxElementId/minElementId, get() values, and
|
||||
// that mutations to the clone do not affect the original.
|
||||
// =============================================================================
|
||||
|
||||
Tinytest.add("binary-heap - MaxHeap.clone (BROKEN) returns an empty heap", (test) => {
|
||||
const heap = new MaxHeap((a, b) => a - b);
|
||||
heap.set("a", 1);
|
||||
heap.set("b", 5);
|
||||
heap.set("c", 3);
|
||||
|
||||
const clone = heap.clone();
|
||||
|
||||
// BUG: clone is empty instead of mirroring the original's 3 entries.
|
||||
test.equal(clone.size(), 0, "clone.size() should be 3 once clone() is fixed");
|
||||
test.isTrue(clone.empty(), "clone.empty() should be false once clone() is fixed");
|
||||
test.equal(
|
||||
clone.maxElementId(),
|
||||
null,
|
||||
"clone.maxElementId() should be 'b' once clone() is fixed",
|
||||
);
|
||||
test.equal(clone.get("a"), null, "clone.get('a') should be 1 once clone() is fixed");
|
||||
|
||||
// The original is not structurally damaged — it still works — but
|
||||
// as a side effect the constructor attached an IdMap property to the
|
||||
// original heap's internal array. We intentionally do NOT assert on
|
||||
// that side effect here: it is an implementation leak that should
|
||||
// simply disappear once the fix lands.
|
||||
test.equal(heap.size(), 3, "original should still have its 3 entries");
|
||||
test.equal(heap.maxElementId(), "b");
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MinHeap sorts ascending", (test) => {
|
||||
const heap = new MinHeap((a, b) => a - b);
|
||||
heap.set("a", 5);
|
||||
heap.set("b", 1);
|
||||
heap.set("c", 3);
|
||||
heap.set("d", 8);
|
||||
heap.set("e", -2);
|
||||
|
||||
test.equal(heap.minElementId(), "e");
|
||||
|
||||
const drained = [];
|
||||
while (!heap.empty()) {
|
||||
const id = heap.minElementId();
|
||||
drained.push(heap.get(id));
|
||||
heap.remove(id);
|
||||
}
|
||||
test.equal(drained, [-2, 1, 3, 5, 8]);
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MinHeap.maxElementId throws", (test) => {
|
||||
const heap = new MinHeap((a, b) => a - b);
|
||||
heap.set("a", 1);
|
||||
test.throws(() => heap.maxElementId(), /Cannot call maxElementId on MinHeap/);
|
||||
});
|
||||
|
||||
Tinytest.add("binary-heap - MinMaxHeap.clone (BROKEN) returns an empty heap", (test) => {
|
||||
// See the lengthy bug comment above the MaxHeap.clone pinned test.
|
||||
// MinMaxHeap.clone() has the exact same bug: it passes this._heap as
|
||||
// the options argument instead of { initData: this._heap }.
|
||||
//
|
||||
// Once packages/binary-heap/min-max-heap.js clone() is fixed, delete
|
||||
// this test and replace it with one that asserts:
|
||||
// clone.size() === 4
|
||||
// clone.maxElementId() === "b"
|
||||
// clone.minElementId() === "c"
|
||||
// clone.get("a") === 1
|
||||
// mutations to clone do not affect the original.
|
||||
|
||||
const heap = new MinMaxHeap((a, b) => a - b);
|
||||
heap.set("a", 1);
|
||||
heap.set("b", 10);
|
||||
heap.set("c", -5);
|
||||
heap.set("d", 7);
|
||||
|
||||
const clone = heap.clone();
|
||||
|
||||
// BUG: clone is empty instead of mirroring the original's 4 entries.
|
||||
test.equal(clone.size(), 0, "clone.size() should be 4 once clone() is fixed");
|
||||
test.isTrue(clone.empty(), "clone.empty() should be false once clone() is fixed");
|
||||
test.equal(
|
||||
clone.maxElementId(),
|
||||
null,
|
||||
"clone.maxElementId() should be 'b' once clone() is fixed",
|
||||
);
|
||||
// minElementId on an empty MinMaxHeap returns null via MinHeap.maxElementId
|
||||
// which proxies to the inner min heap.
|
||||
test.equal(
|
||||
clone.minElementId(),
|
||||
null,
|
||||
"clone.minElementId() should be 'c' once clone() is fixed",
|
||||
);
|
||||
test.equal(clone.get("a"), null, "clone.get('a') should be 1 once clone() is fixed");
|
||||
|
||||
// Original is not structurally damaged.
|
||||
test.equal(heap.size(), 4);
|
||||
test.equal(heap.maxElementId(), "b");
|
||||
test.equal(heap.minElementId(), "c");
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { MaxHeap } from './max-heap.js';
|
||||
export { MinHeap } from './min-heap.js';
|
||||
export { MinMaxHeap } from './min-max-heap.js';
|
||||
export { MaxHeap } from "./max-heap.js";
|
||||
export { MinHeap } from "./min-heap.js";
|
||||
export { MinMaxHeap } from "./min-max-heap.js";
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
// each value is retained
|
||||
// - IdMap - Constructor - Optional - custom IdMap class to store id->index
|
||||
// mappings internally. Standard IdMap is used by default.
|
||||
export class MaxHeap {
|
||||
export class MaxHeap {
|
||||
constructor(comparator, options = {}) {
|
||||
if (typeof comparator !== 'function') {
|
||||
throw new Error('Passed comparator is invalid, should be a comparison function');
|
||||
if (typeof comparator !== "function") {
|
||||
throw new Error("Passed comparator is invalid, should be a comparison function");
|
||||
}
|
||||
|
||||
// a C-style comparator that is given two values and returns a number,
|
||||
@@ -19,13 +19,13 @@ export class MaxHeap {
|
||||
// value is greater than the first and zero if they are equal.
|
||||
this._comparator = comparator;
|
||||
|
||||
if (! options.IdMap) {
|
||||
if (!options.IdMap) {
|
||||
options.IdMap = IdMap;
|
||||
}
|
||||
|
||||
// _heapIdx maps an id to an index in the Heap array the corresponding value
|
||||
// is located on.
|
||||
this._heapIdx = new options.IdMap;
|
||||
this._heapIdx = new options.IdMap();
|
||||
|
||||
// The Heap data-structure implemented as a 0-based contiguous array where
|
||||
// every item on index idx is a node in a complete binary tree. Every node can
|
||||
@@ -47,7 +47,7 @@ export class MaxHeap {
|
||||
|
||||
data.forEach(({ id }, i) => this._heapIdx.set(id, i));
|
||||
|
||||
if (! data.length) {
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export class MaxHeap {
|
||||
while (idx > 0) {
|
||||
const parent = parentIdx(idx);
|
||||
if (this._maxIndex(parent, idx) === idx) {
|
||||
this._swap(parent, idx)
|
||||
this._swap(parent, idx);
|
||||
idx = parent;
|
||||
} else {
|
||||
break;
|
||||
@@ -115,9 +115,7 @@ export class MaxHeap {
|
||||
}
|
||||
|
||||
get(id) {
|
||||
return this.has(id) ?
|
||||
this._get(this._heapIdx.get(id)) :
|
||||
null;
|
||||
return this.has(id) ? this._get(this._heapIdx.get(id)) : null;
|
||||
}
|
||||
|
||||
set(id, value) {
|
||||
@@ -176,7 +174,7 @@ export class MaxHeap {
|
||||
|
||||
// iterate over values in no particular order
|
||||
forEach(iterator) {
|
||||
this._heap.forEach(obj => iterator(obj.value, obj.id));
|
||||
this._heap.forEach((obj) => iterator(obj.value, obj.id));
|
||||
}
|
||||
|
||||
size() {
|
||||
@@ -204,14 +202,14 @@ export class MaxHeap {
|
||||
_selfCheck() {
|
||||
for (let i = 1; i < this._heap.length; i++) {
|
||||
if (this._maxIndex(parentIdx(i), i) !== parentIdx(i)) {
|
||||
throw new Error(`An item with id ${this._heap[i].id}` +
|
||||
" has a parent younger than it: " +
|
||||
this._heap[parentIdx(i)].id);
|
||||
throw new Error(
|
||||
`An item with id ${this._heap[i].id} has a parent younger than it: ${this._heap[parentIdx(i)].id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const leftChildIdx = i => i * 2 + 1;
|
||||
const rightChildIdx = i => i * 2 + 2;
|
||||
const parentIdx = i => (i - 1) >> 1;
|
||||
const leftChildIdx = (i) => i * 2 + 1;
|
||||
const rightChildIdx = (i) => i * 2 + 2;
|
||||
const parentIdx = (i) => (i - 1) >> 1;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MaxHeap } from './max-heap.js';
|
||||
import { MaxHeap } from "./max-heap.js";
|
||||
|
||||
export class MinHeap extends MaxHeap {
|
||||
constructor(comparator, options) {
|
||||
@@ -12,4 +12,4 @@ export class MinHeap extends MaxHeap {
|
||||
minElementId() {
|
||||
return super.maxElementId();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MaxHeap } from './max-heap.js';
|
||||
import { MinHeap } from './min-heap.js';
|
||||
import { MaxHeap } from "./max-heap.js";
|
||||
import { MinHeap } from "./min-heap.js";
|
||||
|
||||
// This implementation of Min/Max-Heap is just a subclass of Max-Heap
|
||||
// with a Min-Heap as an encapsulated property.
|
||||
@@ -47,5 +47,4 @@ export class MinMaxHeap extends MaxHeap {
|
||||
minElementId() {
|
||||
return this._minHeap.minElementId();
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
Package.describe({
|
||||
summary: "Binary Heap datastructure implementation",
|
||||
version: '1.0.12',
|
||||
version: "1.0.12",
|
||||
});
|
||||
|
||||
Package.onUse(api => {
|
||||
api.export(['MaxHeap', 'MinHeap', 'MinMaxHeap']);
|
||||
api.use(['id-map', 'ecmascript']);
|
||||
api.mainModule('binary-heap.js');
|
||||
Package.onUse((api) => {
|
||||
api.export(["MaxHeap", "MinHeap", "MinMaxHeap"]);
|
||||
api.use(["id-map", "ecmascript"]);
|
||||
api.mainModule("binary-heap.js");
|
||||
});
|
||||
|
||||
Package.onTest(api => {
|
||||
api.use(['tinytest', 'binary-heap', 'ecmascript']);
|
||||
api.addFiles('binary-heap-tests.js');
|
||||
Package.onTest((api) => {
|
||||
api.use(["tinytest", "binary-heap", "ecmascript"]);
|
||||
api.addFiles("binary-heap-tests.js");
|
||||
});
|
||||
|
||||
@@ -65,12 +65,14 @@ export class Hook {
|
||||
}
|
||||
|
||||
register(callback) {
|
||||
const exceptionHandler = this.exceptionHandler || function (exception) {
|
||||
// Note: this relies on the undocumented fact that if bindEnvironment's
|
||||
// onException throws, and you are invoking the callback either in the
|
||||
// browser or from within a Fiber in Node, the exception is propagated.
|
||||
throw exception;
|
||||
};
|
||||
const exceptionHandler =
|
||||
this.exceptionHandler ||
|
||||
function (exception) {
|
||||
// Note: this relies on the undocumented fact that if bindEnvironment's
|
||||
// onException throws, and you are invoking the callback either in the
|
||||
// browser or from within a Fiber in Node, the exception is propagated.
|
||||
throw exception;
|
||||
};
|
||||
|
||||
if (this.bindEnvironment) {
|
||||
callback = Meteor.bindEnvironment(callback, exceptionHandler);
|
||||
@@ -89,7 +91,7 @@ export class Hook {
|
||||
callback,
|
||||
stop: () => {
|
||||
delete this.callbacks[id];
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,14 +112,13 @@ export class Hook {
|
||||
* @param iterator
|
||||
*/
|
||||
forEach(iterator) {
|
||||
|
||||
const ids = Object.keys(this.callbacks);
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
const id = ids[i];
|
||||
// check to see if the callback was removed during iteration
|
||||
if (hasOwn.call(this.callbacks, id)) {
|
||||
const callback = this.callbacks[id];
|
||||
if (! iterator(callback)) {
|
||||
if (!iterator(callback)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -134,12 +135,12 @@ export class Hook {
|
||||
*/
|
||||
async forEachAsync(iterator) {
|
||||
const ids = Object.keys(this.callbacks);
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
const id = ids[i];
|
||||
// check to see if the callback was removed during iteration
|
||||
if (hasOwn.call(this.callbacks, id)) {
|
||||
const callback = this.callbacks[id];
|
||||
if (!await iterator(callback)) {
|
||||
if (!(await iterator(callback))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -157,13 +158,10 @@ export class Hook {
|
||||
|
||||
// Copied from Meteor.bindEnvironment and removed all the env stuff.
|
||||
function dontBindEnvironment(func, onException, _this) {
|
||||
if (!onException || typeof(onException) === 'string') {
|
||||
if (!onException || typeof onException === "string") {
|
||||
const description = onException || "callback of async function";
|
||||
onException = function (error) {
|
||||
Meteor._debug(
|
||||
"Exception in " + description,
|
||||
error
|
||||
);
|
||||
Meteor._debug(`Exception in ${description}`, error);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
Tinytest.add("callback-hook - binds to registrar's env by default", function (test) {
|
||||
const hook = new Hook();
|
||||
const envVar = new Meteor.EnvironmentVariable;
|
||||
envVar.withValue("registrar's value", function() {
|
||||
hook.register(function() {
|
||||
const envVar = new Meteor.EnvironmentVariable();
|
||||
envVar.withValue("registrar's value", function () {
|
||||
hook.register(function () {
|
||||
test.equal(envVar.get(), "registrar's value");
|
||||
});
|
||||
});
|
||||
envVar.withValue("invoker's value", function() {
|
||||
hook.forEach(function(callback) {
|
||||
envVar.withValue("invoker's value", function () {
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -15,14 +15,14 @@ Tinytest.add("callback-hook - binds to registrar's env by default", function (te
|
||||
|
||||
Tinytest.add("callback-hook - uses invoker's env with {bindEnvironment: false}", function (test) {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const envVar = new Meteor.EnvironmentVariable;
|
||||
envVar.withValue("registrar's value", function() {
|
||||
hook.register(function() {
|
||||
const envVar = new Meteor.EnvironmentVariable();
|
||||
envVar.withValue("registrar's value", function () {
|
||||
hook.register(function () {
|
||||
test.equal(envVar.get(), "invoker's value");
|
||||
});
|
||||
});
|
||||
envVar.withValue("invoker's value", function() {
|
||||
hook.each(function(callback) {
|
||||
envVar.withValue("invoker's value", function () {
|
||||
hook.each(function (callback) {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -30,26 +30,225 @@ Tinytest.add("callback-hook - uses invoker's env with {bindEnvironment: false}",
|
||||
|
||||
Tinytest.add("callback-hook - exceptions unhandled with {bindEnvironment: false}", function (test) {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
hook.register(function() {
|
||||
hook.register(function () {
|
||||
throw new Error("Test error");
|
||||
});
|
||||
hook.forEach(function(callback) {
|
||||
hook.forEach(function (callback) {
|
||||
test.throws(callback, "Test error");
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add("callback-hook - exceptionHandler used with {bindEnvironment: false}", function (test) {
|
||||
const exToThrow = new Error("Test error");
|
||||
let thrownEx = null;
|
||||
const hook = new Hook({
|
||||
bindEnvironment: false,
|
||||
exceptionHandler: function (ex) { thrownEx = ex; }
|
||||
});
|
||||
hook.register(function() {
|
||||
throw exToThrow;
|
||||
});
|
||||
hook.each(function(callback) {
|
||||
callback();
|
||||
});
|
||||
test.equal(exToThrow, thrownEx);
|
||||
Tinytest.add(
|
||||
"callback-hook - exceptionHandler used with {bindEnvironment: false}",
|
||||
function (test) {
|
||||
const exToThrow = new Error("Test error");
|
||||
let thrownEx = null;
|
||||
const hook = new Hook({
|
||||
bindEnvironment: false,
|
||||
exceptionHandler: function (ex) {
|
||||
thrownEx = ex;
|
||||
},
|
||||
});
|
||||
hook.register(function () {
|
||||
throw exToThrow;
|
||||
});
|
||||
hook.each(function (callback) {
|
||||
callback();
|
||||
});
|
||||
test.equal(exToThrow, thrownEx);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add("callback-hook - register returns object with stop", function (test) {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const handle = hook.register(function () {});
|
||||
test.isTrue(typeof handle === "object" && handle !== null);
|
||||
test.equal(typeof handle.stop, "function");
|
||||
test.equal(typeof handle.callback, "function");
|
||||
});
|
||||
|
||||
Tinytest.add("callback-hook - stop unregisters the callback", function (test) {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const calls = [];
|
||||
const h1 = hook.register(function () {
|
||||
calls.push("a");
|
||||
});
|
||||
hook.register(function () {
|
||||
calls.push("b");
|
||||
});
|
||||
h1.stop();
|
||||
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
return true;
|
||||
});
|
||||
test.equal(calls, ["b"]);
|
||||
});
|
||||
|
||||
Tinytest.add("callback-hook - forEach iterates callbacks in registration order", (test) => {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const order = [];
|
||||
hook.register(function () {
|
||||
order.push(1);
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push(2);
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push(3);
|
||||
});
|
||||
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
return true;
|
||||
});
|
||||
test.equal(order, [1, 2, 3]);
|
||||
});
|
||||
|
||||
Tinytest.add("callback-hook - forEach stops when iterator returns falsy", (test) => {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const order = [];
|
||||
hook.register(function () {
|
||||
order.push(1);
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push(2);
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push(3);
|
||||
});
|
||||
|
||||
let seen = 0;
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
seen++;
|
||||
return seen < 2; // stop after the second
|
||||
});
|
||||
test.equal(order, [1, 2]);
|
||||
});
|
||||
|
||||
Tinytest.add("callback-hook - callback can safely stop itself during iteration", (test) => {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const calls = [];
|
||||
const holder = {};
|
||||
hook.register(function () {
|
||||
calls.push("a");
|
||||
});
|
||||
holder.h2 = hook.register(function () {
|
||||
calls.push("b");
|
||||
holder.h2.stop();
|
||||
});
|
||||
hook.register(function () {
|
||||
calls.push("c");
|
||||
});
|
||||
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
return true;
|
||||
});
|
||||
test.equal(calls, ["a", "b", "c"]);
|
||||
|
||||
// A second pass confirms h2 is really gone.
|
||||
calls.length = 0;
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
return true;
|
||||
});
|
||||
test.equal(calls, ["a", "c"]);
|
||||
});
|
||||
|
||||
Tinytest.add("callback-hook - clear removes all callbacks", (test) => {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
hook.register(function () {});
|
||||
hook.register(function () {});
|
||||
let seen = 0;
|
||||
hook.forEach(function () {
|
||||
seen++;
|
||||
return true;
|
||||
});
|
||||
test.equal(seen, 2);
|
||||
|
||||
hook.clear();
|
||||
seen = 0;
|
||||
hook.forEach(function () {
|
||||
seen++;
|
||||
return true;
|
||||
});
|
||||
test.equal(seen, 0);
|
||||
});
|
||||
|
||||
Tinytest.addAsync(
|
||||
"callback-hook - forEachAsync iterates and honors falsy return",
|
||||
async function (test) {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const order = [];
|
||||
hook.register(function () {
|
||||
order.push(1);
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push(2);
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push(3);
|
||||
});
|
||||
|
||||
await hook.forEachAsync(async function (callback) {
|
||||
callback();
|
||||
return order.length < 2; // stop after the second
|
||||
});
|
||||
test.equal(order, [1, 2]);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add("callback-hook - debugPrintExceptions must be a string", function (test) {
|
||||
test.throws(function () {
|
||||
new Hook({ debugPrintExceptions: true });
|
||||
}, /debugPrintExceptions should be a string/);
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
"callback-hook - debugPrintExceptions swallows errors with {bindEnvironment: false}",
|
||||
function (test) {
|
||||
const originalDebug = Meteor._debug;
|
||||
const logged = [];
|
||||
Meteor._debug = function (...args) {
|
||||
logged.push(args);
|
||||
};
|
||||
try {
|
||||
const hook = new Hook({
|
||||
bindEnvironment: false,
|
||||
debugPrintExceptions: "test-hook",
|
||||
});
|
||||
hook.register(function () {
|
||||
throw new Error("boom");
|
||||
});
|
||||
// Should not throw — dontBindEnvironment wraps it and logs via Meteor._debug.
|
||||
hook.forEach(function (callback) {
|
||||
callback();
|
||||
return true;
|
||||
});
|
||||
} finally {
|
||||
Meteor._debug = originalDebug;
|
||||
}
|
||||
test.equal(logged.length, 1);
|
||||
// First arg is the description, second is the error.
|
||||
test.matches(logged[0][0], /test-hook/);
|
||||
test.instanceOf(logged[0][1], Error);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add("callback-hook - each is an alias for forEach", function (test) {
|
||||
const hook = new Hook({ bindEnvironment: false });
|
||||
const order = [];
|
||||
hook.register(function () {
|
||||
order.push("a");
|
||||
});
|
||||
hook.register(function () {
|
||||
order.push("b");
|
||||
});
|
||||
hook.each(function (callback) {
|
||||
callback();
|
||||
return true;
|
||||
});
|
||||
test.equal(order, ["a", "b"]);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Package.describe({
|
||||
summary: "Register callbacks on a hook",
|
||||
version: '1.6.1',
|
||||
version: "1.6.1",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.mainModule('hook.js');
|
||||
api.export('Hook');
|
||||
api.use("ecmascript");
|
||||
api.mainModule("hook.js");
|
||||
api.export("Hook");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('callback-hook');
|
||||
api.use('tinytest');
|
||||
api.addFiles('hook_tests.js', 'server');
|
||||
api.use("callback-hook");
|
||||
api.use("tinytest");
|
||||
api.addFiles("hook_tests.js", "server");
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ export const DiffSequence = {};
|
||||
const hasOwn = Object.prototype.hasOwnProperty;
|
||||
|
||||
function isObjEmpty(obj) {
|
||||
for (let key in Object(obj)) {
|
||||
for (const key in Object(obj)) {
|
||||
if (hasOwn.call(obj, key)) {
|
||||
return false;
|
||||
}
|
||||
@@ -15,39 +15,32 @@ function isObjEmpty(obj) {
|
||||
// old_results and new_results: collections of documents.
|
||||
// if ordered, they are arrays.
|
||||
// if unordered, they are IdMaps
|
||||
DiffSequence.diffQueryChanges = function (ordered, oldResults, newResults,
|
||||
observer, options) {
|
||||
if (ordered)
|
||||
DiffSequence.diffQueryOrderedChanges(
|
||||
oldResults, newResults, observer, options);
|
||||
else
|
||||
DiffSequence.diffQueryUnorderedChanges(
|
||||
oldResults, newResults, observer, options);
|
||||
DiffSequence.diffQueryChanges = function (ordered, oldResults, newResults, observer, options) {
|
||||
if (ordered) DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer, options);
|
||||
else DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, observer, options);
|
||||
};
|
||||
|
||||
DiffSequence.diffQueryUnorderedChanges = function (oldResults, newResults,
|
||||
observer, options) {
|
||||
DiffSequence.diffQueryUnorderedChanges = function (oldResults, newResults, observer, options) {
|
||||
options = options || {};
|
||||
var projectionFn = options.projectionFn || EJSON.clone;
|
||||
const projectionFn = options.projectionFn || EJSON.clone;
|
||||
|
||||
if (observer.movedBefore) {
|
||||
throw new Error("_diffQueryUnordered called with a movedBefore observer!");
|
||||
}
|
||||
|
||||
newResults.forEach(function (newDoc, id) {
|
||||
var oldDoc = oldResults.get(id);
|
||||
const oldDoc = oldResults.get(id);
|
||||
if (oldDoc) {
|
||||
if (observer.changed && !EJSON.equals(oldDoc, newDoc)) {
|
||||
var projectedNew = projectionFn(newDoc);
|
||||
var projectedOld = projectionFn(oldDoc);
|
||||
var changedFields =
|
||||
DiffSequence.makeChangedFields(projectedNew, projectedOld);
|
||||
if (! isObjEmpty(changedFields)) {
|
||||
const projectedNew = projectionFn(newDoc);
|
||||
const projectedOld = projectionFn(oldDoc);
|
||||
const changedFields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
|
||||
if (!isObjEmpty(changedFields)) {
|
||||
observer.changed(id, changedFields);
|
||||
}
|
||||
}
|
||||
} else if (observer.added) {
|
||||
var fields = projectionFn(newDoc);
|
||||
const fields = projectionFn(newDoc);
|
||||
delete fields._id;
|
||||
observer.added(newDoc._id, fields);
|
||||
}
|
||||
@@ -55,28 +48,24 @@ DiffSequence.diffQueryUnorderedChanges = function (oldResults, newResults,
|
||||
|
||||
if (observer.removed) {
|
||||
oldResults.forEach(function (oldDoc, id) {
|
||||
if (!newResults.has(id))
|
||||
observer.removed(id);
|
||||
if (!newResults.has(id)) observer.removed(id);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
observer, options) {
|
||||
DiffSequence.diffQueryOrderedChanges = function (old_results, new_results, observer, options) {
|
||||
options = options || {};
|
||||
var projectionFn = options.projectionFn || EJSON.clone;
|
||||
const projectionFn = options.projectionFn || EJSON.clone;
|
||||
|
||||
var new_presence_of_id = {};
|
||||
const new_presence_of_id = {};
|
||||
new_results.forEach(function (doc) {
|
||||
if (new_presence_of_id[doc._id])
|
||||
Meteor._debug("Duplicate _id in new_results");
|
||||
if (new_presence_of_id[doc._id]) Meteor._debug("Duplicate _id in new_results");
|
||||
new_presence_of_id[doc._id] = true;
|
||||
});
|
||||
|
||||
var old_index_of_id = {};
|
||||
const old_index_of_id = {};
|
||||
old_results.forEach(function (doc, i) {
|
||||
if (doc._id in old_index_of_id)
|
||||
Meteor._debug("Duplicate _id in old_results");
|
||||
if (doc._id in old_index_of_id) Meteor._debug("Duplicate _id in old_results");
|
||||
old_index_of_id[doc._id] = i;
|
||||
});
|
||||
|
||||
@@ -106,7 +95,6 @@ DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
// Asymptotically: O(N k) where k is number of ops, or potentially
|
||||
// O(N log N) if inner loop of LCS were made to be binary search.
|
||||
|
||||
|
||||
//////// LCS (longest common sequence, with respect to _id)
|
||||
// (see Wikipedia article on Longest Increasing Subsequence,
|
||||
// where the LIS is taken of the sequence of old indices of the
|
||||
@@ -114,46 +102,44 @@ DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
//
|
||||
// unmoved: the output of the algorithm; members of the LCS,
|
||||
// in the form of indices into new_results
|
||||
var unmoved = [];
|
||||
const unmoved = [];
|
||||
// max_seq_len: length of LCS found so far
|
||||
var max_seq_len = 0;
|
||||
let max_seq_len = 0;
|
||||
// seq_ends[i]: the index into new_results of the last doc in a
|
||||
// common subsequence of length of i+1 <= max_seq_len
|
||||
var N = new_results.length;
|
||||
var seq_ends = new Array(N);
|
||||
const N = new_results.length;
|
||||
const seq_ends = Array.from({ length: N });
|
||||
// ptrs: the common subsequence ending with new_results[n] extends
|
||||
// a common subsequence ending with new_results[ptr[n]], unless
|
||||
// ptr[n] is -1.
|
||||
var ptrs = new Array(N);
|
||||
const ptrs = Array.from({ length: N });
|
||||
// virtual sequence of old indices of new results
|
||||
var old_idx_seq = function(i_new) {
|
||||
const old_idx_seq = function (i_new) {
|
||||
return old_index_of_id[new_results[i_new]._id];
|
||||
};
|
||||
// for each item in new_results, use it to extend a common subsequence
|
||||
// of length j <= max_seq_len
|
||||
for(var i=0; i<N; i++) {
|
||||
for (let i = 0; i < N; i++) {
|
||||
if (old_index_of_id[new_results[i]._id] !== undefined) {
|
||||
var j = max_seq_len;
|
||||
let j = max_seq_len;
|
||||
// this inner loop would traditionally be a binary search,
|
||||
// but scanning backwards we will likely find a subseq to extend
|
||||
// pretty soon, bounded for example by the total number of ops.
|
||||
// If this were to be changed to a binary search, we'd still want
|
||||
// to scan backwards a bit as an optimization.
|
||||
while (j > 0) {
|
||||
if (old_idx_seq(seq_ends[j-1]) < old_idx_seq(i))
|
||||
break;
|
||||
if (old_idx_seq(seq_ends[j - 1]) < old_idx_seq(i)) break;
|
||||
j--;
|
||||
}
|
||||
|
||||
ptrs[i] = (j === 0 ? -1 : seq_ends[j-1]);
|
||||
ptrs[i] = j === 0 ? -1 : seq_ends[j - 1];
|
||||
seq_ends[j] = i;
|
||||
if (j+1 > max_seq_len)
|
||||
max_seq_len = j+1;
|
||||
if (j + 1 > max_seq_len) max_seq_len = j + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// pull out the LCS/LIS into unmoved
|
||||
var idx = (max_seq_len === 0 ? -1 : seq_ends[max_seq_len-1]);
|
||||
let idx = max_seq_len === 0 ? -1 : seq_ends[max_seq_len - 1];
|
||||
while (idx >= 0) {
|
||||
unmoved.push(idx);
|
||||
idx = ptrs[idx];
|
||||
@@ -166,23 +152,24 @@ DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
unmoved.push(new_results.length);
|
||||
|
||||
old_results.forEach(function (doc) {
|
||||
if (!new_presence_of_id[doc._id])
|
||||
observer.removed && observer.removed(doc._id);
|
||||
if (!new_presence_of_id[doc._id]) {
|
||||
if (observer.removed) observer.removed(doc._id);
|
||||
}
|
||||
});
|
||||
|
||||
// for each group of things in the new_results that is anchored by an unmoved
|
||||
// element, iterate through the things before it.
|
||||
var startOfGroup = 0;
|
||||
let startOfGroup = 0;
|
||||
unmoved.forEach(function (endOfGroup) {
|
||||
var groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null;
|
||||
var oldDoc, newDoc, fields, projectedNew, projectedOld;
|
||||
for (var i = startOfGroup; i < endOfGroup; i++) {
|
||||
const groupId = new_results[endOfGroup] ? new_results[endOfGroup]._id : null;
|
||||
let oldDoc, newDoc, fields, projectedNew, projectedOld;
|
||||
for (let i = startOfGroup; i < endOfGroup; i++) {
|
||||
newDoc = new_results[i];
|
||||
if (!hasOwn.call(old_index_of_id, newDoc._id)) {
|
||||
fields = projectionFn(newDoc);
|
||||
delete fields._id;
|
||||
observer.addedBefore && observer.addedBefore(newDoc._id, fields, groupId);
|
||||
observer.added && observer.added(newDoc._id, fields);
|
||||
if (observer.addedBefore) observer.addedBefore(newDoc._id, fields, groupId);
|
||||
if (observer.added) observer.added(newDoc._id, fields);
|
||||
} else {
|
||||
// moved
|
||||
oldDoc = old_results[old_index_of_id[newDoc._id]];
|
||||
@@ -190,9 +177,9 @@ DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
projectedOld = projectionFn(oldDoc);
|
||||
fields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
|
||||
if (!isObjEmpty(fields)) {
|
||||
observer.changed && observer.changed(newDoc._id, fields);
|
||||
if (observer.changed) observer.changed(newDoc._id, fields);
|
||||
}
|
||||
observer.movedBefore && observer.movedBefore(newDoc._id, groupId);
|
||||
if (observer.movedBefore) observer.movedBefore(newDoc._id, groupId);
|
||||
}
|
||||
}
|
||||
if (groupId) {
|
||||
@@ -202,16 +189,13 @@ DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
projectedOld = projectionFn(oldDoc);
|
||||
fields = DiffSequence.makeChangedFields(projectedNew, projectedOld);
|
||||
if (!isObjEmpty(fields)) {
|
||||
observer.changed && observer.changed(newDoc._id, fields);
|
||||
if (observer.changed) observer.changed(newDoc._id, fields);
|
||||
}
|
||||
}
|
||||
startOfGroup = endOfGroup+1;
|
||||
startOfGroup = endOfGroup + 1;
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
// General helper for diff-ing two objects.
|
||||
// callbacks is an object like so:
|
||||
// { leftOnly: function (key, leftValue) {...},
|
||||
@@ -219,19 +203,19 @@ DiffSequence.diffQueryOrderedChanges = function (old_results, new_results,
|
||||
// both: function (key, leftValue, rightValue) {...},
|
||||
// }
|
||||
DiffSequence.diffObjects = function (left, right, callbacks) {
|
||||
Object.keys(left).forEach(key => {
|
||||
Object.keys(left).forEach((key) => {
|
||||
const leftValue = left[key];
|
||||
if (hasOwn.call(right, key)) {
|
||||
callbacks.both && callbacks.both(key, leftValue, right[key]);
|
||||
if (callbacks.both) callbacks.both(key, leftValue, right[key]);
|
||||
} else {
|
||||
callbacks.leftOnly && callbacks.leftOnly(key, leftValue);
|
||||
if (callbacks.leftOnly) callbacks.leftOnly(key, leftValue);
|
||||
}
|
||||
});
|
||||
|
||||
if (callbacks.rightOnly) {
|
||||
Object.keys(right).forEach(key => {
|
||||
Object.keys(right).forEach((key) => {
|
||||
const rightValue = right[key];
|
||||
if (! hasOwn.call(left, key)) {
|
||||
if (!hasOwn.call(left, key)) {
|
||||
callbacks.rightOnly(key, rightValue);
|
||||
}
|
||||
});
|
||||
@@ -240,42 +224,40 @@ DiffSequence.diffObjects = function (left, right, callbacks) {
|
||||
|
||||
DiffSequence.diffMaps = function (left, right, callbacks) {
|
||||
left.forEach(function (leftValue, key) {
|
||||
if (right.has(key)){
|
||||
callbacks.both && callbacks.both(key, leftValue, right.get(key));
|
||||
if (right.has(key)) {
|
||||
if (callbacks.both) callbacks.both(key, leftValue, right.get(key));
|
||||
} else {
|
||||
callbacks.leftOnly && callbacks.leftOnly(key, leftValue);
|
||||
if (callbacks.leftOnly) callbacks.leftOnly(key, leftValue);
|
||||
}
|
||||
});
|
||||
|
||||
if (callbacks.rightOnly) {
|
||||
right.forEach(function (rightValue, key) {
|
||||
if (!left.has(key)){
|
||||
if (!left.has(key)) {
|
||||
callbacks.rightOnly(key, rightValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
DiffSequence.makeChangedFields = function (newDoc, oldDoc) {
|
||||
var fields = {};
|
||||
const fields = {};
|
||||
DiffSequence.diffObjects(oldDoc, newDoc, {
|
||||
leftOnly: function (key, value) {
|
||||
leftOnly: function (key, _value) {
|
||||
fields[key] = undefined;
|
||||
},
|
||||
rightOnly: function (key, value) {
|
||||
fields[key] = value;
|
||||
},
|
||||
both: function (key, leftValue, rightValue) {
|
||||
if (!EJSON.equals(leftValue, rightValue))
|
||||
fields[key] = rightValue;
|
||||
}
|
||||
if (!EJSON.equals(leftValue, rightValue)) fields[key] = rightValue;
|
||||
},
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
DiffSequence.applyChanges = function (doc, changeFields) {
|
||||
Object.keys(changeFields).forEach(key => {
|
||||
Object.keys(changeFields).forEach((key) => {
|
||||
const value = changeFields[key];
|
||||
if (typeof value === "undefined") {
|
||||
delete doc[key];
|
||||
@@ -284,4 +266,3 @@ DiffSequence.applyChanges = function (doc, changeFields) {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
Package.describe({
|
||||
summary: "An implementation of a diff algorithm on arrays and objects.",
|
||||
version: '1.1.3',
|
||||
documentation: null
|
||||
version: "1.1.3",
|
||||
documentation: null,
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.use('ejson');
|
||||
api.mainModule('diff.js');
|
||||
api.export('DiffSequence');
|
||||
api.use("ecmascript");
|
||||
api.use("ejson");
|
||||
api.mainModule("diff.js");
|
||||
api.export("DiffSequence");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use([
|
||||
'tinytest',
|
||||
'ejson'
|
||||
]);
|
||||
api.use(["tinytest", "ejson", "id-map"]);
|
||||
|
||||
api.use('diff-sequence');
|
||||
api.addFiles([
|
||||
'tests.js'
|
||||
]);
|
||||
api.use("diff-sequence");
|
||||
api.addFiles(["tests.js"]);
|
||||
});
|
||||
|
||||
@@ -1,133 +1,128 @@
|
||||
Tinytest.add("diff-sequence - diff changes ordering", function (test) {
|
||||
var makeDocs = function (ids) {
|
||||
return ids.map(function (id) { return {_id: id};});
|
||||
const makeDocs = function (ids) {
|
||||
return ids.map(function (id) {
|
||||
return { _id: id };
|
||||
});
|
||||
};
|
||||
var testMutation = function (a, b) {
|
||||
var aa = makeDocs(a);
|
||||
var bb = makeDocs(b);
|
||||
var aaCopy = EJSON.clone(aa);
|
||||
const testMutation = function (a, b) {
|
||||
const aa = makeDocs(a);
|
||||
const bb = makeDocs(b);
|
||||
const aaCopy = EJSON.clone(aa);
|
||||
DiffSequence.diffQueryOrderedChanges(aa, bb, {
|
||||
|
||||
addedBefore: function (id, doc, before) {
|
||||
if (before === null) {
|
||||
aaCopy.push( Object.assign({_id: id}, doc));
|
||||
aaCopy.push(Object.assign({ _id: id }, doc));
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < aaCopy.length; i++) {
|
||||
for (let i = 0; i < aaCopy.length; i++) {
|
||||
if (aaCopy[i]._id === before) {
|
||||
aaCopy.splice(i, 0, Object.assign({_id: id}, doc));
|
||||
aaCopy.splice(i, 0, Object.assign({ _id: id }, doc));
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
movedBefore: function (id, before) {
|
||||
var found;
|
||||
for (var i = 0; i < aaCopy.length; i++) {
|
||||
let found;
|
||||
for (let i = 0; i < aaCopy.length; i++) {
|
||||
if (aaCopy[i]._id === id) {
|
||||
found = aaCopy[i];
|
||||
aaCopy.splice(i, 1);
|
||||
}
|
||||
}
|
||||
if (before === null) {
|
||||
aaCopy.push( Object.assign({_id: id}, found));
|
||||
aaCopy.push(Object.assign({ _id: id }, found));
|
||||
return;
|
||||
}
|
||||
for (i = 0; i < aaCopy.length; i++) {
|
||||
for (let i = 0; i < aaCopy.length; i++) {
|
||||
if (aaCopy[i]._id === before) {
|
||||
aaCopy.splice(i, 0, Object.assign({_id: id}, found));
|
||||
aaCopy.splice(i, 0, Object.assign({ _id: id }, found));
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
removed: function (id) {
|
||||
var found;
|
||||
for (var i = 0; i < aaCopy.length; i++) {
|
||||
for (let i = 0; i < aaCopy.length; i++) {
|
||||
if (aaCopy[i]._id === id) {
|
||||
found = aaCopy[i];
|
||||
aaCopy.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
test.equal(aaCopy, bb);
|
||||
};
|
||||
|
||||
var testBothWays = function (a, b) {
|
||||
const testBothWays = function (a, b) {
|
||||
testMutation(a, b);
|
||||
testMutation(b, a);
|
||||
};
|
||||
|
||||
testBothWays(["a", "b", "c"], ["c", "b", "a"]);
|
||||
testBothWays(["a", "b", "c"], []);
|
||||
testBothWays(["a", "b", "c"], ["e","f"]);
|
||||
testBothWays(["a", "b", "c"], ["e", "f"]);
|
||||
testBothWays(["a", "b", "c", "d"], ["c", "b", "a"]);
|
||||
testBothWays(['A','B','C','D','E','F','G','H','I'],
|
||||
['A','B','F','G','C','D','I','L','M','N','H']);
|
||||
testBothWays(['A','B','C','D','E','F','G','H','I'],['A','B','C','D','F','G','H','E','I']);
|
||||
testBothWays(
|
||||
["A", "B", "C", "D", "E", "F", "G", "H", "I"],
|
||||
["A", "B", "F", "G", "C", "D", "I", "L", "M", "N", "H"],
|
||||
);
|
||||
testBothWays(
|
||||
["A", "B", "C", "D", "E", "F", "G", "H", "I"],
|
||||
["A", "B", "C", "D", "F", "G", "H", "E", "I"],
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - diff", function (test) {
|
||||
|
||||
// test correctness
|
||||
|
||||
var diffTest = function(origLen, newOldIdx) {
|
||||
var oldResults = new Array(origLen);
|
||||
for (var i = 1; i <= origLen; i++)
|
||||
oldResults[i-1] = {_id: i};
|
||||
const diffTest = function (origLen, newOldIdx) {
|
||||
const oldResults = Array.from({ length: origLen });
|
||||
for (let i = 1; i <= origLen; i++) oldResults[i - 1] = { _id: i };
|
||||
|
||||
var newResults = newOldIdx.map(function(n) {
|
||||
var doc = {_id: Math.abs(n)};
|
||||
if (n < 0)
|
||||
doc.changed = true;
|
||||
const newResults = newOldIdx.map(function (n) {
|
||||
const doc = { _id: Math.abs(n) };
|
||||
if (n < 0) doc.changed = true;
|
||||
return doc;
|
||||
});
|
||||
var find = function (arr, id) {
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
if (EJSON.equals(arr[i]._id, id))
|
||||
return i;
|
||||
const find = function (arr, id) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (EJSON.equals(arr[i]._id, id)) return i;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
var results = [...oldResults];
|
||||
var observer = {
|
||||
addedBefore: function(id, fields, before) {
|
||||
var before_idx;
|
||||
if (before === null)
|
||||
before_idx = results.length;
|
||||
else
|
||||
before_idx = find (results, before);
|
||||
var doc = Object.assign({_id: id}, fields);
|
||||
const results = [...oldResults];
|
||||
const observer = {
|
||||
addedBefore: function (id, fields, before) {
|
||||
let before_idx;
|
||||
if (before === null) before_idx = results.length;
|
||||
else before_idx = find(results, before);
|
||||
const doc = Object.assign({ _id: id }, fields);
|
||||
test.isFalse(before_idx < 0 || before_idx > results.length);
|
||||
results.splice(before_idx, 0, doc);
|
||||
},
|
||||
removed: function(id) {
|
||||
var at_idx = find (results, id);
|
||||
removed: function (id) {
|
||||
const at_idx = find(results, id);
|
||||
test.isFalse(at_idx < 0 || at_idx >= results.length);
|
||||
results.splice(at_idx, 1);
|
||||
},
|
||||
changed: function(id, fields) {
|
||||
var at_idx = find (results, id);
|
||||
var oldDoc = results[at_idx];
|
||||
var doc = EJSON.clone(oldDoc);
|
||||
changed: function (id, fields) {
|
||||
const at_idx = find(results, id);
|
||||
const oldDoc = results[at_idx];
|
||||
const doc = EJSON.clone(oldDoc);
|
||||
DiffSequence.applyChanges(doc, fields);
|
||||
test.isFalse(at_idx < 0 || at_idx >= results.length);
|
||||
test.equal(doc._id, oldDoc._id);
|
||||
results[at_idx] = doc;
|
||||
},
|
||||
movedBefore: function(id, before) {
|
||||
var old_idx = find(results, id);
|
||||
var new_idx;
|
||||
if (before === null)
|
||||
new_idx = results.length;
|
||||
else
|
||||
new_idx = find (results, before);
|
||||
if (new_idx > old_idx)
|
||||
new_idx--;
|
||||
movedBefore: function (id, before) {
|
||||
const old_idx = find(results, id);
|
||||
let new_idx;
|
||||
if (before === null) new_idx = results.length;
|
||||
else new_idx = find(results, before);
|
||||
if (new_idx > old_idx) new_idx--;
|
||||
test.isFalse(old_idx < 0 || old_idx >= results.length);
|
||||
test.isFalse(new_idx < 0 || new_idx >= results.length);
|
||||
results.splice(new_idx, 0, results.splice(old_idx, 1)[0]);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
DiffSequence.diffQueryOrderedChanges(oldResults, newResults, observer);
|
||||
@@ -157,3 +152,193 @@ Tinytest.add("diff-sequence - diff", function (test) {
|
||||
diffTest(3, [-3, -2, -1]);
|
||||
diffTest(10, [-2, 7, 4, 6, 11, -3, -8, 9]);
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - diffObjects partitions keys", (test) => {
|
||||
const left = { a: 1, b: 2, c: 3 };
|
||||
const right = { b: 2, c: 4, d: 5 };
|
||||
|
||||
const leftOnly = [];
|
||||
const rightOnly = [];
|
||||
const both = [];
|
||||
|
||||
DiffSequence.diffObjects(left, right, {
|
||||
leftOnly: (key, value) => leftOnly.push([key, value]),
|
||||
rightOnly: (key, value) => rightOnly.push([key, value]),
|
||||
both: (key, leftValue, rightValue) => both.push([key, leftValue, rightValue]),
|
||||
});
|
||||
|
||||
test.equal(leftOnly, [["a", 1]]);
|
||||
test.equal(rightOnly, [["d", 5]]);
|
||||
// Sort `both` to make the test independent of Object.keys iteration order.
|
||||
both.sort((x, y) => x[0].localeCompare(y[0]));
|
||||
test.equal(both, [
|
||||
["b", 2, 2],
|
||||
["c", 3, 4],
|
||||
]);
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - diffObjects omits missing callbacks", (test) => {
|
||||
const left = { a: 1, b: 2 };
|
||||
const right = { b: 3, c: 4 };
|
||||
|
||||
let bothCount = 0;
|
||||
// Only `both` is provided — leftOnly and rightOnly are absent and must not throw.
|
||||
DiffSequence.diffObjects(left, right, {
|
||||
both: () => {
|
||||
bothCount++;
|
||||
},
|
||||
});
|
||||
test.equal(bothCount, 1);
|
||||
test.ok(); // reaching here means no exception was thrown
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - diffMaps partitions keys", (test) => {
|
||||
const left = new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]);
|
||||
const right = new Map([
|
||||
["b", 2],
|
||||
["c", 4],
|
||||
["d", 5],
|
||||
]);
|
||||
|
||||
const leftOnly = [];
|
||||
const rightOnly = [];
|
||||
const both = [];
|
||||
|
||||
DiffSequence.diffMaps(left, right, {
|
||||
leftOnly: (key, value) => leftOnly.push([key, value]),
|
||||
rightOnly: (key, value) => rightOnly.push([key, value]),
|
||||
both: (key, leftValue, rightValue) => both.push([key, leftValue, rightValue]),
|
||||
});
|
||||
|
||||
test.equal(leftOnly, [["a", 1]]);
|
||||
test.equal(rightOnly, [["d", 5]]);
|
||||
both.sort((x, y) => x[0].localeCompare(y[0]));
|
||||
test.equal(both, [
|
||||
["b", 2, 2],
|
||||
["c", 3, 4],
|
||||
]);
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - makeChangedFields detects adds, removes, changes", (test) => {
|
||||
const oldDoc = { a: 1, b: 2, c: 3 };
|
||||
const newDoc = { a: 1, b: 99, d: 4 };
|
||||
|
||||
const changed = DiffSequence.makeChangedFields(newDoc, oldDoc);
|
||||
|
||||
// 'a' unchanged, 'b' changed, 'c' removed (undefined), 'd' added.
|
||||
test.equal(changed, { b: 99, c: undefined, d: 4 });
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - makeChangedFields uses EJSON.equals for deep compare", (test) => {
|
||||
const same = DiffSequence.makeChangedFields(
|
||||
{ a: [1, 2, 3], b: { nested: true } },
|
||||
{ a: [1, 2, 3], b: { nested: true } },
|
||||
);
|
||||
test.equal(same, {}, "deeply equal values should not produce a change");
|
||||
|
||||
const changed = DiffSequence.makeChangedFields(
|
||||
{ a: [1, 2, 4], b: { nested: true } },
|
||||
{ a: [1, 2, 3], b: { nested: true } },
|
||||
);
|
||||
test.equal(changed, { a: [1, 2, 4] });
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - applyChanges adds, replaces, removes fields", (test) => {
|
||||
const doc = { a: 1, b: 2 };
|
||||
DiffSequence.applyChanges(doc, { a: 99, c: 3, b: undefined });
|
||||
test.equal(doc, { a: 99, c: 3 });
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - diffQueryUnorderedChanges detects added/removed/changed", (test) => {
|
||||
const oldResults = new IdMap();
|
||||
oldResults.set("x", { _id: "x", v: 1 });
|
||||
oldResults.set("y", { _id: "y", v: 2 });
|
||||
|
||||
const newResults = new IdMap();
|
||||
newResults.set("y", { _id: "y", v: 99 });
|
||||
newResults.set("z", { _id: "z", v: 3 });
|
||||
|
||||
const added = [];
|
||||
const removed = [];
|
||||
const changed = [];
|
||||
|
||||
DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, {
|
||||
added: (id, fields) => added.push([id, fields]),
|
||||
removed: (id) => removed.push(id),
|
||||
changed: (id, fields) => changed.push([id, fields]),
|
||||
});
|
||||
|
||||
test.equal(added, [["z", { v: 3 }]]);
|
||||
test.equal(removed, ["x"]);
|
||||
test.equal(changed, [["y", { v: 99 }]]);
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
"diff-sequence - diffQueryUnorderedChanges throws with a movedBefore observer",
|
||||
(test) => {
|
||||
const oldResults = new IdMap();
|
||||
const newResults = new IdMap();
|
||||
test.throws(
|
||||
() =>
|
||||
DiffSequence.diffQueryUnorderedChanges(oldResults, newResults, {
|
||||
movedBefore: () => {},
|
||||
}),
|
||||
/movedBefore/,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add("diff-sequence - diffQueryChanges dispatches based on ordered flag", (test) => {
|
||||
// Unordered path.
|
||||
const oldUnordered = new IdMap();
|
||||
oldUnordered.set("a", { _id: "a", v: 1 });
|
||||
const newUnordered = new IdMap();
|
||||
newUnordered.set("a", { _id: "a", v: 2 });
|
||||
|
||||
const unorderedChanged = [];
|
||||
DiffSequence.diffQueryChanges(false, oldUnordered, newUnordered, {
|
||||
changed: (id, fields) => unorderedChanged.push([id, fields]),
|
||||
});
|
||||
test.equal(unorderedChanged, [["a", { v: 2 }]]);
|
||||
|
||||
// Ordered path — uses addedBefore/movedBefore.
|
||||
const oldOrdered = [{ _id: "a", v: 1 }];
|
||||
const newOrdered = [
|
||||
{ _id: "a", v: 1 },
|
||||
{ _id: "b", v: 2 },
|
||||
];
|
||||
|
||||
const orderedAddedBefore = [];
|
||||
DiffSequence.diffQueryChanges(true, oldOrdered, newOrdered, {
|
||||
addedBefore: (id, fields, before) => orderedAddedBefore.push([id, fields, before]),
|
||||
});
|
||||
test.equal(orderedAddedBefore, [["b", { v: 2 }, null]]);
|
||||
});
|
||||
|
||||
Tinytest.add("diff-sequence - projectionFn is applied before change detection", (test) => {
|
||||
const oldResults = new IdMap();
|
||||
oldResults.set("a", { _id: "a", visible: 1, hidden: 10 });
|
||||
|
||||
const newResults = new IdMap();
|
||||
// Only the `hidden` field changed.
|
||||
newResults.set("a", { _id: "a", visible: 1, hidden: 999 });
|
||||
|
||||
const changed = [];
|
||||
DiffSequence.diffQueryUnorderedChanges(
|
||||
oldResults,
|
||||
newResults,
|
||||
{
|
||||
changed: (id, fields) => changed.push([id, fields]),
|
||||
},
|
||||
{
|
||||
projectionFn: (doc) => ({ _id: doc._id, visible: doc.visible }),
|
||||
},
|
||||
);
|
||||
|
||||
// The projection drops `hidden`, so no change is reported.
|
||||
test.equal(changed, []);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EJSON } from './ejson';
|
||||
import { EJSON } from "./ejson";
|
||||
|
||||
class Address {
|
||||
constructor(city, state) {
|
||||
@@ -7,7 +7,7 @@ class Address {
|
||||
}
|
||||
|
||||
typeName() {
|
||||
return 'Address';
|
||||
return "Address";
|
||||
}
|
||||
|
||||
toJSONValue() {
|
||||
@@ -18,7 +18,7 @@ class Address {
|
||||
}
|
||||
}
|
||||
|
||||
EJSON.addType('Address', value => new Address(value.city, value.state));
|
||||
EJSON.addType("Address", (value) => new Address(value.city, value.state));
|
||||
|
||||
class Person {
|
||||
constructor(name, dob, address) {
|
||||
@@ -28,7 +28,7 @@ class Person {
|
||||
}
|
||||
|
||||
typeName() {
|
||||
return 'Person';
|
||||
return "Person";
|
||||
}
|
||||
|
||||
toJSONValue() {
|
||||
@@ -41,12 +41,9 @@ class Person {
|
||||
}
|
||||
|
||||
EJSON.addType(
|
||||
'Person',
|
||||
value => new Person(
|
||||
value.name,
|
||||
EJSON.fromJSONValue(value.dob),
|
||||
EJSON.fromJSONValue(value.address)
|
||||
)
|
||||
"Person",
|
||||
(value) =>
|
||||
new Person(value.name, EJSON.fromJSONValue(value.dob), EJSON.fromJSONValue(value.address)),
|
||||
);
|
||||
|
||||
class Holder {
|
||||
@@ -55,7 +52,7 @@ class Holder {
|
||||
}
|
||||
|
||||
typeName() {
|
||||
return 'Holder';
|
||||
return "Holder";
|
||||
}
|
||||
|
||||
toJSONValue() {
|
||||
@@ -63,7 +60,7 @@ class Holder {
|
||||
}
|
||||
}
|
||||
|
||||
EJSON.addType('Holder', value => new Holder(value));
|
||||
EJSON.addType("Holder", (value) => new Holder(value));
|
||||
|
||||
const EJSONTest = {
|
||||
Address,
|
||||
|
||||
21
packages/ejson/ejson.d.ts
vendored
21
packages/ejson/ejson.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
export interface EJSONableCustomType {
|
||||
clone?(): EJSONableCustomType;
|
||||
equals?(other: Object): boolean;
|
||||
equals?(other: object): boolean;
|
||||
toJSONValue(): JSONable;
|
||||
typeName(): string;
|
||||
}
|
||||
@@ -9,10 +9,10 @@ export type EJSONableProperty =
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| Object
|
||||
| object
|
||||
| number[]
|
||||
| string[]
|
||||
| Object[]
|
||||
| object[]
|
||||
| Date
|
||||
| Uint8Array
|
||||
| EJSONableCustomType
|
||||
@@ -28,10 +28,10 @@ export interface JSONable {
|
||||
| number
|
||||
| string
|
||||
| boolean
|
||||
| Object
|
||||
| object
|
||||
| number[]
|
||||
| string[]
|
||||
| Object[]
|
||||
| object[]
|
||||
| undefined
|
||||
| null;
|
||||
}
|
||||
@@ -39,22 +39,19 @@ export interface JSONable {
|
||||
export interface EJSON extends EJSONable {}
|
||||
|
||||
export namespace EJSON {
|
||||
function addType(
|
||||
name: string,
|
||||
factory: (val: JSONable) => EJSONableCustomType
|
||||
): void;
|
||||
function addType(name: string, factory: (val: JSONable) => EJSONableCustomType): void;
|
||||
|
||||
function clone<T>(val: T): T;
|
||||
|
||||
function equals(
|
||||
a: EJSON,
|
||||
b: EJSON,
|
||||
options?: { keyOrderSensitive?: boolean | undefined }
|
||||
options?: { keyOrderSensitive?: boolean | undefined },
|
||||
): boolean;
|
||||
|
||||
function fromJSONValue(val: JSONable): any;
|
||||
|
||||
function isBinary(x: Object): x is Uint8Array;
|
||||
function isBinary(x: object): x is Uint8Array;
|
||||
function newBinary(size: number): Uint8Array;
|
||||
|
||||
function parse(str: string): EJSON;
|
||||
@@ -64,7 +61,7 @@ export namespace EJSON {
|
||||
options?: {
|
||||
indent?: boolean | number | string | undefined;
|
||||
canonical?: boolean | undefined;
|
||||
}
|
||||
},
|
||||
): string;
|
||||
|
||||
function toJSONValue(val: EJSON): JSONable;
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
isArguments,
|
||||
isInfOrNaN,
|
||||
handleError,
|
||||
} from './utils';
|
||||
import canonicalStringify from './stringify';
|
||||
} from "./utils";
|
||||
import canonicalStringify from "./stringify";
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
@@ -96,25 +96,25 @@ EJSON.addType = (name, factory) => {
|
||||
};
|
||||
|
||||
const builtinConverters = [
|
||||
{ // Date
|
||||
{
|
||||
// Date
|
||||
matchJSONValue(obj) {
|
||||
return hasOwn(obj, '$date') && lengthOfWithLimit(obj, 1) === 1;
|
||||
return hasOwn(obj, "$date") && lengthOfWithLimit(obj, 1) === 1;
|
||||
},
|
||||
matchObject(obj) {
|
||||
return obj instanceof Date;
|
||||
},
|
||||
toJSONValue(obj) {
|
||||
return {$date: obj.getTime()};
|
||||
return { $date: obj.getTime() };
|
||||
},
|
||||
fromJSONValue(obj) {
|
||||
return new Date(obj.$date);
|
||||
},
|
||||
},
|
||||
{ // RegExp
|
||||
{
|
||||
// RegExp
|
||||
matchJSONValue(obj) {
|
||||
return hasOwn(obj, '$regexp')
|
||||
&& hasOwn(obj, '$flags')
|
||||
&& lengthOfWithLimit(obj, 2) === 2;
|
||||
return hasOwn(obj, "$regexp") && hasOwn(obj, "$flags") && lengthOfWithLimit(obj, 2) === 2;
|
||||
},
|
||||
matchObject(obj) {
|
||||
return obj instanceof RegExp;
|
||||
@@ -122,7 +122,7 @@ const builtinConverters = [
|
||||
toJSONValue(regexp) {
|
||||
return {
|
||||
$regexp: regexp.source,
|
||||
$flags: regexp.flags
|
||||
$flags: regexp.flags,
|
||||
};
|
||||
},
|
||||
fromJSONValue(obj) {
|
||||
@@ -132,15 +132,16 @@ const builtinConverters = [
|
||||
obj.$flags
|
||||
// Cut off flags at 50 chars to avoid abusing RegExp for DOS.
|
||||
.slice(0, 50)
|
||||
.replace(/[^gimuy]/g,'')
|
||||
.replace(/(.)(?=.*\1)/g, '')
|
||||
.replace(/[^gimuy]/g, "")
|
||||
.replace(/(.)(?=.*\1)/g, ""),
|
||||
);
|
||||
},
|
||||
},
|
||||
{ // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object'
|
||||
{
|
||||
// NaN, Inf, -Inf. (These are the only objects with typeof !== 'object'
|
||||
// which we match.)
|
||||
matchJSONValue(obj) {
|
||||
return hasOwn(obj, '$InfNaN') && lengthOfWithLimit(obj, 1) === 1;
|
||||
return hasOwn(obj, "$InfNaN") && lengthOfWithLimit(obj, 1) === 1;
|
||||
},
|
||||
matchObject: isInfOrNaN,
|
||||
toJSONValue(obj) {
|
||||
@@ -152,68 +153,71 @@ const builtinConverters = [
|
||||
} else {
|
||||
sign = -1;
|
||||
}
|
||||
return {$InfNaN: sign};
|
||||
return { $InfNaN: sign };
|
||||
},
|
||||
fromJSONValue(obj) {
|
||||
return obj.$InfNaN / 0;
|
||||
},
|
||||
},
|
||||
{ // Binary
|
||||
{
|
||||
// Binary
|
||||
matchJSONValue(obj) {
|
||||
return hasOwn(obj, '$binary') && lengthOfWithLimit(obj, 1) === 1;
|
||||
return hasOwn(obj, "$binary") && lengthOfWithLimit(obj, 1) === 1;
|
||||
},
|
||||
matchObject(obj) {
|
||||
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array
|
||||
|| (obj && hasOwn(obj, '$Uint8ArrayPolyfill'));
|
||||
return (
|
||||
(typeof Uint8Array !== "undefined" && obj instanceof Uint8Array) ||
|
||||
(obj && hasOwn(obj, "$Uint8ArrayPolyfill"))
|
||||
);
|
||||
},
|
||||
toJSONValue(obj) {
|
||||
return {$binary: Base64.encode(obj)};
|
||||
return { $binary: Base64.encode(obj) };
|
||||
},
|
||||
fromJSONValue(obj) {
|
||||
return Base64.decode(obj.$binary);
|
||||
},
|
||||
},
|
||||
{ // Escaping one level
|
||||
{
|
||||
// Escaping one level
|
||||
matchJSONValue(obj) {
|
||||
return hasOwn(obj, '$escape') && lengthOfWithLimit(obj, 1) === 1;
|
||||
return hasOwn(obj, "$escape") && lengthOfWithLimit(obj, 1) === 1;
|
||||
},
|
||||
matchObject(obj) {
|
||||
let match = false;
|
||||
if (obj) {
|
||||
const keyCount = lengthOfWithLimit(obj, 2);
|
||||
if (keyCount === 1 || keyCount === 2) {
|
||||
match =
|
||||
builtinConverters.some(converter => converter.matchJSONValue(obj));
|
||||
match = builtinConverters.some((converter) => converter.matchJSONValue(obj));
|
||||
}
|
||||
}
|
||||
return match;
|
||||
},
|
||||
toJSONValue(obj) {
|
||||
const newObj = {};
|
||||
keysOf(obj).forEach(key => {
|
||||
keysOf(obj).forEach((key) => {
|
||||
newObj[key] = EJSON.toJSONValue(obj[key]);
|
||||
});
|
||||
return {$escape: newObj};
|
||||
return { $escape: newObj };
|
||||
},
|
||||
fromJSONValue(obj) {
|
||||
const newObj = {};
|
||||
keysOf(obj.$escape).forEach(key => {
|
||||
keysOf(obj.$escape).forEach((key) => {
|
||||
newObj[key] = EJSON.fromJSONValue(obj.$escape[key]);
|
||||
});
|
||||
return newObj;
|
||||
},
|
||||
},
|
||||
{ // Custom
|
||||
{
|
||||
// Custom
|
||||
matchJSONValue(obj) {
|
||||
return hasOwn(obj, '$type')
|
||||
&& hasOwn(obj, '$value') && lengthOfWithLimit(obj, 2) === 2;
|
||||
return hasOwn(obj, "$type") && hasOwn(obj, "$value") && lengthOfWithLimit(obj, 2) === 2;
|
||||
},
|
||||
matchObject(obj) {
|
||||
return EJSON._isCustomType(obj);
|
||||
},
|
||||
toJSONValue(obj) {
|
||||
const jsonValue = Meteor._noYieldsAllowed(() => obj.toJSONValue());
|
||||
return {$type: obj.typeName(), $value: jsonValue};
|
||||
return { $type: obj.typeName(), $value: jsonValue };
|
||||
},
|
||||
fromJSONValue(obj) {
|
||||
const typeName = obj.$type;
|
||||
@@ -226,20 +230,17 @@ const builtinConverters = [
|
||||
},
|
||||
];
|
||||
|
||||
EJSON._isCustomType = (obj) => (
|
||||
obj &&
|
||||
isFunction(obj.toJSONValue) &&
|
||||
isFunction(obj.typeName) &&
|
||||
customTypes.has(obj.typeName())
|
||||
);
|
||||
EJSON._isCustomType = (obj) =>
|
||||
obj && isFunction(obj.toJSONValue) && isFunction(obj.typeName) && customTypes.has(obj.typeName());
|
||||
|
||||
EJSON._getTypes = (isOriginal = false) => (isOriginal ? customTypes : convertMapToObject(customTypes));
|
||||
EJSON._getTypes = (isOriginal = false) =>
|
||||
isOriginal ? customTypes : convertMapToObject(customTypes);
|
||||
|
||||
EJSON._getConverters = () => builtinConverters;
|
||||
|
||||
// Either return the JSON-compatible version of the argument, or undefined (if
|
||||
// the item isn't itself replaceable, but maybe some fields in it are)
|
||||
const toJSONValueHelper = item => {
|
||||
const toJSONValueHelper = (item) => {
|
||||
for (let i = 0; i < builtinConverters.length; i++) {
|
||||
const converter = builtinConverters[i];
|
||||
if (converter.matchObject(item)) {
|
||||
@@ -250,7 +251,7 @@ const toJSONValueHelper = item => {
|
||||
};
|
||||
|
||||
// for both arrays and objects, in-place modification.
|
||||
const adjustTypesToJSONValue = obj => {
|
||||
const adjustTypesToJSONValue = (obj) => {
|
||||
// Is it an atom that we need to adjust?
|
||||
if (obj === null) {
|
||||
return null;
|
||||
@@ -267,10 +268,9 @@ const adjustTypesToJSONValue = obj => {
|
||||
}
|
||||
|
||||
// Iterate over array or object structure.
|
||||
keysOf(obj).forEach(key => {
|
||||
keysOf(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
if (!isObject(value) && value !== undefined &&
|
||||
!isInfOrNaN(value)) {
|
||||
if (!isObject(value) && value !== undefined && !isInfOrNaN(value)) {
|
||||
return; // continue
|
||||
}
|
||||
|
||||
@@ -291,12 +291,15 @@ EJSON._adjustTypesToJSONValue = adjustTypesToJSONValue;
|
||||
// Copy-on-write recursive EJSON→JSON converter.
|
||||
// Only allocates new objects/arrays along paths that actually change,
|
||||
// returning the original reference when nothing needs conversion.
|
||||
const toJSONValueDeep = value => {
|
||||
const toJSONValueDeep = (value) => {
|
||||
// Short-circuit for primitives that toJSONValueHelper can never match.
|
||||
if (value === null || value === undefined
|
||||
|| typeof value === 'boolean'
|
||||
|| typeof value === 'string'
|
||||
|| (typeof value === 'number' && !isInfOrNaN(value))) {
|
||||
if (
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
typeof value === "boolean" ||
|
||||
typeof value === "string" ||
|
||||
(typeof value === "number" && !isInfOrNaN(value))
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -352,17 +355,16 @@ const toJSONValueDeep = value => {
|
||||
* @locus Anywhere
|
||||
* @param {EJSON} val A value to serialize to plain JSON.
|
||||
*/
|
||||
EJSON.toJSONValue = item => toJSONValueDeep(item);
|
||||
EJSON.toJSONValue = (item) => toJSONValueDeep(item);
|
||||
|
||||
// Either return the argument changed to have the non-json
|
||||
// rep of itself (the Object version) or the argument itself.
|
||||
// DOES NOT RECURSE. For actually getting the fully-changed value, use
|
||||
// EJSON.fromJSONValue
|
||||
const fromJSONValueHelper = value => {
|
||||
const fromJSONValueHelper = (value) => {
|
||||
if (isObject(value) && value !== null) {
|
||||
const keys = keysOf(value);
|
||||
if (keys.length <= 2
|
||||
&& keys.every(k => typeof k === 'string' && k.substr(0, 1) === '$')) {
|
||||
if (keys.length <= 2 && keys.every((k) => typeof k === "string" && k.substr(0, 1) === "$")) {
|
||||
for (let i = 0; i < builtinConverters.length; i++) {
|
||||
const converter = builtinConverters[i];
|
||||
if (converter.matchJSONValue(value)) {
|
||||
@@ -377,7 +379,7 @@ const fromJSONValueHelper = value => {
|
||||
// for both arrays and objects. Tries its best to just
|
||||
// use the object you hand it, but may return something
|
||||
// different if the object you hand it itself needs changing.
|
||||
const adjustTypesFromJSONValue = obj => {
|
||||
const adjustTypesFromJSONValue = (obj) => {
|
||||
if (obj === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -392,7 +394,7 @@ const adjustTypesFromJSONValue = obj => {
|
||||
return obj;
|
||||
}
|
||||
|
||||
keysOf(obj).forEach(key => {
|
||||
keysOf(obj).forEach((key) => {
|
||||
const value = obj[key];
|
||||
if (isObject(value)) {
|
||||
const changed = fromJSONValueHelper(value);
|
||||
@@ -412,8 +414,8 @@ EJSON._adjustTypesFromJSONValue = adjustTypesFromJSONValue;
|
||||
|
||||
// Copy-on-write recursive JSON→EJSON converter.
|
||||
// Same lazy-allocation strategy as toJSONValueDeep.
|
||||
const fromJSONValueDeep = value => {
|
||||
if (value === null || typeof value !== 'object') {
|
||||
const fromJSONValueDeep = (value) => {
|
||||
if (value === null || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -466,7 +468,7 @@ const fromJSONValueDeep = value => {
|
||||
* @locus Anywhere
|
||||
* @param {JSONCompatible} val A value to deserialize into EJSON.
|
||||
*/
|
||||
EJSON.fromJSONValue = item => fromJSONValueDeep(item);
|
||||
EJSON.fromJSONValue = (item) => fromJSONValueDeep(item);
|
||||
|
||||
/**
|
||||
* @summary Serialize a value to a string. For EJSON values, the serialization
|
||||
@@ -499,9 +501,9 @@ EJSON.stringify = handleError((item, options) => {
|
||||
* @locus Anywhere
|
||||
* @param {String} str A string to parse into an EJSON value.
|
||||
*/
|
||||
EJSON.parse = item => {
|
||||
if (typeof item !== 'string') {
|
||||
throw new Error('EJSON.parse argument should be a string');
|
||||
EJSON.parse = (item) => {
|
||||
if (typeof item !== "string") {
|
||||
throw new Error("EJSON.parse argument should be a string");
|
||||
}
|
||||
return EJSON.fromJSONValue(JSON.parse(item));
|
||||
};
|
||||
@@ -512,9 +514,11 @@ EJSON.parse = item => {
|
||||
* @param {Object} x The variable to check.
|
||||
* @locus Anywhere
|
||||
*/
|
||||
EJSON.isBinary = obj => {
|
||||
return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) ||
|
||||
(obj && obj.$Uint8ArrayPolyfill));
|
||||
EJSON.isBinary = (obj) => {
|
||||
return !!(
|
||||
(typeof Uint8Array !== "undefined" && obj instanceof Uint8Array) ||
|
||||
(obj && obj.$Uint8ArrayPolyfill)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -545,7 +549,7 @@ EJSON.equals = (a, b, options) => {
|
||||
|
||||
// Same-type primitives that aren't === can only be equal if both are NaN.
|
||||
// This skips the NaN check entirely for strings, booleans, etc.
|
||||
if (typeof a !== 'object') {
|
||||
if (typeof a !== "object") {
|
||||
return Number.isNaN(a) && Number.isNaN(b);
|
||||
}
|
||||
|
||||
@@ -602,8 +606,10 @@ EJSON.equals = (a, b, options) => {
|
||||
|
||||
// fallback for custom types that don't implement their own equals
|
||||
switch (EJSON._isCustomType(a) + EJSON._isCustomType(b)) {
|
||||
case 1: return false;
|
||||
case 2: return EJSON.equals(EJSON.toJSONValue(a), EJSON.toJSONValue(b));
|
||||
case 1:
|
||||
return false;
|
||||
case 2:
|
||||
return EJSON.equals(EJSON.toJSONValue(a), EJSON.toJSONValue(b));
|
||||
default: // Do nothing
|
||||
}
|
||||
|
||||
@@ -616,7 +622,7 @@ EJSON.equals = (a, b, options) => {
|
||||
}
|
||||
if (keyOrderSensitive) {
|
||||
i = 0;
|
||||
ret = aKeys.every(key => {
|
||||
ret = aKeys.every((key) => {
|
||||
if (i >= bKeys.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -631,7 +637,7 @@ EJSON.equals = (a, b, options) => {
|
||||
});
|
||||
} else {
|
||||
i = 0;
|
||||
ret = aKeys.every(key => {
|
||||
ret = aKeys.every((key) => {
|
||||
if (!hasOwn(b, key)) {
|
||||
return false;
|
||||
}
|
||||
@@ -650,7 +656,7 @@ EJSON.equals = (a, b, options) => {
|
||||
* @locus Anywhere
|
||||
* @param {EJSON} val A value to copy.
|
||||
*/
|
||||
EJSON.clone = v => {
|
||||
EJSON.clone = (v) => {
|
||||
let ret;
|
||||
if (!isObject(v)) {
|
||||
return v;
|
||||
|
||||
@@ -1,124 +1,141 @@
|
||||
import { EJSON } from './ejson';
|
||||
import EJSONTest from './custom_models_for_tests';
|
||||
import { EJSON } from "./ejson";
|
||||
import EJSONTest from "./custom_models_for_tests";
|
||||
|
||||
Tinytest.add('ejson - keyOrderSensitive', test => {
|
||||
test.isTrue(EJSON.equals({
|
||||
a: {b: 1, c: 2},
|
||||
d: {e: 3, f: 4},
|
||||
}, {
|
||||
d: {f: 4, e: 3},
|
||||
a: {c: 2, b: 1},
|
||||
}));
|
||||
Tinytest.add("ejson - keyOrderSensitive", (test) => {
|
||||
test.isTrue(
|
||||
EJSON.equals(
|
||||
{
|
||||
a: { b: 1, c: 2 },
|
||||
d: { e: 3, f: 4 },
|
||||
},
|
||||
{
|
||||
d: { f: 4, e: 3 },
|
||||
a: { c: 2, b: 1 },
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
test.isFalse(EJSON.equals({
|
||||
a: {b: 1, c: 2},
|
||||
d: {e: 3, f: 4},
|
||||
}, {
|
||||
d: {f: 4, e: 3},
|
||||
a: {c: 2, b: 1},
|
||||
}, {keyOrderSensitive: true}));
|
||||
test.isFalse(
|
||||
EJSON.equals(
|
||||
{
|
||||
a: { b: 1, c: 2 },
|
||||
d: { e: 3, f: 4 },
|
||||
},
|
||||
{
|
||||
d: { f: 4, e: 3 },
|
||||
a: { c: 2, b: 1 },
|
||||
},
|
||||
{ keyOrderSensitive: true },
|
||||
),
|
||||
);
|
||||
|
||||
test.isFalse(EJSON.equals({
|
||||
a: {b: 1, c: 2},
|
||||
d: {e: 3, f: 4},
|
||||
}, {
|
||||
a: {c: 2, b: 1},
|
||||
d: {f: 4, e: 3},
|
||||
}, {keyOrderSensitive: true}));
|
||||
test.isFalse(EJSON.equals({a: {}}, {a: {b: 2}}, {keyOrderSensitive: true}));
|
||||
test.isFalse(EJSON.equals({a: {b: 2}}, {a: {}}, {keyOrderSensitive: true}));
|
||||
test.isFalse(
|
||||
EJSON.equals(
|
||||
{
|
||||
a: { b: 1, c: 2 },
|
||||
d: { e: 3, f: 4 },
|
||||
},
|
||||
{
|
||||
a: { c: 2, b: 1 },
|
||||
d: { f: 4, e: 3 },
|
||||
},
|
||||
{ keyOrderSensitive: true },
|
||||
),
|
||||
);
|
||||
test.isFalse(EJSON.equals({ a: {} }, { a: { b: 2 } }, { keyOrderSensitive: true }));
|
||||
test.isFalse(EJSON.equals({ a: { b: 2 } }, { a: {} }, { keyOrderSensitive: true }));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - nesting and literal', test => {
|
||||
Tinytest.add("ejson - nesting and literal", (test) => {
|
||||
const d = new Date();
|
||||
const obj = {$date: d};
|
||||
const obj = { $date: d };
|
||||
const eObj = EJSON.toJSONValue(obj);
|
||||
const roundTrip = EJSON.fromJSONValue(eObj);
|
||||
test.equal(obj, roundTrip);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - some equality tests', test => {
|
||||
test.isTrue(EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, c: 3, b: 2}));
|
||||
test.isFalse(EJSON.equals({a: 1, b: 2}, {a: 1, c: 3, b: 2}));
|
||||
test.isFalse(EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, b: 2}));
|
||||
test.isFalse(EJSON.equals({a: 1, b: 2, c: 3}, {a: 1, c: 3, b: 4}));
|
||||
test.isFalse(EJSON.equals({a: {}}, {a: {b: 2}}));
|
||||
test.isFalse(EJSON.equals({a: {b: 2}}, {a: {}}));
|
||||
Tinytest.add("ejson - some equality tests", (test) => {
|
||||
test.isTrue(EJSON.equals({ a: 1, b: 2, c: 3 }, { a: 1, c: 3, b: 2 }));
|
||||
test.isFalse(EJSON.equals({ a: 1, b: 2 }, { a: 1, c: 3, b: 2 }));
|
||||
test.isFalse(EJSON.equals({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 }));
|
||||
test.isFalse(EJSON.equals({ a: 1, b: 2, c: 3 }, { a: 1, c: 3, b: 4 }));
|
||||
test.isFalse(EJSON.equals({ a: {} }, { a: { b: 2 } }));
|
||||
test.isFalse(EJSON.equals({ a: { b: 2 } }, { a: {} }));
|
||||
// XXX: Object and Array were previously mistaken, which is why
|
||||
// we add some extra tests for them here
|
||||
test.isTrue(EJSON.equals([1, 2, 3, 4, 5], [1, 2, 3, 4, 5]));
|
||||
test.isFalse(EJSON.equals([1, 2, 3, 4, 5], [1, 2, 3, 4]));
|
||||
test.isFalse(EJSON.equals([1,2,3,4], {0: 1, 1: 2, 2: 3, 3: 4}));
|
||||
test.isFalse(EJSON.equals({0: 1, 1: 2, 2: 3, 3: 4}, [1,2,3,4]));
|
||||
test.isFalse(EJSON.equals([1, 2, 3, 4], { 0: 1, 1: 2, 2: 3, 3: 4 }));
|
||||
test.isFalse(EJSON.equals({ 0: 1, 1: 2, 2: 3, 3: 4 }, [1, 2, 3, 4]));
|
||||
test.isFalse(EJSON.equals({}, []));
|
||||
test.isFalse(EJSON.equals([], {}));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - equality and falsiness', test => {
|
||||
Tinytest.add("ejson - equality and falsiness", (test) => {
|
||||
test.isTrue(EJSON.equals(null, null));
|
||||
test.isTrue(EJSON.equals(undefined, undefined));
|
||||
test.isFalse(EJSON.equals({foo: 'foo'}, null));
|
||||
test.isFalse(EJSON.equals(null, {foo: 'foo'}));
|
||||
test.isFalse(EJSON.equals(undefined, {foo: 'foo'}));
|
||||
test.isFalse(EJSON.equals({foo: 'foo'}, undefined));
|
||||
test.isFalse(EJSON.equals({ foo: "foo" }, null));
|
||||
test.isFalse(EJSON.equals(null, { foo: "foo" }));
|
||||
test.isFalse(EJSON.equals(undefined, { foo: "foo" }));
|
||||
test.isFalse(EJSON.equals({ foo: "foo" }, undefined));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - equals type-mismatch early exit', test => {
|
||||
Tinytest.add("ejson - equals type-mismatch early exit", (test) => {
|
||||
// Cross-type primitives: typeof a !== typeof b → false
|
||||
test.isFalse(EJSON.equals('hello', 42));
|
||||
test.isFalse(EJSON.equals(42, 'hello'));
|
||||
test.isFalse(EJSON.equals("hello", 42));
|
||||
test.isFalse(EJSON.equals(42, "hello"));
|
||||
test.isFalse(EJSON.equals(1, true));
|
||||
test.isFalse(EJSON.equals(true, 1));
|
||||
test.isFalse(EJSON.equals('true', true));
|
||||
test.isFalse(EJSON.equals(true, 'true'));
|
||||
test.isFalse(EJSON.equals('1', 1));
|
||||
test.isFalse(EJSON.equals(1, '1'));
|
||||
test.isFalse(EJSON.equals("true", true));
|
||||
test.isFalse(EJSON.equals(true, "true"));
|
||||
test.isFalse(EJSON.equals("1", 1));
|
||||
test.isFalse(EJSON.equals(1, "1"));
|
||||
|
||||
// Falsy cross-type: both are falsy but different types
|
||||
test.isFalse(EJSON.equals(0, false));
|
||||
test.isFalse(EJSON.equals(false, 0));
|
||||
test.isFalse(EJSON.equals('', 0));
|
||||
test.isFalse(EJSON.equals(0, ''));
|
||||
test.isFalse(EJSON.equals('', false));
|
||||
test.isFalse(EJSON.equals(false, ''));
|
||||
test.isFalse(EJSON.equals("", 0));
|
||||
test.isFalse(EJSON.equals(0, ""));
|
||||
test.isFalse(EJSON.equals("", false));
|
||||
test.isFalse(EJSON.equals(false, ""));
|
||||
|
||||
// null/undefined vs primitives (typeof null is 'object', differs from 'number'/'string')
|
||||
test.isFalse(EJSON.equals(null, 0));
|
||||
test.isFalse(EJSON.equals(0, null));
|
||||
test.isFalse(EJSON.equals(null, ''));
|
||||
test.isFalse(EJSON.equals('', null));
|
||||
test.isFalse(EJSON.equals(null, ""));
|
||||
test.isFalse(EJSON.equals("", null));
|
||||
test.isFalse(EJSON.equals(null, false));
|
||||
test.isFalse(EJSON.equals(false, null));
|
||||
test.isFalse(EJSON.equals(undefined, 0));
|
||||
test.isFalse(EJSON.equals(0, undefined));
|
||||
test.isFalse(EJSON.equals(undefined, ''));
|
||||
test.isFalse(EJSON.equals('', undefined));
|
||||
test.isFalse(EJSON.equals(undefined, ""));
|
||||
test.isFalse(EJSON.equals("", undefined));
|
||||
test.isFalse(EJSON.equals(undefined, false));
|
||||
test.isFalse(EJSON.equals(false, undefined));
|
||||
test.isFalse(EJSON.equals(null, undefined));
|
||||
test.isFalse(EJSON.equals(undefined, null));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - equals same-type primitives', test => {
|
||||
Tinytest.add("ejson - equals same-type primitives", (test) => {
|
||||
// Same-type, same-value → caught by a === b
|
||||
test.isTrue(EJSON.equals(0, 0));
|
||||
test.isTrue(EJSON.equals(1, 1));
|
||||
test.isTrue(EJSON.equals(-1, -1));
|
||||
test.isTrue(EJSON.equals('', ''));
|
||||
test.isTrue(EJSON.equals('hello', 'hello'));
|
||||
test.isTrue(EJSON.equals("", ""));
|
||||
test.isTrue(EJSON.equals("hello", "hello"));
|
||||
test.isTrue(EJSON.equals(true, true));
|
||||
test.isTrue(EJSON.equals(false, false));
|
||||
|
||||
// Same-type, different-value → typeof a !== 'object', then NaN check returns false
|
||||
test.isFalse(EJSON.equals(1, 2));
|
||||
test.isFalse(EJSON.equals('a', 'b'));
|
||||
test.isFalse(EJSON.equals("a", "b"));
|
||||
test.isFalse(EJSON.equals(true, false));
|
||||
test.isFalse(EJSON.equals(false, true));
|
||||
test.isFalse(EJSON.equals(0, 1));
|
||||
test.isTrue(EJSON.equals(0, -0)); // 0 === -0 in JS, caught by a === b
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - equals null vs object', test => {
|
||||
Tinytest.add("ejson - equals null vs object", (test) => {
|
||||
// Both typeof 'object', but one is null
|
||||
test.isFalse(EJSON.equals(null, {}));
|
||||
test.isFalse(EJSON.equals({}, null));
|
||||
@@ -128,45 +145,36 @@ Tinytest.add('ejson - equals null vs object', test => {
|
||||
test.isFalse(EJSON.equals(new Date(), null));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - equals nested falsy and type-mismatch fields', test => {
|
||||
Tinytest.add("ejson - equals nested falsy and type-mismatch fields", (test) => {
|
||||
// Objects with falsy fields of different types
|
||||
test.isFalse(EJSON.equals({a: 0}, {a: false}));
|
||||
test.isFalse(EJSON.equals({a: ''}, {a: 0}));
|
||||
test.isFalse(EJSON.equals({a: ''}, {a: false}));
|
||||
test.isFalse(EJSON.equals({a: null}, {a: undefined}));
|
||||
test.isFalse(EJSON.equals({a: null}, {a: 0}));
|
||||
test.isFalse(EJSON.equals({a: null}, {a: ''}));
|
||||
test.isFalse(EJSON.equals({a: null}, {a: false}));
|
||||
test.isFalse(EJSON.equals({ a: 0 }, { a: false }));
|
||||
test.isFalse(EJSON.equals({ a: "" }, { a: 0 }));
|
||||
test.isFalse(EJSON.equals({ a: "" }, { a: false }));
|
||||
test.isFalse(EJSON.equals({ a: null }, { a: undefined }));
|
||||
test.isFalse(EJSON.equals({ a: null }, { a: 0 }));
|
||||
test.isFalse(EJSON.equals({ a: null }, { a: "" }));
|
||||
test.isFalse(EJSON.equals({ a: null }, { a: false }));
|
||||
|
||||
// Objects with same falsy values should be equal
|
||||
test.isTrue(EJSON.equals({a: 0}, {a: 0}));
|
||||
test.isTrue(EJSON.equals({a: ''}, {a: ''}));
|
||||
test.isTrue(EJSON.equals({a: false}, {a: false}));
|
||||
test.isTrue(EJSON.equals({a: null}, {a: null}));
|
||||
test.isTrue(EJSON.equals({a: undefined}, {a: undefined}));
|
||||
test.isTrue(EJSON.equals({ a: 0 }, { a: 0 }));
|
||||
test.isTrue(EJSON.equals({ a: "" }, { a: "" }));
|
||||
test.isTrue(EJSON.equals({ a: false }, { a: false }));
|
||||
test.isTrue(EJSON.equals({ a: null }, { a: null }));
|
||||
test.isTrue(EJSON.equals({ a: undefined }, { a: undefined }));
|
||||
|
||||
// Deeply nested type mismatches
|
||||
test.isFalse(EJSON.equals(
|
||||
{a: {b: {c: 0}}},
|
||||
{a: {b: {c: false}}}
|
||||
));
|
||||
test.isFalse(EJSON.equals(
|
||||
{a: {b: {c: null}}},
|
||||
{a: {b: {c: undefined}}}
|
||||
));
|
||||
test.isTrue(EJSON.equals(
|
||||
{a: {b: {c: 0}}},
|
||||
{a: {b: {c: 0}}}
|
||||
));
|
||||
test.isFalse(EJSON.equals({ a: { b: { c: 0 } } }, { a: { b: { c: false } } }));
|
||||
test.isFalse(EJSON.equals({ a: { b: { c: null } } }, { a: { b: { c: undefined } } }));
|
||||
test.isTrue(EJSON.equals({ a: { b: { c: 0 } } }, { a: { b: { c: 0 } } }));
|
||||
|
||||
// Arrays with type-mismatched elements
|
||||
test.isFalse(EJSON.equals([0, 1, 2], [false, 1, 2]));
|
||||
test.isFalse(EJSON.equals([0, '', 2], [0, false, 2]));
|
||||
test.isFalse(EJSON.equals([0, "", 2], [0, false, 2]));
|
||||
test.isFalse(EJSON.equals([null], [undefined]));
|
||||
test.isTrue(EJSON.equals([0, '', null], [0, '', null]));
|
||||
test.isTrue(EJSON.equals([0, "", null], [0, "", null]));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - NaN and Inf', test => {
|
||||
Tinytest.add("ejson - NaN and Inf", (test) => {
|
||||
test.equal(EJSON.parse('{"$InfNaN": 1}'), Infinity);
|
||||
test.equal(EJSON.parse('{"$InfNaN": -1}'), -Infinity);
|
||||
test.isTrue(Number.isNaN(EJSON.parse('{"$InfNaN": 0}')));
|
||||
@@ -181,134 +189,138 @@ Tinytest.add('ejson - NaN and Inf', test => {
|
||||
test.isFalse(EJSON.equals(Infinity, 0));
|
||||
test.isFalse(EJSON.equals(NaN, 0));
|
||||
|
||||
test.isTrue(EJSON.equals(
|
||||
EJSON.parse('{"a": {"$InfNaN": 1}}'),
|
||||
{a: Infinity}
|
||||
));
|
||||
test.isTrue(EJSON.equals(
|
||||
EJSON.parse('{"a": {"$InfNaN": 0}}'),
|
||||
{a: NaN}
|
||||
));
|
||||
test.isTrue(EJSON.equals(EJSON.parse('{"a": {"$InfNaN": 1}}'), { a: Infinity }));
|
||||
test.isTrue(EJSON.equals(EJSON.parse('{"a": {"$InfNaN": 0}}'), { a: NaN }));
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue primitives pass through unchanged', test => {
|
||||
Tinytest.add("ejson - toJSONValue primitives pass through unchanged", (test) => {
|
||||
test.equal(EJSON.toJSONValue(42), 42);
|
||||
test.equal(EJSON.toJSONValue('hello'), 'hello');
|
||||
test.equal(EJSON.toJSONValue("hello"), "hello");
|
||||
test.equal(EJSON.toJSONValue(true), true);
|
||||
test.equal(EJSON.toJSONValue(false), false);
|
||||
test.equal(EJSON.toJSONValue(null), null);
|
||||
test.equal(EJSON.toJSONValue(undefined), undefined);
|
||||
test.equal(EJSON.toJSONValue(0), 0);
|
||||
test.equal(EJSON.toJSONValue(''), '');
|
||||
test.equal(EJSON.toJSONValue(""), "");
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue converts Date to {$date}', test => {
|
||||
const d = new Date('2024-06-15T12:00:00Z');
|
||||
Tinytest.add("ejson - toJSONValue converts Date to {$date}", (test) => {
|
||||
const d = new Date("2024-06-15T12:00:00Z");
|
||||
const result = EJSON.toJSONValue(d);
|
||||
test.equal(result, {$date: d.getTime()});
|
||||
test.equal(result, { $date: d.getTime() });
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue converts NaN and Infinity', test => {
|
||||
test.equal(EJSON.toJSONValue(NaN), {$InfNaN: 0});
|
||||
test.equal(EJSON.toJSONValue(Infinity), {$InfNaN: 1});
|
||||
test.equal(EJSON.toJSONValue(-Infinity), {$InfNaN: -1});
|
||||
Tinytest.add("ejson - toJSONValue converts NaN and Infinity", (test) => {
|
||||
test.equal(EJSON.toJSONValue(NaN), { $InfNaN: 0 });
|
||||
test.equal(EJSON.toJSONValue(Infinity), { $InfNaN: 1 });
|
||||
test.equal(EJSON.toJSONValue(-Infinity), { $InfNaN: -1 });
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue handles pure-primitive objects', test => {
|
||||
const obj = {a: 1, b: 'hello', c: true, d: null};
|
||||
Tinytest.add("ejson - toJSONValue handles pure-primitive objects", (test) => {
|
||||
const obj = { a: 1, b: "hello", c: true, d: null };
|
||||
const result = EJSON.toJSONValue(obj);
|
||||
test.equal(result, {a: 1, b: 'hello', c: true, d: null});
|
||||
test.equal(result, { a: 1, b: "hello", c: true, d: null });
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue converts nested Dates', test => {
|
||||
const d = new Date('2024-01-01');
|
||||
const obj = {name: 'test', createdAt: d, meta: {updatedAt: d}};
|
||||
Tinytest.add("ejson - toJSONValue converts nested Dates", (test) => {
|
||||
const d = new Date("2024-01-01");
|
||||
const obj = { name: "test", createdAt: d, meta: { updatedAt: d } };
|
||||
const result = EJSON.toJSONValue(obj);
|
||||
test.equal(result, {name: 'test', createdAt: {$date: d.getTime()}, meta: {updatedAt: {$date: d.getTime()}}});
|
||||
test.equal(result, {
|
||||
name: "test",
|
||||
createdAt: { $date: d.getTime() },
|
||||
meta: { updatedAt: { $date: d.getTime() } },
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue handles arrays', test => {
|
||||
Tinytest.add("ejson - toJSONValue handles arrays", (test) => {
|
||||
// Pure-primitive array
|
||||
const arr = [1, 'two', true, null];
|
||||
const arr = [1, "two", true, null];
|
||||
const result = EJSON.toJSONValue(arr);
|
||||
test.equal(result, [1, 'two', true, null]);
|
||||
test.equal(result, [1, "two", true, null]);
|
||||
|
||||
// Array with a Date
|
||||
const d = new Date();
|
||||
const arrWithDate = ['a', d, 'b'];
|
||||
const arrWithDate = ["a", d, "b"];
|
||||
const result2 = EJSON.toJSONValue(arrWithDate);
|
||||
test.equal(result2, ['a', {$date: d.getTime()}, 'b']);
|
||||
test.equal(result2, ["a", { $date: d.getTime() }, "b"]);
|
||||
|
||||
// Empty array
|
||||
test.equal(EJSON.toJSONValue([]), []);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue handles NaN/Infinity inside objects and arrays', test => {
|
||||
const obj = {a: 1, b: NaN, c: Infinity, d: -Infinity, e: 'normal'};
|
||||
Tinytest.add("ejson - toJSONValue handles NaN/Infinity inside objects and arrays", (test) => {
|
||||
const obj = { a: 1, b: NaN, c: Infinity, d: -Infinity, e: "normal" };
|
||||
const result = EJSON.toJSONValue(obj);
|
||||
test.equal(result, {a: 1, b: {$InfNaN: 0}, c: {$InfNaN: 1}, d: {$InfNaN: -1}, e: 'normal'});
|
||||
test.equal(result, {
|
||||
a: 1,
|
||||
b: { $InfNaN: 0 },
|
||||
c: { $InfNaN: 1 },
|
||||
d: { $InfNaN: -1 },
|
||||
e: "normal",
|
||||
});
|
||||
|
||||
const arr = [NaN, 42, Infinity];
|
||||
const result2 = EJSON.toJSONValue(arr);
|
||||
test.equal(result2, [{$InfNaN: 0}, 42, {$InfNaN: 1}]);
|
||||
test.equal(result2, [{ $InfNaN: 0 }, 42, { $InfNaN: 1 }]);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue escapes $-prefixed keys that look like EJSON types', test => {
|
||||
const obj = {$date: 12345};
|
||||
Tinytest.add("ejson - toJSONValue escapes $-prefixed keys that look like EJSON types", (test) => {
|
||||
const obj = { $date: 12345 };
|
||||
const result = EJSON.toJSONValue(obj);
|
||||
// Should be wrapped in $escape to prevent misinterpretation
|
||||
test.isTrue('$escape' in result);
|
||||
test.isTrue("$escape" in result);
|
||||
test.equal(result.$escape.$date, 12345);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue primitives pass through unchanged', test => {
|
||||
Tinytest.add("ejson - fromJSONValue primitives pass through unchanged", (test) => {
|
||||
test.equal(EJSON.fromJSONValue(42), 42);
|
||||
test.equal(EJSON.fromJSONValue('hello'), 'hello');
|
||||
test.equal(EJSON.fromJSONValue("hello"), "hello");
|
||||
test.equal(EJSON.fromJSONValue(true), true);
|
||||
test.equal(EJSON.fromJSONValue(false), false);
|
||||
test.equal(EJSON.fromJSONValue(null), null);
|
||||
test.equal(EJSON.fromJSONValue(0), 0);
|
||||
test.equal(EJSON.fromJSONValue(''), '');
|
||||
test.equal(EJSON.fromJSONValue(""), "");
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue converts {$date} to Date', test => {
|
||||
Tinytest.add("ejson - fromJSONValue converts {$date} to Date", (test) => {
|
||||
const ts = 1718452800000;
|
||||
const result = EJSON.fromJSONValue({$date: ts});
|
||||
const result = EJSON.fromJSONValue({ $date: ts });
|
||||
test.instanceOf(result, Date);
|
||||
test.equal(result.getTime(), ts);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue converts {$InfNaN} back', test => {
|
||||
test.isTrue(Number.isNaN(EJSON.fromJSONValue({$InfNaN: 0})));
|
||||
test.equal(EJSON.fromJSONValue({$InfNaN: 1}), Infinity);
|
||||
test.equal(EJSON.fromJSONValue({$InfNaN: -1}), -Infinity);
|
||||
Tinytest.add("ejson - fromJSONValue converts {$InfNaN} back", (test) => {
|
||||
test.isTrue(Number.isNaN(EJSON.fromJSONValue({ $InfNaN: 0 })));
|
||||
test.equal(EJSON.fromJSONValue({ $InfNaN: 1 }), Infinity);
|
||||
test.equal(EJSON.fromJSONValue({ $InfNaN: -1 }), -Infinity);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue handles pure-primitive objects', test => {
|
||||
const obj = {a: 1, b: 'hello', c: true, d: null};
|
||||
Tinytest.add("ejson - fromJSONValue handles pure-primitive objects", (test) => {
|
||||
const obj = { a: 1, b: "hello", c: true, d: null };
|
||||
const result = EJSON.fromJSONValue(obj);
|
||||
test.equal(result, {a: 1, b: 'hello', c: true, d: null});
|
||||
test.equal(result, { a: 1, b: "hello", c: true, d: null });
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue converts nested {$date} values', test => {
|
||||
Tinytest.add("ejson - fromJSONValue converts nested {$date} values", (test) => {
|
||||
const ts = Date.now();
|
||||
const obj = {name: 'test', createdAt: {$date: ts}, meta: {updatedAt: {$date: ts}}};
|
||||
const obj = { name: "test", createdAt: { $date: ts }, meta: { updatedAt: { $date: ts } } };
|
||||
const result = EJSON.fromJSONValue(obj);
|
||||
test.equal(result.name, 'test');
|
||||
test.equal(result.name, "test");
|
||||
test.instanceOf(result.createdAt, Date);
|
||||
test.equal(result.createdAt.getTime(), ts);
|
||||
test.instanceOf(result.meta.updatedAt, Date);
|
||||
test.equal(result.meta.updatedAt.getTime(), ts);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue handles arrays with EJSON types', test => {
|
||||
Tinytest.add("ejson - fromJSONValue handles arrays with EJSON types", (test) => {
|
||||
const ts = Date.now();
|
||||
const arr = ['a', {$date: ts}, 'b'];
|
||||
const arr = ["a", { $date: ts }, "b"];
|
||||
const result = EJSON.fromJSONValue(arr);
|
||||
test.equal(result[0], 'a');
|
||||
test.equal(result[0], "a");
|
||||
test.instanceOf(result[1], Date);
|
||||
test.equal(result[1].getTime(), ts);
|
||||
test.equal(result[2], 'b');
|
||||
test.equal(result[2], "b");
|
||||
test.length(result, 3);
|
||||
|
||||
// Pure-primitive array
|
||||
@@ -318,48 +330,48 @@ Tinytest.add('ejson - fromJSONValue handles arrays with EJSON types', test => {
|
||||
test.equal(EJSON.fromJSONValue([]), []);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - fromJSONValue unescapes $escape wrapper', test => {
|
||||
const input = {$escape: {$date: 12345}};
|
||||
Tinytest.add("ejson - fromJSONValue unescapes $escape wrapper", (test) => {
|
||||
const input = { $escape: { $date: 12345 } };
|
||||
const result = EJSON.fromJSONValue(input);
|
||||
test.equal(result, {$date: 12345});
|
||||
test.isFalse('$escape' in result);
|
||||
test.equal(result, { $date: 12345 });
|
||||
test.isFalse("$escape" in result);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue/fromJSONValue round-trip', test => {
|
||||
Tinytest.add("ejson - toJSONValue/fromJSONValue round-trip", (test) => {
|
||||
const d = new Date();
|
||||
const cases = [
|
||||
42,
|
||||
'hello',
|
||||
"hello",
|
||||
true,
|
||||
null,
|
||||
{a: 1, b: 'two'},
|
||||
{ a: 1, b: "two" },
|
||||
[1, 2, 3],
|
||||
d,
|
||||
NaN,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
{name: 'test', ts: d, scores: [1, 2, 3]},
|
||||
{nested: {deep: {date: d, val: 42}}},
|
||||
[d, 'a', {x: d}],
|
||||
{$date: 12345}, // $-prefixed key → escape/unescape round-trip
|
||||
{a: NaN, b: Infinity, c: -Infinity, d: 'normal'},
|
||||
{ name: "test", ts: d, scores: [1, 2, 3] },
|
||||
{ nested: { deep: { date: d, val: 42 } } },
|
||||
[d, "a", { x: d }],
|
||||
{ $date: 12345 }, // $-prefixed key → escape/unescape round-trip
|
||||
{ a: NaN, b: Infinity, c: -Infinity, d: "normal" },
|
||||
{}, // empty object
|
||||
[], // empty array
|
||||
];
|
||||
|
||||
cases.forEach(original => {
|
||||
cases.forEach((original) => {
|
||||
const json = EJSON.toJSONValue(original);
|
||||
const restored = EJSON.fromJSONValue(json);
|
||||
test.isTrue(
|
||||
EJSON.equals(original, restored),
|
||||
`Round-trip failed for: ${EJSON.stringify(original)}`
|
||||
`Round-trip failed for: ${EJSON.stringify(original)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - toJSONValue does not mutate the input', test => {
|
||||
Tinytest.add("ejson - toJSONValue does not mutate the input", (test) => {
|
||||
const d = new Date();
|
||||
const obj = {name: 'test', createdAt: d, tags: ['a', 'b']};
|
||||
const obj = { name: "test", createdAt: d, tags: ["a", "b"] };
|
||||
const originalName = obj.name;
|
||||
const originalDate = obj.createdAt;
|
||||
const originalTags = obj.tags;
|
||||
@@ -371,10 +383,10 @@ Tinytest.add('ejson - toJSONValue does not mutate the input', test => {
|
||||
test.equal(obj.createdAt, originalDate);
|
||||
test.equal(obj.tags, originalTags);
|
||||
test.instanceOf(obj.createdAt, Date);
|
||||
test.equal(obj.tags[0], 'a');
|
||||
test.equal(obj.tags[0], "a");
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - clone', test => {
|
||||
Tinytest.add("ejson - clone", (test) => {
|
||||
const cloneTest = (x, identical) => {
|
||||
const y = EJSON.clone(x);
|
||||
test.isTrue(EJSON.equals(x, y));
|
||||
@@ -383,127 +395,104 @@ Tinytest.add('ejson - clone', test => {
|
||||
cloneTest(null, true);
|
||||
cloneTest(undefined, true);
|
||||
cloneTest(42, true);
|
||||
cloneTest('asdf', true);
|
||||
cloneTest("asdf", true);
|
||||
cloneTest([1, 2, 3]);
|
||||
cloneTest([1, 'fasdf', {foo: 42}]);
|
||||
cloneTest({x: 42, y: 'asdf'});
|
||||
cloneTest([1, "fasdf", { foo: 42 }]);
|
||||
cloneTest({ x: 42, y: "asdf" });
|
||||
|
||||
function testCloneArgs(/*arguments*/) {
|
||||
const clonedArgs = EJSON.clone(arguments);
|
||||
test.equal(clonedArgs, [1, 2, 'foo', [4]]);
|
||||
};
|
||||
testCloneArgs(1, 2, 'foo', [4]);
|
||||
test.equal(clonedArgs, [1, 2, "foo", [4]]);
|
||||
}
|
||||
testCloneArgs(1, 2, "foo", [4]);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - stringify', test => {
|
||||
test.equal(EJSON.stringify(null), 'null');
|
||||
test.equal(EJSON.stringify(true), 'true');
|
||||
test.equal(EJSON.stringify(false), 'false');
|
||||
test.equal(EJSON.stringify(123), '123');
|
||||
test.equal(EJSON.stringify('abc'), '"abc"');
|
||||
Tinytest.add("ejson - stringify", (test) => {
|
||||
test.equal(EJSON.stringify(null), "null");
|
||||
test.equal(EJSON.stringify(true), "true");
|
||||
test.equal(EJSON.stringify(false), "false");
|
||||
test.equal(EJSON.stringify(123), "123");
|
||||
test.equal(EJSON.stringify("abc"), '"abc"');
|
||||
|
||||
test.equal(EJSON.stringify([1, 2, 3]),
|
||||
'[1,2,3]'
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: true}),
|
||||
'[\n 1,\n 2,\n 3\n]'
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {canonical: false}),
|
||||
'[1,2,3]'
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: true, canonical: false}),
|
||||
'[\n 1,\n 2,\n 3\n]'
|
||||
test.equal(EJSON.stringify([1, 2, 3]), "[1,2,3]");
|
||||
test.equal(EJSON.stringify([1, 2, 3], { indent: true }), "[\n 1,\n 2,\n 3\n]");
|
||||
test.equal(EJSON.stringify([1, 2, 3], { canonical: false }), "[1,2,3]");
|
||||
test.equal(
|
||||
EJSON.stringify([1, 2, 3], { indent: true, canonical: false }),
|
||||
"[\n 1,\n 2,\n 3\n]",
|
||||
);
|
||||
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: 4}),
|
||||
'[\n 1,\n 2,\n 3\n]'
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], {indent: '--'}),
|
||||
'[\n--1,\n--2,\n--3\n]'
|
||||
);
|
||||
test.equal(EJSON.stringify([1, 2, 3], { indent: 4 }), "[\n 1,\n 2,\n 3\n]");
|
||||
test.equal(EJSON.stringify([1, 2, 3], { indent: "--" }), "[\n--1,\n--2,\n--3\n]");
|
||||
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{canonical: true}
|
||||
),
|
||||
'{"a":1,"b":[2,{"c":3,"d":4}]}'
|
||||
EJSON.stringify({ b: [2, { d: 4, c: 3 }], a: 1 }, { canonical: true }),
|
||||
'{"a":1,"b":[2,{"c":3,"d":4}]}',
|
||||
);
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{ b: [2, { d: 4, c: 3 }], a: 1 },
|
||||
{
|
||||
indent: true,
|
||||
canonical: true,
|
||||
}
|
||||
},
|
||||
),
|
||||
'{\n' +
|
||||
' "a": 1,\n' +
|
||||
' "b": [\n' +
|
||||
' 2,\n' +
|
||||
' {\n' +
|
||||
' "c": 3,\n' +
|
||||
' "d": 4\n' +
|
||||
' }\n' +
|
||||
' ]\n' +
|
||||
'}'
|
||||
"{\n" +
|
||||
' "a": 1,\n' +
|
||||
' "b": [\n' +
|
||||
" 2,\n" +
|
||||
" {\n" +
|
||||
' "c": 3,\n' +
|
||||
' "d": 4\n' +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}",
|
||||
);
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{canonical: false}
|
||||
),
|
||||
'{"b":[2,{"d":4,"c":3}],"a":1}'
|
||||
EJSON.stringify({ b: [2, { d: 4, c: 3 }], a: 1 }, { canonical: false }),
|
||||
'{"b":[2,{"d":4,"c":3}],"a":1}',
|
||||
);
|
||||
test.equal(
|
||||
EJSON.stringify(
|
||||
{b: [2, {d: 4, c: 3}], a: 1},
|
||||
{indent: true, canonical: false}
|
||||
),
|
||||
'{\n' +
|
||||
' "b": [\n' +
|
||||
' 2,\n' +
|
||||
' {\n' +
|
||||
' "d": 4,\n' +
|
||||
' "c": 3\n' +
|
||||
' }\n' +
|
||||
' ],\n' +
|
||||
' "a": 1\n' +
|
||||
'}'
|
||||
EJSON.stringify({ b: [2, { d: 4, c: 3 }], a: 1 }, { indent: true, canonical: false }),
|
||||
"{\n" +
|
||||
' "b": [\n' +
|
||||
" 2,\n" +
|
||||
" {\n" +
|
||||
' "d": 4,\n' +
|
||||
' "c": 3\n' +
|
||||
" }\n" +
|
||||
" ],\n" +
|
||||
' "a": 1\n' +
|
||||
"}",
|
||||
);
|
||||
|
||||
test.throws(
|
||||
() => {
|
||||
const col = new Mongo.Collection('test');
|
||||
EJSON.stringify(col)
|
||||
},
|
||||
/Converting circular structure to JSON/
|
||||
);
|
||||
test.throws(() => {
|
||||
const col = new Mongo.Collection("test");
|
||||
EJSON.stringify(col);
|
||||
}, /Converting circular structure to JSON/);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - parse', test => {
|
||||
test.equal(EJSON.parse('[1,2,3]'), [1, 2, 3]);
|
||||
test.throws(
|
||||
() => { EJSON.parse(null); },
|
||||
/argument should be a string/
|
||||
);
|
||||
Tinytest.add("ejson - parse", (test) => {
|
||||
test.equal(EJSON.parse("[1,2,3]"), [1, 2, 3]);
|
||||
test.throws(() => {
|
||||
EJSON.parse(null);
|
||||
}, /argument should be a string/);
|
||||
});
|
||||
|
||||
Tinytest.add("ejson - regexp", test => {
|
||||
test.equal(EJSON.stringify(/foo/gi), "{\"$regexp\":\"foo\",\"$flags\":\"gi\"}");
|
||||
var d = new RegExp("foo", "gi");
|
||||
var obj = { $regexp: "foo", $flags: "gi" };
|
||||
Tinytest.add("ejson - regexp", (test) => {
|
||||
test.equal(EJSON.stringify(/foo/gi), '{"$regexp":"foo","$flags":"gi"}');
|
||||
const obj = { $regexp: "foo", $flags: "gi" };
|
||||
|
||||
var eObj = EJSON.toJSONValue(obj);
|
||||
var roundTrip = EJSON.fromJSONValue(eObj);
|
||||
const eObj = EJSON.toJSONValue(obj);
|
||||
const roundTrip = EJSON.fromJSONValue(eObj);
|
||||
test.equal(obj, roundTrip);
|
||||
});
|
||||
|
||||
Tinytest.add('ejson - custom types', test => {
|
||||
Tinytest.add("ejson - custom types", (test) => {
|
||||
const testSameConstructors = (someObj, compareWith) => {
|
||||
test.equal(someObj.constructor, compareWith.constructor);
|
||||
if (typeof someObj === 'object') {
|
||||
Object.keys(someObj).forEach(key => {
|
||||
if (typeof someObj === "object") {
|
||||
Object.keys(someObj).forEach((key) => {
|
||||
const value = someObj[key];
|
||||
testSameConstructors(value, compareWith[key]);
|
||||
});
|
||||
@@ -526,11 +515,11 @@ Tinytest.add('ejson - custom types', test => {
|
||||
testReallyEqual(someObj, EJSON.clone(someObj));
|
||||
};
|
||||
|
||||
const a = new EJSONTest.Address('Montreal', 'Quebec');
|
||||
testCustomObject( {address: a} );
|
||||
const a = new EJSONTest.Address("Montreal", "Quebec");
|
||||
testCustomObject({ address: a });
|
||||
// Test that difference is detected even if they
|
||||
// have similar toJSONValue results:
|
||||
const nakedA = {city: 'Montreal', state: 'Quebec'};
|
||||
const nakedA = { city: "Montreal", state: "Quebec" };
|
||||
test.notEqual(nakedA, a);
|
||||
test.notEqual(a, nakedA);
|
||||
const holder = new EJSONTest.Holder(nakedA);
|
||||
@@ -539,18 +528,18 @@ Tinytest.add('ejson - custom types', test => {
|
||||
test.notEqual(a, holder);
|
||||
|
||||
const d = new Date();
|
||||
const obj = new EJSONTest.Person('John Doe', d, a);
|
||||
testCustomObject( obj );
|
||||
const obj = new EJSONTest.Person("John Doe", d, a);
|
||||
testCustomObject(obj);
|
||||
|
||||
// Test clone is deep:
|
||||
const clone = EJSON.clone(obj);
|
||||
clone.address.city = 'Sherbrooke';
|
||||
test.notEqual( obj, clone );
|
||||
clone.address.city = "Sherbrooke";
|
||||
test.notEqual(obj, clone);
|
||||
});
|
||||
|
||||
// Verify objects with a property named "length" can be handled by the EJSON
|
||||
// API properly (see https://github.com/meteor/meteor/issues/5175).
|
||||
Tinytest.add('ejson - handle objects with properties named "length"', test => {
|
||||
Tinytest.add('ejson - handle objects with properties named "length"', (test) => {
|
||||
class Widget {
|
||||
constructor() {
|
||||
this.length = 10;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
Package.describe({
|
||||
summary: 'Extended and Extensible JSON library',
|
||||
version: '1.2.0-beta350.7',
|
||||
summary: "Extended and Extensible JSON library",
|
||||
version: "1.2.0-beta350.7",
|
||||
});
|
||||
|
||||
Package.onUse(function onUse(api) {
|
||||
api.use(['ecmascript', 'base64']);
|
||||
api.addAssets('ejson.d.ts', 'server');
|
||||
api.mainModule('ejson.js');
|
||||
api.export('EJSON');
|
||||
api.use(["ecmascript", "base64"]);
|
||||
api.addAssets("ejson.d.ts", "server");
|
||||
api.mainModule("ejson.js");
|
||||
api.export("EJSON");
|
||||
});
|
||||
|
||||
Package.onTest(function onTest(api) {
|
||||
api.use(['ecmascript', 'tinytest', 'mongo']);
|
||||
api.use('ejson');
|
||||
api.mainModule('ejson_tests.js');
|
||||
api.use(["ecmascript", "tinytest", "mongo"]);
|
||||
api.use("ejson");
|
||||
api.mainModule("ejson_tests.js");
|
||||
});
|
||||
|
||||
@@ -16,86 +16,73 @@ const str = (key, holder, singleIndent, outerIndent, canonical) => {
|
||||
|
||||
// What happens next depends on the value's type.
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return quote(value);
|
||||
case 'number':
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
return isFinite(value) ? String(value) : 'null';
|
||||
case 'boolean':
|
||||
return String(value);
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
case 'object': {
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
if (!value) {
|
||||
return 'null';
|
||||
}
|
||||
// Make an array to hold the partial results of stringifying this object
|
||||
// value.
|
||||
const innerIndent = outerIndent + singleIndent;
|
||||
const partial = [];
|
||||
let v;
|
||||
case "string":
|
||||
return quote(value);
|
||||
case "number":
|
||||
// JSON numbers must be finite. Encode non-finite numbers as null.
|
||||
return isFinite(value) ? String(value) : "null";
|
||||
case "boolean":
|
||||
return String(value);
|
||||
// If the type is 'object', we might be dealing with an object or an array or
|
||||
// null.
|
||||
case "object": {
|
||||
// Due to a specification blunder in ECMAScript, typeof null is 'object',
|
||||
// so watch out for that case.
|
||||
if (!value) {
|
||||
return "null";
|
||||
}
|
||||
// Make an array to hold the partial results of stringifying this object
|
||||
// value.
|
||||
const innerIndent = outerIndent + singleIndent;
|
||||
const partial = [];
|
||||
let v;
|
||||
|
||||
// Is the value an array?
|
||||
if (Array.isArray(value) || ({}).hasOwnProperty.call(value, 'callee')) {
|
||||
// The value is an array. Stringify every element. Use null as a
|
||||
// placeholder for non-JSON values.
|
||||
const length = value.length;
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
partial[i] =
|
||||
str(i, value, singleIndent, innerIndent, canonical) || 'null';
|
||||
// Is the value an array?
|
||||
if (Array.isArray(value) || {}.hasOwnProperty.call(value, "callee")) {
|
||||
// The value is an array. Stringify every element. Use null as a
|
||||
// placeholder for non-JSON values.
|
||||
const length = value.length;
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
partial[i] = str(i, value, singleIndent, innerIndent, canonical) || "null";
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap
|
||||
// them in brackets.
|
||||
if (partial.length === 0) {
|
||||
v = "[]";
|
||||
} else if (innerIndent) {
|
||||
v = `[\n${innerIndent}${partial.join(`,\n${innerIndent}`)}\n${outerIndent}]`;
|
||||
} else {
|
||||
v = `[${partial.join(",")}]`;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Join all of the elements together, separated with commas, and wrap
|
||||
// them in brackets.
|
||||
// Iterate through all of the keys in the object.
|
||||
let keys = Object.keys(value);
|
||||
if (canonical) {
|
||||
keys = keys.sort();
|
||||
}
|
||||
keys.forEach((k) => {
|
||||
v = str(k, value, singleIndent, innerIndent, canonical);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (innerIndent ? ": " : ":") + v);
|
||||
}
|
||||
});
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
if (partial.length === 0) {
|
||||
v = '[]';
|
||||
v = "{}";
|
||||
} else if (innerIndent) {
|
||||
v = '[\n' +
|
||||
innerIndent +
|
||||
partial.join(',\n' +
|
||||
innerIndent) +
|
||||
'\n' +
|
||||
outerIndent +
|
||||
']';
|
||||
v = `{\n${innerIndent}${partial.join(`,\n${innerIndent}`)}\n${outerIndent}}`;
|
||||
} else {
|
||||
v = '[' + partial.join(',') + ']';
|
||||
v = `{${partial.join(",")}}`;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Iterate through all of the keys in the object.
|
||||
let keys = Object.keys(value);
|
||||
if (canonical) {
|
||||
keys = keys.sort();
|
||||
}
|
||||
keys.forEach(k => {
|
||||
v = str(k, value, singleIndent, innerIndent, canonical);
|
||||
if (v) {
|
||||
partial.push(quote(k) + (innerIndent ? ': ' : ':') + v);
|
||||
}
|
||||
});
|
||||
|
||||
// Join all of the member texts together, separated with commas,
|
||||
// and wrap them in braces.
|
||||
if (partial.length === 0) {
|
||||
v = '{}';
|
||||
} else if (innerIndent) {
|
||||
v = '{\n' +
|
||||
innerIndent +
|
||||
partial.join(',\n' +
|
||||
innerIndent) +
|
||||
'\n' +
|
||||
outerIndent +
|
||||
'}';
|
||||
} else {
|
||||
v = '{' + partial.join(',') + '}';
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
default: // Do nothing
|
||||
default: // Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,20 +90,23 @@ const str = (key, holder, singleIndent, outerIndent, canonical) => {
|
||||
const canonicalStringify = (value, options) => {
|
||||
// Make a fake root object containing our value under the key of ''.
|
||||
// Return the result of stringifying the value.
|
||||
const allOptions = Object.assign({
|
||||
indent: '',
|
||||
canonical: false,
|
||||
}, options);
|
||||
const allOptions = Object.assign(
|
||||
{
|
||||
indent: "",
|
||||
canonical: false,
|
||||
},
|
||||
options,
|
||||
);
|
||||
if (allOptions.indent === true) {
|
||||
allOptions.indent = ' ';
|
||||
} else if (typeof allOptions.indent === 'number') {
|
||||
let newIndent = '';
|
||||
allOptions.indent = " ";
|
||||
} else if (typeof allOptions.indent === "number") {
|
||||
let newIndent = "";
|
||||
for (let i = 0; i < allOptions.indent; i++) {
|
||||
newIndent += ' ';
|
||||
newIndent += " ";
|
||||
}
|
||||
allOptions.indent = newIndent;
|
||||
}
|
||||
return str('', {'': value}, allOptions.indent, '', allOptions.canonical);
|
||||
return str("", { "": value }, allOptions.indent, "", allOptions.canonical);
|
||||
};
|
||||
|
||||
export default canonicalStringify;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const isFunction = (fn) => typeof fn === 'function';
|
||||
export const isFunction = (fn) => typeof fn === "function";
|
||||
|
||||
export const isObject = (fn) => typeof fn === 'object';
|
||||
export const isObject = (fn) => typeof fn === "object";
|
||||
|
||||
export const keysOf = (obj) => Object.keys(obj);
|
||||
|
||||
@@ -30,29 +30,30 @@ export const lengthOfWithLimit = (obj, limit) => {
|
||||
|
||||
export const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
|
||||
export const convertMapToObject = (map) => Array.from(map).reduce((acc, [key, value]) => {
|
||||
// reassign to not create new object
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
export const convertMapToObject = (map) =>
|
||||
Array.from(map).reduce((acc, [key, value]) => {
|
||||
// reassign to not create new object
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
export const isArguments = obj => obj != null && hasOwn(obj, 'callee');
|
||||
export const isArguments = (obj) => obj != null && hasOwn(obj, "callee");
|
||||
|
||||
export const isInfOrNaN =
|
||||
obj => Number.isNaN(obj) || obj === Infinity || obj === -Infinity;
|
||||
export const isInfOrNaN = (obj) => Number.isNaN(obj) || obj === Infinity || obj === -Infinity;
|
||||
|
||||
export const checkError = {
|
||||
maxStack: (msgError) => new RegExp('Maximum call stack size exceeded', 'g').test(msgError),
|
||||
maxStack: (msgError) => new RegExp("Maximum call stack size exceeded", "g").test(msgError),
|
||||
};
|
||||
|
||||
export const handleError = (fn) => function() {
|
||||
try {
|
||||
return fn.apply(this, arguments);
|
||||
} catch (error) {
|
||||
const isMaxStack = checkError.maxStack(error.message);
|
||||
if (isMaxStack) {
|
||||
throw new Error('Converting circular structure to JSON')
|
||||
export const handleError = (fn) =>
|
||||
function () {
|
||||
try {
|
||||
return fn.apply(this, arguments);
|
||||
} catch (error) {
|
||||
const isMaxStack = checkError.maxStack(error.message);
|
||||
if (isMaxStack) {
|
||||
throw new Error("Converting circular structure to JSON");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
208
packages/id-map/id-map-tests.js
Normal file
208
packages/id-map/id-map-tests.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { IdMap } from "./id-map.js";
|
||||
|
||||
const collectForEach = (map) => {
|
||||
const entries = [];
|
||||
map.forEach((value, id) => {
|
||||
entries.push([id, value]);
|
||||
});
|
||||
return entries;
|
||||
};
|
||||
|
||||
Tinytest.add("idmap - starts empty", (test) => {
|
||||
const map = new IdMap();
|
||||
test.isTrue(map.empty(), "new IdMap should be empty");
|
||||
test.equal(map.size(), 0);
|
||||
test.equal(map.get("anything"), undefined);
|
||||
test.isFalse(map.has("anything"));
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - set/get/has with string keys", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("a", 1);
|
||||
map.set("b", 2);
|
||||
map.set("c", 3);
|
||||
|
||||
test.equal(map.size(), 3);
|
||||
test.isFalse(map.empty());
|
||||
test.equal(map.get("a"), 1);
|
||||
test.equal(map.get("b"), 2);
|
||||
test.equal(map.get("c"), 3);
|
||||
test.isTrue(map.has("a"));
|
||||
test.isTrue(map.has("b"));
|
||||
test.isTrue(map.has("c"));
|
||||
test.isFalse(map.has("d"));
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - set/get/has with object keys", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set({ id: 1 }, "first");
|
||||
map.set({ id: 2 }, "second");
|
||||
|
||||
test.equal(map.size(), 2);
|
||||
test.equal(map.get({ id: 1 }), "first");
|
||||
test.equal(map.get({ id: 2 }), "second");
|
||||
test.isTrue(map.has({ id: 1 }));
|
||||
test.isFalse(map.has({ id: 3 }));
|
||||
|
||||
// Structurally-equal objects collide onto the same slot.
|
||||
map.set({ id: 1 }, "overwritten");
|
||||
test.equal(map.size(), 2);
|
||||
test.equal(map.get({ id: 1 }), "overwritten");
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - set overwrites existing value", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("k", 1);
|
||||
map.set("k", 2);
|
||||
test.equal(map.get("k"), 2);
|
||||
test.equal(map.size(), 1);
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - remove deletes and is idempotent", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("a", 1);
|
||||
map.set("b", 2);
|
||||
|
||||
map.remove("a");
|
||||
test.isFalse(map.has("a"));
|
||||
test.equal(map.get("a"), undefined);
|
||||
test.equal(map.size(), 1);
|
||||
|
||||
// Removing an absent key is a silent no-op.
|
||||
map.remove("nonexistent");
|
||||
test.equal(map.size(), 1);
|
||||
test.isTrue(map.has("b"));
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - clear empties everything", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("a", 1);
|
||||
map.set("b", 2);
|
||||
map.set("c", 3);
|
||||
|
||||
map.clear();
|
||||
|
||||
test.isTrue(map.empty());
|
||||
test.equal(map.size(), 0);
|
||||
test.equal(map.get("a"), undefined);
|
||||
test.isFalse(map.has("a"));
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - setDefault inserts when missing, returns existing when present", (test) => {
|
||||
const map = new IdMap();
|
||||
|
||||
// Missing key: writes default, returns it.
|
||||
const v1 = map.setDefault("k", 42);
|
||||
test.equal(v1, 42);
|
||||
test.equal(map.get("k"), 42);
|
||||
test.equal(map.size(), 1);
|
||||
|
||||
// Present key: returns existing, does not overwrite.
|
||||
const v2 = map.setDefault("k", 999);
|
||||
test.equal(v2, 42);
|
||||
test.equal(map.get("k"), 42);
|
||||
test.equal(map.size(), 1);
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - forEach iterates all entries", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("a", 1);
|
||||
map.set("b", 2);
|
||||
map.set("c", 3);
|
||||
|
||||
const entries = collectForEach(map);
|
||||
test.equal(entries.length, 3);
|
||||
|
||||
// Order is not guaranteed — check membership.
|
||||
const byKey = Object.create(null);
|
||||
entries.forEach(([k, v]) => {
|
||||
byKey[k] = v;
|
||||
});
|
||||
test.equal(byKey.a, 1);
|
||||
test.equal(byKey.b, 2);
|
||||
test.equal(byKey.c, 3);
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - forEach stops when iterator returns false", (test) => {
|
||||
const map = new IdMap();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
map.set(`k${i}`, i);
|
||||
}
|
||||
|
||||
let visited = 0;
|
||||
map.forEach((_value, _id) => {
|
||||
visited++;
|
||||
if (visited === 2) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
test.equal(visited, 2, "forEach should break after iterator returns false");
|
||||
});
|
||||
|
||||
Tinytest.addAsync("idmap - forEachAsync iterates and supports break", async (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("a", 1);
|
||||
map.set("b", 2);
|
||||
map.set("c", 3);
|
||||
|
||||
const collected = [];
|
||||
await map.forEachAsync(async (value, id) => {
|
||||
await Promise.resolve();
|
||||
collected.push([id, value]);
|
||||
});
|
||||
test.equal(collected.length, 3);
|
||||
|
||||
// Break path.
|
||||
let visited = 0;
|
||||
await map.forEachAsync(async () => {
|
||||
await Promise.resolve();
|
||||
visited++;
|
||||
if (visited === 1) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
test.equal(visited, 1);
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - clone produces independent copy", (test) => {
|
||||
const map = new IdMap();
|
||||
map.set("a", { n: 1 });
|
||||
map.set("b", { n: 2 });
|
||||
|
||||
const clone = map.clone();
|
||||
test.equal(clone.size(), 2);
|
||||
test.equal(clone.get("a"), { n: 1 });
|
||||
test.equal(clone.get("b"), { n: 2 });
|
||||
|
||||
// Mutating the clone does not affect the original.
|
||||
clone.set("a", { n: 99 });
|
||||
clone.set("c", { n: 3 });
|
||||
|
||||
test.equal(map.get("a"), { n: 1 }, "original.a should be unchanged");
|
||||
test.isFalse(map.has("c"), "original should not see the new key");
|
||||
test.equal(map.size(), 2);
|
||||
test.equal(clone.size(), 3);
|
||||
});
|
||||
|
||||
Tinytest.add("idmap - custom stringify/parse round-trip", (test) => {
|
||||
const map = new IdMap(
|
||||
(n) => String(n),
|
||||
(s) => Number(s),
|
||||
);
|
||||
map.set(1, "one");
|
||||
map.set(2, "two");
|
||||
map.set(10, "ten");
|
||||
|
||||
test.equal(map.get(1), "one");
|
||||
test.equal(map.get(10), "ten");
|
||||
|
||||
// forEach should deliver parsed (numeric) ids.
|
||||
const ids = [];
|
||||
map.forEach((value, id) => {
|
||||
ids.push(id);
|
||||
test.equal(typeof id, "number", "custom parse should return a number");
|
||||
});
|
||||
ids.sort((a, b) => a - b);
|
||||
test.equal(ids, [1, 2, 10]);
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export class IdMap {
|
||||
constructor(idStringify, idParse) {
|
||||
this._map = new Map();
|
||||
@@ -6,10 +5,10 @@ export class IdMap {
|
||||
this._idParse = idParse || JSON.parse;
|
||||
}
|
||||
|
||||
// Some of these methods are designed to match methods on OrderedDict, since
|
||||
// (eg) ObserveMultiplex and _CachingChangeObserver use them interchangeably.
|
||||
// (Conceivably, this should be replaced with "UnorderedDict" with a specific
|
||||
// set of methods that overlap between the two.)
|
||||
// Some of these methods are designed to match methods on OrderedDict, since
|
||||
// (eg) ObserveMultiplex and _CachingChangeObserver use them interchangeably.
|
||||
// (Conceivably, this should be replaced with "UnorderedDict" with a specific
|
||||
// set of methods that overlap between the two.)
|
||||
|
||||
get(id) {
|
||||
const key = this._idStringify(id);
|
||||
@@ -42,12 +41,8 @@ export class IdMap {
|
||||
// Iterates over the items in the map. Return `false` to break the loop.
|
||||
forEach(iterator) {
|
||||
// don't use _.each, because we can't break out of it.
|
||||
for (let [key, value] of this._map){
|
||||
const breakIfFalse = iterator.call(
|
||||
null,
|
||||
value,
|
||||
this._idParse(key)
|
||||
);
|
||||
for (const [key, value] of this._map) {
|
||||
const breakIfFalse = iterator.call(null, value, this._idParse(key));
|
||||
if (breakIfFalse === false) {
|
||||
return;
|
||||
}
|
||||
@@ -55,12 +50,8 @@ export class IdMap {
|
||||
}
|
||||
|
||||
async forEachAsync(iterator) {
|
||||
for (let [key, value] of this._map){
|
||||
const breakIfFalse = await iterator.call(
|
||||
null,
|
||||
value,
|
||||
this._idParse(key)
|
||||
);
|
||||
for (const [key, value] of this._map) {
|
||||
const breakIfFalse = await iterator.call(null, value, this._idParse(key));
|
||||
if (breakIfFalse === false) {
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +76,7 @@ export class IdMap {
|
||||
clone() {
|
||||
const clone = new IdMap(this._idStringify, this._idParse);
|
||||
// copy directly to avoid stringify/parse overhead
|
||||
this._map.forEach(function(value, key){
|
||||
this._map.forEach(function (value, key) {
|
||||
clone._map.set(key, EJSON.clone(value));
|
||||
});
|
||||
return clone;
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
Package.describe({
|
||||
summary: "Dictionary data structure allowing non-string keys",
|
||||
version: '1.2.0',
|
||||
version: "1.2.0",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.use('ejson');
|
||||
api.mainModule('id-map.js');
|
||||
api.export('IdMap');
|
||||
api.use("ecmascript");
|
||||
api.use("ejson");
|
||||
api.mainModule("id-map.js");
|
||||
api.export("IdMap");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(["ecmascript", "tinytest", "ejson", "id-map"]);
|
||||
api.addFiles("id-map-tests.js", ["client", "server"]);
|
||||
});
|
||||
|
||||
18
packages/logging/logging.d.ts
vendored
18
packages/logging/logging.d.ts
vendored
@@ -9,7 +9,7 @@ type LogInput = string | LogJSONInput;
|
||||
type formatInput = {
|
||||
message: string;
|
||||
time: Date;
|
||||
level: 'debug' | 'info' | 'warn' | 'error'
|
||||
level: "debug" | "info" | "warn" | "error";
|
||||
timeInexact?: boolean;
|
||||
file: string;
|
||||
line: number;
|
||||
@@ -23,23 +23,23 @@ type formatInput = {
|
||||
export declare function Log(input: LogInput, ...optionalParams: any[]): void;
|
||||
|
||||
export declare namespace Log {
|
||||
var outputFormat: 'json' | 'colored-text';
|
||||
var outputFormat: "json" | "colored-text";
|
||||
var showTime: boolean;
|
||||
function _intercept(count: number): void;
|
||||
function _suppress(count: number): void;
|
||||
function _intercepted(): string[];
|
||||
function _getCallerDetails(): { line: number; file: string };
|
||||
function parse(line: object | string): object
|
||||
function parse(line: object | string): object;
|
||||
function format(object: formatInput, options: { color: true }): object | string;
|
||||
function objFromText(
|
||||
line: string,
|
||||
override: object
|
||||
override: object,
|
||||
): {
|
||||
message: string
|
||||
level: 'info'
|
||||
time: Date
|
||||
timeInexact: true
|
||||
}
|
||||
message: string;
|
||||
level: "info";
|
||||
time: Date;
|
||||
timeInexact: true;
|
||||
};
|
||||
|
||||
function debug(input: LogInput, ...optionalParams: any[]): void;
|
||||
function info(input: LogInput, ...optionalParams: any[]): void;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Meteor } from "meteor/meteor";
|
||||
|
||||
const hasOwn = Object.prototype.hasOwnProperty;
|
||||
|
||||
@@ -42,7 +42,7 @@ Log._intercepted = () => {
|
||||
// When this is set to 'colored-text', call 'Log.format' before printing.
|
||||
// This should be used for logging from within satellite, since there is no
|
||||
// other process that will be reading its standard output.
|
||||
Log.outputFormat = 'json';
|
||||
Log.outputFormat = "json";
|
||||
|
||||
// Defaults to true for local development and for backwards compatibility.
|
||||
// for cloud environments is interesting to leave it false as most of them have the timestamp in the console.
|
||||
@@ -50,39 +50,48 @@ Log.outputFormat = 'json';
|
||||
Log.showTime = true;
|
||||
|
||||
const LEVEL_COLORS = {
|
||||
debug: 'green',
|
||||
debug: "green",
|
||||
// leave info as the default color
|
||||
warn: 'magenta',
|
||||
error: 'red'
|
||||
warn: "magenta",
|
||||
error: "red",
|
||||
};
|
||||
|
||||
const META_COLOR = 'blue';
|
||||
const META_COLOR = "blue";
|
||||
|
||||
// Default colors cause readability problems on Windows Powershell,
|
||||
// switch to bright variants. While still capable of millions of
|
||||
// operations per second, the benchmark showed a 25%+ increase in
|
||||
// ops per second (on Node 8) by caching "process.platform".
|
||||
const isWin32 = typeof process === 'object' && process.platform === 'win32';
|
||||
const isWin32 = typeof process === "object" && process.platform === "win32";
|
||||
const platformColor = (color) => {
|
||||
if (isWin32 && typeof color === 'string' && !color.endsWith('Bright')) {
|
||||
if (isWin32 && typeof color === "string" && !color.endsWith("Bright")) {
|
||||
return `${color}Bright`;
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
// XXX package
|
||||
const RESTRICTED_KEYS = ['time', 'timeInexact', 'level', 'file', 'line',
|
||||
'program', 'originApp', 'satellite', 'stderr'];
|
||||
const RESTRICTED_KEYS = [
|
||||
"time",
|
||||
"timeInexact",
|
||||
"level",
|
||||
"file",
|
||||
"line",
|
||||
"program",
|
||||
"originApp",
|
||||
"satellite",
|
||||
"stderr",
|
||||
];
|
||||
|
||||
const FORMATTED_KEYS = [...RESTRICTED_KEYS, 'app', 'message'];
|
||||
const FORMATTED_KEYS = [...RESTRICTED_KEYS, "app", "message"];
|
||||
|
||||
const logInBrowser = obj => {
|
||||
const logInBrowser = (obj) => {
|
||||
const str = Log.format(obj);
|
||||
|
||||
// XXX Some levels should be probably be sent to the server
|
||||
const level = obj.level;
|
||||
|
||||
if ((typeof console !== 'undefined') && console[level]) {
|
||||
if (typeof console !== "undefined" && console[level]) {
|
||||
console[level](str);
|
||||
} else {
|
||||
// IE doesn't have console.log.apply, it's not a real Object.
|
||||
@@ -91,7 +100,6 @@ const logInBrowser = obj => {
|
||||
if (typeof console.log.apply === "function") {
|
||||
// Most browsers
|
||||
console.log.apply(console, [str]);
|
||||
|
||||
} else if (typeof Function.prototype.bind === "function") {
|
||||
// IE9
|
||||
const log = Function.prototype.bind.call(console.log, console);
|
||||
@@ -106,7 +114,7 @@ Log._getCallerDetails = () => {
|
||||
// We do NOT use Error.prepareStackTrace here (a V8 extension that gets us a
|
||||
// pre-parsed stack) since it's impossible to compose it with the use of
|
||||
// Error.prepareStackTrace used on the server for source maps.
|
||||
const err = new Error;
|
||||
const err = new Error();
|
||||
const stack = err.stack;
|
||||
return stack;
|
||||
};
|
||||
@@ -118,10 +126,10 @@ Log._getCallerDetails = () => {
|
||||
// looking for the first line outside the logging package (or an
|
||||
// eval if we find that first)
|
||||
let line;
|
||||
const lines = stack.split('\n').slice(1);
|
||||
const lines = stack.split("\n").slice(1);
|
||||
for (line of lines) {
|
||||
if (line.match(/^\s*(at eval \(eval)|(eval:)/)) {
|
||||
return {file: "eval"};
|
||||
return { file: "eval" };
|
||||
}
|
||||
|
||||
if (!line.match(/packages\/(?:local-test[:_])?logging(?:\/|\.js)/)) {
|
||||
@@ -140,84 +148,85 @@ Log._getCallerDetails = () => {
|
||||
}
|
||||
|
||||
// in case the matched block here is line:column
|
||||
details.line = match[2].split(':')[0];
|
||||
details.line = match[2].split(":")[0];
|
||||
|
||||
// Possible format: https://foo.bar.com/scripts/file.js?random=foobar
|
||||
// XXX: if you can write the following in better way, please do it
|
||||
// XXX: what about evals?
|
||||
details.file = match[1].split('/').slice(-1)[0].split('?')[0];
|
||||
details.file = match[1].split("/").slice(-1)[0].split("?")[0];
|
||||
|
||||
return details;
|
||||
};
|
||||
|
||||
['debug', 'info', 'warn', 'error'].forEach((level) => {
|
||||
// @param arg {String|Object}
|
||||
Log[level] = (arg) => {
|
||||
if (suppress) {
|
||||
suppress--;
|
||||
return;
|
||||
}
|
||||
|
||||
let intercepted = false;
|
||||
if (intercept) {
|
||||
intercept--;
|
||||
intercepted = true;
|
||||
}
|
||||
|
||||
let obj = (arg === Object(arg)
|
||||
&& !(arg instanceof RegExp)
|
||||
&& !(arg instanceof Date))
|
||||
? arg
|
||||
: { message: new String(arg).toString() };
|
||||
|
||||
RESTRICTED_KEYS.forEach(key => {
|
||||
if (obj[key]) {
|
||||
throw new Error(`Can't set '${key}' in log message`);
|
||||
["debug", "info", "warn", "error"].forEach((level) => {
|
||||
// @param arg {String|Object}
|
||||
Log[level] = (arg) => {
|
||||
if (suppress) {
|
||||
suppress--;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasOwn.call(obj, 'message') && typeof obj.message !== 'string') {
|
||||
throw new Error("The 'message' field in log objects must be a string");
|
||||
}
|
||||
let intercepted = false;
|
||||
if (intercept) {
|
||||
intercept--;
|
||||
intercepted = true;
|
||||
}
|
||||
|
||||
if (!obj.omitCallerDetails) {
|
||||
obj = { ...Log._getCallerDetails(), ...obj };
|
||||
}
|
||||
let obj =
|
||||
arg === Object(arg) && !(arg instanceof RegExp) && !(arg instanceof Date)
|
||||
? arg
|
||||
: { message: new String(arg).toString() };
|
||||
|
||||
obj.time = new Date();
|
||||
obj.level = level;
|
||||
RESTRICTED_KEYS.forEach((key) => {
|
||||
if (obj[key]) {
|
||||
throw new Error(`Can't set '${key}' in log message`);
|
||||
}
|
||||
});
|
||||
|
||||
// If we are in production don't write out debug logs.
|
||||
if (level === 'debug' && Meteor.isProduction) {
|
||||
return;
|
||||
}
|
||||
if (hasOwn.call(obj, "message") && typeof obj.message !== "string") {
|
||||
throw new Error("The 'message' field in log objects must be a string");
|
||||
}
|
||||
|
||||
if (intercepted) {
|
||||
interceptedLines.push(EJSON.stringify(obj));
|
||||
} else if (Meteor.isServer) {
|
||||
if (Log.outputFormat === 'colored-text') {
|
||||
console.log(Log.format(obj, {color: true}));
|
||||
} else if (Log.outputFormat === 'json') {
|
||||
console.log(EJSON.stringify(obj));
|
||||
if (!obj.omitCallerDetails) {
|
||||
obj = { ...Log._getCallerDetails(), ...obj };
|
||||
}
|
||||
|
||||
obj.time = new Date();
|
||||
obj.level = level;
|
||||
|
||||
// If we are in production don't write out debug logs.
|
||||
if (level === "debug" && Meteor.isProduction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intercepted) {
|
||||
interceptedLines.push(EJSON.stringify(obj));
|
||||
} else if (Meteor.isServer) {
|
||||
if (Log.outputFormat === "colored-text") {
|
||||
console.log(Log.format(obj, { color: true }));
|
||||
} else if (Log.outputFormat === "json") {
|
||||
console.log(EJSON.stringify(obj));
|
||||
} else {
|
||||
throw new Error(`Unknown logging output format: ${Log.outputFormat}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown logging output format: ${Log.outputFormat}`);
|
||||
logInBrowser(obj);
|
||||
}
|
||||
} else {
|
||||
logInBrowser(obj);
|
||||
}
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
// tries to parse line as EJSON. returns object if parse is successful, or null if not
|
||||
Log.parse = (line) => {
|
||||
let obj = null;
|
||||
if (line && line.startsWith('{')) { // might be json generated from calling 'Log'
|
||||
try { obj = EJSON.parse(line); } catch (e) {}
|
||||
if (line && line.startsWith("{")) {
|
||||
// might be json generated from calling 'Log'
|
||||
try {
|
||||
obj = EJSON.parse(line);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// XXX should probably check fields other than 'time'
|
||||
if (obj && obj.time && (obj.time instanceof Date)) {
|
||||
if (obj && obj.time && obj.time instanceof Date) {
|
||||
return obj;
|
||||
} else {
|
||||
return null;
|
||||
@@ -227,51 +236,46 @@ Log.parse = (line) => {
|
||||
// formats a log object into colored human and machine-readable text
|
||||
Log.format = (obj, options = {}) => {
|
||||
obj = { ...obj }; // don't mutate the argument
|
||||
let {
|
||||
const {
|
||||
time,
|
||||
timeInexact,
|
||||
level = 'info',
|
||||
level = "info",
|
||||
file,
|
||||
line: lineNumber,
|
||||
app: appName = '',
|
||||
app: appName = "",
|
||||
originApp,
|
||||
message = '',
|
||||
program = '',
|
||||
satellite = '',
|
||||
stderr = '',
|
||||
program = "",
|
||||
satellite = "",
|
||||
stderr = "",
|
||||
} = obj;
|
||||
let { message = "" } = obj;
|
||||
|
||||
if (!(time instanceof Date)) {
|
||||
throw new Error("'time' must be a Date object");
|
||||
}
|
||||
|
||||
FORMATTED_KEYS.forEach((key) => { delete obj[key]; });
|
||||
FORMATTED_KEYS.forEach((key) => {
|
||||
delete obj[key];
|
||||
});
|
||||
|
||||
if (Object.keys(obj).length > 0) {
|
||||
if (message) {
|
||||
message += ' ';
|
||||
message += " ";
|
||||
}
|
||||
message += EJSON.stringify(obj);
|
||||
}
|
||||
|
||||
const pad2 = n => n.toString().padStart(2, '0');
|
||||
const pad3 = n => n.toString().padStart(3, '0');
|
||||
const pad2 = (n) => n.toString().padStart(2, "0");
|
||||
const pad3 = (n) => n.toString().padStart(3, "0");
|
||||
|
||||
const dateStamp = time.getFullYear().toString() +
|
||||
pad2(time.getMonth() + 1 /*0-based*/) +
|
||||
pad2(time.getDate());
|
||||
const timeStamp = pad2(time.getHours()) +
|
||||
':' +
|
||||
pad2(time.getMinutes()) +
|
||||
':' +
|
||||
pad2(time.getSeconds()) +
|
||||
'.' +
|
||||
pad3(time.getMilliseconds());
|
||||
const dateStamp =
|
||||
time.getFullYear().toString() + pad2(time.getMonth() + 1 /*0-based*/) + pad2(time.getDate());
|
||||
const timeStamp = `${pad2(time.getHours())}:${pad2(time.getMinutes())}:${pad2(time.getSeconds())}.${pad3(time.getMilliseconds())}`;
|
||||
|
||||
// eg in San Francisco in June this will be '(-7)'
|
||||
const utcOffsetStr = `(${(-(new Date().getTimezoneOffset() / 60))})`;
|
||||
const utcOffsetStr = `(${-(new Date().getTimezoneOffset() / 60)})`;
|
||||
|
||||
let appInfo = '';
|
||||
let appInfo = "";
|
||||
if (appName) {
|
||||
appInfo += appName;
|
||||
}
|
||||
@@ -293,30 +297,30 @@ Log.format = (obj, options = {}) => {
|
||||
sourceInfoParts.push(lineNumber);
|
||||
}
|
||||
|
||||
let sourceInfo = !sourceInfoParts.length ?
|
||||
'' : `(${sourceInfoParts.join(':')}) `;
|
||||
let sourceInfo = !sourceInfoParts.length ? "" : `(${sourceInfoParts.join(":")}) `;
|
||||
|
||||
if (satellite)
|
||||
sourceInfo += `[${satellite}]`;
|
||||
if (satellite) sourceInfo += `[${satellite}]`;
|
||||
|
||||
const stderrIndicator = stderr ? '(STDERR) ' : '';
|
||||
const stderrIndicator = stderr ? "(STDERR) " : "";
|
||||
|
||||
const timeString = Log.showTime
|
||||
? `${dateStamp}-${timeStamp}${utcOffsetStr}${timeInexact ? '? ' : ' '}`
|
||||
: ' ';
|
||||
|
||||
|
||||
? `${dateStamp}-${timeStamp}${utcOffsetStr}${timeInexact ? "? " : " "}`
|
||||
: " ";
|
||||
|
||||
const metaPrefix = [
|
||||
level.charAt(0).toUpperCase(),
|
||||
timeString,
|
||||
appInfo,
|
||||
sourceInfo,
|
||||
stderrIndicator].join('');
|
||||
stderrIndicator,
|
||||
].join("");
|
||||
|
||||
|
||||
return Formatter.prettify(metaPrefix, options.color && platformColor(options.metaColor || META_COLOR)) +
|
||||
Formatter.prettify(message, options.color && platformColor(LEVEL_COLORS[level]));
|
||||
return (
|
||||
Formatter.prettify(
|
||||
metaPrefix,
|
||||
options.color && platformColor(options.metaColor || META_COLOR),
|
||||
) + Formatter.prettify(message, options.color && platformColor(LEVEL_COLORS[level]))
|
||||
);
|
||||
};
|
||||
|
||||
// Turn a line of text into a loggable object.
|
||||
@@ -325,10 +329,10 @@ Log.format = (obj, options = {}) => {
|
||||
Log.objFromText = (line, override) => {
|
||||
return {
|
||||
message: line,
|
||||
level: 'info',
|
||||
level: "info",
|
||||
time: new Date(),
|
||||
timeInexact: true,
|
||||
...override
|
||||
...override,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Formatter = {};
|
||||
Formatter.prettify = function(line, color){
|
||||
return line;
|
||||
Formatter.prettify = function (line, _color) {
|
||||
return line;
|
||||
};
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Log all uncaught errors so they can be printed to the developer.
|
||||
// But since Android's adb catalog already prints the uncaught exceptions, we
|
||||
// can disable it for Android.
|
||||
if (! /Android/i.test(navigator.userAgent)) {
|
||||
if (!/Android/i.test(navigator.userAgent)) {
|
||||
window.onerror = function (msg, url, line) {
|
||||
// Cut off the url prefix, the meaningful part always starts at 'www/' in
|
||||
// Cordova apps.
|
||||
url = url.replace(/^.*?\/www\//, '');
|
||||
url = url.replace(/^.*?\/www\//, "");
|
||||
console.log(`Uncaught Error: ${msg}:${line}:${url}`);
|
||||
};
|
||||
}
|
||||
|
||||
export * from './logging.js';
|
||||
export * from "./logging.js";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Formatter = {};
|
||||
Formatter.prettify = function(line, color){
|
||||
if(!color) return line;
|
||||
return require("chalk")[color](line);
|
||||
Formatter.prettify = function (line, color) {
|
||||
if (!color) return line;
|
||||
return require("chalk")[color](line);
|
||||
};
|
||||
|
||||
@@ -20,8 +20,8 @@ Tinytest.add("logging - _getCallerDetails", function (test) {
|
||||
// Note that we want this to work in --production too, so we need to allow
|
||||
// for the minified filename
|
||||
test.matches(
|
||||
eval(code),
|
||||
/^(?:eval|local-test_logging\.js|[a-f0-9]{40}\.js)/
|
||||
eval(code), // oxlint-disable-line no-eval -- intentional eval for testing caller details
|
||||
/^(?:eval|local-test_logging\.js|[a-f0-9]{40}\.js)/,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -64,11 +64,7 @@ Tinytest.add("logging - log", function (test) {
|
||||
[0, "0", "falsy - 0"],
|
||||
[null, "null", "falsy - null"],
|
||||
[undefined, "undefined", "falsy - undefined"],
|
||||
[
|
||||
new Date("2013-06-13T01:15:16.000Z"),
|
||||
new Date("2013-06-13T01:15:16.000Z"),
|
||||
"date",
|
||||
],
|
||||
[new Date("2013-06-13T01:15:16.000Z"), new Date("2013-06-13T01:15:16.000Z"), "date"],
|
||||
[/[^regexp]{0,1}/g, "/[^regexp]{0,1}/g", "regexp"],
|
||||
[true, "true", "boolean - true"],
|
||||
[false, "false", "boolean - false"],
|
||||
@@ -94,8 +90,7 @@ Tinytest.add("logging - log", function (test) {
|
||||
if (
|
||||
expected &&
|
||||
expected.toString &&
|
||||
(expected.toString() === "NaN" ||
|
||||
expected.toString() === "Invalid Date")
|
||||
(expected.toString() === "NaN" || expected.toString() === "Invalid Date")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -116,9 +111,7 @@ Tinytest.add("logging - log", function (test) {
|
||||
test.throws(function () {
|
||||
log({ level: "not the right level" });
|
||||
});
|
||||
["file", "line", "program", "originApp", "satellite"].forEach(function (
|
||||
restrictedKey
|
||||
) {
|
||||
["file", "line", "program", "originApp", "satellite"].forEach(function (restrictedKey) {
|
||||
test.throws(function () {
|
||||
const obj = {};
|
||||
obj[restrictedKey] = "usage of restricted key";
|
||||
@@ -172,14 +165,12 @@ Tinytest.add("logging - parse", function (test) {
|
||||
|
||||
Tinytest.add("logging - format", function (test) {
|
||||
const time = new Date(2012, 9 - 1 /*0-based*/, 8, 7, 6, 5, 4);
|
||||
const utcOffsetStr = "(" + -(new Date().getTimezoneOffset() / 60) + ")";
|
||||
const utcOffsetStr = `(${-(new Date().getTimezoneOffset() / 60)})`;
|
||||
|
||||
["debug", "info", "warn", "error"].forEach(function (level) {
|
||||
test.equal(
|
||||
Log.format({ message: "message", time, level }),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} message`
|
||||
`${level.charAt(0).toUpperCase()}20120908-07:06:05.004${utcOffsetStr} message`,
|
||||
);
|
||||
|
||||
test.equal(
|
||||
@@ -189,23 +180,19 @@ Tinytest.add("logging - format", function (test) {
|
||||
timeInexact: true,
|
||||
level,
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr}? message`
|
||||
`${level.charAt(0).toUpperCase()}20120908-07:06:05.004${utcOffsetStr}? message`,
|
||||
);
|
||||
|
||||
test.equal(
|
||||
Log.format({ foo1: "bar1", foo2: "bar2", time, level }),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} {"foo1":"bar1","foo2":"bar2"}`
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} {"foo1":"bar1","foo2":"bar2"}`,
|
||||
);
|
||||
|
||||
test.equal(
|
||||
Log.format({ message: "message", foo: "bar", time, level }),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} message {"foo":"bar"}`
|
||||
`${level.charAt(0).toUpperCase()}20120908-07:06:05.004${utcOffsetStr} message {"foo":"bar"}`,
|
||||
);
|
||||
|
||||
// Has everything except stderr field
|
||||
@@ -223,7 +210,7 @@ Tinytest.add("logging - format", function (test) {
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} [myApp via proxy] (server:app.js:42) message {\"foo\":\"bar\"}`
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} [myApp via proxy] (server:app.js:42) message {"foo":"bar"}`,
|
||||
);
|
||||
|
||||
// stderr
|
||||
@@ -236,7 +223,7 @@ Tinytest.add("logging - format", function (test) {
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} (STDERR) message from stderr`
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} (STDERR) message from stderr`,
|
||||
);
|
||||
|
||||
// app/originApp
|
||||
@@ -248,9 +235,7 @@ Tinytest.add("logging - format", function (test) {
|
||||
app: "app",
|
||||
originApp: "app",
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} [app] message`
|
||||
`${level.charAt(0).toUpperCase()}20120908-07:06:05.004${utcOffsetStr} [app] message`,
|
||||
);
|
||||
test.equal(
|
||||
Log.format({
|
||||
@@ -262,7 +247,7 @@ Tinytest.add("logging - format", function (test) {
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} [app via proxy] message`
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} [app via proxy] message`,
|
||||
);
|
||||
|
||||
// source info
|
||||
@@ -277,7 +262,7 @@ Tinytest.add("logging - format", function (test) {
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} (server:app.js:42) message`
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} (server:app.js:42) message`,
|
||||
);
|
||||
test.equal(
|
||||
Log.format({
|
||||
@@ -287,9 +272,7 @@ Tinytest.add("logging - format", function (test) {
|
||||
file: "app.js",
|
||||
line: 42,
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()}20120908-07:06:05.004${utcOffsetStr} (app.js:42) message`
|
||||
`${level.charAt(0).toUpperCase()}20120908-07:06:05.004${utcOffsetStr} (app.js:42) message`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -302,12 +285,69 @@ Tinytest.add("logging - format", function (test) {
|
||||
},
|
||||
{
|
||||
color: true,
|
||||
}
|
||||
},
|
||||
),
|
||||
/oyez/
|
||||
/oyez/,
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("logging - Log() delegates to Log.info", function (test) {
|
||||
Log._intercept(1);
|
||||
Log("hello");
|
||||
const intercepted = Log._intercepted();
|
||||
test.equal(intercepted.length, 1);
|
||||
const obj = EJSON.parse(intercepted[0]);
|
||||
test.equal(obj.message, "hello");
|
||||
test.equal(obj.level, "info");
|
||||
});
|
||||
|
||||
Tinytest.add("logging - _suppress drops the next N calls", function (test) {
|
||||
Log._suppress(2);
|
||||
// These should be dropped — not intercepted, not printed.
|
||||
Log.info("dropped-1");
|
||||
Log.info("dropped-2");
|
||||
|
||||
// Now an interception should capture the *next* line, not the suppressed ones.
|
||||
Log._intercept(1);
|
||||
Log.info("kept");
|
||||
const intercepted = Log._intercepted();
|
||||
test.equal(intercepted.length, 1);
|
||||
test.equal(EJSON.parse(intercepted[0]).message, "kept");
|
||||
});
|
||||
|
||||
Tinytest.add("logging - objFromText contract and override merging", function (test) {
|
||||
const obj = Log.objFromText("raw line");
|
||||
test.equal(obj.message, "raw line");
|
||||
test.equal(obj.level, "info");
|
||||
test.equal(obj.timeInexact, true);
|
||||
test.instanceOf(obj.time, Date);
|
||||
|
||||
// Override wins for provided fields, leaves others intact.
|
||||
const overridden = Log.objFromText("raw line", {
|
||||
level: "error",
|
||||
extra: "payload",
|
||||
});
|
||||
test.equal(overridden.level, "error");
|
||||
test.equal(overridden.message, "raw line");
|
||||
test.equal(overridden.timeInexact, true);
|
||||
test.equal(overridden.extra, "payload");
|
||||
});
|
||||
|
||||
Tinytest.add("logging - format throws when time is not a Date", function (test) {
|
||||
test.throws(function () {
|
||||
Log.format({ message: "hi", level: "info", time: "not a date" });
|
||||
}, /'time' must be a Date object/);
|
||||
});
|
||||
|
||||
Tinytest.add("logging - parse returns null for invalid payloads", function (test) {
|
||||
// Empty object has no time field — parse returns null.
|
||||
test.equal(Log.parse("{}"), null);
|
||||
// JSON with time present but not a Date → null.
|
||||
test.equal(Log.parse('{"time": 1234567890}'), null);
|
||||
// Doesn't start with '{' → not EJSON; not parsed.
|
||||
test.equal(Log.parse("plain string"), null);
|
||||
});
|
||||
|
||||
Tinytest.add("logging - formats - without time", function (test) {
|
||||
const time = new Date(2012, 9 - 1 /*0-based*/, 8, 7, 6, 5, 4);
|
||||
// even tho time and offset are provided they should not be included in the output
|
||||
@@ -316,7 +356,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
for (const level of levels) {
|
||||
test.equal(
|
||||
Log.format({ message: "message", time, level }),
|
||||
`${level.charAt(0).toUpperCase()} message`
|
||||
`${level.charAt(0).toUpperCase()} message`,
|
||||
);
|
||||
|
||||
test.equal(
|
||||
@@ -326,17 +366,17 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
timeInexact: true,
|
||||
level,
|
||||
}),
|
||||
`${level.charAt(0).toUpperCase()} message`
|
||||
`${level.charAt(0).toUpperCase()} message`,
|
||||
);
|
||||
|
||||
test.equal(
|
||||
Log.format({ foo1: "bar1", foo2: "bar2", time, level }),
|
||||
`${level.charAt(0).toUpperCase()} {"foo1":"bar1","foo2":"bar2"}`
|
||||
`${level.charAt(0).toUpperCase()} {"foo1":"bar1","foo2":"bar2"}`,
|
||||
);
|
||||
|
||||
test.equal(
|
||||
Log.format({ message: "message", foo: "bar", time, level }),
|
||||
`${level.charAt(0).toUpperCase()} message {"foo":"bar"}`
|
||||
`${level.charAt(0).toUpperCase()} message {"foo":"bar"}`,
|
||||
);
|
||||
|
||||
// Has everything except stderr field
|
||||
@@ -352,9 +392,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
originApp: "proxy",
|
||||
program: "server",
|
||||
}),
|
||||
`${level
|
||||
.charAt(0)
|
||||
.toUpperCase()} [myApp via proxy] (server:app.js:42) message {\"foo\":\"bar\"}`
|
||||
`${level.charAt(0).toUpperCase()} [myApp via proxy] (server:app.js:42) message {"foo":"bar"}`,
|
||||
);
|
||||
|
||||
// stderr
|
||||
@@ -365,7 +403,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
level,
|
||||
stderr: true,
|
||||
}),
|
||||
`${level.charAt(0).toUpperCase()} (STDERR) message from stderr`
|
||||
`${level.charAt(0).toUpperCase()} (STDERR) message from stderr`,
|
||||
);
|
||||
|
||||
// app/originApp
|
||||
@@ -377,7 +415,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
app: "app",
|
||||
originApp: "app",
|
||||
}),
|
||||
`${level.charAt(0).toUpperCase()} [app] message`
|
||||
`${level.charAt(0).toUpperCase()} [app] message`,
|
||||
);
|
||||
test.equal(
|
||||
Log.format({
|
||||
@@ -387,7 +425,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
app: "app",
|
||||
originApp: "proxy",
|
||||
}),
|
||||
`${level.charAt(0).toUpperCase()} [app via proxy] message`
|
||||
`${level.charAt(0).toUpperCase()} [app via proxy] message`,
|
||||
);
|
||||
|
||||
// source info
|
||||
@@ -400,7 +438,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
line: 42,
|
||||
program: "server",
|
||||
}),
|
||||
`${level.charAt(0).toUpperCase()} (server:app.js:42) message`
|
||||
`${level.charAt(0).toUpperCase()} (server:app.js:42) message`,
|
||||
);
|
||||
test.equal(
|
||||
Log.format({
|
||||
@@ -410,7 +448,7 @@ Tinytest.add("logging - formats - without time", function (test) {
|
||||
file: "app.js",
|
||||
line: 42,
|
||||
}),
|
||||
`${level.charAt(0).toUpperCase()} (app.js:42) message`
|
||||
`${level.charAt(0).toUpperCase()} (app.js:42) message`,
|
||||
);
|
||||
}
|
||||
Log.showTime = true; // reset
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
Package.describe({
|
||||
summary: 'Logging facility.',
|
||||
version: '1.3.6',
|
||||
summary: "Logging facility.",
|
||||
version: "1.3.6",
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
'chalk': '4.1.2',
|
||||
'@babel/runtime': '7.20.7',
|
||||
chalk: "4.1.2",
|
||||
"@babel/runtime": "7.20.7",
|
||||
});
|
||||
|
||||
Npm.strip({
|
||||
'es5-ext': ['test/']
|
||||
"es5-ext": ["test/"],
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.export('Log');
|
||||
api.export("Log");
|
||||
// The `ecmascript-runtime-client` package is explicitly depended upon
|
||||
// here due to this package's dependency on
|
||||
// `String.prototype.padRight` which is polyfilled only in
|
||||
// `ecmascript-runtime-client@0.6.2` or newer.
|
||||
api.use(['ejson', 'ecmascript', 'typescript', 'ecmascript-runtime-client']);
|
||||
api.mainModule('logging.js');
|
||||
api.addFiles('logging_server.js', 'server');
|
||||
api.addFiles('logging_browser.js', 'client');
|
||||
api.mainModule('logging_cordova.js', 'web.cordova');
|
||||
api.addAssets('logging.d.ts', 'server');
|
||||
api.use(["ejson", "ecmascript", "typescript", "ecmascript-runtime-client"]);
|
||||
api.mainModule("logging.js");
|
||||
api.addFiles("logging_server.js", "server");
|
||||
api.addFiles("logging_browser.js", "client");
|
||||
api.mainModule("logging_cordova.js", "web.cordova");
|
||||
api.addAssets("logging.d.ts", "server");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(['tinytest', 'ejson', 'ecmascript']);
|
||||
api.use('logging', ['client', 'server']);
|
||||
api.mainModule('logging_test.js', ['server', 'client']);
|
||||
api.use(["tinytest", "ejson", "ecmascript"]);
|
||||
api.use("logging", ["client", "server"]);
|
||||
api.mainModule("logging_test.js", ["server", "client"]);
|
||||
});
|
||||
|
||||
268
packages/ordered-dict/ordered-dict-tests.js
Normal file
268
packages/ordered-dict/ordered-dict-tests.js
Normal file
@@ -0,0 +1,268 @@
|
||||
import { OrderedDict } from "./ordered_dict.js";
|
||||
|
||||
const keysInOrder = (dict) => {
|
||||
const keys = [];
|
||||
dict.forEach((value, key) => {
|
||||
keys.push(key);
|
||||
});
|
||||
return keys;
|
||||
};
|
||||
|
||||
Tinytest.add("ordered-dict - starts empty", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
test.isTrue(dict.empty());
|
||||
test.equal(dict.size(), 0);
|
||||
test.equal(dict.first(), undefined);
|
||||
test.equal(dict.firstValue(), undefined);
|
||||
test.equal(dict.last(), undefined);
|
||||
test.equal(dict.lastValue(), undefined);
|
||||
test.isFalse(dict.has("anything"));
|
||||
test.equal(dict.get("anything"), undefined);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - append maintains insertion order", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
|
||||
test.equal(dict.size(), 3);
|
||||
test.equal(keysInOrder(dict), ["A", "B", "C"]);
|
||||
test.equal(dict.first(), "A");
|
||||
test.equal(dict.firstValue(), 1);
|
||||
test.equal(dict.last(), "C");
|
||||
test.equal(dict.lastValue(), 3);
|
||||
|
||||
// prev/next on the middle node.
|
||||
test.equal(dict.prev("B"), "A");
|
||||
test.equal(dict.next("B"), "C");
|
||||
test.equal(dict.prev("A"), null);
|
||||
test.equal(dict.next("C"), null);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - putBefore inserts before target", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.putBefore("X", 99, "B");
|
||||
|
||||
test.equal(keysInOrder(dict), ["A", "X", "B"]);
|
||||
test.equal(dict.prev("X"), "A");
|
||||
test.equal(dict.next("X"), "B");
|
||||
test.equal(dict.size(), 3);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - putBefore(null) appends to end", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.putBefore("B", 2, null);
|
||||
test.equal(keysInOrder(dict), ["A", "B"]);
|
||||
test.equal(dict.last(), "B");
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - putBefore throws on duplicate key", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
test.throws(() => dict.putBefore("A", 2, null), /already present/);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - putBefore throws when 'before' key is missing", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
test.throws(
|
||||
() => dict.putBefore("X", 99, "nonexistent"),
|
||||
/could not find item to put this one before/,
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - remove unlinks and returns value", (test) => {
|
||||
// Remove from the middle.
|
||||
let dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
const removed = dict.remove("B");
|
||||
test.equal(removed, 2);
|
||||
test.equal(keysInOrder(dict), ["A", "C"]);
|
||||
test.equal(dict.next("A"), "C");
|
||||
test.equal(dict.prev("C"), "A");
|
||||
test.equal(dict.size(), 2);
|
||||
test.isFalse(dict.has("B"));
|
||||
|
||||
// Remove from the head.
|
||||
dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
dict.remove("A");
|
||||
test.equal(keysInOrder(dict), ["B", "C"]);
|
||||
test.equal(dict.first(), "B");
|
||||
test.equal(dict.prev("B"), null);
|
||||
|
||||
// Remove from the tail.
|
||||
dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
dict.remove("C");
|
||||
test.equal(keysInOrder(dict), ["A", "B"]);
|
||||
test.equal(dict.last(), "B");
|
||||
test.equal(dict.next("B"), null);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - remove throws when key missing", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
test.throws(() => dict.remove("missing"), /not present/);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - moveBefore reorders items", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
|
||||
dict.moveBefore("C", "A");
|
||||
|
||||
test.equal(keysInOrder(dict), ["C", "A", "B"]);
|
||||
test.equal(dict.first(), "C");
|
||||
test.equal(dict.last(), "B");
|
||||
test.equal(dict.prev("C"), null);
|
||||
test.equal(dict.next("C"), "A");
|
||||
test.equal(dict.prev("A"), "C");
|
||||
test.equal(dict.next("A"), "B");
|
||||
test.equal(dict.prev("B"), "A");
|
||||
test.equal(dict.next("B"), null);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - moveBefore(null) moves to end", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
|
||||
dict.moveBefore("A", null);
|
||||
|
||||
test.equal(keysInOrder(dict), ["B", "C", "A"]);
|
||||
test.equal(dict.last(), "A");
|
||||
test.equal(dict.first(), "B");
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - moveBefore is a no-op when already in place", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
|
||||
// Moving A before B means A stays where it is (A.next === B already).
|
||||
dict.moveBefore("A", "B");
|
||||
test.equal(keysInOrder(dict), ["A", "B", "C"]);
|
||||
|
||||
// Same but for middle.
|
||||
dict.moveBefore("B", "C");
|
||||
test.equal(keysInOrder(dict), ["A", "B", "C"]);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - moveBefore throws on missing keys", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
test.throws(() => dict.moveBefore("missing", null), /Item to move is not present/);
|
||||
test.throws(
|
||||
() => dict.moveBefore("A", "nonexistent"),
|
||||
/Could not find element to move this one before/,
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - indexOf returns position or null", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
|
||||
test.equal(dict.indexOf("A"), 0);
|
||||
test.equal(dict.indexOf("B"), 1);
|
||||
test.equal(dict.indexOf("C"), 2);
|
||||
test.equal(dict.indexOf("missing"), null);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - forEach receives (value, key, index)", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", "valA");
|
||||
dict.append("B", "valB");
|
||||
dict.append("C", "valC");
|
||||
|
||||
const calls = [];
|
||||
dict.forEach((value, key, index) => {
|
||||
calls.push({ value, key, index });
|
||||
});
|
||||
|
||||
test.equal(calls, [
|
||||
{ value: "valA", key: "A", index: 0 },
|
||||
{ value: "valB", key: "B", index: 1 },
|
||||
{ value: "valC", key: "C", index: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - forEach breaks on OrderedDict.BREAK", (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
dict.append("D", 4);
|
||||
|
||||
let visited = 0;
|
||||
dict.forEach((_value, _key) => {
|
||||
visited++;
|
||||
if (visited === 2) {
|
||||
return OrderedDict.BREAK;
|
||||
}
|
||||
});
|
||||
test.equal(visited, 2);
|
||||
});
|
||||
|
||||
Tinytest.addAsync("ordered-dict - forEachAsync iterates and supports break", async (test) => {
|
||||
const dict = new OrderedDict();
|
||||
dict.append("A", 1);
|
||||
dict.append("B", 2);
|
||||
dict.append("C", 3);
|
||||
|
||||
const collected = [];
|
||||
await dict.forEachAsync(async (value, key, index) => {
|
||||
await Promise.resolve();
|
||||
collected.push([key, value, index]);
|
||||
});
|
||||
test.equal(collected, [
|
||||
["A", 1, 0],
|
||||
["B", 2, 1],
|
||||
["C", 3, 2],
|
||||
]);
|
||||
|
||||
// Break path.
|
||||
let visited = 0;
|
||||
await dict.forEachAsync(async () => {
|
||||
await Promise.resolve();
|
||||
visited++;
|
||||
if (visited === 1) {
|
||||
return OrderedDict.BREAK;
|
||||
}
|
||||
});
|
||||
test.equal(visited, 1);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - constructor accepts initial pairs", (test) => {
|
||||
const dict = new OrderedDict(["a", 1], ["b", 2], ["c", 3]);
|
||||
test.equal(keysInOrder(dict), ["a", "b", "c"]);
|
||||
test.equal(dict.get("a"), 1);
|
||||
test.equal(dict.get("b"), 2);
|
||||
test.equal(dict.get("c"), 3);
|
||||
test.equal(dict.size(), 3);
|
||||
});
|
||||
|
||||
Tinytest.add("ordered-dict - constructor accepts stringify + initial pairs", (test) => {
|
||||
const dict = new OrderedDict((k) => String(k), [1, "a"], [2, "b"], [3, "c"]);
|
||||
test.equal(dict.size(), 3);
|
||||
test.equal(dict.get(1), "a");
|
||||
test.equal(dict.get(2), "b");
|
||||
test.equal(dict.get(3), "c");
|
||||
test.equal(keysInOrder(dict), [1, 2, 3]);
|
||||
});
|
||||
@@ -12,7 +12,7 @@ function element(key, value, next, prev) {
|
||||
key: key,
|
||||
value: value,
|
||||
next: next,
|
||||
prev: prev
|
||||
prev: prev,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,19 +23,21 @@ export class OrderedDict {
|
||||
this._last = null;
|
||||
this._size = 0;
|
||||
|
||||
if (typeof args[0] === 'function') {
|
||||
if (typeof args[0] === "function") {
|
||||
this._stringify = args.shift();
|
||||
} else {
|
||||
this._stringify = function (x) { return x; };
|
||||
this._stringify = function (x) {
|
||||
return x;
|
||||
};
|
||||
}
|
||||
|
||||
args.forEach(kv => this.putBefore(kv[0], kv[1], null));
|
||||
args.forEach((kv) => this.putBefore(kv[0], kv[1], null));
|
||||
}
|
||||
|
||||
// the "prefix keys with a space" thing comes from here
|
||||
// https://github.com/documentcloud/underscore/issues/376#issuecomment-2815649
|
||||
_k(key) {
|
||||
return " " + this._stringify(key);
|
||||
return ` ${this._stringify(key)}`;
|
||||
}
|
||||
|
||||
empty() {
|
||||
@@ -49,36 +51,26 @@ export class OrderedDict {
|
||||
_linkEltIn(elt) {
|
||||
if (!elt.next) {
|
||||
elt.prev = this._last;
|
||||
if (this._last)
|
||||
this._last.next = elt;
|
||||
if (this._last) this._last.next = elt;
|
||||
this._last = elt;
|
||||
} else {
|
||||
elt.prev = elt.next.prev;
|
||||
elt.next.prev = elt;
|
||||
if (elt.prev)
|
||||
elt.prev.next = elt;
|
||||
if (elt.prev) elt.prev.next = elt;
|
||||
}
|
||||
if (this._first === null || this._first === elt.next)
|
||||
this._first = elt;
|
||||
if (this._first === null || this._first === elt.next) this._first = elt;
|
||||
}
|
||||
|
||||
_linkEltOut(elt) {
|
||||
if (elt.next)
|
||||
elt.next.prev = elt.prev;
|
||||
if (elt.prev)
|
||||
elt.prev.next = elt.next;
|
||||
if (elt === this._last)
|
||||
this._last = elt.prev;
|
||||
if (elt === this._first)
|
||||
this._first = elt.next;
|
||||
if (elt.next) elt.next.prev = elt.prev;
|
||||
if (elt.prev) elt.prev.next = elt.next;
|
||||
if (elt === this._last) this._last = elt.prev;
|
||||
if (elt === this._first) this._first = elt.next;
|
||||
}
|
||||
|
||||
putBefore(key, item, before) {
|
||||
if (this._dict[this._k(key)])
|
||||
throw new Error("Item " + key + " already present in OrderedDict");
|
||||
var elt = before ?
|
||||
element(key, item, this._dict[this._k(before)]) :
|
||||
element(key, item, null);
|
||||
if (this._dict[this._k(key)]) throw new Error(`Item ${key} already present in OrderedDict`);
|
||||
const elt = before ? element(key, item, this._dict[this._k(before)]) : element(key, item, null);
|
||||
if (typeof elt.next === "undefined")
|
||||
throw new Error("could not find item to put this one before");
|
||||
this._linkEltIn(elt);
|
||||
@@ -91,9 +83,8 @@ export class OrderedDict {
|
||||
}
|
||||
|
||||
remove(key) {
|
||||
var elt = this._dict[this._k(key)];
|
||||
if (typeof elt === "undefined")
|
||||
throw new Error("Item " + key + " not present in OrderedDict");
|
||||
const elt = this._dict[this._k(key)];
|
||||
if (typeof elt === "undefined") throw new Error(`Item ${key} not present in OrderedDict`);
|
||||
this._linkEltOut(elt);
|
||||
this._size--;
|
||||
delete this._dict[this._k(key)];
|
||||
@@ -107,10 +98,7 @@ export class OrderedDict {
|
||||
}
|
||||
|
||||
has(key) {
|
||||
return Object.prototype.hasOwnProperty.call(
|
||||
this._dict,
|
||||
this._k(key)
|
||||
);
|
||||
return Object.prototype.hasOwnProperty.call(this._dict, this._k(key));
|
||||
}
|
||||
|
||||
// Iterate through the items in this dictionary in order, calling
|
||||
@@ -118,10 +106,10 @@ export class OrderedDict {
|
||||
|
||||
// Stops whenever iter returns OrderedDict.BREAK, or after the last element.
|
||||
forEach(iter, context = null) {
|
||||
var i = 0;
|
||||
var elt = this._first;
|
||||
let i = 0;
|
||||
let elt = this._first;
|
||||
while (elt !== null) {
|
||||
var b = iter.call(context, elt.value, elt.key, i);
|
||||
const b = iter.call(context, elt.value, elt.key, i);
|
||||
if (b === OrderedDict.BREAK) return;
|
||||
elt = elt.next;
|
||||
i++;
|
||||
@@ -169,32 +157,31 @@ export class OrderedDict {
|
||||
|
||||
prev(key) {
|
||||
if (this.has(key)) {
|
||||
var elt = this._dict[this._k(key)];
|
||||
if (elt.prev)
|
||||
return elt.prev.key;
|
||||
const elt = this._dict[this._k(key)];
|
||||
if (elt.prev) return elt.prev.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
next(key) {
|
||||
if (this.has(key)) {
|
||||
var elt = this._dict[this._k(key)];
|
||||
if (elt.next)
|
||||
return elt.next.key;
|
||||
const elt = this._dict[this._k(key)];
|
||||
if (elt.next) return elt.next.key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
moveBefore(key, before) {
|
||||
var elt = this._dict[this._k(key)];
|
||||
var eltBefore = before ? this._dict[this._k(before)] : null;
|
||||
const elt = this._dict[this._k(key)];
|
||||
const eltBefore = before ? this._dict[this._k(before)] : null;
|
||||
if (typeof elt === "undefined") {
|
||||
throw new Error("Item to move is not present");
|
||||
}
|
||||
if (typeof eltBefore === "undefined") {
|
||||
throw new Error("Could not find element to move this one before");
|
||||
}
|
||||
if (eltBefore === elt.next) // no moving necessary
|
||||
if (eltBefore === elt.next)
|
||||
// no moving necessary
|
||||
return;
|
||||
// remove from its old place
|
||||
this._linkEltOut(elt);
|
||||
@@ -205,7 +192,7 @@ export class OrderedDict {
|
||||
|
||||
// Linear, sadly.
|
||||
indexOf(key) {
|
||||
var ret = null;
|
||||
let ret = null;
|
||||
this.forEach((v, k, i) => {
|
||||
if (this._k(k) === this._k(key)) {
|
||||
ret = i;
|
||||
@@ -217,7 +204,7 @@ export class OrderedDict {
|
||||
}
|
||||
|
||||
_checkRep() {
|
||||
Object.keys(this._dict).forEach(k => {
|
||||
Object.keys(this._dict).forEach((k) => {
|
||||
const v = this._dict[k];
|
||||
if (v.next === v) {
|
||||
throw new Error("Next is a loop");
|
||||
@@ -229,4 +216,4 @@ export class OrderedDict {
|
||||
}
|
||||
}
|
||||
|
||||
OrderedDict.BREAK = {"break": true};
|
||||
OrderedDict.BREAK = { break: true };
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
Package.describe({
|
||||
summary: "Ordered traversable dictionary with a mutable ordering",
|
||||
version: '1.2.0',
|
||||
documentation: null
|
||||
version: "1.2.0",
|
||||
documentation: null,
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.mainModule('ordered_dict.js');
|
||||
api.export('OrderedDict');
|
||||
api.use("ecmascript");
|
||||
api.mainModule("ordered_dict.js");
|
||||
api.export("OrderedDict");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(["ecmascript", "tinytest", "ordered-dict"]);
|
||||
api.addFiles("ordered-dict-tests.js", ["client", "server"]);
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
Package.describe({
|
||||
name: 'rate-limit',
|
||||
version: '1.2.0-beta350.7',
|
||||
name: "rate-limit",
|
||||
version: "1.2.0-beta350.7",
|
||||
// Brief, one-line summary of the package.
|
||||
summary: 'An algorithm for rate limiting anything',
|
||||
summary: "An algorithm for rate limiting anything",
|
||||
// URL to the Git repository containing the source code for this package.
|
||||
git: '',
|
||||
git: "",
|
||||
// By default, Meteor will default to using README.md for documentation.
|
||||
// To avoid submitting documentation, set this field to null.
|
||||
documentation: 'README.md',
|
||||
documentation: "README.md",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('random');
|
||||
api.use('ecmascript');
|
||||
api.mainModule('rate-limit.js');
|
||||
api.export('RateLimiter');
|
||||
api.use("random");
|
||||
api.use("ecmascript");
|
||||
api.mainModule("rate-limit.js");
|
||||
api.export("RateLimiter");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('test-helpers', ['client', 'server']);
|
||||
api.use('ecmascript');
|
||||
api.use('random');
|
||||
api.use('ddp-rate-limiter');
|
||||
api.use('tinytest');
|
||||
api.use('rate-limit');
|
||||
api.use('ddp-common');
|
||||
api.mainModule('rate-limit-tests.js');
|
||||
api.use("test-helpers", ["client", "server"]);
|
||||
api.use("ecmascript");
|
||||
api.use("random");
|
||||
api.use("ddp-rate-limiter");
|
||||
api.use("tinytest");
|
||||
api.use("rate-limit");
|
||||
api.use("ddp-common");
|
||||
api.mainModule("rate-limit-tests.js");
|
||||
});
|
||||
|
||||
@@ -19,44 +19,39 @@
|
||||
// XXX These tests should be refactored to use Tinytest.add instead of
|
||||
// testAsyncMulti as they're all on the server. Any future tests should be
|
||||
// written that way.
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { RateLimiter } from 'meteor/rate-limit';
|
||||
import { DDPCommon } from 'meteor/ddp-common';
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import { RateLimiter } from "meteor/rate-limit";
|
||||
import { DDPCommon } from "meteor/ddp-common";
|
||||
|
||||
Tinytest.add('rate limit tests - Check empty constructor creation',
|
||||
Tinytest.add("rate limit tests - Check empty constructor creation", function (test) {
|
||||
const r = new RateLimiter();
|
||||
test.equal(r.rules, {});
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
"rate limit tests - Check single rule with multiple " + "invocations, only 1 that matches",
|
||||
function (test) {
|
||||
const r = new RateLimiter();
|
||||
test.equal(r.rules, {});
|
||||
const userIdOne = 1;
|
||||
const restrictJustUserIdOneRule = {
|
||||
userId: userIdOne,
|
||||
IPAddr: null,
|
||||
method: null,
|
||||
};
|
||||
r.addRule(restrictJustUserIdOneRule, 1, 1000);
|
||||
const connectionHandle = createTempConnectionHandle(123, "127.0.0.1");
|
||||
const methodInvc1 = createTempMethodInvocation(userIdOne, connectionHandle, "login");
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle, "login");
|
||||
for (let i = 0; i < 2; i++) {
|
||||
r.increment(methodInvc1);
|
||||
r.increment(methodInvc2);
|
||||
}
|
||||
test.equal(r.check(methodInvc1).allowed, false);
|
||||
test.equal(r.check(methodInvc2).allowed, true);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add('rate limit tests - Check single rule with multiple ' +
|
||||
'invocations, only 1 that matches',
|
||||
function (test) {
|
||||
const r = new RateLimiter();
|
||||
const userIdOne = 1;
|
||||
const restrictJustUserIdOneRule = {
|
||||
userId: userIdOne,
|
||||
IPAddr: null,
|
||||
method: null,
|
||||
};
|
||||
r.addRule(restrictJustUserIdOneRule, 1, 1000);
|
||||
const connectionHandle = createTempConnectionHandle(123, '127.0.0.1');
|
||||
const methodInvc1 = createTempMethodInvocation(userIdOne, connectionHandle,
|
||||
'login');
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle,
|
||||
'login');
|
||||
for (let i = 0; i < 2; i++) {
|
||||
r.increment(methodInvc1);
|
||||
r.increment(methodInvc2);
|
||||
}
|
||||
test.equal(r.check(methodInvc1).allowed, false);
|
||||
test.equal(r.check(methodInvc2).allowed, true);
|
||||
},
|
||||
);
|
||||
|
||||
testAsyncMulti('rate limit tests - Run multiple invocations and wait for one' +
|
||||
' to reset', [
|
||||
testAsyncMulti("rate limit tests - Run multiple invocations and wait for one" + " to reset", [
|
||||
function (test, expect) {
|
||||
this.r = new RateLimiter();
|
||||
this.userIdOne = 1;
|
||||
@@ -67,18 +62,19 @@ testAsyncMulti('rate limit tests - Run multiple invocations and wait for one' +
|
||||
method: null,
|
||||
};
|
||||
this.r.addRule(this.restrictJustUserIdOneRule, 1, 500);
|
||||
this.connectionHandle = createTempConnectionHandle(123, '127.0.0.1')
|
||||
this.methodInvc1 = createTempMethodInvocation(this.userIdOne,
|
||||
this.connectionHandle, 'login');
|
||||
this.methodInvc2 = createTempMethodInvocation(this.userIdTwo,
|
||||
this.connectionHandle, 'login');
|
||||
this.connectionHandle = createTempConnectionHandle(123, "127.0.0.1");
|
||||
this.methodInvc1 = createTempMethodInvocation(this.userIdOne, this.connectionHandle, "login");
|
||||
this.methodInvc2 = createTempMethodInvocation(this.userIdTwo, this.connectionHandle, "login");
|
||||
for (let i = 0; i < 2; i++) {
|
||||
this.r.increment(this.methodInvc1);
|
||||
this.r.increment(this.methodInvc2);
|
||||
}
|
||||
test.equal(this.r.check(this.methodInvc1).allowed, false);
|
||||
test.equal(this.r.check(this.methodInvc2).allowed, true);
|
||||
Meteor.setTimeout(expect(function () { }), 1000);
|
||||
Meteor.setTimeout(
|
||||
expect(function () {}),
|
||||
1000,
|
||||
);
|
||||
},
|
||||
function (test) {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
@@ -89,74 +85,73 @@ testAsyncMulti('rate limit tests - Run multiple invocations and wait for one' +
|
||||
},
|
||||
]);
|
||||
|
||||
Tinytest.add('rate limit tests - Check two rules that affect same methodInvc' +
|
||||
' still throw', function (test) {
|
||||
const r = new RateLimiter();
|
||||
const loginMethodRule = {
|
||||
userId: null,
|
||||
IPAddr: null,
|
||||
method: 'login',
|
||||
};
|
||||
const onlyLimitEvenUserIdRule = {
|
||||
userId: userId => userId % 2 === 0,
|
||||
IPAddr: null,
|
||||
method: null,
|
||||
};
|
||||
r.addRule(loginMethodRule, 10, 100);
|
||||
r.addRule(onlyLimitEvenUserIdRule, 4, 100);
|
||||
const connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
|
||||
const methodInvc1 = createTempMethodInvocation(1, connectionHandle,
|
||||
'login');
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle,
|
||||
'login');
|
||||
const methodInvc3 = createTempMethodInvocation(3, connectionHandle,
|
||||
'test');
|
||||
for (let i = 0; i < 5; i++) {
|
||||
Tinytest.add(
|
||||
"rate limit tests - Check two rules that affect same methodInvc" + " still throw",
|
||||
function (test) {
|
||||
const r = new RateLimiter();
|
||||
const loginMethodRule = {
|
||||
userId: null,
|
||||
IPAddr: null,
|
||||
method: "login",
|
||||
};
|
||||
const onlyLimitEvenUserIdRule = {
|
||||
userId: (userId) => userId % 2 === 0,
|
||||
IPAddr: null,
|
||||
method: null,
|
||||
};
|
||||
r.addRule(loginMethodRule, 10, 100);
|
||||
r.addRule(onlyLimitEvenUserIdRule, 4, 100);
|
||||
const connectionHandle = createTempConnectionHandle(1234, "127.0.0.1");
|
||||
const methodInvc1 = createTempMethodInvocation(1, connectionHandle, "login");
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle, "login");
|
||||
const methodInvc3 = createTempMethodInvocation(3, connectionHandle, "test");
|
||||
for (let i = 0; i < 5; i++) {
|
||||
r.increment(methodInvc1);
|
||||
r.increment(methodInvc2);
|
||||
r.increment(methodInvc3);
|
||||
}
|
||||
// After for loop runs, we only have 10 runs, so that's under the limit
|
||||
test.equal(r.check(methodInvc1).allowed, true);
|
||||
// However, this triggers userId rule since this userId is even
|
||||
test.equal(r.check(methodInvc2).allowed, false);
|
||||
test.equal(r.check(methodInvc2).allowed, false);
|
||||
// Running one more test causes it to be false, since we're at 11 now.
|
||||
r.increment(methodInvc1);
|
||||
r.increment(methodInvc2);
|
||||
r.increment(methodInvc3);
|
||||
}
|
||||
// After for loop runs, we only have 10 runs, so that's under the limit
|
||||
test.equal(r.check(methodInvc1).allowed, true);
|
||||
// However, this triggers userId rule since this userId is even
|
||||
test.equal(r.check(methodInvc2).allowed, false);
|
||||
test.equal(r.check(methodInvc2).allowed, false);
|
||||
// Running one more test causes it to be false, since we're at 11 now.
|
||||
r.increment(methodInvc1);
|
||||
test.equal(r.check(methodInvc1).allowed, false);
|
||||
// 3rd Method Invocation isn't affected by either rules.
|
||||
test.equal(r.check(methodInvc3).allowed, true);
|
||||
});
|
||||
test.equal(r.check(methodInvc1).allowed, false);
|
||||
// 3rd Method Invocation isn't affected by either rules.
|
||||
test.equal(r.check(methodInvc3).allowed, true);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add('rate limit tests - Check one rule affected by two different ' +
|
||||
'invocations', function (test) {
|
||||
const r = new RateLimiter();
|
||||
const loginMethodRule = {
|
||||
userId: null,
|
||||
IPAddr: null,
|
||||
method: 'login',
|
||||
};
|
||||
r.addRule(loginMethodRule, 10, 10000);
|
||||
Tinytest.add(
|
||||
"rate limit tests - Check one rule affected by two different " + "invocations",
|
||||
function (test) {
|
||||
const r = new RateLimiter();
|
||||
const loginMethodRule = {
|
||||
userId: null,
|
||||
IPAddr: null,
|
||||
method: "login",
|
||||
};
|
||||
r.addRule(loginMethodRule, 10, 10000);
|
||||
|
||||
const connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
|
||||
const methodInvc1 = createTempMethodInvocation(1, connectionHandle,
|
||||
'login');
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle,
|
||||
'login');
|
||||
const connectionHandle = createTempConnectionHandle(1234, "127.0.0.1");
|
||||
const methodInvc1 = createTempMethodInvocation(1, connectionHandle, "login");
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle, "login");
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
r.increment(methodInvc1);
|
||||
r.increment(methodInvc2);
|
||||
}
|
||||
// This throws us over the limit since both increment the login rule
|
||||
// counter
|
||||
r.increment(methodInvc1);
|
||||
r.increment(methodInvc2);
|
||||
}
|
||||
// This throws us over the limit since both increment the login rule
|
||||
// counter
|
||||
r.increment(methodInvc1);
|
||||
|
||||
test.equal(r.check(methodInvc1).allowed, false);
|
||||
test.equal(r.check(methodInvc2).allowed, false);
|
||||
});
|
||||
test.equal(r.check(methodInvc1).allowed, false);
|
||||
test.equal(r.check(methodInvc2).allowed, false);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add('rate limit tests - add global rule', function (test) {
|
||||
Tinytest.add("rate limit tests - add global rule", function (test) {
|
||||
const r = new RateLimiter();
|
||||
const globalRule = {
|
||||
userId: null,
|
||||
@@ -165,15 +160,12 @@ Tinytest.add('rate limit tests - add global rule', function (test) {
|
||||
};
|
||||
r.addRule(globalRule, 1, 10000);
|
||||
|
||||
const connectionHandle = createTempConnectionHandle(1234, '127.0.0.1');
|
||||
const connectionHandle2 = createTempConnectionHandle(1234, '127.0.0.2');
|
||||
const connectionHandle = createTempConnectionHandle(1234, "127.0.0.1");
|
||||
const connectionHandle2 = createTempConnectionHandle(1234, "127.0.0.2");
|
||||
|
||||
const methodInvc1 = createTempMethodInvocation(1, connectionHandle,
|
||||
'login');
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle2,
|
||||
'test');
|
||||
const methodInvc3 = createTempMethodInvocation(3, connectionHandle,
|
||||
'user-accounts');
|
||||
const methodInvc1 = createTempMethodInvocation(1, connectionHandle, "login");
|
||||
const methodInvc2 = createTempMethodInvocation(2, connectionHandle2, "test");
|
||||
const methodInvc3 = createTempMethodInvocation(3, connectionHandle, "user-accounts");
|
||||
|
||||
// First invocation, all methods would still be allowed.
|
||||
r.increment(methodInvc2);
|
||||
@@ -187,55 +179,52 @@ Tinytest.add('rate limit tests - add global rule', function (test) {
|
||||
test.equal(r.check(methodInvc3).allowed, false);
|
||||
});
|
||||
|
||||
Tinytest.add('rate limit tests - Fuzzy rule match does not trigger rate limit',
|
||||
function (test) {
|
||||
const r = new RateLimiter();
|
||||
const rule = {
|
||||
a: inp => inp % 3 === 0,
|
||||
b: 5,
|
||||
c: 'hi',
|
||||
};
|
||||
r.addRule(rule, 1, 10000);
|
||||
const input = {
|
||||
a: 3,
|
||||
b: 5,
|
||||
};
|
||||
for (let i = 0; i < 5; i++) {
|
||||
r.increment(input);
|
||||
}
|
||||
test.equal(r.check(input).allowed, true);
|
||||
const matchingInput = {
|
||||
a: 3,
|
||||
b: 5,
|
||||
c: 'hi',
|
||||
d: 1,
|
||||
};
|
||||
r.increment(matchingInput);
|
||||
r.increment(matchingInput);
|
||||
// Past limit so should be false
|
||||
test.equal(r.check(matchingInput).allowed, false);
|
||||
|
||||
// Add secondary rule and check that longer time is returned when multiple
|
||||
// rules limits are hit
|
||||
const newRule = {
|
||||
a: inp => inp % 3 === 0,
|
||||
b: 5,
|
||||
c: 'hi',
|
||||
d: 1,
|
||||
};
|
||||
r.addRule(newRule, 1, 10);
|
||||
// First rule should still throw while second rule will trigger as well,
|
||||
// causing us to return longer time to reset to user
|
||||
r.increment(matchingInput);
|
||||
r.increment(matchingInput);
|
||||
test.equal(r.check(matchingInput).timeToReset > 50, true);
|
||||
},
|
||||
);
|
||||
Tinytest.add("rate limit tests - Fuzzy rule match does not trigger rate limit", function (test) {
|
||||
const r = new RateLimiter();
|
||||
const rule = {
|
||||
a: (inp) => inp % 3 === 0,
|
||||
b: 5,
|
||||
c: "hi",
|
||||
};
|
||||
r.addRule(rule, 1, 10000);
|
||||
const input = {
|
||||
a: 3,
|
||||
b: 5,
|
||||
};
|
||||
for (let i = 0; i < 5; i++) {
|
||||
r.increment(input);
|
||||
}
|
||||
test.equal(r.check(input).allowed, true);
|
||||
const matchingInput = {
|
||||
a: 3,
|
||||
b: 5,
|
||||
c: "hi",
|
||||
d: 1,
|
||||
};
|
||||
r.increment(matchingInput);
|
||||
r.increment(matchingInput);
|
||||
// Past limit so should be false
|
||||
test.equal(r.check(matchingInput).allowed, false);
|
||||
|
||||
// Add secondary rule and check that longer time is returned when multiple
|
||||
// rules limits are hit
|
||||
const newRule = {
|
||||
a: (inp) => inp % 3 === 0,
|
||||
b: 5,
|
||||
c: "hi",
|
||||
d: 1,
|
||||
};
|
||||
r.addRule(newRule, 1, 10);
|
||||
// First rule should still throw while second rule will trigger as well,
|
||||
// causing us to return longer time to reset to user
|
||||
r.increment(matchingInput);
|
||||
r.increment(matchingInput);
|
||||
test.equal(r.check(matchingInput).timeToReset > 50, true);
|
||||
});
|
||||
|
||||
/****** Test Our Helper Methods *****/
|
||||
|
||||
Tinytest.add('rate limit tests - test matchRule method', function (test) {
|
||||
Tinytest.add("rate limit tests - test matchRule method", function (test) {
|
||||
const r = new RateLimiter();
|
||||
const globalRule = {
|
||||
userId: null,
|
||||
@@ -247,9 +236,9 @@ Tinytest.add('rate limit tests - test matchRule method', function (test) {
|
||||
|
||||
const rateLimiterInput = {
|
||||
userId: 1023,
|
||||
IPAddr: '127.0.0.1',
|
||||
type: 'sub',
|
||||
name: 'getSubLists',
|
||||
IPAddr: "127.0.0.1",
|
||||
type: "sub",
|
||||
name: "getSubLists",
|
||||
};
|
||||
|
||||
test.equal(r.rules[globalRuleId].match(rateLimiterInput), true);
|
||||
@@ -269,54 +258,52 @@ Tinytest.add('rate limit tests - test matchRule method', function (test) {
|
||||
|
||||
const notCompleteInput = {
|
||||
userId: 102,
|
||||
IPAddr: '127.0.0.1',
|
||||
IPAddr: "127.0.0.1",
|
||||
};
|
||||
test.equal(r.rules[globalRuleId].match(notCompleteInput), true);
|
||||
test.equal(r.rules[oneNotNullId].match(notCompleteInput), false);
|
||||
});
|
||||
|
||||
Tinytest.add('rate limit tests - test generateMethodKey string',
|
||||
function (test) {
|
||||
const r = new RateLimiter();
|
||||
const globalRule = {
|
||||
userId: null,
|
||||
IPAddr: null,
|
||||
type: null,
|
||||
name: null,
|
||||
};
|
||||
const globalRuleId = r.addRule(globalRule);
|
||||
Tinytest.add("rate limit tests - test generateMethodKey string", function (test) {
|
||||
const r = new RateLimiter();
|
||||
const globalRule = {
|
||||
userId: null,
|
||||
IPAddr: null,
|
||||
type: null,
|
||||
name: null,
|
||||
};
|
||||
const globalRuleId = r.addRule(globalRule);
|
||||
|
||||
const rateLimiterInput = {
|
||||
userId: 1023,
|
||||
IPAddr: '127.0.0.1',
|
||||
type: 'sub',
|
||||
name: 'getSubLists',
|
||||
};
|
||||
const rateLimiterInput = {
|
||||
userId: 1023,
|
||||
IPAddr: "127.0.0.1",
|
||||
type: "sub",
|
||||
name: "getSubLists",
|
||||
};
|
||||
|
||||
test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), '');
|
||||
globalRule.userId = 1023;
|
||||
test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), "");
|
||||
globalRule.userId = 1023;
|
||||
|
||||
test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput),
|
||||
'userId1023');
|
||||
test.equal(r.rules[globalRuleId]._generateKeyString(rateLimiterInput), "userId1023");
|
||||
|
||||
const ruleWithFuncs = {
|
||||
userId: input => input % 2 === 0,
|
||||
IPAddr: null,
|
||||
type: null,
|
||||
};
|
||||
const funcRuleId = r.addRule(ruleWithFuncs);
|
||||
test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), '');
|
||||
rateLimiterInput.userId = 1024;
|
||||
test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput),
|
||||
'userId1024');
|
||||
const ruleWithFuncs = {
|
||||
userId: (input) => input % 2 === 0,
|
||||
IPAddr: null,
|
||||
type: null,
|
||||
};
|
||||
const funcRuleId = r.addRule(ruleWithFuncs);
|
||||
test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), "");
|
||||
rateLimiterInput.userId = 1024;
|
||||
test.equal(r.rules[funcRuleId]._generateKeyString(rateLimiterInput), "userId1024");
|
||||
|
||||
const multipleRules = ruleWithFuncs;
|
||||
multipleRules.IPAddr = '127.0.0.1';
|
||||
const multipleRuleId = r.addRule(multipleRules);
|
||||
test.equal(r.rules[multipleRuleId]._generateKeyString(rateLimiterInput),
|
||||
'userId1024IPAddr127.0.0.1');
|
||||
},
|
||||
);
|
||||
const multipleRules = ruleWithFuncs;
|
||||
multipleRules.IPAddr = "127.0.0.1";
|
||||
const multipleRuleId = r.addRule(multipleRules);
|
||||
test.equal(
|
||||
r.rules[multipleRuleId]._generateKeyString(rateLimiterInput),
|
||||
"userId1024IPAddr127.0.0.1",
|
||||
);
|
||||
});
|
||||
|
||||
function createTempConnectionHandle(id, clientIP) {
|
||||
return {
|
||||
@@ -325,7 +312,7 @@ function createTempConnectionHandle(id, clientIP) {
|
||||
this.close();
|
||||
},
|
||||
onClose(fn) {
|
||||
const cb = Meteor.bindEnvironment(fn, 'connection onClose callback');
|
||||
const cb = Meteor.bindEnvironment(fn, "connection onClose callback");
|
||||
if (this.inQueue) {
|
||||
this._closeCallbacks.push(cb);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Random } from 'meteor/random';
|
||||
import { Meteor } from "meteor/meteor";
|
||||
import { Random } from "meteor/random";
|
||||
|
||||
// Default time interval (in milliseconds) to reset rate limit counters
|
||||
const DEFAULT_INTERVAL_TIME_IN_MILLISECONDS = 1000;
|
||||
@@ -39,38 +39,36 @@ class Rule {
|
||||
// rule.matchers. If the match fails, search short circuits instead of
|
||||
// iterating through all matchers.
|
||||
match(input) {
|
||||
return Object
|
||||
.entries(this._matchers)
|
||||
.every(([key, matcher]) => {
|
||||
if (matcher !== null) {
|
||||
if (!hasOwn.call(input, key)) {
|
||||
return false;
|
||||
} else if (typeof matcher === 'function') {
|
||||
if (!(matcher(input[key]))) {
|
||||
return false;
|
||||
}
|
||||
} else if (matcher !== input[key]) {
|
||||
return Object.entries(this._matchers).every(([key, matcher]) => {
|
||||
if (matcher !== null) {
|
||||
if (!hasOwn.call(input, key)) {
|
||||
return false;
|
||||
} else if (typeof matcher === "function") {
|
||||
if (!matcher(input[key])) {
|
||||
return false;
|
||||
}
|
||||
} else if (matcher !== input[key]) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async matchAsync(input) {
|
||||
for (const [key, matcher] of Object.entries(this._matchers)) {
|
||||
if (matcher !== null) {
|
||||
if (!hasOwn.call(input, key)) {
|
||||
return false;
|
||||
} else if (typeof matcher === 'function') {
|
||||
if (!(await matcher(input[key]))) {
|
||||
return false;
|
||||
}
|
||||
} else if (matcher !== input[key]) {
|
||||
for (const [key, matcher] of Object.entries(this._matchers)) {
|
||||
if (matcher !== null) {
|
||||
if (!hasOwn.call(input, key)) {
|
||||
return false;
|
||||
} else if (typeof matcher === "function") {
|
||||
if (!(await matcher(input[key]))) {
|
||||
return false;
|
||||
}
|
||||
} else if (matcher !== input[key]) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -81,7 +79,7 @@ class Rule {
|
||||
return Object.entries(this._matchers)
|
||||
.filter(([key]) => this._matchers[key] !== null)
|
||||
.reduce((returnString, [key, matcher]) => {
|
||||
if (typeof matcher === 'function') {
|
||||
if (typeof matcher === "function") {
|
||||
if (matcher(input[key])) {
|
||||
returnString += key + input[key];
|
||||
}
|
||||
@@ -89,7 +87,7 @@ class Rule {
|
||||
returnString += key + input[key];
|
||||
}
|
||||
return returnString;
|
||||
}, '');
|
||||
}, "");
|
||||
}
|
||||
|
||||
// Applies the provided input and returns the key string, time since counters
|
||||
@@ -137,19 +135,19 @@ class RateLimiter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this input has exceeded any rate limits.
|
||||
* @param {object} input dictionary containing key-value pairs of attributes
|
||||
* that match to rules
|
||||
* @return {object} Returns object of following structure
|
||||
* { 'allowed': boolean - is this input allowed
|
||||
* 'timeToReset': integer | Infinity - returns time until counters are reset
|
||||
* in milliseconds
|
||||
* 'numInvocationsLeft': integer | Infinity - returns number of calls left
|
||||
* before limit is reached
|
||||
* }
|
||||
* If multiple rules match, the least number of invocations left is returned.
|
||||
* If the rate limit has been reached, the longest timeToReset is returned.
|
||||
*/
|
||||
* Checks if this input has exceeded any rate limits.
|
||||
* @param {object} input dictionary containing key-value pairs of attributes
|
||||
* that match to rules
|
||||
* @return {object} Returns object of following structure
|
||||
* { 'allowed': boolean - is this input allowed
|
||||
* 'timeToReset': integer | Infinity - returns time until counters are reset
|
||||
* in milliseconds
|
||||
* 'numInvocationsLeft': integer | Infinity - returns number of calls left
|
||||
* before limit is reached
|
||||
* }
|
||||
* If multiple rules match, the least number of invocations left is returned.
|
||||
* If the rate limit has been reached, the longest timeToReset is returned.
|
||||
*/
|
||||
check(input) {
|
||||
const reply = {
|
||||
allowed: true,
|
||||
@@ -185,8 +183,7 @@ class RateLimiter {
|
||||
if (ruleResult.timeToNextReset < 0) {
|
||||
// Reset all the counters since the rule has reset
|
||||
rule.resetCounter();
|
||||
ruleResult.timeSinceLastReset = new Date().getTime() -
|
||||
rule._lastResetTime;
|
||||
ruleResult.timeSinceLastReset = new Date().getTime() - rule._lastResetTime;
|
||||
ruleResult.timeToNextReset = rule.options.intervalTime;
|
||||
numInvocations = 0;
|
||||
}
|
||||
@@ -206,11 +203,12 @@ class RateLimiter {
|
||||
} else {
|
||||
// If this is an allowed attempt and we haven't failed on any of the
|
||||
// other rules that match, update the reply field.
|
||||
if (rule.options.numRequestsAllowed - numInvocations <
|
||||
reply.numInvocationsLeft && reply.allowed) {
|
||||
if (
|
||||
rule.options.numRequestsAllowed - numInvocations < reply.numInvocationsLeft &&
|
||||
reply.allowed
|
||||
) {
|
||||
reply.timeToReset = ruleResult.timeToNextReset;
|
||||
reply.numInvocationsLeft = rule.options.numRequestsAllowed -
|
||||
numInvocations;
|
||||
reply.numInvocationsLeft = rule.options.numRequestsAllowed - numInvocations;
|
||||
}
|
||||
reply.ruleId = rule.id;
|
||||
rule._executeCallback(reply, input);
|
||||
@@ -218,31 +216,31 @@ class RateLimiter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a rule to dictionary of rules that are checked against on every call.
|
||||
* Only inputs that pass all of the rules will be allowed. Returns unique rule
|
||||
* id that can be passed to `removeRule`.
|
||||
* @param {object} rule Input dictionary defining certain attributes and
|
||||
* rules associated with them.
|
||||
* Each attribute's value can either be a value, a function or null. All
|
||||
* functions must return a boolean of whether the input is matched by that
|
||||
* attribute's rule or not
|
||||
* @param {integer} numRequestsAllowed Optional. Number of events allowed per
|
||||
* interval. Default = 10.
|
||||
* @param {integer} intervalTime Optional. Number of milliseconds before
|
||||
* rule's counters are reset. Default = 1000.
|
||||
* @param {function} callback Optional. Function to be called after a
|
||||
* rule is executed. Two objects will be passed to this function.
|
||||
* The first one is the result of RateLimiter.prototype.check
|
||||
* The second is the input object of the rule, it has the following structure:
|
||||
* {
|
||||
* 'type': string - either 'method' or 'subscription'
|
||||
* 'name': string - the name of the method or subscription being called
|
||||
* 'userId': string - the user ID attempting the method or subscription
|
||||
* 'connectionId': string - a string representing the user's DDP connection
|
||||
* 'clientAddress': string - the IP address of the user
|
||||
* }
|
||||
* @return {string} Returns unique rule id
|
||||
*/
|
||||
* Adds a rule to dictionary of rules that are checked against on every call.
|
||||
* Only inputs that pass all of the rules will be allowed. Returns unique rule
|
||||
* id that can be passed to `removeRule`.
|
||||
* @param {object} rule Input dictionary defining certain attributes and
|
||||
* rules associated with them.
|
||||
* Each attribute's value can either be a value, a function or null. All
|
||||
* functions must return a boolean of whether the input is matched by that
|
||||
* attribute's rule or not
|
||||
* @param {integer} numRequestsAllowed Optional. Number of events allowed per
|
||||
* interval. Default = 10.
|
||||
* @param {integer} intervalTime Optional. Number of milliseconds before
|
||||
* rule's counters are reset. Default = 1000.
|
||||
* @param {function} callback Optional. Function to be called after a
|
||||
* rule is executed. Two objects will be passed to this function.
|
||||
* The first one is the result of RateLimiter.prototype.check
|
||||
* The second is the input object of the rule, it has the following structure:
|
||||
* {
|
||||
* 'type': string - either 'method' or 'subscription'
|
||||
* 'name': string - the name of the method or subscription being called
|
||||
* 'userId': string - the user ID attempting the method or subscription
|
||||
* 'connectionId': string - a string representing the user's DDP connection
|
||||
* 'clientAddress': string - the IP address of the user
|
||||
* }
|
||||
* @return {string} Returns unique rule id
|
||||
*/
|
||||
addRule(rule, numRequestsAllowed, intervalTime, callback) {
|
||||
const options = {
|
||||
numRequestsAllowed: numRequestsAllowed || DEFAULT_REQUESTS_PER_INTERVAL,
|
||||
@@ -256,10 +254,10 @@ class RateLimiter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment counters in every rule that match to this input
|
||||
* @param {object} input Dictionary object containing attributes that may
|
||||
* match to rules
|
||||
*/
|
||||
* Increment counters in every rule that match to this input
|
||||
* @param {object} input Dictionary object containing attributes that may
|
||||
* match to rules
|
||||
*/
|
||||
increment(input) {
|
||||
// Only increment rule counters that match this input
|
||||
const matchedRules = this._findAllMatchingRules(input);
|
||||
@@ -268,11 +266,11 @@ class RateLimiter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment counters in every rule that match to this input
|
||||
* @param {array} rules Array of rules to increment
|
||||
* @param {object} input Dictionary object containing attributes that may
|
||||
* match to rules
|
||||
*/
|
||||
* Increment counters in every rule that match to this input
|
||||
* @param {array} rules Array of rules to increment
|
||||
* @param {object} input Dictionary object containing attributes that may
|
||||
* match to rules
|
||||
*/
|
||||
incrementRules(rules, input) {
|
||||
const _incrementForInput = (rule) => this._incrementRule(rule, input);
|
||||
rules.forEach(_incrementForInput);
|
||||
@@ -297,7 +295,7 @@ class RateLimiter {
|
||||
|
||||
// Returns an array of all rules that apply to provided input
|
||||
_findAllMatchingRules(input) {
|
||||
return Object.values(this.rules).filter(rule => rule.match(input));
|
||||
return Object.values(this.rules).filter((rule) => rule.match(input));
|
||||
}
|
||||
|
||||
async _findAllMatchingRulesAsync(input) {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
Package.describe({
|
||||
summary: "Retry logic with exponential backoff",
|
||||
version: '1.1.1',
|
||||
version: "1.1.1",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.use('random');
|
||||
api.mainModule('retry.js');
|
||||
api.export('Retry');
|
||||
api.use("ecmascript");
|
||||
api.use("random");
|
||||
api.mainModule("retry.js");
|
||||
api.export("Retry");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use(["ecmascript", "tinytest", "random", "retry"]);
|
||||
api.addFiles("retry-tests.js", ["client", "server"]);
|
||||
});
|
||||
|
||||
108
packages/retry/retry-tests.js
Normal file
108
packages/retry/retry-tests.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Retry } from "./retry.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
Tinytest.add("retry - retryLater with count < minCount returns minTimeout", (test) => {
|
||||
const retry = new Retry({ minCount: 3, minTimeout: 42 });
|
||||
try {
|
||||
test.equal(retry.retryLater(0, noop), 42);
|
||||
retry.clear();
|
||||
test.equal(retry.retryLater(1, noop), 42);
|
||||
retry.clear();
|
||||
test.equal(retry.retryLater(2, noop), 42);
|
||||
} finally {
|
||||
retry.clear();
|
||||
}
|
||||
});
|
||||
|
||||
Tinytest.add("retry - retryLater with count >= minCount respects fuzz bounds", (test) => {
|
||||
const retry = new Retry({
|
||||
baseTimeout: 1000,
|
||||
exponent: 2,
|
||||
fuzz: 0.5,
|
||||
minCount: 0,
|
||||
maxTimeout: 1e9,
|
||||
});
|
||||
|
||||
// Unfuzzed value at count=2 is 1000 * 2^2 = 4000.
|
||||
// Fuzz factor: random * 0.5 + 0.75 ∈ [0.75, 1.25].
|
||||
// Admissible window: [3000, 5000].
|
||||
try {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const timeout = retry.retryLater(2, noop);
|
||||
retry.clear();
|
||||
test.isTrue(
|
||||
timeout >= 3000 && timeout <= 5000,
|
||||
`sample ${i}: expected [3000, 5000], got ${timeout}`,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
retry.clear();
|
||||
}
|
||||
});
|
||||
|
||||
Tinytest.add("retry - retryLater caps at maxTimeout", (test) => {
|
||||
const retry = new Retry({
|
||||
maxTimeout: 100,
|
||||
baseTimeout: 1000,
|
||||
exponent: 10,
|
||||
fuzz: 0,
|
||||
minCount: 0,
|
||||
});
|
||||
try {
|
||||
// Unfuzzed value at count=5 is 1000 * 10^5 = 1e8, capped to 100.
|
||||
// With fuzz=0: factor is (random * 0 + 1) = 1. Result: 100.
|
||||
test.equal(retry.retryLater(5, noop), 100);
|
||||
} finally {
|
||||
retry.clear();
|
||||
}
|
||||
});
|
||||
|
||||
Tinytest.addAsync("retry - retryLater actually fires the callback", async (test) => {
|
||||
const retry = new Retry({ minCount: 1, minTimeout: 1, fuzz: 0 });
|
||||
await new Promise((resolve) => {
|
||||
retry.retryLater(0, resolve);
|
||||
});
|
||||
test.ok();
|
||||
retry.clear();
|
||||
});
|
||||
|
||||
Tinytest.addAsync("retry - clear cancels pending callback", async (test) => {
|
||||
const retry = new Retry({ minCount: 1, minTimeout: 1000 });
|
||||
let fired = false;
|
||||
retry.retryLater(0, () => {
|
||||
fired = true;
|
||||
});
|
||||
retry.clear();
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
test.isFalse(fired, "cancelled callback should not fire");
|
||||
});
|
||||
|
||||
Tinytest.addAsync("retry - retryLater replaces pending timer", async (test) => {
|
||||
const retry = new Retry({ minCount: 1, minTimeout: 1, fuzz: 0 });
|
||||
let firedFirst = false;
|
||||
let firedSecond = false;
|
||||
|
||||
retry.retryLater(0, () => {
|
||||
firedFirst = true;
|
||||
});
|
||||
retry.retryLater(0, () => {
|
||||
firedSecond = true;
|
||||
});
|
||||
|
||||
// Wait long enough that both timers would have fired if not cancelled.
|
||||
await new Promise((resolve) => setTimeout(resolve, 30));
|
||||
|
||||
test.isFalse(firedFirst, "first callback should have been replaced");
|
||||
test.isTrue(firedSecond, "second callback should have fired");
|
||||
retry.clear();
|
||||
});
|
||||
|
||||
Tinytest.add("retry - clear is idempotent", (test) => {
|
||||
const retry = new Retry();
|
||||
retry.clear();
|
||||
retry.clear();
|
||||
retry.clear();
|
||||
test.ok();
|
||||
});
|
||||
@@ -45,21 +45,17 @@ export class Retry {
|
||||
|
||||
// fuzz the timeout randomly, to avoid reconnect storms when a
|
||||
// server goes down.
|
||||
var timeout = Math.min(
|
||||
this.maxTimeout,
|
||||
this.baseTimeout * Math.pow(this.exponent, count)
|
||||
) * (
|
||||
Random.fraction() * this.fuzz + (1 - this.fuzz / 2)
|
||||
);
|
||||
const timeout =
|
||||
Math.min(this.maxTimeout, this.baseTimeout * Math.pow(this.exponent, count)) *
|
||||
(Random.fraction() * this.fuzz + (1 - this.fuzz / 2));
|
||||
|
||||
return timeout;
|
||||
}
|
||||
|
||||
// Call `fn` after a delay, based on the `count` of which retry this is.
|
||||
retryLater(count, fn) {
|
||||
var timeout = this._timeout(count);
|
||||
if (this.retryTimer)
|
||||
clearTimeout(this.retryTimer);
|
||||
const timeout = this._timeout(count);
|
||||
if (this.retryTimer) clearTimeout(this.retryTimer);
|
||||
this.retryTimer = Meteor.setTimeout(fn, timeout);
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
Package.describe({
|
||||
summary: "Session variable",
|
||||
version: '1.2.2',
|
||||
version: "1.2.2",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use(['ecmascript', 'reactive-dict'], 'client');
|
||||
api.use(["ecmascript", "reactive-dict"], "client");
|
||||
|
||||
// Session can work with or without reload, but if reload is present
|
||||
// it should load first so we can detect it at startup and populate
|
||||
// the session.
|
||||
api.use('reload', 'client', {weak: true});
|
||||
api.use("reload", "client", { weak: true });
|
||||
|
||||
api.export('Session', 'client');
|
||||
api.mainModule('session.js', 'client');
|
||||
api.addAssets('session.d.ts', 'server');
|
||||
api.export("Session", "client");
|
||||
api.mainModule("session.js", "client");
|
||||
api.addAssets("session.d.ts", "server");
|
||||
});
|
||||
|
||||
Package.onTest(function (api) {
|
||||
api.use('ecmascript');
|
||||
api.use('tinytest');
|
||||
api.use('session', 'client');
|
||||
api.use('tracker');
|
||||
api.use('mongo');
|
||||
api.addFiles('session_tests.js', 'client');
|
||||
api.use("ecmascript");
|
||||
api.use("tinytest");
|
||||
api.use("session", "client");
|
||||
api.use("reactive-dict", "client");
|
||||
api.use("tracker");
|
||||
api.use("mongo");
|
||||
api.addFiles("session_tests.js", "client");
|
||||
});
|
||||
|
||||
2
packages/session/session.d.ts
vendored
2
packages/session/session.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { EJSONable } from 'meteor/ejson';
|
||||
import { EJSONable } from "meteor/ejson";
|
||||
|
||||
export namespace Session {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactiveDict } from 'meteor/reactive-dict';
|
||||
import { ReactiveDict } from "meteor/reactive-dict";
|
||||
|
||||
export const Session = new ReactiveDict('session');
|
||||
export const Session = new ReactiveDict("session");
|
||||
|
||||
// Documentation here is really awkward because the methods are defined
|
||||
// elsewhere
|
||||
|
||||
@@ -1,194 +1,240 @@
|
||||
import { Session } from 'meteor/session';
|
||||
import { Session } from "meteor/session";
|
||||
import { ReactiveDict } from "meteor/reactive-dict";
|
||||
|
||||
Tinytest.add('session - setDefault', function (test) {
|
||||
Session.setDefault('def', "argyle");
|
||||
test.equal(Session.get('def'), "argyle");
|
||||
Session.set('def', "noodle");
|
||||
test.equal(Session.get('def'), "noodle");
|
||||
Session.set('nondef', "potato");
|
||||
test.equal(Session.get('nondef'), "potato");
|
||||
Session.setDefault('nondef', "eggs");
|
||||
test.equal(Session.get('nondef'), "potato");
|
||||
Tinytest.add("session - setDefault", function (test) {
|
||||
Session.setDefault("def", "argyle");
|
||||
test.equal(Session.get("def"), "argyle");
|
||||
Session.set("def", "noodle");
|
||||
test.equal(Session.get("def"), "noodle");
|
||||
Session.set("nondef", "potato");
|
||||
test.equal(Session.get("nondef"), "potato");
|
||||
Session.setDefault("nondef", "eggs");
|
||||
test.equal(Session.get("nondef"), "potato");
|
||||
// This is so the test passes the next time, after hot code push. I know it
|
||||
// doesn't return it to the completely untouched state, but we don't have
|
||||
// Session.clear() yet. When we do, this should be that.
|
||||
delete Session.keys['def'];
|
||||
delete Session.keys['nondef'];
|
||||
delete Session.keys["def"];
|
||||
delete Session.keys["nondef"];
|
||||
});
|
||||
|
||||
Tinytest.add('session - get/set/equals types', function (test) {
|
||||
test.equal(Session.get('u'), undefined);
|
||||
test.isTrue(Session.equals('u', undefined));
|
||||
test.isFalse(Session.equals('u', null));
|
||||
test.isFalse(Session.equals('u', 0));
|
||||
test.isFalse(Session.equals('u', ''));
|
||||
Tinytest.add("session - get/set/equals types", function (test) {
|
||||
test.equal(Session.get("u"), undefined);
|
||||
test.isTrue(Session.equals("u", undefined));
|
||||
test.isFalse(Session.equals("u", null));
|
||||
test.isFalse(Session.equals("u", 0));
|
||||
test.isFalse(Session.equals("u", ""));
|
||||
|
||||
Session.set('u', undefined);
|
||||
test.equal(Session.get('u'), undefined);
|
||||
test.isTrue(Session.equals('u', undefined));
|
||||
test.isFalse(Session.equals('u', null));
|
||||
test.isFalse(Session.equals('u', 0));
|
||||
test.isFalse(Session.equals('u', ''));
|
||||
test.isFalse(Session.equals('u', 'undefined'));
|
||||
test.isFalse(Session.equals('u', 'null'));
|
||||
Session.set("u", undefined);
|
||||
test.equal(Session.get("u"), undefined);
|
||||
test.isTrue(Session.equals("u", undefined));
|
||||
test.isFalse(Session.equals("u", null));
|
||||
test.isFalse(Session.equals("u", 0));
|
||||
test.isFalse(Session.equals("u", ""));
|
||||
test.isFalse(Session.equals("u", "undefined"));
|
||||
test.isFalse(Session.equals("u", "null"));
|
||||
|
||||
Session.set('n', null);
|
||||
test.equal(Session.get('n'), null);
|
||||
test.isFalse(Session.equals('n', undefined));
|
||||
test.isTrue(Session.equals('n', null));
|
||||
test.isFalse(Session.equals('n', 0));
|
||||
test.isFalse(Session.equals('n', ''));
|
||||
test.isFalse(Session.equals('n', 'undefined'));
|
||||
test.isFalse(Session.equals('n', 'null'));
|
||||
Session.set("n", null);
|
||||
test.equal(Session.get("n"), null);
|
||||
test.isFalse(Session.equals("n", undefined));
|
||||
test.isTrue(Session.equals("n", null));
|
||||
test.isFalse(Session.equals("n", 0));
|
||||
test.isFalse(Session.equals("n", ""));
|
||||
test.isFalse(Session.equals("n", "undefined"));
|
||||
test.isFalse(Session.equals("n", "null"));
|
||||
|
||||
Session.set('t', true);
|
||||
test.equal(Session.get('t'), true);
|
||||
test.isTrue(Session.equals('t', true));
|
||||
test.isFalse(Session.equals('t', false));
|
||||
test.isFalse(Session.equals('t', 1));
|
||||
test.isFalse(Session.equals('t', 'true'));
|
||||
Session.set("t", true);
|
||||
test.equal(Session.get("t"), true);
|
||||
test.isTrue(Session.equals("t", true));
|
||||
test.isFalse(Session.equals("t", false));
|
||||
test.isFalse(Session.equals("t", 1));
|
||||
test.isFalse(Session.equals("t", "true"));
|
||||
|
||||
Session.set('f', false);
|
||||
test.equal(Session.get('f'), false);
|
||||
test.isFalse(Session.equals('f', true));
|
||||
test.isTrue(Session.equals('f', false));
|
||||
test.isFalse(Session.equals('f', 1));
|
||||
test.isFalse(Session.equals('f', 'false'));
|
||||
Session.set("f", false);
|
||||
test.equal(Session.get("f"), false);
|
||||
test.isFalse(Session.equals("f", true));
|
||||
test.isTrue(Session.equals("f", false));
|
||||
test.isFalse(Session.equals("f", 1));
|
||||
test.isFalse(Session.equals("f", "false"));
|
||||
|
||||
Session.set('num', 0);
|
||||
test.equal(Session.get('num'), 0);
|
||||
test.isTrue(Session.equals('num', 0));
|
||||
test.isFalse(Session.equals('num', false));
|
||||
test.isFalse(Session.equals('num', '0'));
|
||||
test.isFalse(Session.equals('num', 1));
|
||||
Session.set("num", 0);
|
||||
test.equal(Session.get("num"), 0);
|
||||
test.isTrue(Session.equals("num", 0));
|
||||
test.isFalse(Session.equals("num", false));
|
||||
test.isFalse(Session.equals("num", "0"));
|
||||
test.isFalse(Session.equals("num", 1));
|
||||
|
||||
Session.set('str', 'true');
|
||||
test.equal(Session.get('str'), 'true');
|
||||
test.isTrue(Session.equals('str', 'true'));
|
||||
test.isFalse(Session.equals('str', true));
|
||||
Session.set("str", "true");
|
||||
test.equal(Session.get("str"), "true");
|
||||
test.isTrue(Session.equals("str", "true"));
|
||||
test.isFalse(Session.equals("str", true));
|
||||
|
||||
Session.set('arr', [1, 2, {a: 1, b: [5, 6]}]);
|
||||
test.equal(Session.get('arr'), [1, 2, {b: [5, 6], a: 1}]);
|
||||
test.isFalse(Session.equals('arr', 1));
|
||||
test.isFalse(Session.equals('arr', '[1,2,{"a":1,"b":[5,6]}]'));
|
||||
Session.set("arr", [1, 2, { a: 1, b: [5, 6] }]);
|
||||
test.equal(Session.get("arr"), [1, 2, { b: [5, 6], a: 1 }]);
|
||||
test.isFalse(Session.equals("arr", 1));
|
||||
test.isFalse(Session.equals("arr", '[1,2,{"a":1,"b":[5,6]}]'));
|
||||
test.throws(function () {
|
||||
Session.equals('arr', [1, 2, {a: 1, b: [5, 6]}]);
|
||||
Session.equals("arr", [1, 2, { a: 1, b: [5, 6] }]);
|
||||
});
|
||||
|
||||
Session.set('obj', {a: 1, b: [5, 6]});
|
||||
test.equal(Session.get('obj'), {b: [5, 6], a: 1});
|
||||
test.isFalse(Session.equals('obj', 1));
|
||||
test.isFalse(Session.equals('obj', '{"a":1,"b":[5,6]}'));
|
||||
test.throws(function() { Session.equals('obj', {a: 1, b: [5, 6]}); });
|
||||
Session.set("obj", { a: 1, b: [5, 6] });
|
||||
test.equal(Session.get("obj"), { b: [5, 6], a: 1 });
|
||||
test.isFalse(Session.equals("obj", 1));
|
||||
test.isFalse(Session.equals("obj", '{"a":1,"b":[5,6]}'));
|
||||
test.throws(function () {
|
||||
Session.equals("obj", { a: 1, b: [5, 6] });
|
||||
});
|
||||
|
||||
Session.set("date", new Date(1234));
|
||||
test.equal(Session.get("date"), new Date(1234));
|
||||
test.isFalse(Session.equals("date", new Date(3455)));
|
||||
test.isTrue(Session.equals("date", new Date(1234)));
|
||||
|
||||
Session.set('date', new Date(1234));
|
||||
test.equal(Session.get('date'), new Date(1234));
|
||||
test.isFalse(Session.equals('date', new Date(3455)));
|
||||
test.isTrue(Session.equals('date', new Date(1234)));
|
||||
|
||||
Session.set('oid', new Mongo.ObjectID('ffffffffffffffffffffffff'));
|
||||
test.equal(Session.get('oid'), new Mongo.ObjectID('ffffffffffffffffffffffff'));
|
||||
test.isFalse(Session.equals('oid', new Mongo.ObjectID('fffffffffffffffffffffffa')));
|
||||
test.isTrue(Session.equals('oid', new Mongo.ObjectID('ffffffffffffffffffffffff')));
|
||||
Session.set("oid", new Mongo.ObjectID("ffffffffffffffffffffffff"));
|
||||
test.equal(Session.get("oid"), new Mongo.ObjectID("ffffffffffffffffffffffff"));
|
||||
test.isFalse(Session.equals("oid", new Mongo.ObjectID("fffffffffffffffffffffffa")));
|
||||
test.isTrue(Session.equals("oid", new Mongo.ObjectID("ffffffffffffffffffffffff")));
|
||||
});
|
||||
|
||||
Tinytest.add('session - objects are cloned', function (test) {
|
||||
Session.set('frozen-array', [1, 2, 3]);
|
||||
Session.get('frozen-array')[1] = 42;
|
||||
test.equal(Session.get('frozen-array'), [1, 2, 3]);
|
||||
Tinytest.add("session - objects are cloned", function (test) {
|
||||
Session.set("frozen-array", [1, 2, 3]);
|
||||
Session.get("frozen-array")[1] = 42;
|
||||
test.equal(Session.get("frozen-array"), [1, 2, 3]);
|
||||
|
||||
Session.set('frozen-object', {a: 1, b: 2});
|
||||
Session.get('frozen-object').a = 43;
|
||||
test.equal(Session.get('frozen-object'), {a: 1, b: 2});
|
||||
Session.set("frozen-object", { a: 1, b: 2 });
|
||||
Session.get("frozen-object").a = 43;
|
||||
test.equal(Session.get("frozen-object"), { a: 1, b: 2 });
|
||||
});
|
||||
|
||||
Tinytest.add('session - context invalidation for get', function (test) {
|
||||
var xGetExecutions = 0;
|
||||
Tinytest.add("session - context invalidation for get", function (test) {
|
||||
let xGetExecutions = 0;
|
||||
Tracker.autorun(function () {
|
||||
++xGetExecutions;
|
||||
Session.get('x');
|
||||
Session.get("x");
|
||||
});
|
||||
test.equal(xGetExecutions, 1);
|
||||
Session.set('x', 1);
|
||||
Session.set("x", 1);
|
||||
// Invalidation shouldn't happen until flush time.
|
||||
test.equal(xGetExecutions, 1);
|
||||
Tracker.flush();
|
||||
test.equal(xGetExecutions, 2);
|
||||
// Setting to the same value doesn't re-run.
|
||||
Session.set('x', 1);
|
||||
Session.set("x", 1);
|
||||
Tracker.flush();
|
||||
test.equal(xGetExecutions, 2);
|
||||
Session.set('x', '1');
|
||||
Session.set("x", "1");
|
||||
Tracker.flush();
|
||||
test.equal(xGetExecutions, 3);
|
||||
});
|
||||
|
||||
Tinytest.add('session - context invalidation for equals', function (test) {
|
||||
var xEqualsExecutions = 0;
|
||||
Tinytest.add("session - context invalidation for equals", function (test) {
|
||||
let xEqualsExecutions = 0;
|
||||
Tracker.autorun(function () {
|
||||
++xEqualsExecutions;
|
||||
Session.equals('x', 5);
|
||||
Session.equals("x", 5);
|
||||
});
|
||||
test.equal(xEqualsExecutions, 1);
|
||||
Session.set('x', 1);
|
||||
Session.set("x", 1);
|
||||
Tracker.flush();
|
||||
// Changing undefined -> 1 shouldn't affect equals(5).
|
||||
test.equal(xEqualsExecutions, 1);
|
||||
Session.set('x', 5);
|
||||
Session.set("x", 5);
|
||||
// Invalidation shouldn't happen until flush time.
|
||||
test.equal(xEqualsExecutions, 1);
|
||||
Tracker.flush();
|
||||
test.equal(xEqualsExecutions, 2);
|
||||
Session.set('x', 5);
|
||||
Session.set("x", 5);
|
||||
Tracker.flush();
|
||||
// Setting to the same value doesn't re-run.
|
||||
test.equal(xEqualsExecutions, 2);
|
||||
Session.set('x', '5');
|
||||
Session.set("x", "5");
|
||||
test.equal(xEqualsExecutions, 2);
|
||||
Tracker.flush();
|
||||
test.equal(xEqualsExecutions, 3);
|
||||
Session.set('x', 5);
|
||||
Session.set("x", 5);
|
||||
test.equal(xEqualsExecutions, 3);
|
||||
Tracker.flush();
|
||||
test.equal(xEqualsExecutions, 4);
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
'session - context invalidation for equals with undefined',
|
||||
function (test) {
|
||||
// Make sure the special casing for equals undefined works.
|
||||
var yEqualsExecutions = 0;
|
||||
Tracker.autorun(function () {
|
||||
++yEqualsExecutions;
|
||||
Session.equals('y', undefined);
|
||||
});
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Session.set('y', undefined);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Session.set('y', 5);
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set('y', 3);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set('y', 'undefined');
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set('y', undefined);
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 3);
|
||||
Tinytest.add("session - context invalidation for equals with undefined", function (test) {
|
||||
// Make sure the special casing for equals undefined works.
|
||||
let yEqualsExecutions = 0;
|
||||
Tracker.autorun(function () {
|
||||
++yEqualsExecutions;
|
||||
Session.equals("y", undefined);
|
||||
});
|
||||
|
||||
Tinytest.add('session - parse an object of key/value pairs', function (test) {
|
||||
Session._setObject({fruit: 'apple', vegetable: 'potato'});
|
||||
|
||||
test.equal(Session.get('fruit'), 'apple');
|
||||
test.equal(Session.get('vegetable'), 'potato');
|
||||
|
||||
delete Session.keys['fruit'];
|
||||
delete Session.keys['vegetable'];
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Session.set("y", undefined);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Session.set("y", 5);
|
||||
test.equal(yEqualsExecutions, 1);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set("y", 3);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set("y", "undefined");
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Session.set("y", undefined);
|
||||
test.equal(yEqualsExecutions, 2);
|
||||
Tracker.flush();
|
||||
test.equal(yEqualsExecutions, 3);
|
||||
});
|
||||
|
||||
Tinytest.add("session - parse an object of key/value pairs", function (test) {
|
||||
Session._setObject({ fruit: "apple", vegetable: "potato" });
|
||||
|
||||
test.equal(Session.get("fruit"), "apple");
|
||||
test.equal(Session.get("vegetable"), "potato");
|
||||
|
||||
delete Session.keys["fruit"];
|
||||
delete Session.keys["vegetable"];
|
||||
});
|
||||
|
||||
Tinytest.add("session - Session is a ReactiveDict instance", function (test) {
|
||||
test.instanceOf(Session, ReactiveDict);
|
||||
});
|
||||
|
||||
Tinytest.add("session - _setObject triggers reactive invalidation", function (test) {
|
||||
let runs = 0;
|
||||
let lastFruit;
|
||||
Tracker.autorun(function () {
|
||||
runs++;
|
||||
lastFruit = Session.get("so-fruit");
|
||||
});
|
||||
test.equal(runs, 1);
|
||||
test.equal(lastFruit, undefined);
|
||||
|
||||
Session._setObject({ "so-fruit": "apple", "so-veggie": "potato" });
|
||||
Tracker.flush();
|
||||
test.equal(runs, 2);
|
||||
test.equal(lastFruit, "apple");
|
||||
|
||||
// Setting again with the same value should not re-run.
|
||||
Session._setObject({ "so-fruit": "apple" });
|
||||
Tracker.flush();
|
||||
test.equal(runs, 2);
|
||||
|
||||
delete Session.keys["so-fruit"];
|
||||
delete Session.keys["so-veggie"];
|
||||
});
|
||||
|
||||
Tinytest.add("session - setDefault on an already-set key does not invalidate", function (test) {
|
||||
Session.set("sd-key", "original");
|
||||
let runs = 0;
|
||||
Tracker.autorun(function () {
|
||||
runs++;
|
||||
Session.get("sd-key");
|
||||
});
|
||||
test.equal(runs, 1);
|
||||
|
||||
// Key already exists — setDefault should be a no-op and NOT invalidate.
|
||||
Session.setDefault("sd-key", "fallback");
|
||||
Tracker.flush();
|
||||
test.equal(runs, 1);
|
||||
test.equal(Session.get("sd-key"), "original");
|
||||
|
||||
delete Session.keys["sd-key"];
|
||||
});
|
||||
|
||||
@@ -11,104 +11,97 @@ TEST_STATUS = {
|
||||
DONE: false,
|
||||
FAILURES: 0,
|
||||
PASSED: null,
|
||||
WHERE_FAILED: []
|
||||
WHERE_FAILED: [],
|
||||
};
|
||||
|
||||
// xUnit format uses XML output
|
||||
var XML_CHAR_MAP = {
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
const XML_CHAR_MAP = {
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"&": "&",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
|
||||
// Escapes a string for insertion into XML
|
||||
var escapeXml = function (s) {
|
||||
const escapeXml = function (s) {
|
||||
return s.replace(/[<>&"']/g, function (c) {
|
||||
return XML_CHAR_MAP[c];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a human name for a test
|
||||
var getName = function (result) {
|
||||
return (result.server ? "S: " : "C: ") +
|
||||
result.groupPath.join(" - ") + " - " + result.test;
|
||||
const getName = function (result) {
|
||||
return `${result.server ? "S: " : "C: "}${result.groupPath.join(" - ")} - ${result.test}`;
|
||||
};
|
||||
|
||||
// Calls console.log, but returns silently if console.log is not available
|
||||
var log = function (/*arguments*/) {
|
||||
if (typeof console !== 'undefined') {
|
||||
const log = function (/*arguments*/) {
|
||||
if (typeof console !== "undefined") {
|
||||
console.log.apply(console, arguments);
|
||||
}
|
||||
};
|
||||
|
||||
var MAGIC_PREFIX = '##_meteor_magic##';
|
||||
const MAGIC_PREFIX = "##_meteor_magic##";
|
||||
// Write output so that other tools can read it
|
||||
// Output is sent to console.log, prefixed with the magic prefix and then the facility
|
||||
// By grepping for the prefix, other tools can get the 'special' output
|
||||
var logMagic = function (facility, s) {
|
||||
log(MAGIC_PREFIX + facility + ': ' + s);
|
||||
const logMagic = function (facility, s) {
|
||||
log(`${MAGIC_PREFIX}${facility}: ${s}`);
|
||||
};
|
||||
|
||||
// Logs xUnit output, if xunit output is enabled
|
||||
// This uses logMagic with a facility of xunit
|
||||
var xunit = function (s) {
|
||||
const xunit = function (s) {
|
||||
if (xunitEnabled) {
|
||||
logMagic('xunit', s);
|
||||
logMagic("xunit", s);
|
||||
}
|
||||
};
|
||||
|
||||
var passed = 0;
|
||||
var failed = 0;
|
||||
var whereFailed = [];
|
||||
var expected = 0;
|
||||
var resultSet = {};
|
||||
var toReport = [];
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const whereFailed = [];
|
||||
let expected = 0;
|
||||
const resultSet = {};
|
||||
let toReport = [];
|
||||
|
||||
var hrefPath = window.location.href.split("/");
|
||||
var platform = decodeURIComponent(hrefPath.length && hrefPath[hrefPath.length - 1]);
|
||||
if (!platform)
|
||||
platform = "local";
|
||||
const hrefPath = window.location.href.split("/");
|
||||
let platform = decodeURIComponent(hrefPath.length && hrefPath[hrefPath.length - 1]);
|
||||
if (!platform) platform = "local";
|
||||
|
||||
// We enable xUnit output when platform is xunit
|
||||
var xunitEnabled = (platform == 'xunit');
|
||||
const xunitEnabled = platform == "xunit";
|
||||
|
||||
var doReport = Meteor &&
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.runId;
|
||||
var report = function (name, last) {
|
||||
const doReport =
|
||||
Meteor && Meteor.settings && Meteor.settings.public && Meteor.settings.public.runId;
|
||||
const report = function (name, last) {
|
||||
if (doReport) {
|
||||
var data = {
|
||||
const data = {
|
||||
run_id: Meteor.settings.public.runId,
|
||||
testPath: resultSet[name].testPath,
|
||||
status: resultSet[name].status,
|
||||
platform: platform,
|
||||
server: resultSet[name].server,
|
||||
fullName: name.substr(3)
|
||||
fullName: name.substr(3),
|
||||
};
|
||||
if ((data.status === "FAIL" || data.status === "EXPECTED") &&
|
||||
!(Object.keys(resultSet[name].events).length === 0)) {
|
||||
if (
|
||||
(data.status === "FAIL" || data.status === "EXPECTED") &&
|
||||
!(Object.keys(resultSet[name].events).length === 0)
|
||||
) {
|
||||
// only send events when bad things happen
|
||||
data.events = resultSet[name].events;
|
||||
}
|
||||
if (last)
|
||||
data.end = new Date();
|
||||
else
|
||||
data.start = new Date();
|
||||
if (last) data.end = new Date();
|
||||
else data.start = new Date();
|
||||
toReport.push(EJSON.toJSONValue(data));
|
||||
}
|
||||
};
|
||||
var sendReports = function (callback) {
|
||||
var reports = toReport;
|
||||
if (!callback)
|
||||
callback = function () {};
|
||||
const sendReports = function (callback) {
|
||||
const reports = toReport;
|
||||
if (!callback) callback = function () {};
|
||||
toReport = [];
|
||||
if (doReport)
|
||||
Meteor.call("report", reports, callback);
|
||||
else
|
||||
callback();
|
||||
if (doReport) Meteor.call("report", reports, callback);
|
||||
else callback();
|
||||
};
|
||||
|
||||
runTests = function () {
|
||||
@@ -117,9 +110,9 @@ runTests = function () {
|
||||
|
||||
Tinytest._runTestsEverywhere(
|
||||
function (results) {
|
||||
var name = getName(results);
|
||||
const name = getName(results);
|
||||
if (!(name in resultSet)) {
|
||||
var testPath = EJSON.clone(results.groupPath);
|
||||
const testPath = EJSON.clone(results.groupPath);
|
||||
testPath.push(results.test);
|
||||
resultSet[name] = {
|
||||
name: name,
|
||||
@@ -127,7 +120,7 @@ runTests = function () {
|
||||
events: [],
|
||||
server: !!results.server,
|
||||
testPath: testPath,
|
||||
test: results.test
|
||||
test: results.test,
|
||||
};
|
||||
report(name, false);
|
||||
}
|
||||
@@ -136,51 +129,48 @@ runTests = function () {
|
||||
results.events.forEach(function (event) {
|
||||
resultSet[name].events.push(event);
|
||||
switch (event.type) {
|
||||
case "ok":
|
||||
break;
|
||||
case "expected_fail":
|
||||
if (resultSet[name].status !== "FAIL")
|
||||
resultSet[name].status = "EXPECTED";
|
||||
break;
|
||||
case "exception":
|
||||
log(name, ":", "!!!!!!!!! FAIL !!!!!!!!!!!");
|
||||
if (event.details && event.details.stack)
|
||||
log(event.details.stack);
|
||||
else
|
||||
log("Test failed with exception");
|
||||
failed++;
|
||||
whereFailed.push({ name: name, info: JSON.stringify(event) });
|
||||
break;
|
||||
case "finish":
|
||||
switch (resultSet[name].status) {
|
||||
case "OK":
|
||||
case "ok":
|
||||
break;
|
||||
case "PENDING":
|
||||
resultSet[name].status = "OK";
|
||||
report(name, true);
|
||||
log(name, ":", "OK");
|
||||
passed++;
|
||||
case "expected_fail":
|
||||
if (resultSet[name].status !== "FAIL") resultSet[name].status = "EXPECTED";
|
||||
break;
|
||||
case "EXPECTED":
|
||||
report(name, true);
|
||||
log(name, ":", "EXPECTED FAILURE");
|
||||
expected++;
|
||||
break;
|
||||
case "FAIL":
|
||||
failed++;
|
||||
report(name, true);
|
||||
case "exception":
|
||||
log(name, ":", "!!!!!!!!! FAIL !!!!!!!!!!!");
|
||||
log(JSON.stringify(resultSet[name].info));
|
||||
whereFailed.push({ name: name, info: JSON.stringify(resultSet[name].info) });
|
||||
if (event.details && event.details.stack) log(event.details.stack);
|
||||
else log("Test failed with exception");
|
||||
failed++;
|
||||
whereFailed.push({ name: name, info: JSON.stringify(event) });
|
||||
break;
|
||||
case "finish":
|
||||
switch (resultSet[name].status) {
|
||||
case "OK":
|
||||
break;
|
||||
case "PENDING":
|
||||
resultSet[name].status = "OK";
|
||||
report(name, true);
|
||||
log(name, ":", "OK");
|
||||
passed++;
|
||||
break;
|
||||
case "EXPECTED":
|
||||
report(name, true);
|
||||
log(name, ":", "EXPECTED FAILURE");
|
||||
expected++;
|
||||
break;
|
||||
case "FAIL":
|
||||
failed++;
|
||||
report(name, true);
|
||||
log(name, ":", "!!!!!!!!! FAIL !!!!!!!!!!!");
|
||||
log(JSON.stringify(resultSet[name].info));
|
||||
whereFailed.push({ name: name, info: JSON.stringify(resultSet[name].info) });
|
||||
break;
|
||||
default:
|
||||
log(name, ": unknown state for the test to be in");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log(name, ": unknown state for the test to be in");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
resultSet[name].status = "FAIL";
|
||||
resultSet[name].info = results;
|
||||
break;
|
||||
resultSet[name].status = "FAIL";
|
||||
resultSet[name].info = results;
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -190,7 +180,16 @@ runTests = function () {
|
||||
if (failed > 0) {
|
||||
log("~~~~~~~ THERE ARE FAILURES ~~~~~~~");
|
||||
}
|
||||
log("passed/expected/failed/total", passed, "/", expected, "/", failed, "/", Object.keys(resultSet).length);
|
||||
log(
|
||||
"passed/expected/failed/total",
|
||||
passed,
|
||||
"/",
|
||||
expected,
|
||||
"/",
|
||||
failed,
|
||||
"/",
|
||||
Object.keys(resultSet).length,
|
||||
);
|
||||
sendReports(function () {
|
||||
if (doReport) {
|
||||
log("Waiting 3s for any last reports to get sent out");
|
||||
@@ -210,42 +209,46 @@ runTests = function () {
|
||||
|
||||
// Also log xUnit output
|
||||
xunit('<testsuite errors="" failures="" name="meteor" skips="" tests="" time="">');
|
||||
resultSet.forEach(function (result, name) {
|
||||
var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client");
|
||||
var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client");
|
||||
var time = "";
|
||||
var error = "";
|
||||
resultSet.forEach(function (result, _name) {
|
||||
const classname =
|
||||
result.testPath.join(".").replace(/ /g, "-") + (result.server ? "-server" : "-client");
|
||||
const testName = result.test.replace(/ /g, "-") + (result.server ? "-server" : "-client");
|
||||
let time = "";
|
||||
let error = "";
|
||||
result.events.forEach(function (event) {
|
||||
switch (event.type) {
|
||||
case "finish":
|
||||
var timeMs = event.timeMs;
|
||||
const timeMs = event.timeMs;
|
||||
if (timeMs !== undefined) {
|
||||
time = (timeMs / 1000) + "";
|
||||
time = `${timeMs / 1000}`;
|
||||
}
|
||||
break;
|
||||
case "exception":
|
||||
var details = event.details || {};
|
||||
error = (details.message || '?') + " filename=" + (details.filename || '?') + " line=" + (details.line || '?');
|
||||
const details = event.details || {};
|
||||
error = `${details.message || "?"} filename=${details.filename || "?"} line=${details.line || "?"}`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
switch (result.status) {
|
||||
case "FAIL":
|
||||
error = error || '?';
|
||||
error = error || "?";
|
||||
break;
|
||||
case "EXPECTED":
|
||||
error = null;
|
||||
break;
|
||||
}
|
||||
|
||||
xunit('<testcase classname="' + escapeXml(classname) + '" name="' + escapeXml(name) + '" time="' + time + '">');
|
||||
xunit(
|
||||
`<testcase classname="${escapeXml(classname)}" name="${escapeXml(testName)}" time="${time}">`,
|
||||
);
|
||||
if (error) {
|
||||
xunit(' <failure message="test failure">' + escapeXml(error) + '</failure>');
|
||||
xunit(` <failure message="test failure">${escapeXml(error)}</failure>`);
|
||||
}
|
||||
xunit('</testcase>');
|
||||
xunit("</testcase>");
|
||||
});
|
||||
xunit('</testsuite>');
|
||||
logMagic('state', 'done');
|
||||
xunit("</testsuite>");
|
||||
logMagic("state", "done");
|
||||
},
|
||||
["tinytest"]);
|
||||
}
|
||||
["tinytest"],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
Package.describe({
|
||||
summary: 'Run tests noninteractively, with results going to the console.',
|
||||
version: '2.0.2-beta350.7',
|
||||
summary: "Run tests noninteractively, with results going to the console.",
|
||||
version: "2.0.2-beta350.7",
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
api.use(['tinytest', 'random', 'ejson', 'check', 'ecmascript']);
|
||||
api.use('fetch', 'server');
|
||||
api.use('jquery', 'client');
|
||||
api.use(["tinytest", "random", "ejson", "check", "ecmascript"]);
|
||||
api.use("fetch", "server");
|
||||
api.use("jquery", "client");
|
||||
|
||||
api.export('TEST_STATUS', 'client');
|
||||
api.export("TEST_STATUS", "client");
|
||||
|
||||
api.addFiles(['driver.js', 'test.css'], 'client');
|
||||
api.addFiles(["driver.js", "test.css"], "client");
|
||||
|
||||
api.addFiles(['reporter.js'], 'server');
|
||||
api.addFiles(["reporter.js"], "server");
|
||||
|
||||
api.addAssets('puppeteer_runner.js', 'server');
|
||||
api.addAssets("puppeteer_runner.js", "server");
|
||||
|
||||
api.export('runTests');
|
||||
api.export("runTests");
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ let puppeteer;
|
||||
try {
|
||||
// Prefer the copy bundled inside dev_bundle (local checkout / CI after first run).
|
||||
puppeteer = require("../../dev_bundle/lib/node_modules/puppeteer");
|
||||
} catch (_) {
|
||||
} catch {
|
||||
// Fallback: globally-installed puppeteer (e.g. on oss-vm where it is pre-installed
|
||||
// via `npm install -g puppeteer@23.6.0` and NODE_PATH is set to `npm root -g`).
|
||||
puppeteer = require("puppeteer");
|
||||
@@ -28,23 +28,19 @@ async function runNextUrl(browser) {
|
||||
else {
|
||||
testNumber++;
|
||||
const currentClientTest = await page.evaluate(() =>
|
||||
__Tinytest._getCurrentRunningTestOnClient()
|
||||
__Tinytest._getCurrentRunningTestOnClient(),
|
||||
);
|
||||
if (currentClientTest !== "") {
|
||||
console.log(
|
||||
`Currently running on the client test: ${currentClientTest}`
|
||||
);
|
||||
console.log(`Currently running on the client test: ${currentClientTest}`);
|
||||
return;
|
||||
}
|
||||
// If we get here is because we have not yet started the test on the client
|
||||
const currentServerTest = await page.evaluate(
|
||||
async () => await __Tinytest._getCurrentRunningTestOnServer()
|
||||
async () => await __Tinytest._getCurrentRunningTestOnServer(),
|
||||
);
|
||||
|
||||
if (currentServerTest !== "") {
|
||||
console.log(
|
||||
`Currently running on the server test: ${currentServerTest}`
|
||||
);
|
||||
console.log(`Currently running on the server test: ${currentServerTest}`);
|
||||
return;
|
||||
}
|
||||
// we were not able to find the name of the test, this is a way to make sure the test is still running
|
||||
@@ -153,17 +149,11 @@ async function runTests() {
|
||||
|
||||
// --no-sandbox and --disable-setuid-sandbox must be disabled for CI compatibility
|
||||
const browser = await puppeteer.launch({
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-web-security",
|
||||
],
|
||||
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-web-security"],
|
||||
headless: "new",
|
||||
});
|
||||
console.log(`Using version: ${await browser.version()}`);
|
||||
await runNextUrl(browser);
|
||||
}
|
||||
|
||||
runTests().catch((e) =>
|
||||
console.log(`something broke while running puppeter: `, e)
|
||||
);
|
||||
runTests().catch((e) => console.log(`something broke while running puppeter: `, e));
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
let url = null;
|
||||
|
||||
if (Meteor.settings?.public?.runId &&
|
||||
Meteor.settings?.public?.reportTo) {
|
||||
url = Meteor.settings.public.reportTo +
|
||||
"/report/" +
|
||||
Meteor.settings.public.runId;
|
||||
if (Meteor.settings?.public?.runId && Meteor.settings?.public?.reportTo) {
|
||||
url = `${Meteor.settings.public.reportTo}/report/${Meteor.settings.public.runId}`;
|
||||
}
|
||||
|
||||
Meteor.methods({
|
||||
@@ -13,15 +10,15 @@ Meteor.methods({
|
||||
check(reports, [Object]);
|
||||
if (url) {
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(reports),
|
||||
});
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// provide some notification we're started. This is to allow use
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
* makes sure that at least one `.css` file can always be found, when the
|
||||
* tests are run.
|
||||
*/
|
||||
.test { }
|
||||
.test {
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
Package.describe({
|
||||
summary: "Used internally by WebApp. Knows how to hash programs from manifests.",
|
||||
version: '1.1.2',
|
||||
version: "1.1.2",
|
||||
});
|
||||
|
||||
Package.onUse(function(api) {
|
||||
api.use('ecmascript');
|
||||
api.addFiles('webapp-hashing.js', 'server');
|
||||
api.export('WebAppHashing');
|
||||
Package.onUse(function (api) {
|
||||
api.use("ecmascript");
|
||||
api.addFiles("webapp-hashing.js", "server");
|
||||
api.export("WebAppHashing");
|
||||
});
|
||||
|
||||
Package.onTest(function(api) {
|
||||
api.use('tinytest');
|
||||
api.use('webapp-hashing');
|
||||
Package.onTest(function (api) {
|
||||
api.use("ecmascript");
|
||||
api.use("tinytest");
|
||||
api.use("webapp-hashing");
|
||||
api.addFiles("webapp-hashing-tests.js", "server");
|
||||
});
|
||||
|
||||
152
packages/webapp-hashing/webapp-hashing-tests.js
Normal file
152
packages/webapp-hashing/webapp-hashing-tests.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const isHex40 = (s) => typeof s === "string" && /^[0-9a-f]{40}$/.test(s);
|
||||
|
||||
const makeResource = (overrides) => ({
|
||||
type: "js",
|
||||
replaceable: false,
|
||||
where: "client",
|
||||
path: "app.js",
|
||||
hash: "abc123",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const fixedOverride = { foo: "bar", baz: 42 };
|
||||
|
||||
Tinytest.add("webapp-hashing - calculateCordovaCompatibilityHash is deterministic", (test) => {
|
||||
const hash1 = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"cordova-plugin-camera": "4.0.0",
|
||||
"cordova-plugin-file": "6.0.0",
|
||||
});
|
||||
const hash2 = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"cordova-plugin-camera": "4.0.0",
|
||||
"cordova-plugin-file": "6.0.0",
|
||||
});
|
||||
|
||||
test.isTrue(isHex40(hash1), "should return 40-char hex string");
|
||||
test.equal(hash1, hash2);
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
"webapp-hashing - calculateCordovaCompatibilityHash is order-independent for plugins",
|
||||
(test) => {
|
||||
const hashAB = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"plugin-a": "1.0.0",
|
||||
"plugin-b": "2.0.0",
|
||||
});
|
||||
const hashBA = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"plugin-b": "2.0.0",
|
||||
"plugin-a": "1.0.0",
|
||||
});
|
||||
|
||||
test.equal(hashAB, hashBA, "plugin insertion order must not affect the hash");
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
"webapp-hashing - calculateCordovaCompatibilityHash differs on input change",
|
||||
(test) => {
|
||||
const baseline = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"plugin-a": "1.0.0",
|
||||
});
|
||||
|
||||
// Different platformVersion.
|
||||
const diffPlatform = WebAppHashing.calculateCordovaCompatibilityHash("6.0.1", {
|
||||
"plugin-a": "1.0.0",
|
||||
});
|
||||
test.notEqual(baseline, diffPlatform);
|
||||
|
||||
// Different plugin version.
|
||||
const diffVersion = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"plugin-a": "1.0.1",
|
||||
});
|
||||
test.notEqual(baseline, diffVersion);
|
||||
|
||||
// Different plugin name.
|
||||
const diffName = WebAppHashing.calculateCordovaCompatibilityHash("6.0.0", {
|
||||
"plugin-b": "1.0.0",
|
||||
});
|
||||
test.notEqual(baseline, diffName);
|
||||
},
|
||||
);
|
||||
|
||||
Tinytest.add("webapp-hashing - calculateClientHash is deterministic", (test) => {
|
||||
const manifest = [makeResource({ path: "app.js", hash: "aaa" })];
|
||||
const hash1 = WebAppHashing.calculateClientHash(manifest, null, fixedOverride);
|
||||
const hash2 = WebAppHashing.calculateClientHash(manifest, null, fixedOverride);
|
||||
|
||||
test.isTrue(isHex40(hash1));
|
||||
test.equal(hash1, hash2);
|
||||
});
|
||||
|
||||
Tinytest.add("webapp-hashing - calculateClientHash ignores non-client resources", (test) => {
|
||||
const baseline = [makeResource({ path: "app.js", hash: "aaa" })];
|
||||
|
||||
// Adding a server-only resource should not change the hash.
|
||||
const withServer = [
|
||||
...baseline,
|
||||
makeResource({ path: "server.js", hash: "bbb", where: "server" }),
|
||||
];
|
||||
test.equal(
|
||||
WebAppHashing.calculateClientHash(baseline, null, fixedOverride),
|
||||
WebAppHashing.calculateClientHash(withServer, null, fixedOverride),
|
||||
);
|
||||
|
||||
// Adding a client resource SHOULD change the hash.
|
||||
const withClient = [
|
||||
...baseline,
|
||||
makeResource({ path: "extra.js", hash: "ccc", where: "client" }),
|
||||
];
|
||||
test.notEqual(
|
||||
WebAppHashing.calculateClientHash(baseline, null, fixedOverride),
|
||||
WebAppHashing.calculateClientHash(withClient, null, fixedOverride),
|
||||
);
|
||||
|
||||
// Adding an internal resource SHOULD also change the hash.
|
||||
const withInternal = [
|
||||
...baseline,
|
||||
makeResource({ path: "internal.js", hash: "ddd", where: "internal" }),
|
||||
];
|
||||
test.notEqual(
|
||||
WebAppHashing.calculateClientHash(baseline, null, fixedOverride),
|
||||
WebAppHashing.calculateClientHash(withInternal, null, fixedOverride),
|
||||
);
|
||||
});
|
||||
|
||||
Tinytest.add("webapp-hashing - calculateClientHash respects includeFilter", (test) => {
|
||||
const manifest = [
|
||||
makeResource({ type: "js", path: "app.js", hash: "aaa" }),
|
||||
makeResource({ type: "css", path: "app.css", hash: "bbb" }),
|
||||
];
|
||||
|
||||
const withCss = WebAppHashing.calculateClientHash(manifest, null, fixedOverride);
|
||||
const withoutCss = WebAppHashing.calculateClientHash(
|
||||
manifest,
|
||||
(type) => type !== "css",
|
||||
fixedOverride,
|
||||
);
|
||||
|
||||
// The filter excludes css resources, so the resulting hash should differ.
|
||||
test.notEqual(withCss, withoutCss);
|
||||
|
||||
// And it should equal the hash of just the js resource.
|
||||
const jsOnly = WebAppHashing.calculateClientHash(
|
||||
[makeResource({ type: "js", path: "app.js", hash: "aaa" })],
|
||||
null,
|
||||
fixedOverride,
|
||||
);
|
||||
test.equal(withoutCss, jsOnly);
|
||||
});
|
||||
|
||||
Tinytest.add(
|
||||
"webapp-hashing - calculateClientHash reacts to runtimeConfigOverride changes",
|
||||
(test) => {
|
||||
const manifest = [makeResource({ path: "app.js", hash: "aaa" })];
|
||||
|
||||
const hash1 = WebAppHashing.calculateClientHash(manifest, null, { foo: "bar" });
|
||||
const hash2 = WebAppHashing.calculateClientHash(manifest, null, { foo: "baz" });
|
||||
test.notEqual(hash1, hash2);
|
||||
|
||||
// Same override → same hash.
|
||||
const hash1b = WebAppHashing.calculateClientHash(manifest, null, { foo: "bar" });
|
||||
test.equal(hash1, hash1b);
|
||||
},
|
||||
);
|
||||
@@ -11,43 +11,46 @@ WebAppHashing = {};
|
||||
// (but the second is a performance enhancement, not a hard
|
||||
// requirement).
|
||||
|
||||
WebAppHashing.calculateClientHash =
|
||||
function (manifest, includeFilter, runtimeConfigOverride) {
|
||||
var hash = createHash('sha1');
|
||||
WebAppHashing.calculateClientHash = function (manifest, includeFilter, runtimeConfigOverride) {
|
||||
const hash = createHash("sha1");
|
||||
|
||||
// Omit the old hashed client values in the new hash. These may be
|
||||
// modified in the new boilerplate.
|
||||
var { autoupdateVersion, autoupdateVersionRefreshable, autoupdateVersionCordova, ...runtimeCfg } = __meteor_runtime_config__;
|
||||
const {
|
||||
autoupdateVersion: _av,
|
||||
autoupdateVersionRefreshable: _avr,
|
||||
autoupdateVersionCordova: _avc,
|
||||
...runtimeCfgBase
|
||||
} = __meteor_runtime_config__;
|
||||
|
||||
if (runtimeConfigOverride) {
|
||||
runtimeCfg = runtimeConfigOverride;
|
||||
}
|
||||
const runtimeCfg = runtimeConfigOverride || runtimeCfgBase;
|
||||
|
||||
hash.update(JSON.stringify(runtimeCfg, 'utf8'));
|
||||
hash.update(JSON.stringify(runtimeCfg, "utf8"));
|
||||
|
||||
manifest.forEach(function (resource) {
|
||||
if ((! includeFilter || includeFilter(resource.type, resource.replaceable)) &&
|
||||
(resource.where === 'client' || resource.where === 'internal')) {
|
||||
if (
|
||||
(!includeFilter || includeFilter(resource.type, resource.replaceable)) &&
|
||||
(resource.where === "client" || resource.where === "internal")
|
||||
) {
|
||||
hash.update(resource.path);
|
||||
hash.update(resource.hash);
|
||||
}
|
||||
});
|
||||
return hash.digest('hex');
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
WebAppHashing.calculateCordovaCompatibilityHash =
|
||||
function(platformVersion, pluginVersions) {
|
||||
const hash = createHash('sha1');
|
||||
WebAppHashing.calculateCordovaCompatibilityHash = function (platformVersion, pluginVersions) {
|
||||
const hash = createHash("sha1");
|
||||
|
||||
hash.update(platformVersion);
|
||||
|
||||
// Sort plugins first so iteration order doesn't affect the hash
|
||||
const plugins = Object.keys(pluginVersions).sort();
|
||||
for (let plugin of plugins) {
|
||||
for (const plugin of plugins) {
|
||||
const version = pluginVersions[plugin];
|
||||
hash.update(plugin);
|
||||
hash.update(version);
|
||||
}
|
||||
|
||||
return hash.digest('hex');
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user