@mertushka/webrtc-node 0.1.0-alpha.0 → 0.1.0

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": "@mertushka/webrtc-node",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0",
4
4
  "description": "W3C-style RTCPeerConnection and RTCDataChannel for Node.js, backed by libdatachannel.",
5
5
  "author": "mertushka",
6
6
  "homepage": "https://github.com/mertushka/webrtc-node#readme",
@@ -64,11 +64,26 @@ forbidMatch("NAN namespace usage", addon, /\bNan::/);
64
64
  forbidMatch("non-Node-API module initializer", addon, /\bNODE_MODULE\s*\(/);
65
65
 
66
66
  const callbackCallMatches = [...addon.matchAll(/\bcallback\.Call\s*\(/g)];
67
- if (callbackCallMatches.length !== 1) {
67
+ if (callbackCallMatches.length !== 3) {
68
68
  fail(
69
- `expected exactly one callback.Call site inside EventDispatcher::Dispatch, found ${callbackCallMatches.length}`,
69
+ `expected exactly three callback.Call sites inside EventDispatcher dispatch paths, found ${callbackCallMatches.length}`,
70
70
  );
71
71
  }
72
+ requireMatch(
73
+ "single native event callback dispatch",
74
+ addon,
75
+ /callback\.Call\s*\(\s*\{\s*EventToObject/,
76
+ );
77
+ requireMatch(
78
+ "batched native event callback dispatch",
79
+ addon,
80
+ /callback\.Call\s*\(\s*\{\s*batch\s*\}\s*\)/,
81
+ );
82
+ requireMatch(
83
+ "direct native event callback dispatch",
84
+ addon,
85
+ /DispatchDirect[\s\S]*callback\.Call\s*\(\s*\{\s*EventToObject/,
86
+ );
72
87
 
73
88
  const cmakePinMatch = /set\s*\(\s*LIBDATACHANNEL_PINNED_COMMIT\s+"([0-9a-f]{40})"/i.exec(cmake);
74
89
  if (!cmakePinMatch) fail("CMake libdatachannel pin is missing");
@@ -87,6 +87,7 @@ function runBuild() {
87
87
 
88
88
  async function main() {
89
89
  const buildFromSource = envFlag("npm_config_build_from_source");
90
+ const prebuildOnly = envFlag("WEBRTC_NODE_PREBUILD_ONLY");
90
91
  if (!buildFromSource && hasNativeAddon()) return;
91
92
 
92
93
  if (isSourceCheckout() && !buildFromSource) {
@@ -101,6 +102,10 @@ async function main() {
101
102
  throw new Error(`downloaded archive did not provide ${moduleName}`);
102
103
  } catch (error) {
103
104
  console.warn(`Prebuilt binary unavailable for ${targetTuple()}: ${error.message}`);
105
+ if (prebuildOnly) {
106
+ console.error("Prebuild-only install requested; refusing to build from source.");
107
+ process.exit(1);
108
+ }
104
109
  }
105
110
  }
106
111
 
@@ -1,73 +1,73 @@
1
- param(
2
- [string]$NodeImage = "node:20-bookworm",
3
- [string]$ArtifactsDir = "ci-artifacts/docker-linux-node20",
4
- [switch]$SkipWpt,
5
- [string[]]$WptSelector = @(),
6
- [int]$WptExpectedTotal = 0
7
- )
8
-
9
- $ErrorActionPreference = "Stop"
10
-
11
- $root = Resolve-Path (Join-Path $PSScriptRoot "..")
12
- $artifactPath = Join-Path $root $ArtifactsDir
13
- New-Item -ItemType Directory -Force $artifactPath | Out-Null
14
-
15
- $rootForDocker = $root.Path -replace "\\", "/"
16
- $artifactForDocker = (Resolve-Path $artifactPath).Path -replace "\\", "/"
17
- $wptSelectorArgs = ($WptSelector | ForEach-Object {
18
- if ($_.Contains("'")) {
19
- throw "WPT selector cannot contain a single quote: $_"
20
- }
21
- "'$_'"
22
- }) -join " "
23
- $wptTestCommand = if ($WptSelector.Count -gt 0) {
24
- "npm run wpt:test -- $wptSelectorArgs"
25
- } else {
26
- "npm run wpt:test"
27
- }
28
- $wptCheckCommand = if ($WptSelector.Count -gt 0) {
29
- $expectedTotal = if ($WptExpectedTotal -gt 0) { $WptExpectedTotal } else { $WptSelector.Count }
30
- "WPT_EXPECTED_TOTAL=$expectedTotal npm run wpt:check:strict"
31
- } else {
32
- "npm run wpt:check:strict"
33
- }
34
- $wptReportCommand = if ($WptSelector.Count -gt 0) {
35
- "true"
36
- } else {
37
- "npm run wpt:report -- --output /out/wpt-report.md && RUNNER_OS=Linux RUNNER_ARCH=X64 node scripts/write-ci-evidence.js --results /out/wpt-results.json --output /out/ci-evidence.json"
38
- }
39
- $wptCommand = if ($SkipWpt) {
40
- "npm run wpt:selection:check"
41
- } else {
42
- "npm run wpt:selection:check && WPT_TEST_TIMEOUT_MS=180000 WPT_WORKER_TIMEOUT_MS=600000 WPT_WORKER_DELAY_MS=2000 WPT_CLEANUP_DELAY_MS=3000 $wptTestCommand 2>&1 | tee /out/wpt-output.txt && cp wpt-results.json /out/wpt-results.json && $wptCheckCommand && $wptReportCommand"
43
- }
44
-
45
- docker run --rm `
46
- -v "${rootForDocker}:/src:ro" `
47
- -v "${artifactForDocker}:/out" `
48
- $NodeImage `
49
- bash -lc "set -euo pipefail; mkdir -p /tmp/webrtc-node; tar -C /src --exclude='./build' --exclude='./node_modules' --exclude='./.git' --exclude='./wpt-results.json' --exclude='./wpt-report.md' --exclude='./ci-artifacts' -cf - . | tar -C /tmp/webrtc-node -xf -; cd /tmp/webrtc-node; if [ -f /etc/apt/sources.list.d/debian.sources ]; then sed -i '0,/URIs: http:\/\/deb.debian.org\/debian$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; sed -i '0,/URIs: http:\/\/deb.debian.org\/debian-security$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian-security\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; fi; for attempt in 1 2 3; do if apt-get -o Acquire::Check-Valid-Until=false update >/out/apt-update.txt 2>&1 && apt-get install -y cmake ninja-build libssl-dev >/out/apt-install.txt 2>&1; then break; fi; if [ ""`$attempt"" = 3 ]; then exit 1; fi; sleep `$((attempt * 10)); done; npm ci 2>&1 | tee /out/npm-ci.txt; npm run check; npm run native:check; npm run build 2>&1 | tee /out/build-output.txt; npm test; npm run api:check; npm run types:check; npm run wpt:ensure; set +e; ${wptCommand}; wpt_status=`$?; set -e; cp wpt-results.json /out/wpt-results.json 2>/dev/null || true; cp wpt-manifest.json /out/wpt-manifest.json; npm run wpt:manifest > /out/wpt-manifest.txt; exit `$wpt_status"
50
-
51
- $dockerExitCode = $LASTEXITCODE
52
-
53
- if (-not $SkipWpt) {
54
- $resultsPath = Join-Path $artifactPath "wpt-results.json"
55
- if (-not (Test-Path $resultsPath)) {
56
- throw "Docker CI did not produce $resultsPath"
57
- }
58
-
59
- $results = Get-Content -Raw -Path $resultsPath | ConvertFrom-Json
60
- if ([int]$results.fail -gt 0) {
61
- throw "Docker CI WPT subset failed: $($results.pass)/$($results.total) passed"
62
- }
63
- $retried = @($results.results | Where-Object {
64
- ($_.PSObject.Properties.Name -contains "retries") -and [int]$_.retries -gt 0
65
- }).Count
66
- if ($retried -gt 0) {
67
- throw "Docker CI WPT subset required retries: $retried"
68
- }
69
- }
70
-
71
- if ($dockerExitCode -ne 0) {
72
- throw "Docker CI failed with exit code $dockerExitCode"
73
- }
1
+ param(
2
+ [string]$NodeImage = "node:20-bookworm",
3
+ [string]$ArtifactsDir = "ci-artifacts/docker-linux-node20",
4
+ [switch]$SkipWpt,
5
+ [string[]]$WptSelector = @(),
6
+ [int]$WptExpectedTotal = 0
7
+ )
8
+
9
+ $ErrorActionPreference = "Stop"
10
+
11
+ $root = Resolve-Path (Join-Path $PSScriptRoot "..")
12
+ $artifactPath = Join-Path $root $ArtifactsDir
13
+ New-Item -ItemType Directory -Force $artifactPath | Out-Null
14
+
15
+ $rootForDocker = $root.Path -replace "\\", "/"
16
+ $artifactForDocker = (Resolve-Path $artifactPath).Path -replace "\\", "/"
17
+ $wptSelectorArgs = ($WptSelector | ForEach-Object {
18
+ if ($_.Contains("'")) {
19
+ throw "WPT selector cannot contain a single quote: $_"
20
+ }
21
+ "'$_'"
22
+ }) -join " "
23
+ $wptTestCommand = if ($WptSelector.Count -gt 0) {
24
+ "npm run wpt:test -- $wptSelectorArgs"
25
+ } else {
26
+ "npm run wpt:test"
27
+ }
28
+ $wptCheckCommand = if ($WptSelector.Count -gt 0) {
29
+ $expectedTotal = if ($WptExpectedTotal -gt 0) { $WptExpectedTotal } else { $WptSelector.Count }
30
+ "WPT_EXPECTED_TOTAL=$expectedTotal npm run wpt:check:strict"
31
+ } else {
32
+ "npm run wpt:check:strict"
33
+ }
34
+ $wptReportCommand = if ($WptSelector.Count -gt 0) {
35
+ "true"
36
+ } else {
37
+ "npm run wpt:report -- --output /out/wpt-report.md && RUNNER_OS=Linux RUNNER_ARCH=X64 node scripts/write-ci-evidence.js --results /out/wpt-results.json --output /out/ci-evidence.json"
38
+ }
39
+ $wptCommand = if ($SkipWpt) {
40
+ "npm run wpt:selection:check"
41
+ } else {
42
+ "npm run wpt:selection:check && WPT_TEST_TIMEOUT_MS=180000 WPT_WORKER_TIMEOUT_MS=600000 WPT_WORKER_DELAY_MS=2000 WPT_CLEANUP_DELAY_MS=3000 $wptTestCommand 2>&1 | tee /out/wpt-output.txt && cp wpt-results.json /out/wpt-results.json && $wptCheckCommand && $wptReportCommand"
43
+ }
44
+
45
+ docker run --rm `
46
+ -v "${rootForDocker}:/src:ro" `
47
+ -v "${artifactForDocker}:/out" `
48
+ $NodeImage `
49
+ bash -lc "set -euo pipefail; mkdir -p /tmp/webrtc-node; tar -C /src --exclude='./build' --exclude='./node_modules' --exclude='./.git' --exclude='./wpt-results.json' --exclude='./wpt-report.md' --exclude='./ci-artifacts' -cf - . | tar -C /tmp/webrtc-node -xf -; cd /tmp/webrtc-node; if [ -f /etc/apt/sources.list.d/debian.sources ]; then sed -i '0,/URIs: http:\/\/deb.debian.org\/debian$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; sed -i '0,/URIs: http:\/\/deb.debian.org\/debian-security$/s//URIs: http:\/\/snapshot.debian.org\/archive\/debian-security\/20260421T000000Z/' /etc/apt/sources.list.d/debian.sources; fi; for attempt in 1 2 3; do if apt-get -o Acquire::Check-Valid-Until=false update >/out/apt-update.txt 2>&1 && apt-get install -y cmake ninja-build libssl-dev >/out/apt-install.txt 2>&1; then break; fi; if [ ""`$attempt"" = 3 ]; then exit 1; fi; sleep `$((attempt * 10)); done; npm ci 2>&1 | tee /out/npm-ci.txt; npm run check; npm run native:check; npm run build 2>&1 | tee /out/build-output.txt; npm test; npm run api:check; npm run types:check; npm run wpt:ensure; set +e; ${wptCommand}; wpt_status=`$?; set -e; cp wpt-results.json /out/wpt-results.json 2>/dev/null || true; cp wpt-manifest.json /out/wpt-manifest.json; npm run wpt:manifest > /out/wpt-manifest.txt; exit `$wpt_status"
50
+
51
+ $dockerExitCode = $LASTEXITCODE
52
+
53
+ if (-not $SkipWpt) {
54
+ $resultsPath = Join-Path $artifactPath "wpt-results.json"
55
+ if (-not (Test-Path $resultsPath)) {
56
+ throw "Docker CI did not produce $resultsPath"
57
+ }
58
+
59
+ $results = Get-Content -Raw -Path $resultsPath | ConvertFrom-Json
60
+ if ([int]$results.fail -gt 0) {
61
+ throw "Docker CI WPT subset failed: $($results.pass)/$($results.total) passed"
62
+ }
63
+ $retried = @($results.results | Where-Object {
64
+ ($_.PSObject.Properties.Name -contains "retries") -and [int]$_.retries -gt 0
65
+ }).Count
66
+ if ($retried -gt 0) {
67
+ throw "Docker CI WPT subset required retries: $retried"
68
+ }
69
+ }
70
+
71
+ if ($dockerExitCode -ne 0) {
72
+ throw "Docker CI failed with exit code $dockerExitCode"
73
+ }
@@ -292,7 +292,7 @@ struct NativeEvent {
292
292
  std::string error;
293
293
  bool binary = false;
294
294
  std::string text;
295
- std::vector<uint8_t> bytes;
295
+ rtc::binary bytes;
296
296
  };
297
297
 
298
298
  struct EventDispatcher : public std::enable_shared_from_this<EventDispatcher> {
@@ -303,6 +303,29 @@ struct EventDispatcher : public std::enable_shared_from_this<EventDispatcher> {
303
303
  ~EventDispatcher() { Close(); }
304
304
 
305
305
  void Emit(NativeEvent event) {
306
+ if (event.target != "datachannel" || event.type != "message") {
307
+ EmitDirect(std::move(event));
308
+ return;
309
+ }
310
+
311
+ bool scheduleDispatch = false;
312
+ std::lock_guard<std::mutex> lock(lifecycleMutex);
313
+ if (!active.load()) {
314
+ return;
315
+ }
316
+
317
+ pendingEvents.push_back(std::move(event));
318
+ if (!dispatchScheduled) {
319
+ dispatchScheduled = true;
320
+ scheduleDispatch = true;
321
+ }
322
+ if (!scheduleDispatch)
323
+ return;
324
+
325
+ QueueDispatchLocked();
326
+ }
327
+
328
+ void EmitDirect(NativeEvent event) {
306
329
  auto *queued = new NativeEvent(std::move(event));
307
330
  std::lock_guard<std::mutex> lock(lifecycleMutex);
308
331
  if (!active.load()) {
@@ -310,13 +333,15 @@ struct EventDispatcher : public std::enable_shared_from_this<EventDispatcher> {
310
333
  return;
311
334
  }
312
335
 
313
- napi_status status = tsfn.NonBlockingCall(queued, Dispatch);
336
+ napi_status status = tsfn.NonBlockingCall(queued, DispatchDirect);
314
337
  if (status != napi_ok)
315
338
  delete queued;
316
339
  }
317
340
 
318
341
  void Close() {
319
342
  std::lock_guard<std::mutex> lock(lifecycleMutex);
343
+ pendingEvents.clear();
344
+ dispatchScheduled = false;
320
345
  if (active.exchange(false))
321
346
  tsfn.Release();
322
347
  }
@@ -325,10 +350,76 @@ private:
325
350
  EventDispatcher(Napi::Env env, Napi::Function callback)
326
351
  : tsfn(Napi::ThreadSafeFunction::New(env, callback, "webrtc-node events", 0, 1)) {}
327
352
 
328
- static void Dispatch(Napi::Env env, Napi::Function callback, NativeEvent *event);
353
+ void Drain(Napi::Env env, Napi::Function callback) {
354
+ std::vector<NativeEvent> events;
355
+ {
356
+ std::lock_guard<std::mutex> lock(lifecycleMutex);
357
+ if (pendingEvents.empty()) {
358
+ dispatchScheduled = false;
359
+ return;
360
+ }
361
+ events.swap(pendingEvents);
362
+ }
363
+
364
+ if (events.size() == 1) {
365
+ callback.Call({EventToObject(env, events.front())});
366
+ } else {
367
+ Napi::Array batch = Napi::Array::New(env, events.size());
368
+ for (uint32_t i = 0; i < events.size(); ++i)
369
+ batch.Set(i, EventToObject(env, events[i]));
370
+ callback.Call({batch});
371
+ }
372
+
373
+ std::lock_guard<std::mutex> lock(lifecycleMutex);
374
+ if (!active.load() || pendingEvents.empty()) {
375
+ dispatchScheduled = false;
376
+ return;
377
+ }
378
+ QueueDispatchLocked();
379
+ }
380
+
381
+ void QueueDispatchLocked() {
382
+ auto *dispatcher = new std::shared_ptr<EventDispatcher>(shared_from_this());
383
+ napi_status status = tsfn.NonBlockingCall(dispatcher, DispatchQueued);
384
+ if (status != napi_ok) {
385
+ dispatchScheduled = false;
386
+ pendingEvents.clear();
387
+ delete dispatcher;
388
+ }
389
+ }
390
+
391
+ static void DispatchQueued(Napi::Env env, Napi::Function callback,
392
+ std::shared_ptr<EventDispatcher> *dispatcher) {
393
+ std::shared_ptr<EventDispatcher> scoped = std::move(*dispatcher);
394
+ delete dispatcher;
395
+ scoped->Drain(env, callback);
396
+ }
397
+
398
+ static void DispatchDirect(Napi::Env env, Napi::Function callback, NativeEvent *event) {
399
+ std::unique_ptr<NativeEvent> scoped(event);
400
+ callback.Call({EventToObject(env, *scoped)});
401
+ }
402
+
403
+ static Napi::Value MessagePayloadToValue(Napi::Env env, NativeEvent &event) {
404
+ if (!event.binary)
405
+ return Napi::String::New(env, event.text);
406
+
407
+ if (event.bytes.empty())
408
+ return Napi::ArrayBuffer::New(env, 0);
409
+
410
+ auto *bytes = new rtc::binary(std::move(event.bytes));
411
+ return Napi::ArrayBuffer::New(
412
+ env, bytes->data(), bytes->size(),
413
+ [](Napi::Env, void *, rtc::binary *finalizedBytes) { delete finalizedBytes; },
414
+ bytes);
415
+ }
416
+
417
+ static Napi::Object EventToObject(Napi::Env env, NativeEvent &event);
329
418
 
330
419
  std::atomic<bool> active{true};
331
420
  std::mutex lifecycleMutex;
421
+ std::vector<NativeEvent> pendingEvents;
422
+ bool dispatchScheduled = false;
332
423
  Napi::ThreadSafeFunction tsfn;
333
424
  };
334
425
 
@@ -452,10 +543,7 @@ private:
452
543
  event.text = std::get<std::string>(std::move(data));
453
544
  } else {
454
545
  event.binary = true;
455
- const auto &binary = std::get<rtc::binary>(data);
456
- event.bytes.reserve(binary.size());
457
- for (std::byte value : binary)
458
- event.bytes.push_back(std::to_integer<uint8_t>(value));
546
+ event.bytes = std::move(std::get<rtc::binary>(data));
459
547
  }
460
548
  self->Emit(std::move(event));
461
549
  }
@@ -619,49 +707,40 @@ private:
619
707
 
620
708
  Napi::FunctionReference NativeDataChannel::constructor;
621
709
 
622
- void EventDispatcher::Dispatch(Napi::Env env, Napi::Function callback, NativeEvent *event) {
623
- std::unique_ptr<NativeEvent> scoped(event);
624
-
710
+ Napi::Object EventDispatcher::EventToObject(Napi::Env env, NativeEvent &event) {
625
711
  Napi::Object object = Napi::Object::New(env);
626
- object.Set("target", scoped->target);
627
- object.Set("type", scoped->type);
628
- if (scoped->channelId)
629
- object.Set("channelId", scoped->channelId);
630
- if (!scoped->state.empty())
631
- object.Set("state", scoped->state);
632
- if (!scoped->descriptionType.empty()) {
712
+ object.Set("target", event.target);
713
+ object.Set("type", event.type);
714
+ if (event.channelId)
715
+ object.Set("channelId", event.channelId);
716
+ if (!event.state.empty())
717
+ object.Set("state", event.state);
718
+ if (!event.descriptionType.empty()) {
633
719
  Napi::Object description = Napi::Object::New(env);
634
- description.Set("type", scoped->descriptionType);
635
- description.Set("sdp", scoped->sdp);
720
+ description.Set("type", event.descriptionType);
721
+ description.Set("sdp", event.sdp);
636
722
  object.Set("description", description);
637
723
  }
638
- if (!scoped->candidate.empty() || !scoped->mid.empty()) {
724
+ if (!event.candidate.empty() || !event.mid.empty()) {
639
725
  Napi::Object candidate = Napi::Object::New(env);
640
- candidate.Set("candidate", scoped->candidate);
641
- if (!scoped->mid.empty())
642
- candidate.Set("sdpMid", scoped->mid);
726
+ candidate.Set("candidate", event.candidate);
727
+ if (!event.mid.empty())
728
+ candidate.Set("sdpMid", event.mid);
643
729
  object.Set("candidate", candidate);
644
730
  }
645
- if (!scoped->error.empty())
646
- object.Set("error", scoped->error);
647
- if (scoped->type == "message") {
648
- object.Set("binary", scoped->binary);
649
- if (scoped->binary) {
650
- Napi::ArrayBuffer buffer = Napi::ArrayBuffer::New(env, scoped->bytes.size());
651
- if (!scoped->bytes.empty())
652
- std::memcpy(buffer.Data(), scoped->bytes.data(), scoped->bytes.size());
653
- object.Set("data", buffer);
654
- } else {
655
- object.Set("data", scoped->text);
656
- }
731
+ if (!event.error.empty())
732
+ object.Set("error", event.error);
733
+ if (event.type == "message") {
734
+ object.Set("binary", event.binary);
735
+ object.Set("data", MessagePayloadToValue(env, event));
657
736
  }
658
- if (scoped->channel) {
659
- object.Set("channel", NativeDataChannel::NewInstance(env, scoped->channel));
660
- object.Set("channelId", scoped->channel->id);
737
+ if (event.channel) {
738
+ object.Set("channel", NativeDataChannel::NewInstance(env, event.channel));
739
+ object.Set("channelId", event.channel->id);
661
740
  object.Set("channelReadyState", "open");
662
741
  }
663
742
 
664
- callback.Call({object});
743
+ return object;
665
744
  }
666
745
 
667
746
  Napi::Object CandidateToObject(Napi::Env env, const rtc::Candidate &candidate) {
@@ -674,7 +753,9 @@ Napi::Object CandidateToObject(Napi::Env env, const rtc::Candidate &candidate) {
674
753
  rtc::Configuration ParseConfiguration(const Napi::CallbackInfo &info) {
675
754
  rtc::Configuration config;
676
755
  config.disableAutoNegotiation = true;
677
- config.disableAutoGathering = false;
756
+ config.disableAutoGathering = true;
757
+ // Standard Ethernet MTU avoids the conservative default without requiring an API extension.
758
+ config.mtu = 1500;
678
759
 
679
760
  if (info.Length() == 0 || !info[0].IsObject())
680
761
  return config;