@atproto/lex-data 0.0.3 → 0.0.5

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/blob.d.ts +6 -0
  3. package/dist/blob.d.ts.map +1 -1
  4. package/dist/blob.js +2 -2
  5. package/dist/blob.js.map +1 -1
  6. package/dist/cid.d.ts +2 -3
  7. package/dist/cid.d.ts.map +1 -1
  8. package/dist/cid.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/language.d.ts +2 -2
  14. package/dist/language.d.ts.map +1 -1
  15. package/dist/language.js +4 -4
  16. package/dist/language.js.map +1 -1
  17. package/dist/lex-error.d.ts +17 -0
  18. package/dist/lex-error.d.ts.map +1 -0
  19. package/dist/lex-error.js +26 -0
  20. package/dist/lex-error.js.map +1 -0
  21. package/dist/lex.d.ts.map +1 -1
  22. package/dist/lex.js +49 -48
  23. package/dist/lex.js.map +1 -1
  24. package/dist/lib/nodejs-buffer.d.ts +1 -0
  25. package/dist/lib/nodejs-buffer.d.ts.map +1 -1
  26. package/dist/lib/nodejs-buffer.js +1 -1
  27. package/dist/lib/nodejs-buffer.js.map +1 -1
  28. package/dist/object.d.ts +13 -1
  29. package/dist/object.d.ts.map +1 -1
  30. package/dist/object.js +15 -2
  31. package/dist/object.js.map +1 -1
  32. package/dist/uint8array-concat.d.ts +3 -0
  33. package/dist/uint8array-concat.d.ts.map +1 -0
  34. package/dist/uint8array-concat.js +24 -0
  35. package/dist/uint8array-concat.js.map +1 -0
  36. package/dist/uint8array-from-base64.d.ts.map +1 -1
  37. package/dist/uint8array-from-base64.js +1 -1
  38. package/dist/uint8array-from-base64.js.map +1 -1
  39. package/dist/uint8array.d.ts +1 -0
  40. package/dist/uint8array.d.ts.map +1 -1
  41. package/dist/uint8array.js +14 -3
  42. package/dist/uint8array.js.map +1 -1
  43. package/dist/utf8-grapheme-len.d.ts.map +1 -1
  44. package/dist/utf8-grapheme-len.js +2 -2
  45. package/dist/utf8-grapheme-len.js.map +1 -1
  46. package/dist/utf8-len.d.ts.map +1 -1
  47. package/dist/utf8-len.js +1 -1
  48. package/dist/utf8-len.js.map +1 -1
  49. package/package.json +5 -5
  50. package/src/blob.test.ts +83 -2
  51. package/src/blob.ts +8 -2
  52. package/src/cid.test.ts +126 -0
  53. package/src/cid.ts +7 -7
  54. package/src/index.ts +1 -0
  55. package/src/language.test.ts +34 -33
  56. package/src/language.ts +2 -2
  57. package/src/lex-equals.test.ts +30 -0
  58. package/src/lex-error.ts +34 -0
  59. package/src/lex.test.ts +156 -1
  60. package/src/lex.ts +50 -43
  61. package/src/lib/nodejs-buffer.ts +2 -1
  62. package/src/object.test.ts +2 -0
  63. package/src/object.ts +16 -4
  64. package/src/uint8array-concat.test.ts +197 -0
  65. package/src/uint8array-concat.ts +21 -0
  66. package/src/uint8array-from-base64.test.ts +4 -1
  67. package/src/uint8array-from-base64.ts +1 -1
  68. package/src/uint8array-to-base64.test.ts +1 -1
  69. package/src/uint8array.test.ts +484 -0
  70. package/src/uint8array.ts +14 -2
  71. package/src/utf8-grapheme-len.test.ts +1 -0
  72. package/src/utf8-grapheme-len.ts +2 -2
  73. package/src/utf8-len.test.ts +1 -0
  74. package/src/utf8-len.ts +1 -1
  75. package/tsconfig.tests.json +2 -2
@@ -1,62 +1,63 @@
1
- import { isLanguage, parseLanguage } from './language'
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isLanguageString, parseLanguageString } from './language'
2
3
 
3
4
  describe('string', () => {
4
5
  describe('languages', () => {
5
6
  it('validates BCP 47', () => {
6
7
  // valid
7
- expect(isLanguage('de')).toEqual(true)
8
- expect(isLanguage('de-CH')).toEqual(true)
9
- expect(isLanguage('de-DE-1901')).toEqual(true)
10
- expect(isLanguage('es-419')).toEqual(true)
11
- expect(isLanguage('sl-IT-nedis')).toEqual(true)
12
- expect(isLanguage('mn-Cyrl-MN')).toEqual(true)
13
- expect(isLanguage('x-fr-CH')).toEqual(true)
14
- expect(isLanguage('en-GB-boont-r-extended-sequence-x-private')).toEqual(
15
- true,
16
- )
17
- expect(isLanguage('sr-Cyrl')).toEqual(true)
18
- expect(isLanguage('hy-Latn-IT-arevela')).toEqual(true)
19
- expect(isLanguage('i-klingon')).toEqual(true)
8
+ expect(isLanguageString('de')).toEqual(true)
9
+ expect(isLanguageString('de-CH')).toEqual(true)
10
+ expect(isLanguageString('de-DE-1901')).toEqual(true)
11
+ expect(isLanguageString('es-419')).toEqual(true)
12
+ expect(isLanguageString('sl-IT-nedis')).toEqual(true)
13
+ expect(isLanguageString('mn-Cyrl-MN')).toEqual(true)
14
+ expect(isLanguageString('x-fr-CH')).toEqual(true)
15
+ expect(
16
+ isLanguageString('en-GB-boont-r-extended-sequence-x-private'),
17
+ ).toEqual(true)
18
+ expect(isLanguageString('sr-Cyrl')).toEqual(true)
19
+ expect(isLanguageString('hy-Latn-IT-arevela')).toEqual(true)
20
+ expect(isLanguageString('i-klingon')).toEqual(true)
20
21
  // invalid
21
- expect(isLanguage('')).toEqual(false)
22
- expect(isLanguage('x')).toEqual(false)
23
- expect(isLanguage('de-CH-')).toEqual(false)
24
- expect(isLanguage('i-bad-grandfathered')).toEqual(false)
22
+ expect(isLanguageString('')).toEqual(false)
23
+ expect(isLanguageString('x')).toEqual(false)
24
+ expect(isLanguageString('de-CH-')).toEqual(false)
25
+ expect(isLanguageString('i-bad-grandfathered')).toEqual(false)
25
26
  })
26
27
 
27
28
  it('parses BCP 47', () => {
28
29
  // valid
29
- expect(parseLanguage('de')).toEqual({
30
+ expect(parseLanguageString('de')).toEqual({
30
31
  language: 'de',
31
32
  })
32
- expect(parseLanguage('de-CH')).toEqual({
33
+ expect(parseLanguageString('de-CH')).toEqual({
33
34
  language: 'de',
34
35
  region: 'CH',
35
36
  })
36
- expect(parseLanguage('de-DE-1901')).toEqual({
37
+ expect(parseLanguageString('de-DE-1901')).toEqual({
37
38
  language: 'de',
38
39
  region: 'DE',
39
40
  variant: '1901',
40
41
  })
41
- expect(parseLanguage('es-419')).toEqual({
42
+ expect(parseLanguageString('es-419')).toEqual({
42
43
  language: 'es',
43
44
  region: '419',
44
45
  })
45
- expect(parseLanguage('sl-IT-nedis')).toEqual({
46
+ expect(parseLanguageString('sl-IT-nedis')).toEqual({
46
47
  language: 'sl',
47
48
  region: 'IT',
48
49
  variant: 'nedis',
49
50
  })
50
- expect(parseLanguage('mn-Cyrl-MN')).toEqual({
51
+ expect(parseLanguageString('mn-Cyrl-MN')).toEqual({
51
52
  language: 'mn',
52
53
  script: 'Cyrl',
53
54
  region: 'MN',
54
55
  })
55
- expect(parseLanguage('x-fr-CH')).toEqual({
56
+ expect(parseLanguageString('x-fr-CH')).toEqual({
56
57
  privateUse: 'x-fr-CH',
57
58
  })
58
59
  expect(
59
- parseLanguage('en-GB-boont-r-extended-sequence-x-private'),
60
+ parseLanguageString('en-GB-boont-r-extended-sequence-x-private'),
60
61
  ).toEqual({
61
62
  language: 'en',
62
63
  region: 'GB',
@@ -64,24 +65,24 @@ describe('string', () => {
64
65
  extension: 'r-extended-sequence',
65
66
  privateUse: 'x-private',
66
67
  })
67
- expect(parseLanguage('sr-Cyrl')).toEqual({
68
+ expect(parseLanguageString('sr-Cyrl')).toEqual({
68
69
  language: 'sr',
69
70
  script: 'Cyrl',
70
71
  })
71
- expect(parseLanguage('hy-Latn-IT-arevela')).toEqual({
72
+ expect(parseLanguageString('hy-Latn-IT-arevela')).toEqual({
72
73
  language: 'hy',
73
74
  script: 'Latn',
74
75
  region: 'IT',
75
76
  variant: 'arevela',
76
77
  })
77
- expect(parseLanguage('i-klingon')).toEqual({
78
+ expect(parseLanguageString('i-klingon')).toEqual({
78
79
  grandfathered: 'i-klingon',
79
80
  })
80
81
  // invalid
81
- expect(parseLanguage('')).toEqual(null)
82
- expect(parseLanguage('x')).toEqual(null)
83
- expect(parseLanguage('de-CH-')).toEqual(null)
84
- expect(parseLanguage('i-bad-grandfathered')).toEqual(null)
82
+ expect(parseLanguageString('')).toEqual(null)
83
+ expect(parseLanguageString('x')).toEqual(null)
84
+ expect(parseLanguageString('de-CH-')).toEqual(null)
85
+ expect(parseLanguageString('i-bad-grandfathered')).toEqual(null)
85
86
  })
86
87
  })
87
88
  })
package/src/language.ts CHANGED
@@ -12,7 +12,7 @@ export type LanguageTag = {
12
12
  privateUse?: string
13
13
  }
14
14
 
15
- export function parseLanguage(input: string): LanguageTag | null {
15
+ export function parseLanguageString(input: string): LanguageTag | null {
16
16
  const parsed = input.match(BCP47_REGEXP)
17
17
  if (!parsed?.groups) return null
18
18
 
@@ -34,6 +34,6 @@ export function parseLanguage(input: string): LanguageTag | null {
34
34
  *
35
35
  * @see {@link https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1}
36
36
  */
37
- export function isLanguage(input: string): boolean {
37
+ export function isLanguageString(input: string): boolean {
38
38
  return BCP47_REGEXP.test(input)
39
39
  }
@@ -1,3 +1,4 @@
1
+ import { describe, expect, it } from 'vitest'
1
2
  import { parseCid } from './cid.js'
2
3
  import { lexEquals } from './lex-equals.js'
3
4
  import { LexValue } from './lex.js'
@@ -38,6 +39,7 @@ describe('lexEquals', () => {
38
39
  expectLexEqual([1, 2, 3], [1, 2, 4], false)
39
40
  expectLexEqual([1, 2, 3], [1, 2], false)
40
41
  expectLexEqual([1, 2, 3], 'not an array', false)
42
+ expectLexEqual([1, 2, 3], { 0: 1, 1: 2, 2: 3 }, false)
41
43
  })
42
44
 
43
45
  it('compares Uint8Arrays', () => {
@@ -59,10 +61,23 @@ describe('lexEquals', () => {
59
61
  expectLexEqual(cid2, cid3, true)
60
62
 
61
63
  expectLexEqual(cid1, cid1.toString(), false)
64
+ expectLexEqual(cid1, { not: 'a cid' }, false)
65
+ expectLexEqual(cid1, [], false)
66
+ expectLexEqual(cid1, cid1.bytes, false)
62
67
  })
63
68
 
64
69
  it('compares objects', () => {
65
70
  expectLexEqual({ a: 1, b: 2 }, { a: 1, b: 2 }, true)
71
+ expectLexEqual(
72
+ { a: 1, b: 2, c: undefined },
73
+ { a: 1, b: 2, c: undefined },
74
+ true,
75
+ )
76
+ expectLexEqual(
77
+ { a: 1, b: 2, c: { e: 1, d: undefined } },
78
+ { a: 1, b: 2, c: { d: undefined, e: 1 } },
79
+ true,
80
+ )
66
81
  expectLexEqual(
67
82
  { a: 1, b: { unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧' } },
68
83
  { a: 1, b: { unicode: 'a~öñ©⽘☎𓋓😀👨‍👩‍👧‍👧' } },
@@ -75,6 +90,21 @@ describe('lexEquals', () => {
75
90
  expectLexEqual({ a: 1, b: 2 }, null, false)
76
91
  })
77
92
 
93
+ it('accounts for undefined (but present) properties in objects', () => {
94
+ expectLexEqual({ a: 1, b: undefined }, { a: 1 }, false)
95
+ expectLexEqual(
96
+ { a: 1, b: { c: undefined, d: 2 } },
97
+ { a: 1, b: { d: 2 } },
98
+ false,
99
+ )
100
+
101
+ expectLexEqual(
102
+ { a: 1, b: { c: undefined, d: 2 } },
103
+ { a: 1, b: { c: 3, d: 2 } },
104
+ false,
105
+ )
106
+ })
107
+
78
108
  it('compares nested structures', () => {
79
109
  const lex1 = {
80
110
  foo: [1, 2, { bar: new Uint8Array([3, 4, 5]) }],
@@ -0,0 +1,34 @@
1
+ export type LexErrorCode = string
2
+
3
+ export type LexErrorData<N extends LexErrorCode = LexErrorCode> = {
4
+ error: N
5
+ message?: string
6
+ }
7
+
8
+ export class LexError<N extends LexErrorCode = LexErrorCode> extends Error {
9
+ name = 'LexError'
10
+
11
+ constructor(
12
+ readonly error: N,
13
+ message?: string,
14
+ options?: ErrorOptions,
15
+ ) {
16
+ super(message, options)
17
+ }
18
+
19
+ toString(): string {
20
+ return `${this.name}: [${this.error}] ${this.message}`
21
+ }
22
+
23
+ toJSON(): LexErrorData<N> {
24
+ const { error, message } = this
25
+ return { error, message: message ?? undefined }
26
+ }
27
+
28
+ /**
29
+ * Translate into an HTTP response for downstream clients.
30
+ */
31
+ toResponse(): Response {
32
+ return Response.json(this.toJSON(), { status: 400 })
33
+ }
34
+ }
package/src/lex.test.ts CHANGED
@@ -1,4 +1,159 @@
1
- import { isTypedLexMap } from './lex'
1
+ import { describe, expect, it } from 'vitest'
2
+ import { parseCid } from './cid.js'
3
+ import { isLexArray, isLexScalar, isLexValue, isTypedLexMap } from './lex.js'
4
+
5
+ describe('isLexScalar', () => {
6
+ for (const { note, value, expected } of [
7
+ { note: 'string', value: 'hello', expected: true },
8
+ { note: 'boolean', value: true, expected: true },
9
+ { note: 'null', value: null, expected: true },
10
+ { note: 'Uint8Array', value: new Uint8Array([1, 2, 3]), expected: true },
11
+ {
12
+ note: 'Cid',
13
+ value: parseCid(
14
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
15
+ ),
16
+ expected: true,
17
+ },
18
+ { note: 'number (integer)', value: 42, expected: true },
19
+ { note: 'number (float)', value: 3.14, expected: false },
20
+ { note: 'object', value: { a: 1 }, expected: false },
21
+ { note: 'array', value: [1, 2, 3], expected: false },
22
+ { note: 'undefined', value: undefined, expected: false },
23
+ { note: 'function', value: () => {}, expected: false },
24
+ ]) {
25
+ it(note, () => {
26
+ const result = isLexScalar(value)
27
+ expect(result).toBe(expected)
28
+ })
29
+ }
30
+ })
31
+
32
+ describe('isLexArray', () => {
33
+ it('returns true for valid LexArray', () => {
34
+ const list = [123, 'blah', true, null, new Uint8Array([1, 2, 3]), { a: 1 }]
35
+ expect(isLexArray(list)).toBe(true)
36
+ })
37
+
38
+ it('returns false for non-arrays', () => {
39
+ const values = [
40
+ 123,
41
+ 'blah',
42
+ true,
43
+ null,
44
+ new Uint8Array([1, 2, 3]),
45
+ { a: 1 },
46
+ ]
47
+ for (const value of values) {
48
+ expect(isLexArray(value)).toBe(false)
49
+ }
50
+ })
51
+
52
+ it('returns false for arrays with non-Lex values', () => {
53
+ expect(isLexArray([123, 'blah', () => {}])).toBe(false)
54
+ expect(isLexArray([123, 'blah', undefined])).toBe(false)
55
+ })
56
+ })
57
+
58
+ describe('isLexValue', () => {
59
+ describe('valid values', () => {
60
+ for (const { note, value } of [
61
+ { note: 'string', value: 'hello' },
62
+ { note: 'boolean', value: true },
63
+ { note: 'null', value: null },
64
+ { note: 'Uint8Array', value: new Uint8Array([1, 2, 3]) },
65
+ {
66
+ note: 'Cid',
67
+ value: parseCid(
68
+ 'bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a',
69
+ ),
70
+ },
71
+ {
72
+ note: 'record with Lex values',
73
+ value: {
74
+ a: 123,
75
+ b: 'blah',
76
+ c: true,
77
+ d: null,
78
+ e: new Uint8Array([1, 2, 3]),
79
+ f: {
80
+ nested: 'value',
81
+ },
82
+ g: [1, 2, 3],
83
+ },
84
+ },
85
+ {
86
+ note: 'list with Lex values',
87
+ value: [
88
+ 123,
89
+ 'blah',
90
+ true,
91
+ null,
92
+ new Uint8Array([1, 2, 3]),
93
+ {
94
+ nested: 'value',
95
+ },
96
+ [1, 2, 3],
97
+ ],
98
+ },
99
+ ]) {
100
+ it(note, () => {
101
+ expect(isLexValue(value)).toBe(true)
102
+ })
103
+ }
104
+ })
105
+
106
+ describe('invalid values', () => {
107
+ for (const { note, value } of [
108
+ { note: 'float', value: 123.456 },
109
+ { note: 'undefined', value: undefined },
110
+ { note: 'function', value: () => {} },
111
+ { note: 'obj with fn', value: { a: 123, b: () => {} } },
112
+ { note: 'list with non-Lex value', value: [123, 'blah', () => {}] },
113
+ { note: 'Date object', value: new Date() },
114
+ { note: 'Map object', value: new Map() },
115
+ { note: 'Set object', value: new Set() },
116
+ { note: 'class instance', value: new (class A {})() },
117
+ ]) {
118
+ it(note, () => {
119
+ expect(isLexValue(value)).toBe(false)
120
+ })
121
+ }
122
+ })
123
+
124
+ it('handles cyclic structures', () => {
125
+ const record: any = {
126
+ a: 123,
127
+ b: 'blah',
128
+ }
129
+ record.c = record
130
+
131
+ expect(isLexValue(record)).toBe(false)
132
+
133
+ const list: any[] = [123, 'blah']
134
+ list.push(list)
135
+
136
+ expect(isLexValue(list)).toBe(false)
137
+
138
+ const complex: any = {
139
+ a: {
140
+ b: [1, 2, 3],
141
+ },
142
+ }
143
+ complex.a.b.push(complex)
144
+
145
+ expect(isLexValue(complex)).toBe(false)
146
+ })
147
+
148
+ it('handles deeply nested structures', () => {
149
+ type Value = null | { nested: Value }
150
+ let value: Value = null
151
+ for (let i = 0; i < 1_000_000; i++) {
152
+ value = { nested: value }
153
+ }
154
+ expect(isLexValue(value)).toBe(true)
155
+ })
156
+ })
2
157
 
3
158
  describe('isLexMap', () => {
4
159
  it('returns true for valid LexMap', () => {
package/src/lex.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import { Cid, isCid } from './cid.js'
2
- import { isPlainObject } from './object.js'
3
-
4
- // @NOTE BlobRef is just a special case of LexMap.
2
+ import { isPlainObject, isPlainProto } from './object.js'
5
3
 
6
4
  export type LexScalar = number | string | boolean | null | Cid | Uint8Array
7
5
  export type LexValue = LexScalar | LexValue[] | { [_ in string]?: LexValue }
@@ -9,65 +7,74 @@ export type LexMap = { [_ in string]?: LexValue }
9
7
  export type LexArray = LexValue[]
10
8
 
11
9
  export function isLexMap(value: unknown): value is LexMap {
12
- if (!isPlainObject(value)) return false
13
- for (const key in value) {
14
- if (!isLexValue(value[key])) return false
15
- }
16
- return true
10
+ return isPlainObject(value) && Object.values(value).every(isLexValue)
17
11
  }
18
12
 
19
13
  export function isLexArray(value: unknown): value is LexArray {
20
- if (!Array.isArray(value)) return false
21
- for (let i = 0; i < value.length; i++) {
22
- if (!isLexValue(value[i])) return false
23
- }
24
- return true
14
+ return Array.isArray(value) && value.every(isLexValue)
25
15
  }
26
16
 
27
17
  export function isLexScalar(value: unknown): value is LexScalar {
28
18
  switch (typeof value) {
29
19
  case 'object':
30
- if (value === null) return true
31
- return value instanceof Uint8Array || isCid(value)
20
+ return value === null || value instanceof Uint8Array || isCid(value)
32
21
  case 'string':
33
22
  case 'boolean':
34
23
  return true
35
24
  case 'number':
36
25
  if (Number.isInteger(value)) return true
37
- throw new TypeError(`Invalid Lex value: ${value}`)
26
+ // fallthrough
38
27
  default:
39
- throw new TypeError(`Invalid Lex value: ${typeof value}`)
28
+ return false
40
29
  }
41
30
  }
42
31
 
43
32
  export function isLexValue(value: unknown): value is LexValue {
44
- switch (typeof value) {
45
- case 'number':
46
- if (!Number.isInteger(value)) return false
47
- // fallthrough
48
- case 'string':
49
- case 'boolean':
50
- return true
51
- case 'object':
52
- if (value === null) return true
53
- if (Array.isArray(value)) {
54
- for (let i = 0; i < value.length; i++) {
55
- if (!isLexValue(value[i])) return false
56
- }
57
- return true
58
- }
59
- if (isPlainObject(value)) {
60
- for (const key in value) {
61
- if (!isLexValue(value[key])) return false
33
+ // Using a stack to avoid recursion depth issues.
34
+ const stack: unknown[] = [value]
35
+ // Cyclic structures are not valid LexValues as they cannot be serialized to
36
+ // JSON or CBOR. This also allows us to avoid infinite loops when traversing
37
+ // the structure.
38
+ const visited = new Set<object>()
39
+
40
+ do {
41
+ const value = stack.pop()!
42
+
43
+ // Optimization: we are not using `isLexScalar` here to avoid extra function
44
+ // calls, and to avoid computing `typeof value` multiple times.
45
+ switch (typeof value) {
46
+ case 'object':
47
+ if (value === null) {
48
+ // LexScalar
49
+ } else if (isPlainProto(value)) {
50
+ if (visited.has(value)) return false
51
+ visited.add(value)
52
+ stack.push(...Object.values(value))
53
+ } else if (Array.isArray(value)) {
54
+ if (visited.has(value)) return false
55
+ visited.add(value)
56
+ stack.push(...value)
57
+ } else if (value instanceof Uint8Array || isCid(value)) {
58
+ // LexScalar
59
+ } else {
60
+ return false
62
61
  }
63
- return true
64
- }
65
- if (value instanceof Uint8Array) return true
66
- if (isCid(value)) return true
67
- // fallthrough
68
- default:
69
- return false
70
- }
62
+ break
63
+ case 'string':
64
+ case 'boolean':
65
+ break
66
+ case 'number':
67
+ if (Number.isInteger(value)) break
68
+ // fallthrough
69
+ default:
70
+ return false
71
+ }
72
+ } while (stack.length > 0)
73
+
74
+ // Optimization: ease GC's work
75
+ visited.clear()
76
+
77
+ return true
71
78
  }
72
79
 
73
80
  export type TypedLexMap = LexMap & { $type: string }
@@ -12,6 +12,7 @@ interface NodeJSBufferConstructor {
12
12
  input: Uint8Array | ArrayBuffer | ArrayBufferView,
13
13
  ): NodeJSBuffer<ArrayBuffer>
14
14
  from(input: string, encoding?: Encoding): NodeJSBuffer<ArrayBuffer>
15
+ concat(list: readonly Uint8Array[], totalLength?: number): NodeJSBuffer
15
16
  byteLength(input: string, encoding?: Encoding): number
16
17
  prototype: NodeJSBuffer
17
18
  }
@@ -24,4 +25,4 @@ export const NodeJSBuffer: NodeJSBufferConstructor | null =
24
25
  (globalThis as any)?.[BUFFER]?.prototype instanceof Uint8Array &&
25
26
  'byteLength' in (globalThis as any)[BUFFER]
26
27
  ? ((globalThis as any)[BUFFER] as NodeJSBufferConstructor)
27
- : null
28
+ : /* v8 ignore next -- @preserve */ null
@@ -1,5 +1,7 @@
1
+ import { describe, expect, it } from 'vitest'
1
2
  import { parseCid } from './cid.js'
2
3
  import { isObject, isPlainObject } from './object.js'
4
+
3
5
  describe('isObject', () => {
4
6
  it('returns true for plain objects', () => {
5
7
  expect(isObject({})).toBe(true)
package/src/object.ts CHANGED
@@ -1,3 +1,6 @@
1
+ /**
2
+ * Checks whether the input is an object (not null).
3
+ */
1
4
  export function isObject(input: unknown): input is object {
2
5
  return input != null && typeof input === 'object'
3
6
  }
@@ -5,10 +8,19 @@ export function isObject(input: unknown): input is object {
5
8
  const ObjectProto = Object.prototype
6
9
  const ObjectToString = Object.prototype.toString
7
10
 
8
- export function isPlainObject(
9
- input: unknown,
10
- ): input is object & Record<string, unknown> {
11
- if (!input || typeof input !== 'object') return false
11
+ /**
12
+ * Checks whether the input is an object (not null) whose prototype is either
13
+ * null or `Object.prototype`.
14
+ */
15
+ export function isPlainObject(input: unknown) {
16
+ return isObject(input) && isPlainProto(input)
17
+ }
18
+
19
+ /**
20
+ * Checks whether the prototype of the input object is either null or
21
+ * `Object.prototype`.
22
+ */
23
+ export function isPlainProto(input: object): input is Record<string, unknown> {
12
24
  const proto = Object.getPrototypeOf(input)
13
25
  if (proto === null) return true
14
26
  return (