@bitblit/ratchet-common 6.0.146-alpha → 6.0.147-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/2d/line-2d.ts +6 -0
- package/src/2d/matrix-factory.ts +94 -0
- package/src/2d/plane-2d-type.ts +6 -0
- package/src/2d/plane-2d.ts +7 -0
- package/src/2d/point-2d.ts +4 -0
- package/src/2d/poly-line-2d.ts +5 -0
- package/src/2d/ratchet-2d.spec.ts +205 -0
- package/src/2d/ratchet-2d.ts +350 -0
- package/src/2d/transformation-matrix.ts +19 -0
- package/src/build/build-information.ts +8 -0
- package/src/build/ratchet-common-info.ts +19 -0
- package/src/histogram/histogram-entry.ts +4 -0
- package/src/histogram/histogram.spec.ts +25 -0
- package/src/histogram/histogram.ts +61 -0
- package/src/jwt/common-jwt-token.ts +17 -0
- package/src/jwt/expired-jwt-handling.ts +5 -0
- package/src/jwt/jwt-decode-only-ratchet.ts +26 -0
- package/src/jwt/jwt-payload-expiration-ratchet.ts +45 -0
- package/src/jwt/jwt-token-base.ts +14 -0
- package/src/lang/array-ratchet.spec.ts +79 -0
- package/src/lang/array-ratchet.ts +141 -0
- package/src/lang/base64-ratchet.spec.ts +48 -0
- package/src/lang/base64-ratchet.ts +247 -0
- package/src/lang/boolean-ratchet.spec.ts +95 -0
- package/src/lang/boolean-ratchet.ts +52 -0
- package/src/lang/composite-last-success-provider.spec.ts +31 -0
- package/src/lang/composite-last-success-provider.ts +30 -0
- package/src/lang/currency-ratchet.ts +29 -0
- package/src/lang/date-ratchet.spec.ts +27 -0
- package/src/lang/date-ratchet.ts +42 -0
- package/src/lang/duration-ratchet.spec.ts +47 -0
- package/src/lang/duration-ratchet.ts +77 -0
- package/src/lang/enum-ratchet.spec.ts +45 -0
- package/src/lang/enum-ratchet.ts +41 -0
- package/src/lang/error-handling-approach.ts +6 -0
- package/src/lang/error-ratchet.spec.ts +25 -0
- package/src/lang/error-ratchet.ts +70 -0
- package/src/lang/esm-ratchet.ts +81 -0
- package/src/lang/expiring-object.spec.ts +56 -0
- package/src/lang/expiring-object.ts +84 -0
- package/src/lang/geolocation-ratchet.spec.ts +177 -0
- package/src/lang/geolocation-ratchet.ts +341 -0
- package/src/lang/global-ratchet.spec.ts +17 -0
- package/src/lang/global-ratchet.ts +105 -0
- package/src/lang/key-value.ts +8 -0
- package/src/lang/last-success-provider.ts +4 -0
- package/src/lang/map-ratchet.spec.ts +113 -0
- package/src/lang/map-ratchet.ts +220 -0
- package/src/lang/no.spec.ts +9 -0
- package/src/lang/no.ts +7 -0
- package/src/lang/number-ratchet.spec.ts +154 -0
- package/src/lang/number-ratchet.ts +253 -0
- package/src/lang/parsed-url.ts +10 -0
- package/src/lang/promise-ratchet.spec.ts +104 -0
- package/src/lang/promise-ratchet.ts +196 -0
- package/src/lang/range.ts +4 -0
- package/src/lang/require-ratchet.spec.ts +85 -0
- package/src/lang/require-ratchet.ts +68 -0
- package/src/lang/simple-arg-ratchet.spec.ts +13 -0
- package/src/lang/simple-arg-ratchet.ts +47 -0
- package/src/lang/simple-encryption-ratchet.ts +88 -0
- package/src/lang/sort-ratchet.spec.ts +58 -0
- package/src/lang/sort-ratchet.ts +50 -0
- package/src/lang/stop-watch.spec.ts +53 -0
- package/src/lang/stop-watch.ts +202 -0
- package/src/lang/string-ratchet.spec.ts +226 -0
- package/src/lang/string-ratchet.ts +676 -0
- package/src/lang/time-zone-ratchet.spec.ts +51 -0
- package/src/lang/time-zone-ratchet.ts +148 -0
- package/src/lang/timeout-token.spec.ts +12 -0
- package/src/lang/timeout-token.ts +21 -0
- package/src/lang/uint-8-array-ratchet.spec.ts +22 -0
- package/src/lang/uint-8-array-ratchet.ts +48 -0
- package/src/lang/web-stream-ratchet.spec.ts +12 -0
- package/src/lang/web-stream-ratchet.ts +96 -0
- package/src/logger/classic-single-line-log-message-formatter.ts +19 -0
- package/src/logger/log-message-builder.ts +60 -0
- package/src/logger/log-message-format-type.ts +11 -0
- package/src/logger/log-message-formatter.ts +6 -0
- package/src/logger/log-message-processor.ts +6 -0
- package/src/logger/log-message.ts +9 -0
- package/src/logger/log-snapshot.ts +6 -0
- package/src/logger/logger-instance.ts +269 -0
- package/src/logger/logger-level-name.ts +11 -0
- package/src/logger/logger-meta.ts +7 -0
- package/src/logger/logger-options.ts +14 -0
- package/src/logger/logger-output-function.ts +10 -0
- package/src/logger/logger-ring-buffer.ts +89 -0
- package/src/logger/logger-util.spec.ts +11 -0
- package/src/logger/logger-util.ts +68 -0
- package/src/logger/logger.spec.ts +177 -0
- package/src/logger/logger.ts +213 -0
- package/src/logger/none-log-message-formatter.ts +10 -0
- package/src/logger/single-line-no-level-log-message-formatter.ts +18 -0
- package/src/logger/structured-json-log-message-formatter.ts +25 -0
- package/src/mail/archive-email-result.ts +8 -0
- package/src/mail/email-attachment.ts +23 -0
- package/src/mail/mail-sending-provider.ts +21 -0
- package/src/mail/mailer-config.ts +30 -0
- package/src/mail/mailer-like.ts +38 -0
- package/src/mail/mailer-util.ts +65 -0
- package/src/mail/mailer.spec.ts +120 -0
- package/src/mail/mailer.ts +214 -0
- package/src/mail/ready-to-send-email.ts +67 -0
- package/src/mail/resolved-ready-to-send-email.ts +17 -0
- package/src/mail/send-email-result.ts +16 -0
- package/src/mail/test-mail-sending-provider.ts +35 -0
- package/src/network/browser-local-ip-provider.spec.ts +23 -0
- package/src/network/browser-local-ip-provider.ts +26 -0
- package/src/network/fixed-local-ip-provider.ts +9 -0
- package/src/network/local-ip-provider.ts +4 -0
- package/src/network/network-ratchet.spec.ts +17 -0
- package/src/network/network-ratchet.ts +209 -0
- package/src/network/remote-file-tracker/backup-result.ts +6 -0
- package/src/network/remote-file-tracker/file-transfer-result-type.ts +5 -0
- package/src/network/remote-file-tracker/file-transfer-result.ts +9 -0
- package/src/network/remote-file-tracker/remote-file-tracker-options.ts +6 -0
- package/src/network/remote-file-tracker/remote-file-tracker-push-options.ts +4 -0
- package/src/network/remote-file-tracker/remote-file-tracker.ts +117 -0
- package/src/network/remote-file-tracker/remote-file-tracking-provider.ts +19 -0
- package/src/network/remote-file-tracker/remote-status-data-and-content.ts +6 -0
- package/src/network/remote-file-tracker/remote-status-data.ts +7 -0
- package/src/network/restful-api-http-error.spec.ts +13 -0
- package/src/network/restful-api-http-error.ts +173 -0
- package/src/template/ratchet-template-renderer.ts +8 -0
- package/src/third-party/google/google-recaptcha-ratchet.spec.ts +27 -0
- package/src/third-party/google/google-recaptcha-ratchet.ts +36 -0
- package/src/third-party/twilio/twilio-ratchet.ts +92 -0
- package/src/third-party/twilio/twilio-verify-ratchet.ts +83 -0
- package/src/transform/built-in-transforms.ts +214 -0
- package/src/transform/transform-ratchet.spec.ts +134 -0
- package/src/transform/transform-ratchet.ts +88 -0
- package/src/transform/transform-rule.ts +7 -0
- package/src/tx/transaction-configuration.ts +8 -0
- package/src/tx/transaction-final-state.ts +7 -0
- package/src/tx/transaction-ratchet.spec.ts +150 -0
- package/src/tx/transaction-ratchet.ts +98 -0
- package/src/tx/transaction-result.ts +10 -0
- package/src/tx/transaction-step.ts +5 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
From https://graphicmaths.com/pure/matrices/matrix-2d-transformations/
|
|
3
|
+
|
|
4
|
+
Matrix is the form:
|
|
5
|
+
|
|
6
|
+
a b u
|
|
7
|
+
c d v
|
|
8
|
+
0 0 1
|
|
9
|
+
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface TransformationMatrix {
|
|
13
|
+
a: number;
|
|
14
|
+
b: number;
|
|
15
|
+
c: number;
|
|
16
|
+
d: number;
|
|
17
|
+
u?: number;
|
|
18
|
+
v?: number;
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BuildInformation } from './build-information.js';
|
|
2
|
+
|
|
3
|
+
export class RatchetInfo {
|
|
4
|
+
// Prevent instantiation
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
6
|
+
private constructor() {}
|
|
7
|
+
|
|
8
|
+
public static buildInformation(): BuildInformation {
|
|
9
|
+
const val: BuildInformation = {
|
|
10
|
+
version: 'LOCAL-SNAPSHOT',
|
|
11
|
+
hash: 'LOCAL-HASH',
|
|
12
|
+
branch: 'LOCAL-BRANCH',
|
|
13
|
+
tag: 'LOCAL-TAG',
|
|
14
|
+
timeBuiltISO: 'LOCAL-TIME-ISO',
|
|
15
|
+
notes: 'LOCAL-NOTES',
|
|
16
|
+
};
|
|
17
|
+
return val;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Histogram } from './histogram.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('#histogram', function () {
|
|
5
|
+
test('should count the values correctly', function () {
|
|
6
|
+
const histogram: Histogram<string> = new Histogram<string>();
|
|
7
|
+
|
|
8
|
+
histogram.update('a');
|
|
9
|
+
histogram.update('a');
|
|
10
|
+
histogram.update('b');
|
|
11
|
+
histogram.update('c');
|
|
12
|
+
|
|
13
|
+
expect(histogram.getTotalCount()).toEqual(4);
|
|
14
|
+
|
|
15
|
+
expect(histogram.countForValue('a')).toEqual(2);
|
|
16
|
+
expect(histogram.countForValue('b')).toEqual(1);
|
|
17
|
+
expect(histogram.countForValue('c')).toEqual(1);
|
|
18
|
+
expect(histogram.countForValue('d')).toEqual(0);
|
|
19
|
+
|
|
20
|
+
expect(histogram.percentForValue('a')).toEqual(0.5);
|
|
21
|
+
expect(histogram.percentForValue('b')).toEqual(0.25);
|
|
22
|
+
expect(histogram.percentForValue('c')).toEqual(0.25);
|
|
23
|
+
expect(histogram.percentForValue('d')).toEqual(0);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { HistogramEntry } from './histogram-entry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Implements a simple histogram (for each object, how many times does it appear)
|
|
5
|
+
*/
|
|
6
|
+
export class Histogram<T> {
|
|
7
|
+
private entries: HistogramEntry<T>[] = [];
|
|
8
|
+
|
|
9
|
+
public update(val: T, addValue = 1): void {
|
|
10
|
+
if (val) {
|
|
11
|
+
const entry: HistogramEntry<T> = this.entries.find((e) => e.item === val);
|
|
12
|
+
if (entry) {
|
|
13
|
+
entry.count += addValue;
|
|
14
|
+
} else {
|
|
15
|
+
this.entries.push({
|
|
16
|
+
item: val,
|
|
17
|
+
count: addValue,
|
|
18
|
+
} as HistogramEntry<T>);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public sort(): void {
|
|
24
|
+
this.entries.sort((a, b) => {
|
|
25
|
+
let rval: number = b.count - a.count;
|
|
26
|
+
if (rval === 0) {
|
|
27
|
+
rval = String(b.item).localeCompare(String(a.item));
|
|
28
|
+
}
|
|
29
|
+
return rval;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// No need for second level sort, since the same key can only be in there once
|
|
34
|
+
public sortKeys(): void {
|
|
35
|
+
this.entries.sort((a, b) => String(b.item).localeCompare(String(a.item)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public reverse(): void {
|
|
39
|
+
this.entries.reverse();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public getEntries(): HistogramEntry<T>[] {
|
|
43
|
+
return this.entries;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public getTotalCount(): number {
|
|
47
|
+
let rval = 0;
|
|
48
|
+
this.entries.forEach((h) => (rval += h.count));
|
|
49
|
+
return rval;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public countForValue(val: T): number {
|
|
53
|
+
const entry: HistogramEntry<T> = this.entries.find((test) => test.item === val);
|
|
54
|
+
return entry ? entry.count : 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public percentForValue(val: T): number {
|
|
58
|
+
const total: number = this.getTotalCount();
|
|
59
|
+
return total === 0 ? 0 : this.countForValue(val) / total;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is meant to be an object that wraps up the standard JWT fields along with some other helpful fields
|
|
3
|
+
* in a type-safe way. This is used in my Epsilon library, although it can be easily used elsewhere.
|
|
4
|
+
*
|
|
5
|
+
* In the case of a sudo situation, use the proxy field. For example, if I am alice, but I am running as
|
|
6
|
+
* bob, bob should be in the user field and alice should be in the proxy field. While this may seem somewhat
|
|
7
|
+
* backwards, it allows the majority of code to proceed as if bob is logged in, and only code that cares that a
|
|
8
|
+
* proxy is going on (e.g., audit trail code) needs to even check the proxy field.
|
|
9
|
+
*
|
|
10
|
+
* Note: other interfaces can extend this token to gain more functionality
|
|
11
|
+
*/
|
|
12
|
+
import { JwtTokenBase } from './jwt-token-base.js';
|
|
13
|
+
|
|
14
|
+
export interface CommonJwtToken<T> extends JwtTokenBase {
|
|
15
|
+
user: T; // Data for the authenticated user
|
|
16
|
+
proxy: T; // Data for the proxy user (if any)
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jwtDecode } from 'jwt-decode';
|
|
2
|
+
import { ExpiredJwtHandling } from './expired-jwt-handling.js';
|
|
3
|
+
import { JwtTokenBase } from './jwt-token-base.js';
|
|
4
|
+
import { JwtPayloadExpirationRatchet } from './jwt-payload-expiration-ratchet.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* This is a stepped-down version of the original JwtRatchet which is
|
|
8
|
+
* now relocated to ratchet-node-only, as it depends on crypto and
|
|
9
|
+
* stream libraries that aren't available in the browser (and common
|
|
10
|
+
* is meant to work on both the browser and node).
|
|
11
|
+
*
|
|
12
|
+
* This can decode a token, and verify expiration, but it can't check
|
|
13
|
+
* the signature since it lacks the libraries (and keys!) for that.
|
|
14
|
+
*/
|
|
15
|
+
export class JwtDecodeOnlyRatchet {
|
|
16
|
+
public static decodeTokenNoVerify<T extends JwtTokenBase>(
|
|
17
|
+
token: string,
|
|
18
|
+
expiredHandling: ExpiredJwtHandling = ExpiredJwtHandling.RETURN_NULL,
|
|
19
|
+
inDecodeFuntion?: (val: string) => T,
|
|
20
|
+
): T {
|
|
21
|
+
const decodeFunction: (val: string) => T = inDecodeFuntion ?? jwtDecode;
|
|
22
|
+
let decoded: T = decodeFunction(token);
|
|
23
|
+
decoded = JwtPayloadExpirationRatchet.processPayloadExpiration<T>(decoded, expiredHandling);
|
|
24
|
+
return decoded;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ExpiredJwtHandling } from './expired-jwt-handling.js';
|
|
2
|
+
import { JwtTokenBase } from './jwt-token-base.js';
|
|
3
|
+
import { Logger } from '../logger/logger.js';
|
|
4
|
+
import { DurationRatchet } from '../lang/duration-ratchet.js';
|
|
5
|
+
|
|
6
|
+
export class JwtPayloadExpirationRatchet {
|
|
7
|
+
private static readonly EXPIRED_FLAG_NAME: string = '__jwtServiceExpiredFlag';
|
|
8
|
+
|
|
9
|
+
public static processPayloadExpiration<T extends JwtTokenBase>(payload: T, expiredHandling: ExpiredJwtHandling): T {
|
|
10
|
+
if (payload) {
|
|
11
|
+
const nowSeconds: number = Math.floor(Date.now() / 1000);
|
|
12
|
+
// A backwards compatibility hack since some of my old code used to incorrectly write the exp field in milliseconds
|
|
13
|
+
const expSeconds: number = payload?.exp && payload.exp > nowSeconds * 100 ? Math.floor(payload.exp / 1000) : payload?.exp;
|
|
14
|
+
const nbfSeconds: number = payload?.nbf && payload.nbf > nowSeconds * 100 ? Math.floor(payload.nbf / 1000) : payload?.nbf;
|
|
15
|
+
|
|
16
|
+
if ((expSeconds && nowSeconds >= expSeconds) || (nbfSeconds && nowSeconds <= nbfSeconds)) {
|
|
17
|
+
// Only do this if expiration is defined
|
|
18
|
+
const age: number = nowSeconds - expSeconds;
|
|
19
|
+
Logger.debug('JWT token expired or before NBF : on %d, %s ago', payload.exp, DurationRatchet.formatMsDuration(age * 1000));
|
|
20
|
+
switch (expiredHandling) {
|
|
21
|
+
case ExpiredJwtHandling.THROW_EXCEPTION:
|
|
22
|
+
throw new Error('JWT Token was expired');
|
|
23
|
+
case ExpiredJwtHandling.ADD_FLAG:
|
|
24
|
+
payload[JwtPayloadExpirationRatchet.EXPIRED_FLAG_NAME] = true;
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
payload = null;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public static hasExpiredFlag(ob: any): boolean {
|
|
36
|
+
return ob && ob[JwtPayloadExpirationRatchet.EXPIRED_FLAG_NAME] === true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public static removeExpiredFlag(ob: any) {
|
|
40
|
+
if (ob) {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
42
|
+
delete ob[JwtPayloadExpirationRatchet.EXPIRED_FLAG_NAME];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the common fields expected in a JWT token according to the RFC
|
|
3
|
+
* https://www.rfc-editor.org/rfc/rfc7519.html
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface JwtTokenBase {
|
|
7
|
+
iss?: string; // Issuer
|
|
8
|
+
sub?: string; // Subject
|
|
9
|
+
aud?: string; // Audience
|
|
10
|
+
exp?: number; // Expiration time
|
|
11
|
+
nbf?: number; // Not Before
|
|
12
|
+
iat?: number; // Issued at (time of creation)
|
|
13
|
+
jti?: string; // Unique ID for the token
|
|
14
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ArrayRatchet, MatchReport } from './array-ratchet.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const sortedArr: any[] = [
|
|
5
|
+
{
|
|
6
|
+
n: 'test1',
|
|
7
|
+
v: 1,
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
n: 'test1',
|
|
11
|
+
v: 2,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
n: 'test1',
|
|
15
|
+
v: 4,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
n: 'test1',
|
|
19
|
+
v: 4,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
n: 'test1',
|
|
23
|
+
v: 7,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
n: 'test1',
|
|
27
|
+
v: 12,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
n: 'test1',
|
|
31
|
+
v: 25,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
n: 'test1',
|
|
35
|
+
v: 30,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
describe('#extractSubarrayFromSortedByNumberField', function () {
|
|
40
|
+
test('should extract the subarray', function () {
|
|
41
|
+
const subArr1: any[] = ArrayRatchet.extractSubarrayFromSortedByNumberField(sortedArr, 'v', 5, 14);
|
|
42
|
+
expect(subArr1.length).toEqual(2);
|
|
43
|
+
|
|
44
|
+
const subArr2: any[] = ArrayRatchet.extractSubarrayFromSortedByNumberField(sortedArr, 'v', null, 4);
|
|
45
|
+
expect(subArr2.length).toEqual(4);
|
|
46
|
+
|
|
47
|
+
const subArr3: any[] = ArrayRatchet.extractSubarrayFromSortedByNumberField(sortedArr, 'v', 7, null);
|
|
48
|
+
expect(subArr3.length).toEqual(4);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('#compareTwoArrays', function () {
|
|
53
|
+
test('should create a match report', function () {
|
|
54
|
+
const arr1: string[] = ['a', 'b', 'c'];
|
|
55
|
+
const arr2: string[] = ['a', 'e', 'i', 'o', 'u'];
|
|
56
|
+
|
|
57
|
+
const report: MatchReport<string> = ArrayRatchet.compareTwoArrays(arr1, arr2, (a, b) => a.localeCompare(b));
|
|
58
|
+
|
|
59
|
+
expect(report).toBeTruthy();
|
|
60
|
+
expect(report.setOneOnly.length).toEqual(2);
|
|
61
|
+
expect(report.setTwoOnly.length).toEqual(4);
|
|
62
|
+
expect(report.matching.length).toEqual(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('#findSplit', function () {
|
|
67
|
+
test('should find the split', function () {
|
|
68
|
+
// All values past the split point should be larger than the target
|
|
69
|
+
const split10: number = ArrayRatchet.findSplit(sortedArr, 'v', 10);
|
|
70
|
+
const split4: number = ArrayRatchet.findSplit(sortedArr, 'v', 4);
|
|
71
|
+
const split0: number = ArrayRatchet.findSplit(sortedArr, 'v', 0);
|
|
72
|
+
const split32: number = ArrayRatchet.findSplit(sortedArr, 'v', 32);
|
|
73
|
+
|
|
74
|
+
expect(split10).toEqual(4);
|
|
75
|
+
expect(split4).toEqual(3);
|
|
76
|
+
expect(split0).toEqual(null);
|
|
77
|
+
expect(split32).toEqual(7);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Functions for working with arrays
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { RequireRatchet } from './require-ratchet.js';
|
|
6
|
+
import { MapRatchet } from './map-ratchet.js';
|
|
7
|
+
|
|
8
|
+
export class ArrayRatchet {
|
|
9
|
+
public static wrapElementsInArray(input: any[]): any[][] {
|
|
10
|
+
return input.map((i) => [i]);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public static compareTwoArrays<T>(ar1: T[], ar2: T[], fn: ComparisonFunction<T>): MatchReport<T> {
|
|
14
|
+
ar1.sort(fn);
|
|
15
|
+
ar2.sort(fn);
|
|
16
|
+
|
|
17
|
+
let id1 = 0;
|
|
18
|
+
let id2 = 0;
|
|
19
|
+
const rval: MatchReport<T> = {
|
|
20
|
+
matching: [],
|
|
21
|
+
setOneOnly: [],
|
|
22
|
+
setTwoOnly: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
while (id1 < ar1.length && id2 < ar2.length) {
|
|
26
|
+
const aVal: T = ar1[id1];
|
|
27
|
+
const pVal: T = ar2[id2];
|
|
28
|
+
const comp: number = fn(aVal, pVal);
|
|
29
|
+
|
|
30
|
+
if (comp === 0) {
|
|
31
|
+
rval.matching.push(aVal);
|
|
32
|
+
id1++;
|
|
33
|
+
id2++;
|
|
34
|
+
} else if (comp < 0) {
|
|
35
|
+
rval.setOneOnly.push(aVal);
|
|
36
|
+
id1++;
|
|
37
|
+
} else {
|
|
38
|
+
rval.setTwoOnly.push(pVal);
|
|
39
|
+
id2++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (id1 < ar1.length - 1) {
|
|
44
|
+
rval.setOneOnly = rval.setOneOnly.concat(ar1.slice(id1));
|
|
45
|
+
}
|
|
46
|
+
if (id2 < ar2.length - 1) {
|
|
47
|
+
rval.setTwoOnly = rval.setTwoOnly.concat(ar2.slice(id2));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return rval;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Given a sorted array of type T with a field named fieldName of type R, perform
|
|
55
|
+
* binary search to find the top and bottom bounds and extract the result
|
|
56
|
+
* @param input Array to select from
|
|
57
|
+
* @param fieldDotPath Path of the field
|
|
58
|
+
* @param minInclusive min value of field to include
|
|
59
|
+
* @param maxExclusive max value of field to include
|
|
60
|
+
*/
|
|
61
|
+
public static extractSubarrayFromSortedByNumberField<T>(
|
|
62
|
+
input: T[],
|
|
63
|
+
fieldDotPath: string,
|
|
64
|
+
minInclusive: number,
|
|
65
|
+
maxExclusive: number,
|
|
66
|
+
): T[] {
|
|
67
|
+
if (!input || input.length === 0) {
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let bottomIdx: number = minInclusive === null ? 0 : ArrayRatchet.findSplit(input, fieldDotPath, minInclusive) || 0;
|
|
72
|
+
const topIdx: number = maxExclusive === null ? input.length : ArrayRatchet.findSplit(input, fieldDotPath, maxExclusive);
|
|
73
|
+
|
|
74
|
+
const bottomValue: number = MapRatchet.findValueDotPath(input[bottomIdx], fieldDotPath);
|
|
75
|
+
if (bottomIdx === input.length - 1 && bottomValue < minInclusive) {
|
|
76
|
+
// Highest value is still larger than min
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For min inclusive, have to handle this case
|
|
81
|
+
// Since it has to be able to handle the value===min case
|
|
82
|
+
if (bottomIdx < input.length && bottomIdx < topIdx && bottomValue < minInclusive) {
|
|
83
|
+
bottomIdx++;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return input.slice(bottomIdx, topIdx + 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Given a sorted array, find the location in the array where all the entries
|
|
91
|
+
* above that index have a value larger that the target (the index and under
|
|
92
|
+
* are less than or equal to the target)
|
|
93
|
+
* @param input Array to select from
|
|
94
|
+
* @param fieldName Name of the field
|
|
95
|
+
* @param target the value to search for
|
|
96
|
+
*/
|
|
97
|
+
public static findSplit(input: any[], fieldDotPath: string, target: number): number {
|
|
98
|
+
RequireRatchet.notNullOrUndefined(input);
|
|
99
|
+
RequireRatchet.notNullOrUndefined(fieldDotPath);
|
|
100
|
+
RequireRatchet.notNullOrUndefined(target);
|
|
101
|
+
|
|
102
|
+
if (input.length === 0 || MapRatchet.findValueDotPath(input[0], fieldDotPath) > target) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let min = 0;
|
|
107
|
+
let max: number = input.length;
|
|
108
|
+
let rval: number = null;
|
|
109
|
+
|
|
110
|
+
while (rval === null) {
|
|
111
|
+
const curIdx: number = Math.floor((min + max) / 2);
|
|
112
|
+
const curVal: number = MapRatchet.findValueDotPath(input[curIdx], fieldDotPath);
|
|
113
|
+
if (min === max || min === max - 1) {
|
|
114
|
+
rval = min;
|
|
115
|
+
} else if (curVal <= target) {
|
|
116
|
+
min = curIdx;
|
|
117
|
+
} else {
|
|
118
|
+
max = curIdx;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rval;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public static shuffleInPlace(array: any[]): void {
|
|
125
|
+
if (array?.length) {
|
|
126
|
+
// Ignore nulls and non-arrays
|
|
127
|
+
for (let i = array.length - 1; i >= 0; i--) {
|
|
128
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
129
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type ComparisonFunction<T> = (t1: T, t2: T) => number;
|
|
136
|
+
|
|
137
|
+
export interface MatchReport<T> {
|
|
138
|
+
matching: T[];
|
|
139
|
+
setOneOnly: T[];
|
|
140
|
+
setTwoOnly: T[];
|
|
141
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Base64Ratchet } from './base64-ratchet.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('#base64', function () {
|
|
5
|
+
test('should parse a Uint8Array from base64', function () {
|
|
6
|
+
const result: Uint8Array = Base64Ratchet.base64StringToUint8Array('dGVzdHVzZXI6dGVzdHBhc3M=');
|
|
7
|
+
expect(result).toBeTruthy();
|
|
8
|
+
expect(result.length).toEqual(17);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should parse a string from base64', function () {
|
|
12
|
+
const result: string = Base64Ratchet.base64StringToString('dGVzdHVzZXI6dGVzdHBhc3M=');
|
|
13
|
+
expect(result).toBeTruthy();
|
|
14
|
+
expect(result.length).toEqual(17);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should round-trip a string', function () {
|
|
18
|
+
const testString = 'teststring';
|
|
19
|
+
const enc: string = Base64Ratchet.generateBase64VersionOfString(testString);
|
|
20
|
+
const result: string = Base64Ratchet.base64StringToString(enc);
|
|
21
|
+
expect(result).toBeTruthy();
|
|
22
|
+
expect(result).toEqual(testString);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should round-trip an object', function () {
|
|
26
|
+
const testOb: any = { a: 'teststring', b: 27 };
|
|
27
|
+
const enc: string = Base64Ratchet.safeObjectToBase64JSON(testOb);
|
|
28
|
+
const result: any = Base64Ratchet.safeBase64JSONParse(enc);
|
|
29
|
+
expect(result).toBeTruthy();
|
|
30
|
+
expect(result.a).toEqual(testOb.a);
|
|
31
|
+
expect(result.b).toEqual(testOb.b);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should work on special characters', function () {
|
|
35
|
+
const src = '✓ à la mode';
|
|
36
|
+
const b64: string = Base64Ratchet.generateBase64VersionOfString(src);
|
|
37
|
+
const back: string = Base64Ratchet.base64StringToString(b64);
|
|
38
|
+
expect(b64).toEqual('4pyTIMOgIGxhIG1vZGU=');
|
|
39
|
+
expect(back).toEqual(src);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should work on arbitrary data', function () {
|
|
43
|
+
const srcB64 = '7zo_JDdubAcOMnLtkoth_rLRc6Zj5RKRpNGv_nTVYY4';
|
|
44
|
+
const back: Uint8Array = Base64Ratchet.base64UrlStringToBytes(srcB64);
|
|
45
|
+
expect(back).toBeTruthy();
|
|
46
|
+
expect(back.length).toEqual(32);
|
|
47
|
+
});
|
|
48
|
+
});
|