@elefunc/send 0.1.21 → 0.1.30

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elefunc/send",
3
- "version": "0.1.21",
3
+ "version": "0.1.30",
4
4
  "description": "Browser-compatible file transfer CLI and TUI powered by Bun, WebRTC, and Rezi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -310,11 +310,41 @@ export class SessionAbortedError extends Error {
310
310
  export const isSessionAbortedError = (error: unknown): error is SessionAbortedError =>
311
311
  error instanceof SessionAbortedError || error instanceof Error && error.name === "SessionAbortedError"
312
312
 
313
- const timeoutSignal = (ms: number, base?: AbortSignal | null) => {
314
- const timeout = typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(ms) : undefined
315
- if (!base) return timeout
316
- if (!timeout) return base
317
- return typeof AbortSignal.any === "function" ? AbortSignal.any([base, timeout]) : base
313
+ const awaitAbortable = async <T>(promise: Promise<T>, signal?: AbortSignal | null) => {
314
+ if (!signal) return promise
315
+ if (signal.aborted) throw new SessionAbortedError()
316
+ return await new Promise<T>((resolve, reject) => {
317
+ const onAbort = () => {
318
+ signal.removeEventListener("abort", onAbort)
319
+ reject(new SessionAbortedError())
320
+ }
321
+ signal.addEventListener("abort", onAbort, { once: true })
322
+ promise.then(
323
+ value => {
324
+ signal.removeEventListener("abort", onAbort)
325
+ resolve(value)
326
+ },
327
+ error => {
328
+ signal.removeEventListener("abort", onAbort)
329
+ reject(error)
330
+ },
331
+ )
332
+ })
333
+ }
334
+
335
+ const fetchWithTimeout = async (input: string | URL | Request, init: RequestInit, ms: number, base?: AbortSignal | null) => {
336
+ if (typeof AbortController !== "function") return fetch(input, init)
337
+ const controller = new AbortController()
338
+ const timeout = setTimeout(() => controller.abort(new Error(`timed out after ${ms}ms`)), ms)
339
+ const onAbort = () => controller.abort(new SessionAbortedError())
340
+ if (base?.aborted) onAbort()
341
+ else base?.addEventListener("abort", onAbort, { once: true })
342
+ try {
343
+ return await fetch(input, { ...init, signal: controller.signal })
344
+ } finally {
345
+ clearTimeout(timeout)
346
+ base?.removeEventListener("abort", onAbort)
347
+ }
318
348
  }
319
349
 
320
350
  const normalizeCandidateType = (value: unknown) => typeof value === "string" ? value.toLowerCase() : ""
@@ -490,9 +520,8 @@ export class SendSession {
490
520
  this.lifecycleAbortController = typeof AbortController === "function" ? new AbortController() : null
491
521
  this.startPeerStatsPolling()
492
522
  this.startPulsePolling()
493
- void this.loadLocalProfile()
494
- void this.probePulse()
495
- this.connectSocket()
523
+ await awaitAbortable(Promise.allSettled([this.loadLocalProfile(), this.probePulse()]), this.lifecycleAbortController?.signal) // Avoid the Bun Windows TUI TLS startup bug: https://github.com/oven-sh/bun/issues/28612
524
+ this.connectSocket() // Intentionally defer socket startup until after initial HTTPS calls: https://github.com/oven-sh/bun/issues/28612
496
525
  await this.waitFor(() => this.socketState === "open", timeoutMs, this.lifecycleAbortController?.signal)
497
526
  }
498
527
 
@@ -1176,7 +1205,7 @@ export class SendSession {
1176
1205
 
1177
1206
  private async loadLocalProfile() {
1178
1207
  try {
1179
- const response = await fetch(PROFILE_URL, { cache: "no-store", signal: timeoutSignal(4000, this.lifecycleAbortController?.signal) })
1208
+ const response = await fetchWithTimeout(PROFILE_URL, { cache: "no-store" }, 4000, this.lifecycleAbortController?.signal)
1180
1209
  if (!response.ok) throw new Error(`profile ${response.status}`)
1181
1210
  const data = await response.json()
1182
1211
  if (this.stopped) return
@@ -1196,7 +1225,7 @@ export class SendSession {
1196
1225
  this.pulse = { ...this.pulse, state: "checking", error: "" }
1197
1226
  this.notify()
1198
1227
  try {
1199
- const response = await fetch(SIGNAL_PULSE_URL, { cache: "no-store", signal: timeoutSignal(3500, this.lifecycleAbortController?.signal) })
1228
+ const response = await fetchWithTimeout(SIGNAL_PULSE_URL, { cache: "no-store" }, 3500, this.lifecycleAbortController?.signal)
1200
1229
  if (!response.ok) throw new Error(`pulse ${response.status}`)
1201
1230
  if (this.stopped) return
1202
1231
  this.pulse = { state: "open", lastSettledState: "open", at: Date.now(), ms: performance.now() - startedAt, error: "" }
package/src/index.ts CHANGED
@@ -68,7 +68,7 @@ const TURN_OPTIONS = [
68
68
  ["--turn-username <value>", "custom TURN username"],
69
69
  ["--turn-credential <value>", "custom TURN credential"],
70
70
  ] as const
71
- const OVERWRITE_OPTION = ["--overwrite", "overwrite same-name saved files instead of creating copies"] as const
71
+ const OVERWRITE_OPTION = ["-o, --overwrite", "overwrite same-name saved files instead of creating copies"] as const
72
72
  const SAVE_DIR_OPTION = ["--save-dir <dir>", "save directory", { default: "." }] as const
73
73
  const TUI_TOGGLE_OPTIONS = [
74
74
  ["--clean <0|1>", "show only active peers when 1; show terminal peers too when 0", { default: 1 }],
@@ -80,6 +80,13 @@ export const ACCEPT_SESSION_DEFAULTS = { autoAcceptIncoming: true, autoSaveIncom
80
80
  type CliOptionDefinition = readonly [flag: string, description: string, config?: { default?: unknown }]
81
81
  const addOptions = (command: CliCommand, definitions: readonly CliOptionDefinition[]) =>
82
82
  definitions.reduce((next, [flag, description, config]) => next.option(flag, description, config), command)
83
+ const normalizeCliOptions = (options: Record<string, unknown>) => {
84
+ const normalized = { ...options }
85
+ if (normalized.overwrite == null && normalized.o != null) normalized.overwrite = normalized.o
86
+ delete normalized.h
87
+ delete normalized.o
88
+ return normalized
89
+ }
83
90
  const withTrailingHelpLine = <T extends { outputHelp: () => void }>(target: T) => {
84
91
  const outputHelp = target.outputHelp.bind(target)
85
92
  target.outputHelp = () => {
@@ -323,7 +330,7 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
323
330
  ["--json", "print a json snapshot"],
324
331
  SAVE_DIR_OPTION,
325
332
  ...TURN_OPTIONS,
326
- ])).action(handlers.peers)
333
+ ])).action(options => handlers.peers(normalizeCliOptions(options)))
327
334
 
328
335
  withTrailingHelpLine(addOptions(cli.command("offer [...files]", "offer files to browser-compatible peers").ignoreOptionDefaultValue(), [
329
336
  ...ROOM_SELF_OPTIONS,
@@ -332,7 +339,7 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
332
339
  ["--json", "emit ndjson events"],
333
340
  SAVE_DIR_OPTION,
334
341
  ...TURN_OPTIONS,
335
- ])).action(handlers.offer)
342
+ ])).action((files, options) => handlers.offer(files, normalizeCliOptions(options)))
336
343
 
337
344
  withTrailingHelpLine(addOptions(cli.command("accept", "receive and save files").ignoreOptionDefaultValue(), [
338
345
  ...ROOM_SELF_OPTIONS,
@@ -341,7 +348,7 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
341
348
  ["--once", "exit after the first saved incoming transfer"],
342
349
  ["--json", "emit ndjson events"],
343
350
  ...TURN_OPTIONS,
344
- ])).action(handlers.accept)
351
+ ])).action(options => handlers.accept(normalizeCliOptions(options)))
345
352
 
346
353
  withTrailingHelpLine(addOptions(cli.command("tui", "launch the interactive terminal UI").ignoreOptionDefaultValue(), [
347
354
  ...ROOM_SELF_OPTIONS,
@@ -350,7 +357,7 @@ export const createCli = (handlers: CliHandlers = defaultCliHandlers) => {
350
357
  SAVE_DIR_OPTION,
351
358
  OVERWRITE_OPTION,
352
359
  ...TURN_OPTIONS,
353
- ])).action(handlers.tui)
360
+ ])).action(options => handlers.tui(normalizeCliOptions(options)))
354
361
 
355
362
  cli.help(sections => {
356
363
  const usage = sections.find(section => section.title === "Usage:")
@@ -391,7 +398,7 @@ export const runCli = async (argv = process.argv, handlers: CliHandlers = defaul
391
398
  printSubcommandHelp(argv, handlers, "tui")
392
399
  return
393
400
  }
394
- await handlers.tui(parsed.options)
401
+ await handlers.tui(normalizeCliOptions(parsed.options))
395
402
  return
396
403
  }
397
404
  await cli.runMatchedCommand()