@atproto/common-web
Advanced tools
Comparing version 0.1.0 to 0.2.0
@@ -10,1 +10,2 @@ import { CID } from 'multiformats/cid'; | ||
export declare const ipldToJson: (val: IpldValue) => JsonValue; | ||
export declare const ipldEquals: (a: IpldValue, b: IpldValue) => boolean; |
export declare const utf8Len: (str: string) => number; | ||
export declare const graphemeLen: (str: string) => number; | ||
export declare const utf8ToB64Url: (utf8: string) => string; | ||
export declare const b64UrlToUtf8: (b64: string) => string; | ||
export declare const parseLanguage: (langTag: string) => LanguageTag | null; | ||
export declare const validateLanguage: (langTag: string) => boolean; | ||
export declare type LanguageTag = { | ||
grandfathered?: string; | ||
language?: string; | ||
extlang?: string; | ||
script?: string; | ||
region?: string; | ||
variant?: string; | ||
extension?: string; | ||
privateUse?: string; | ||
}; |
@@ -8,4 +8,4 @@ export declare class TID { | ||
static fromStr(str: string): TID; | ||
static oldestFirst(a: TID, b: TID): number; | ||
static newestFirst(a: TID, b: TID): number; | ||
static oldestFirst(a: TID, b: TID): number; | ||
static is(str: string): boolean; | ||
@@ -12,0 +12,0 @@ timestamp(): number; |
@@ -20,1 +20,2 @@ import { CID } from 'multiformats/cid'; | ||
export declare type ArrayEl<A> = A extends readonly (infer T)[] ? T : never; | ||
export declare type NotEmptyArray<T> = [T, ...T[]]; |
@@ -1,3 +0,3 @@ | ||
/// <reference types="node" /> | ||
export declare const noUndefinedVals: <T>(obj: Record<string, T>) => Record<string, T>; | ||
export declare const jitter: (maxMs: number) => number; | ||
export declare const wait: (ms: number) => Promise<unknown>; | ||
@@ -9,3 +9,3 @@ export declare const bailableWait: (ms: number) => { | ||
export declare const flattenUint8Arrays: (arrs: Uint8Array[]) => Uint8Array; | ||
export declare const streamToArray: (stream: AsyncIterable<Uint8Array>) => Promise<Uint8Array>; | ||
export declare const streamToBuffer: (stream: AsyncIterable<Uint8Array>) => Promise<Uint8Array>; | ||
export declare const s32encode: (i: number) => string; | ||
@@ -18,1 +18,3 @@ export declare const s32decode: (s: string) => number; | ||
export declare const range: (num: number) => number[]; | ||
export declare const dedupeStrs: (strs: string[]) => string[]; | ||
export declare const parseIntWithFallback: <T>(value: string | undefined, fallback: T) => number | T; |
{ | ||
"name": "@atproto/common-web", | ||
"version": "0.1.0", | ||
"version": "0.2.0", | ||
"main": "dist/index.js", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/bluesky-social/atproto.git", | ||
"directory": "packages/common-web" | ||
}, | ||
"scripts": { | ||
"test": "jest", | ||
"prettier": "prettier --check src/", | ||
"prettier:fix": "prettier --write src/", | ||
"prettier": "prettier --check src/ tests/", | ||
"prettier:fix": "prettier --write src/ tests/", | ||
"lint": "eslint . --ext .ts,.tsx", | ||
@@ -22,6 +27,7 @@ "lint:fix": "yarn lint --fix", | ||
"dependencies": { | ||
"graphemer": "^1.4.0", | ||
"multiformats": "^9.6.4", | ||
"uint8arrays": "3.0.0", | ||
"zod": "^3.14.2" | ||
"zod": "^3.21.4" | ||
} | ||
} |
@@ -78,1 +78,30 @@ import { CID } from 'multiformats/cid' | ||
} | ||
export const ipldEquals = (a: IpldValue, b: IpldValue): boolean => { | ||
// walk arrays | ||
if (Array.isArray(a) && Array.isArray(b)) { | ||
if (a.length !== b.length) return false | ||
for (let i = 0; i < a.length; i++) { | ||
if (!ipldEquals(a[i], b[i])) return false | ||
} | ||
return true | ||
} | ||
// objects | ||
if (a && b && typeof a === 'object' && typeof b === 'object') { | ||
// check bytes | ||
if (a instanceof Uint8Array && b instanceof Uint8Array) { | ||
return ui8.equals(a, b) | ||
} | ||
// check cids | ||
if (CID.asCID(a) && CID.asCID(b)) { | ||
return CID.asCID(a)?.equals(CID.asCID(b)) | ||
} | ||
// walk plain objects | ||
if (Object.keys(a).length !== Object.keys(b).length) return false | ||
for (const key of Object.keys(a)) { | ||
if (!ipldEquals(a[key], b[key])) return false | ||
} | ||
return true | ||
} | ||
return a === b | ||
} |
@@ -0,1 +1,4 @@ | ||
import Graphemer from 'graphemer' | ||
import * as ui8 from 'uint8arrays' | ||
// counts the number of bytes in a utf8 string | ||
@@ -8,3 +11,47 @@ export const utf8Len = (str: string): number => { | ||
export const graphemeLen = (str: string): number => { | ||
return [...new Intl.Segmenter().segment(str)].length | ||
const splitter = new Graphemer() | ||
return splitter.countGraphemes(str) | ||
} | ||
export const utf8ToB64Url = (utf8: string): string => { | ||
return ui8.toString(ui8.fromString(utf8, 'utf8'), 'base64url') | ||
} | ||
export const b64UrlToUtf8 = (b64: string): string => { | ||
return ui8.toString(ui8.fromString(b64, 'base64url'), 'utf8') | ||
} | ||
export const parseLanguage = (langTag: string): LanguageTag | null => { | ||
const parsed = langTag.match(bcp47Regexp) | ||
if (!parsed?.groups) return null | ||
const parts = parsed.groups | ||
return { | ||
grandfathered: parts.grandfathered, | ||
language: parts.language, | ||
extlang: parts.extlang, | ||
script: parts.script, | ||
region: parts.region, | ||
variant: parts.variant, | ||
extension: parts.extension, | ||
privateUse: parts.privateUseA || parts.privateUseB, | ||
} | ||
} | ||
export const validateLanguage = (langTag: string): boolean => { | ||
return bcp47Regexp.test(langTag) | ||
} | ||
export type LanguageTag = { | ||
grandfathered?: string | ||
language?: string | ||
extlang?: string | ||
script?: string | ||
region?: string | ||
variant?: string | ||
extension?: string | ||
privateUse?: string | ||
} | ||
// Validates well-formed BCP 47 syntax: https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1 | ||
const bcp47Regexp = | ||
/^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/ |
import { s32encode, s32decode } from './util' | ||
const TID_LEN = 13 | ||
let lastTimestamp = 0 | ||
@@ -6,2 +9,6 @@ let timestampCount = 0 | ||
function dedash(str: string): string { | ||
return str.replaceAll('-', '') | ||
} | ||
export class TID { | ||
@@ -11,4 +18,4 @@ str: string | ||
constructor(str: string) { | ||
const noDashes = str.replace(/-/g, '') | ||
if (noDashes.length !== 13) { | ||
const noDashes = dedash(str) | ||
if (noDashes.length !== TID_LEN) { | ||
throw new Error(`Poorly formatted TID: ${noDashes.length} length`) | ||
@@ -51,6 +58,2 @@ } | ||
static newestFirst(a: TID, b: TID): number { | ||
return a.compareTo(b) * -1 | ||
} | ||
static oldestFirst(a: TID, b: TID): number { | ||
@@ -60,19 +63,16 @@ return a.compareTo(b) | ||
static newestFirst(a: TID, b: TID): number { | ||
return b.compareTo(a) | ||
} | ||
static is(str: string): boolean { | ||
try { | ||
TID.fromStr(str) | ||
return true | ||
} catch (err) { | ||
return false | ||
} | ||
return dedash(str).length === TID_LEN | ||
} | ||
timestamp(): number { | ||
const substr = this.str.slice(0, 11) | ||
return s32decode(substr) | ||
return s32decode(this.str.slice(0, 11)) | ||
} | ||
clockid(): number { | ||
const substr = this.str.slice(11, 13) | ||
return s32decode(substr) | ||
return s32decode(this.str.slice(11, 13)) | ||
} | ||
@@ -100,3 +100,3 @@ | ||
equals(other: TID): boolean { | ||
return this.compareTo(other) === 0 | ||
return this.str === other.str | ||
} | ||
@@ -103,0 +103,0 @@ |
@@ -45,1 +45,3 @@ import { CID } from 'multiformats/cid' | ||
export type ArrayEl<A> = A extends readonly (infer T)[] ? T : never | ||
export type NotEmptyArray<T> = [T, ...T[]] |
@@ -12,2 +12,6 @@ export const noUndefinedVals = <T>( | ||
export const jitter = (maxMs: number) => { | ||
return Math.round((Math.random() - 0.5) * maxMs * 2) | ||
} | ||
export const wait = (ms: number) => { | ||
@@ -44,3 +48,3 @@ return new Promise((res) => setTimeout(res, ms)) | ||
export const streamToArray = async ( | ||
export const streamToBuffer = async ( | ||
stream: AsyncIterable<Uint8Array>, | ||
@@ -111,1 +115,13 @@ ): Promise<Uint8Array> => { | ||
} | ||
export const dedupeStrs = (strs: string[]): string[] => { | ||
return [...new Set(strs)] | ||
} | ||
export const parseIntWithFallback = <T>( | ||
value: string | undefined, | ||
fallback: T, | ||
): number | T => { | ||
const parsed = parseInt(value || '', 10) | ||
return isNaN(parsed) ? fallback : parsed | ||
} |
@@ -1,2 +0,2 @@ | ||
import { graphemeLen, utf8Len } from '../src' | ||
import { graphemeLen, parseLanguage, utf8Len, validateLanguage } from '../src' | ||
@@ -17,3 +17,3 @@ describe('string', () => { | ||
it('caluclates grapheme length', () => { | ||
it('calculates grapheme length', () => { | ||
expect(graphemeLen('a')).toBe(1) | ||
@@ -31,2 +31,86 @@ expect(graphemeLen('~')).toBe(1) | ||
}) | ||
describe('languages', () => { | ||
it('validates BCP 47', () => { | ||
// valid | ||
expect(validateLanguage('de')).toEqual(true) | ||
expect(validateLanguage('de-CH')).toEqual(true) | ||
expect(validateLanguage('de-DE-1901')).toEqual(true) | ||
expect(validateLanguage('es-419')).toEqual(true) | ||
expect(validateLanguage('sl-IT-nedis')).toEqual(true) | ||
expect(validateLanguage('mn-Cyrl-MN')).toEqual(true) | ||
expect(validateLanguage('x-fr-CH')).toEqual(true) | ||
expect( | ||
validateLanguage('en-GB-boont-r-extended-sequence-x-private'), | ||
).toEqual(true) | ||
expect(validateLanguage('sr-Cyrl')).toEqual(true) | ||
expect(validateLanguage('hy-Latn-IT-arevela')).toEqual(true) | ||
expect(validateLanguage('i-klingon')).toEqual(true) | ||
// invalid | ||
expect(validateLanguage('')).toEqual(false) | ||
expect(validateLanguage('x')).toEqual(false) | ||
expect(validateLanguage('de-CH-')).toEqual(false) | ||
expect(validateLanguage('i-bad-grandfathered')).toEqual(false) | ||
}) | ||
it('parses BCP 47', () => { | ||
// valid | ||
expect(parseLanguage('de')).toEqual({ | ||
language: 'de', | ||
}) | ||
expect(parseLanguage('de-CH')).toEqual({ | ||
language: 'de', | ||
region: 'CH', | ||
}) | ||
expect(parseLanguage('de-DE-1901')).toEqual({ | ||
language: 'de', | ||
region: 'DE', | ||
variant: '1901', | ||
}) | ||
expect(parseLanguage('es-419')).toEqual({ | ||
language: 'es', | ||
region: '419', | ||
}) | ||
expect(parseLanguage('sl-IT-nedis')).toEqual({ | ||
language: 'sl', | ||
region: 'IT', | ||
variant: 'nedis', | ||
}) | ||
expect(parseLanguage('mn-Cyrl-MN')).toEqual({ | ||
language: 'mn', | ||
script: 'Cyrl', | ||
region: 'MN', | ||
}) | ||
expect(parseLanguage('x-fr-CH')).toEqual({ | ||
privateUse: 'x-fr-CH', | ||
}) | ||
expect( | ||
parseLanguage('en-GB-boont-r-extended-sequence-x-private'), | ||
).toEqual({ | ||
language: 'en', | ||
region: 'GB', | ||
variant: 'boont', | ||
extension: 'r-extended-sequence', | ||
privateUse: 'x-private', | ||
}) | ||
expect(parseLanguage('sr-Cyrl')).toEqual({ | ||
language: 'sr', | ||
script: 'Cyrl', | ||
}) | ||
expect(parseLanguage('hy-Latn-IT-arevela')).toEqual({ | ||
language: 'hy', | ||
script: 'Latn', | ||
region: 'IT', | ||
variant: 'arevela', | ||
}) | ||
expect(parseLanguage('i-klingon')).toEqual({ | ||
grandfathered: 'i-klingon', | ||
}) | ||
// invalid | ||
expect(parseLanguage('')).toEqual(null) | ||
expect(parseLanguage('x')).toEqual(null) | ||
expect(parseLanguage('de-CH-')).toEqual(null) | ||
expect(parseLanguage('i-bad-grandfathered')).toEqual(null) | ||
}) | ||
}) | ||
}) |
@@ -18,2 +18,120 @@ import TID from '../src/tid' | ||
}) | ||
it('throws if invalid tid passed', () => { | ||
expect(() => new TID('')).toThrow('Poorly formatted TID: 0 length') | ||
}) | ||
describe('nextStr', () => { | ||
it('returns next tid as a string', () => { | ||
const str = TID.nextStr() | ||
expect(typeof str).toEqual('string') | ||
expect(str.length).toEqual(13) | ||
}) | ||
}) | ||
describe('newestFirst', () => { | ||
it('sorts tids newest first', () => { | ||
const oldest = TID.next() | ||
const newest = TID.next() | ||
const tids = [oldest, newest] | ||
tids.sort(TID.newestFirst) | ||
expect(tids).toEqual([newest, oldest]) | ||
}) | ||
}) | ||
describe('oldestFirst', () => { | ||
it('sorts tids oldest first', () => { | ||
const oldest = TID.next() | ||
const newest = TID.next() | ||
const tids = [newest, oldest] | ||
tids.sort(TID.oldestFirst) | ||
expect(tids).toEqual([oldest, newest]) | ||
}) | ||
}) | ||
describe('is', () => { | ||
it('true for valid tids', () => { | ||
const tid = TID.next() | ||
const asStr = tid.toString() | ||
expect(TID.is(asStr)).toBe(true) | ||
}) | ||
it('false for invalid tids', () => { | ||
expect(TID.is('')).toBe(false) | ||
}) | ||
}) | ||
describe('equals', () => { | ||
it('true when same tid', () => { | ||
const tid = TID.next() | ||
expect(tid.equals(tid)).toBe(true) | ||
}) | ||
it('true when different instance, same tid', () => { | ||
const tid0 = TID.next() | ||
const tid1 = new TID(tid0.toString()) | ||
expect(tid0.equals(tid1)).toBe(true) | ||
}) | ||
it('false when different tid', () => { | ||
const tid0 = TID.next() | ||
const tid1 = TID.next() | ||
expect(tid0.equals(tid1)).toBe(false) | ||
}) | ||
}) | ||
describe('newerThan', () => { | ||
it('true for newer tid', () => { | ||
const tid0 = TID.next() | ||
const tid1 = TID.next() | ||
expect(tid1.newerThan(tid0)).toBe(true) | ||
}) | ||
it('false for older tid', () => { | ||
const tid0 = TID.next() | ||
const tid1 = TID.next() | ||
expect(tid0.newerThan(tid1)).toBe(false) | ||
}) | ||
it('false for identical tids', () => { | ||
const tid0 = TID.next() | ||
const tid1 = new TID(tid0.toString()) | ||
expect(tid0.newerThan(tid1)).toBe(false) | ||
}) | ||
}) | ||
describe('olderThan', () => { | ||
it('true for older tid', () => { | ||
const tid0 = TID.next() | ||
const tid1 = TID.next() | ||
expect(tid0.olderThan(tid1)).toBe(true) | ||
}) | ||
it('false for newer tid', () => { | ||
const tid0 = TID.next() | ||
const tid1 = TID.next() | ||
expect(tid1.olderThan(tid0)).toBe(false) | ||
}) | ||
it('false for identical tids', () => { | ||
const tid0 = TID.next() | ||
const tid1 = new TID(tid0.toString()) | ||
expect(tid0.olderThan(tid1)).toBe(false) | ||
}) | ||
}) | ||
}) |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
1895988
33
15113
1
4
1
+ Addedgraphemer@^1.4.0
+ Addedgraphemer@1.4.0(transitive)
Updatedzod@^3.21.4