libp2r2p 0.9.0 → 0.10.1

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/README.md CHANGED
@@ -326,60 +326,6 @@ Low-level relay sockets, subscriptions, message parsing, and serialization are
326
326
  internal implementation details; use `RelayPool` or the `relayPool` singleton
327
327
  from `libp2r2p/relay`.
328
328
 
329
- ## Internationalization
330
-
331
- The dependency-free `libp2r2p/i18n` subpath exposes locale detection and a
332
- small translator suitable for keeping each component's translations beside
333
- that component. Translation keys are literal strings, not dotted paths.
334
-
335
- ```js
336
- import { getT } from 'libp2r2p/i18n'
337
-
338
- const t = getT({
339
- 'Allow {{size}}': {
340
- en: 'Allow {{size}}',
341
- 'pt-BR': 'Permitir {{size}}'
342
- },
343
- 'Delete {{count}} items': {
344
- en: {
345
- one: 'Delete {{count}} item',
346
- other: 'Delete {{count}} items'
347
- }
348
- }
349
- })
350
-
351
- t('Allow {{size}}', { size: '10 MiB' })
352
- ```
353
-
354
- `getCurrentDeviceLocale()` prefers the locale resolved by `Intl`, then browser
355
- language hints, and preserves the full canonical BCP 47 locale. `getT()`
356
- matches exact and compatible language variants, falls back to English and then
357
- to the key itself, interpolates `{{name}}` values, and uses `Intl.PluralRules`
358
- when a locale value supplies plural forms. Missing interpolation values remain
359
- visible in the returned string.
360
-
361
- Catalog validation is opt-in. `validateLocales()` can be used independently,
362
- or `getT()` can validate once when creating the translator:
363
-
364
- ```js
365
- import { getT, validateLocales } from 'libp2r2p/i18n'
366
-
367
- const validation = {
368
- requiredLocales: ['en', 'pt-BR'],
369
- referenceLocale: 'en',
370
- requireReferenceKey: true
371
- }
372
-
373
- validateLocales(locales, validation)
374
- const t = getT(locales, { validation })
375
- ```
376
-
377
- Validation checks catalog structure, required locales, plural `other` forms,
378
- and placeholder parity across every translation. `requireReferenceKey` also
379
- requires the reference locale's string, or its plural `other` form, to equal
380
- the literal translation key. Without `validation`, sparse catalogs continue
381
- to use the normal locale fallback behavior.
382
-
383
329
  ## Binary encodings
384
330
 
385
331
  Base16, Base36, Base62, Base64/Base64URL, and Base93 helpers are available
package/idb/index.js CHANGED
@@ -13,6 +13,7 @@ const READ_METHODS = new Set([
13
13
  function deferred () {
14
14
  let resolve
15
15
  let reject
16
+ // eslint-disable-next-line promise/param-names
16
17
  const promise = new Promise((nextResolve, nextReject) => {
17
18
  resolve = nextResolve
18
19
  reject = nextReject
@@ -34,6 +34,7 @@ The queue state record is absent while the queue is empty.
34
34
  function deferred () {
35
35
  let resolve
36
36
  let reject
37
+ // eslint-disable-next-line promise/param-names
37
38
  const promise = new Promise((nextResolve, nextReject) => {
38
39
  resolve = nextResolve
39
40
  reject = nextReject
package/index.js CHANGED
@@ -12,7 +12,6 @@ export * as error from './error/index.js'
12
12
  export * as event from './event/index.js'
13
13
  export * as idb from './idb/index.js'
14
14
  export * as idbQueue from './idb-queue/index.js'
15
- export * as i18n from './i18n/index.js'
16
15
  export * as key from './key/index.js'
17
16
  export * as kind from './kind/index.js'
18
17
  export * as network from './network/index.js'
package/nip04/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  import { cbc } from '@noble/ciphers/aes.js'
2
2
  import { randomBytes } from '@noble/ciphers/utils.js'
3
-
4
- import { base16ToBytes } from '../base16/index.js'
5
3
  import { base64ToBytes, bytesToBase64 } from '../base64/index.js'
6
4
  import { sharedXOnlySecret } from '../ecdh/index.js'
7
5
  import { ValidationError } from '../error/index.js'
@@ -198,8 +198,7 @@ export class Nip46Client {
198
198
  }
199
199
 
200
200
  #openResponseContext (pointer) {
201
- let context
202
- context = this.#transport.openContext({
201
+ const context = this.#transport.openContext({
203
202
  filter: {
204
203
  kinds: [NIP46_KIND],
205
204
  authors: [pointer.remoteSignerPubkey],
@@ -112,8 +112,7 @@ export class Nip46ServerSession {
112
112
  }
113
113
 
114
114
  #openContext (relays) {
115
- let context
116
- context = this.#transport.openContext({
115
+ const context = this.#transport.openContext({
117
116
  filter: {
118
117
  kinds: [NIP46_KIND],
119
118
  '#p': [this.#transport.pubkey],
package/nip98/index.js CHANGED
@@ -27,6 +27,7 @@ export async function getToken ({
27
27
  payloadHash
28
28
  }) {
29
29
  if (typeof loginUrl !== 'string' || loginUrl.length === 0) throw new ValidationError('INVALID_LOGIN_URL')
30
+ // eslint-disable-next-line no-new
30
31
  try { new URL(loginUrl) } catch (cause) { throw new ValidationError('INVALID_LOGIN_URL', { cause }) }
31
32
  if (typeof httpMethod !== 'string' || httpMethod.trim().length === 0) throw new ValidationError('INVALID_HTTP_METHOD')
32
33
  if (typeof signEvent !== 'function') throw new ValidationError('SIGN_EVENT_SHOULD_BE_A_FUNCTION')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -24,7 +24,6 @@
24
24
  "event",
25
25
  "idb",
26
26
  "idb-queue",
27
- "i18n",
28
27
  "index.js",
29
28
  "key",
30
29
  "kind",
@@ -65,7 +64,6 @@
65
64
  "./event": "./event/index.js",
66
65
  "./idb": "./idb/index.js",
67
66
  "./idb-queue": "./idb-queue/index.js",
68
- "./i18n": "./i18n/index.js",
69
67
  "./key": "./key/index.js",
70
68
  "./kind": "./kind/index.js",
71
69
  "./network": "./network/index.js",
@@ -94,6 +92,9 @@
94
92
  "@scure/base": "2.0.0"
95
93
  },
96
94
  "devDependencies": {
97
- "fake-indexeddb": "6.2.5"
95
+ "eslint": "^9.39.5",
96
+ "fake-indexeddb": "6.2.5",
97
+ "globals": "^17.8.0",
98
+ "neostandard": "^0.13.0"
98
99
  }
99
100
  }
@@ -52,6 +52,7 @@ state, keyPath "key"
52
52
  function deferred () {
53
53
  let resolve
54
54
  let reject
55
+ // eslint-disable-next-line promise/param-names
55
56
  const promise = new Promise((nextResolve, nextReject) => {
56
57
  resolve = nextResolve
57
58
  reject = nextReject
@@ -25,6 +25,7 @@ clearing state owned by another instance of the same principal identity.
25
25
  function deferred () {
26
26
  let resolve
27
27
  let reject
28
+ // eslint-disable-next-line promise/param-names
28
29
  const promise = new Promise((nextResolve, nextReject) => {
29
30
  resolve = nextResolve
30
31
  reject = nextReject
@@ -40,6 +40,7 @@ export function createPublishSettlements (promises, timeout, { onSettled } = {})
40
40
  let isFinished = false
41
41
  let resolve
42
42
 
43
+ // eslint-disable-next-line promise/param-names
43
44
  const promise = new Promise(nextResolve => { resolve = nextResolve })
44
45
  const finish = () => {
45
46
  if (isFinished) return
@@ -175,7 +175,7 @@ export class RelayPool {
175
175
 
176
176
  // Disconnect from a relay
177
177
  async disconnect (url) {
178
- const normalizedUrl = normalizeURL(url)
178
+ const normalizedUrl = normalizeRelayUrl(url)
179
179
  if (this.#relays.has(normalizedUrl)) {
180
180
  const relay = this.#relays.get(normalizedUrl)
181
181
  if (relay.ws?.readyState < 2) await relay.close()?.catch(console.log)
@@ -414,6 +414,7 @@ export class RelayPool {
414
414
  this.#getRelay(url).then(relay => {
415
415
  if (isResolved || !pending.has(url)) return
416
416
  let hasEvents = false
417
+ // eslint-disable-next-line prefer-const
417
418
  let sub
418
419
 
419
420
  // Actual EOSE and filter satisfaction share the same graceful close path.
@@ -658,14 +659,13 @@ export class RelayPool {
658
659
  // the historical gap is yielded before newer socket events.
659
660
  let liveBuffer = (gapSince !== null && gapSince > 0) ? [] : null
660
661
  let liveEose = false
661
- let liveSub
662
662
 
663
663
  // Open the live sub first so the relay starts buffering incoming events
664
664
  // before we scan its database for the reconnect gap fill.
665
665
  // Forward until to the relay so it can enforce the boundary server-side.
666
666
  const liveFilter = { ...baseFilter, since: now, limit: 0 }
667
667
  if (filterUntil !== null) liveFilter.until = filterUntil
668
- liveSub = relay.subscribe([liveFilter], {
668
+ const liveSub = relay.subscribe([liveFilter], {
669
669
  onevent: (event) => {
670
670
  // A limit:0 relay may still send retained events before EOSE. Do not
671
671
  // expose them from a strictly-live stream.
package/i18n/index.js DELETED
@@ -1,237 +0,0 @@
1
- import { ValidationError } from '../error/index.js'
2
-
3
- const DEFAULT_LOCALE = 'en'
4
- const INTERPOLATION_RE = /{{\s*([A-Za-z0-9_.-]+)\s*}}/g
5
-
6
- function assertLocales (locales) {
7
- if (!locales || typeof locales !== 'object' || Array.isArray(locales)) {
8
- throw new ValidationError('INVALID_LOCALES', { message: 'locales should be an object' })
9
- }
10
- }
11
-
12
- function placeholderSignature (value) {
13
- return [...String(value).matchAll(INTERPOLATION_RE)]
14
- .map(match => match[1])
15
- .sort()
16
- .join(',')
17
- }
18
-
19
- function validateTranslationValue (key, locale, value, expectedPlaceholders) {
20
- if (typeof value === 'string') {
21
- if (placeholderSignature(value) !== expectedPlaceholders) {
22
- throw new ValidationError('I18N_PLACEHOLDER_MISMATCH', { message: `placeholder mismatch for "${key}" (${locale})` })
23
- }
24
- return
25
- }
26
-
27
- if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.other !== 'string') {
28
- throw new ValidationError('INVALID_TRANSLATION', { message: `translation for "${key}" (${locale}) should be a string or plural object with an other form` })
29
- }
30
-
31
- for (const form of Object.values(value)) {
32
- if (typeof form !== 'string') {
33
- throw new ValidationError('INVALID_PLURAL_TRANSLATION', { message: `plural forms for "${key}" (${locale}) should be strings` })
34
- }
35
- if (placeholderSignature(form) !== expectedPlaceholders) {
36
- throw new ValidationError('I18N_PLACEHOLDER_MISMATCH', { message: `placeholder mismatch for "${key}" (${locale})` })
37
- }
38
- }
39
- }
40
-
41
- export function validateLocales (locales, options = {}) {
42
- assertLocales(locales)
43
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
44
- throw new ValidationError('INVALID_I18N_VALIDATION_OPTIONS', { message: 'validation options should be an object' })
45
- }
46
- const {
47
- requiredLocales = [],
48
- referenceLocale = DEFAULT_LOCALE,
49
- requireReferenceKey = false
50
- } = options
51
- if (!Array.isArray(requiredLocales) || requiredLocales.some(locale => typeof locale !== 'string' || !locale)) {
52
- throw new ValidationError('INVALID_REQUIRED_LOCALES', { message: 'requiredLocales should be an array of non-empty strings' })
53
- }
54
- if (typeof referenceLocale !== 'string' || !referenceLocale) {
55
- throw new ValidationError('INVALID_REFERENCE_LOCALE', { message: 'referenceLocale should be a non-empty string' })
56
- }
57
-
58
- for (const [key, translations] of Object.entries(locales)) {
59
- if (!translations || typeof translations !== 'object' || Array.isArray(translations)) {
60
- throw new ValidationError('INVALID_TRANSLATIONS', { message: `translations for "${key}" should be an object` })
61
- }
62
-
63
- for (const locale of requiredLocales) {
64
- if (!Object.prototype.hasOwnProperty.call(translations, locale)) {
65
- throw new ValidationError('MISSING_TRANSLATION', { message: `missing translation for "${key}" (${locale})` })
66
- }
67
- }
68
-
69
- const expectedPlaceholders = placeholderSignature(key)
70
- for (const [locale, value] of Object.entries(translations)) {
71
- validateTranslationValue(key, locale, value, expectedPlaceholders)
72
- }
73
-
74
- if (requireReferenceKey) {
75
- if (!Object.prototype.hasOwnProperty.call(translations, referenceLocale)) {
76
- throw new ValidationError('MISSING_REFERENCE_TRANSLATION', { message: `missing reference translation for "${key}" (${referenceLocale})` })
77
- }
78
- const reference = translations[referenceLocale]
79
- const referenceValue = typeof reference === 'string' ? reference : reference.other
80
- if (referenceValue !== key) {
81
- throw new ValidationError('REFERENCE_TRANSLATION_MISMATCH', { message: `reference translation should match key "${key}" (${referenceLocale})` })
82
- }
83
- }
84
- }
85
-
86
- return locales
87
- }
88
-
89
- function canonicalizeLocale (locale) {
90
- if (typeof locale !== 'string' || !locale.trim()) return null
91
- const value = locale.trim().replace(/_/g, '-')
92
- try {
93
- return Intl.getCanonicalLocales(value)[0] ?? null
94
- } catch {
95
- return null
96
- }
97
- }
98
-
99
- function getIntlLocale () {
100
- try {
101
- return Intl.DateTimeFormat().resolvedOptions().locale
102
- } catch {
103
- return null
104
- }
105
- }
106
-
107
- function getNavigator () {
108
- try {
109
- return globalThis.navigator
110
- } catch {
111
- return null
112
- }
113
- }
114
-
115
- export function getCurrentDeviceLocale () {
116
- const navigator = getNavigator()
117
- const candidates = [
118
- getIntlLocale(),
119
- navigator?.language,
120
- navigator?.languages?.[0],
121
- DEFAULT_LOCALE
122
- ]
123
-
124
- for (const candidate of candidates) {
125
- const locale = canonicalizeLocale(candidate)
126
- if (locale) return locale
127
- }
128
- return DEFAULT_LOCALE
129
- }
130
-
131
- function localeLanguage (locale) {
132
- try {
133
- return new Intl.Locale(locale).language
134
- } catch {
135
- return locale.split('-')[0].toLowerCase()
136
- }
137
- }
138
-
139
- function preferredChineseLocale (locale) {
140
- if (localeLanguage(locale) !== 'zh') return null
141
- try {
142
- const { script, region } = new Intl.Locale(locale)
143
- if (script === 'Hant' || ['TW', 'HK', 'MO'].includes(region)) return 'zh-TW'
144
- return 'zh-CN'
145
- } catch {
146
- return /(?:^|-)(?:hant|tw|hk|mo)(?:-|$)/i.test(locale) ? 'zh-TW' : 'zh-CN'
147
- }
148
- }
149
-
150
- function localeCandidates (translations, requestedLocale, fallbackLocale) {
151
- const keys = Object.keys(translations)
152
- const canonicalKeys = new Map()
153
- for (const key of keys) {
154
- const canonical = canonicalizeLocale(key)
155
- if (canonical && !canonicalKeys.has(canonical.toLowerCase())) {
156
- canonicalKeys.set(canonical.toLowerCase(), { key, canonical })
157
- }
158
- }
159
-
160
- const result = []
161
- const seen = new Set()
162
- const add = locale => {
163
- const canonical = canonicalizeLocale(locale)
164
- const match = canonical && canonicalKeys.get(canonical.toLowerCase())
165
- if (match && !seen.has(match.key)) {
166
- seen.add(match.key)
167
- result.push(match)
168
- }
169
- }
170
- const addCompatible = locale => {
171
- const canonical = canonicalizeLocale(locale)
172
- if (!canonical) return
173
- add(canonical)
174
- add(preferredChineseLocale(canonical))
175
- const language = localeLanguage(canonical)
176
- const match = [...canonicalKeys.values()].find(candidate => localeLanguage(candidate.canonical) === language)
177
- if (match) add(match.canonical)
178
- }
179
-
180
- addCompatible(requestedLocale)
181
- addCompatible(fallbackLocale)
182
- addCompatible(DEFAULT_LOCALE)
183
- return result
184
- }
185
-
186
- function selectPlural (forms, locale, values) {
187
- if (!forms || typeof forms !== 'object' || Array.isArray(forms)) return null
188
- const count = Number(values?.count)
189
- if (!Number.isFinite(count)) return null
190
-
191
- let category = 'other'
192
- try {
193
- category = new Intl.PluralRules(locale).select(count)
194
- } catch {}
195
- const value = forms[category] ?? forms.other
196
- return typeof value === 'string' ? value : null
197
- }
198
-
199
- function selectTemplate (translations, requestedLocale, fallbackLocale, values) {
200
- if (!translations || typeof translations !== 'object' || Array.isArray(translations)) return null
201
- for (const { key, canonical } of localeCandidates(translations, requestedLocale, fallbackLocale)) {
202
- const value = translations[key]
203
- if (typeof value === 'string') return value
204
- const plural = selectPlural(value, canonical, values)
205
- if (plural !== null) return plural
206
- }
207
- return null
208
- }
209
-
210
- function interpolate (template, values) {
211
- const source = String(template)
212
- if (!values || typeof values !== 'object') return source
213
- return source.replace(INTERPOLATION_RE, (token, name) => (
214
- Object.prototype.hasOwnProperty.call(values, name)
215
- ? String(values[name])
216
- : token
217
- ))
218
- }
219
-
220
- export function getT (locales, {
221
- locale = getCurrentDeviceLocale(),
222
- fallbackLocale = DEFAULT_LOCALE,
223
- validation
224
- } = {}) {
225
- assertLocales(locales)
226
- if (validation !== undefined) validateLocales(locales, validation)
227
-
228
- const requestedLocale = canonicalizeLocale(locale) ?? DEFAULT_LOCALE
229
- const canonicalFallback = canonicalizeLocale(fallbackLocale) ?? DEFAULT_LOCALE
230
-
231
- return function t (key, values) {
232
- if (typeof key !== 'string') throw new ValidationError('INVALID_TRANSLATION_KEY', { message: 'translation key should be a string' })
233
- const translations = Object.prototype.hasOwnProperty.call(locales, key) ? locales[key] : null
234
- const template = selectTemplate(translations, requestedLocale, canonicalFallback, values) ?? key
235
- return interpolate(template, values)
236
- }
237
- }