diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 318bae1f13..0000000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,114 +0,0 @@ -name: build - -on: - # Run on PRs and pushes to the default branch. - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "0 0 * * 0" - -# Declare default permissions as read only. -permissions: read-all - -env: - # Keep for Dart SDK reporting - PUB_ENVIRONMENT: bot.github - # LTS - NODE_VERSION: '20' - # Tool location - BASE_DIR: ${{ github.workspace }} - TOOL_DIR: ${{ github.workspace }}/tool - -jobs: - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: -# - sdk: dev -# experimental: false - - sdk: beta - experimental: false - - sdk: stable - experimental: false - continue-on-error: ${{ matrix.experimental }} - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - with: - submodules: recursive - - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d - with: - sdk: ${{ matrix.sdk }} - - run: dart pub get - - run: tool/test.sh - env: - DART_CHANNEL: ${{ matrix.sdk }} - - deploy: - permissions: - checks: write - pull-requests: write - if: ${{ github.event_name == 'push' && - github.ref == 'refs/heads/main' && - github.repository == 'dart-lang/site-www' }} - needs: test - runs-on: ubuntu-latest - env: - FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} - FIREBASE_PROJECT: default - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - with: - submodules: recursive - - run: make build - - run: make write-prod-robots - - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 - with: - node-version: ${{ env.NODE_VERSION }} - - run: npm install -g firebase-tools@12.8.1 - - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d - with: - sdk: stable - - run: tool/check-links.sh - - uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 - with: - repoToken: '${{ secrets.GITHUB_TOKEN }}' - firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_DART_DEV }}' - projectId: dart-dev - channelId: live - - stage: - permissions: - contents: read - checks: write - pull-requests: write - if: ${{ github.ref != 'refs/heads/main' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - with: - submodules: recursive - - run: make build - - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 - with: - node-version: ${{ env.NODE_VERSION }} - - run: npm install -g firebase-tools@12.8.1 - - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d - with: - sdk: stable - - run: tool/check-links.sh - - name: Stage site on Firebase - if: ${{ - github.event.pull_request.head.repo.full_name == github.repository && - github.event.pull_request.user.login != 'dependabot[bot]' }} - uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 - with: - repoToken: '${{ secrets.GITHUB_TOKEN }}' - firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_DART_DEV }}' - projectId: dart-dev diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000000..f7d85d81cb --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,57 @@ +# This GitHub workflow has been disabled and is no longer used. +# Website deploys are now done through Cloud Build. +# See https://github.com/dart-lang/site-www/pull/5447 +name: deploy + +on: + # Run on pushes to the default branch. + push: + branches: + - main + schedule: + - cron: "0 0 * * 0" + +# Declare default permissions as read only. +permissions: read-all + +env: + # Keep for Dart SDK reporting + PUB_ENVIRONMENT: bot.github + # LTS + NODE_VERSION: '20' + # Tool location + BASE_DIR: ${{ github.workspace }} + TOOL_DIR: ${{ github.workspace }}/tool + +jobs: + deploy: + name: Deploy production site to Firebase hosting + permissions: + checks: write + pull-requests: write + if: ${{ github.event_name == 'push' && + github.ref == 'refs/heads/main' && + github.repository == 'dart-lang/site-www' }} + runs-on: ubuntu-latest + env: + FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} + FIREBASE_PROJECT: default + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + submodules: recursive + - run: make build + - run: make write-prod-robots + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 + with: + node-version: ${{ env.NODE_VERSION }} + - run: npm install -g firebase-tools@13.0.2 + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + with: + sdk: stable + - uses: FirebaseExtended/action-hosting-deploy@120e124148ab7016bec2374e5050f15051255ba2 + with: # TODO(khanhnwin/drewroen): Migrate deploy to Cloud Build + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_DART_DEV }}' + projectId: dart-dev + channelId: live diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000..fa0f802f72 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,66 @@ +name: test + +on: + # Run on PRs and pushes to the default branch. + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 0 * * 0" + +# Declare default permissions as read only. +permissions: read-all + +env: + # Keep for Dart SDK reporting + PUB_ENVIRONMENT: bot.github + # LTS + NODE_VERSION: '20' + # Tool location + BASE_DIR: ${{ github.workspace }} + TOOL_DIR: ${{ github.workspace }}/tool + +jobs: + test: + name: Check excerpts and run tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - sdk: beta + experimental: false + - sdk: stable + experimental: false + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + submodules: recursive + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + with: + sdk: ${{ matrix.sdk }} + - run: dart pub get + - run: tool/test.sh + env: + DART_CHANNEL: ${{ matrix.sdk }} + + linkcheck: + name: Build site and check links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 + with: + submodules: recursive + - run: make build + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 + with: + node-version: ${{ env.NODE_VERSION }} + - run: npm install -g firebase-tools@13.0.2 + - uses: dart-lang/setup-dart@b64355ae6ca0b5d484f0106a033dd1388965d06d + with: + sdk: stable + - run: tool/check-links.sh diff --git a/Dockerfile b/Dockerfile index 762f36faff..a98eca3a9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -101,7 +101,7 @@ RUN BUNDLE_WITHOUT="test production" bundle install --jobs=4 --retry=2 ENV NODE_ENV=development COPY package.json package-lock.json ./ -RUN npm install -g firebase-tools@12.8.1 +RUN npm install -g firebase-tools@13.0.2 RUN npm install COPY ./ ./ @@ -160,7 +160,7 @@ RUN bundle exec jekyll build --config $BUILD_CONFIGS # ============== DEPLOY to FIREBASE ============== FROM build as deploy -RUN npm install -g firebase-tools@12.8.1 +RUN npm install -g firebase-tools@13.0.2 ARG FIREBASE_TOKEN ENV FIREBASE_TOKEN=$FIREBASE_TOKEN ARG FIREBASE_PROJECT=default diff --git a/cloud_build/deploy.yaml b/cloud_build/deploy.yaml new file mode 100644 index 0000000000..dcd20efab7 --- /dev/null +++ b/cloud_build/deploy.yaml @@ -0,0 +1,20 @@ +steps: + - name: gcr.io/cloud-builders/git + args: ['submodule', 'update', '--init', '--recursive'] + - name: gcr.io/cloud-builders/docker + entrypoint: '/bin/bash' + args: + - '-c' + - |- + set -e + echo "Building the website using a makefile..." + make build + make write-prod-robots + - name: gcr.io/flutter-dev-230821/firebase-ghcli + entrypoint: '/bin/bash' + args: + - '-c' + - |- + firebase deploy --project=dart-dev --only=hosting +options: + logging: CLOUD_LOGGING_ONLY diff --git a/cloud_build/stage.yaml b/cloud_build/stage.yaml index e9d3236bf3..43ff1cfa50 100644 --- a/cloud_build/stage.yaml +++ b/cloud_build/stage.yaml @@ -10,7 +10,7 @@ steps: echo "Building the website using a makefile..." make build - - name: gcr.io/flutter-dev-230821/firebase-staging + - name: gcr.io/flutter-dev-230821/firebase-ghcli entrypoint: '/bin/bash' args: - '-c' diff --git a/examples/concurrency/lib/basic_ports_example/complete.dart b/examples/concurrency/lib/basic_ports_example/complete.dart new file mode 100644 index 0000000000..4fc8d987ab --- /dev/null +++ b/examples/concurrency/lib/basic_ports_example/complete.dart @@ -0,0 +1,56 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +void main() async { + final worker = Worker(); + await worker.spawn(); + await worker.parseJson('{"key":"value"}'); +} + +// #docregion handleResponses parseJson +class Worker { + late SendPort _sendPort; + final Completer _isolateReady = Completer.sync(); +// #enddocregion handleResponses parseJson + +// #docregion spawn + Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); + } +// #enddocregion spawn + +// #docregion handleResponses + void _handleResponsesFromIsolate(dynamic message) { + if (message is SendPort) { + _sendPort = message; + _isolateReady.complete(); + } else if (message is Map) { + print(message); + } + } +// #enddocregion handleResponses + +// #docregion startRemoteIsolate + static void _startRemoteIsolate(SendPort port) { + final receivePort = ReceivePort(); + port.send(receivePort.sendPort); + + receivePort.listen((dynamic message) async { + if (message is String) { + final transformed = jsonDecode(message); + port.send(transformed); + } + }); + } +// #enddocregion startRemoteIsolate + +// #docregion parseJson + Future parseJson(String message) async { + await _isolateReady.future; + _sendPort.send(message); + } +// #enddocregion parseJson +} diff --git a/examples/concurrency/lib/basic_ports_example/handle_response_from_isolate.dart b/examples/concurrency/lib/basic_ports_example/handle_response_from_isolate.dart new file mode 100644 index 0000000000..ad7bd7cb48 --- /dev/null +++ b/examples/concurrency/lib/basic_ports_example/handle_response_from_isolate.dart @@ -0,0 +1,44 @@ +// ignore_for_file: unused_field + +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +// #docregion +class Worker { + late SendPort _sendPort; + + // spawn method + + void _handleResponsesFromIsolate(dynamic message) { + if (message is SendPort) { + _sendPort = message; + } else if (message is Map) { + print(message); + } + } + +// rest of class.. +// #enddocregion + + Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); + } + + static void _startRemoteIsolate(SendPort port) { + final receivePort = ReceivePort(); + port.send(receivePort.sendPort); + + receivePort.listen((dynamic message) async { + final decoded = jsonDecode(message as String); + port.send(decoded); + }); + } + + Future parseJson(String message) async { + // TODO: Define a public method that can + // be used to send messages to the worker isolate. + } +} diff --git a/examples/concurrency/lib/basic_ports_example/parse_json.dart b/examples/concurrency/lib/basic_ports_example/parse_json.dart new file mode 100644 index 0000000000..75b8dab15b --- /dev/null +++ b/examples/concurrency/lib/basic_ports_example/parse_json.dart @@ -0,0 +1,44 @@ +// ignore_for_file: unused_field + +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +// #docregion +class Worker { + late SendPort _sendPort; + final Completer _isolateReady = Completer.sync(); // New + + void _handleResponsesFromIsolate(dynamic message) { + if (message is SendPort) { + _sendPort = message; + _isolateReady.complete(); // New + } else if (message is Map) { + print(message); + } + } + + // New + Future parseJson(String message) async { + await _isolateReady.future; + _sendPort.send(message); + } +// rest of class.. +// #enddocregion + + Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); + } + + static void _startRemoteIsolate(SendPort port) { + final receivePort = ReceivePort(); + port.send(receivePort.sendPort); + + receivePort.listen((dynamic message) async { + final decoded = jsonDecode(message as String); + port.send(decoded); + }); + } +} diff --git a/examples/concurrency/lib/basic_ports_example/spawn.dart b/examples/concurrency/lib/basic_ports_example/spawn.dart new file mode 100644 index 0000000000..c150a44275 --- /dev/null +++ b/examples/concurrency/lib/basic_ports_example/spawn.dart @@ -0,0 +1,27 @@ +// ignore_for_file: unused_field + +import 'dart:async'; +import 'dart:isolate'; + +class Worker { + // #docregion + Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); + } + // #enddocregion + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Define code that should be executed on the worker isolate. + } + + static void _startRemoteIsolate(SendPort port) { + // TODO: Handle messages sent back from the worker isolate. + } + + Future parseJson(String message) async { + // TODO: Define a public method that can + // be used to send messages to the worker isolate. + } +} diff --git a/examples/concurrency/lib/basic_ports_example/start.dart b/examples/concurrency/lib/basic_ports_example/start.dart new file mode 100644 index 0000000000..1a494eb6cc --- /dev/null +++ b/examples/concurrency/lib/basic_ports_example/start.dart @@ -0,0 +1,23 @@ +// ignore_for_file: unused_field, unused_element +import 'dart:isolate'; + +// #docregion +class Worker { + Future spawn() async { + // TODO: Add functionality to spawn a worker isolate. + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Define code that should be executed on the worker isolate. + } + + static void _startRemoteIsolate(SendPort port) { + // TODO: Handle messages sent back from the worker isolate. + } + + Future parseJson(String message) async { + // TODO: Define a public method that can + // be used to send messages to the worker isolate. + } +} +// #enddocregion diff --git a/examples/concurrency/lib/basic_ports_example/start_remote_isolate.dart b/examples/concurrency/lib/basic_ports_example/start_remote_isolate.dart new file mode 100644 index 0000000000..4ccb756408 --- /dev/null +++ b/examples/concurrency/lib/basic_ports_example/start_remote_isolate.dart @@ -0,0 +1,34 @@ +// ignore_for_file: unused_field + +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +class Worker { + Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Define code that should be executed on the worker isolate. + } + + // #docregion + static void _startRemoteIsolate(SendPort port) { + final receivePort = ReceivePort(); + port.send(receivePort.sendPort); + + receivePort.listen((dynamic message) async { + final decoded = jsonDecode(message as String); + port.send(decoded); + }); + } + // #enddocregion + + Future parseJson(String message) async { + // TODO: Define a public method that can + // be used to send messages to the worker isolate. + } +} diff --git a/examples/concurrency/lib/robust_ports_example/complete.dart b/examples/concurrency/lib/robust_ports_example/complete.dart new file mode 100644 index 0000000000..660c1dcc06 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/complete.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +void main() async { + final worker = await Worker.spawn(); + print(await worker.parseJson('{"key":"value"}')); + print(await worker.parseJson('"banana"')); + print(await worker.parseJson('[true, false, null, 1, "string"]')); + print( + await Future.wait([worker.parseJson('"yes"'), worker.parseJson('"no"')])); + worker.close(); +} + +// #docregion constructor +class Worker { + final SendPort _commands; + final ReceivePort _responses; +// #enddocregion constructor + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + Future parseJson(String message) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, message)); + return await completer.future; + } + + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(receivePort, sendPort); + } + + Worker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + receivePort.listen((message) { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, String jsonText) = message as (int, String); + try { + final jsonData = jsonDecode(jsonText); + sendPort.send((id, jsonData)); + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + print('--- port closed --- '); + } + } +} diff --git a/examples/concurrency/lib/robust_ports_example/spawn_1.dart b/examples/concurrency/lib/robust_ports_example/spawn_1.dart new file mode 100644 index 0000000000..0f6fe429d5 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/spawn_1.dart @@ -0,0 +1,48 @@ +// ignore_for_file: unused_field, body_might_complete_normally_nullable, unused_element + +import 'dart:async'; +import 'dart:isolate'; + +// #docregion +class Worker { + final SendPort _commands; + final ReceivePort _responses; + + static Future spawn() async { + // Create a receive port and add its initial message handler. + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; +// #enddocregion + throw UnimplementedError(); +// #docregion + } +// #enddocregion + + Future parseJson(String message) async { + // TODO: Ensure the port is still open. + _commands.send(message); + } + + Worker._(this._commands, this._responses) { + // TODO: Initialize main isolate receive port listener. + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _startRemoteIsolate(SendPort sp) { + // TODO: Initialize worker isolate's ports. + } +} diff --git a/examples/concurrency/lib/robust_ports_example/spawn_2.dart b/examples/concurrency/lib/robust_ports_example/spawn_2.dart new file mode 100644 index 0000000000..59b4fcbc53 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/spawn_2.dart @@ -0,0 +1,57 @@ +// ignore_for_file: unused_field, body_might_complete_normally_nullable, unused_element + +import 'dart:async'; +import 'dart:isolate'; + +// #docregion +class Worker { + final SendPort _commands; + final ReceivePort _responses; + + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(sendPort, receivePort); + } +// #enddocregion + + Future parseJson(String message) async { + // TODO: Ensure the port is still open. + _commands.send(message); + } + + Worker._(this._commands, this._responses) { + // TODO: Initialize main isolate receive port listener. + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _startRemoteIsolate(SendPort sp) { + // TODO: Initialize worker isolate's ports. + } +} diff --git a/examples/concurrency/lib/robust_ports_example/start.dart b/examples/concurrency/lib/robust_ports_example/start.dart new file mode 100644 index 0000000000..404d1341c6 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/start.dart @@ -0,0 +1,37 @@ +// ignore_for_file: unused_field, body_might_complete_normally_nullable, unused_element + +import 'dart:isolate'; + +// #docregion +class Worker { + final SendPort _commands; + final ReceivePort _responses; + + Future parseJson(String message) async { + // TODO: Ensure the port is still open. + _commands.send(message); + } + + static Future spawn() async { + // TODO: Add functionality to create a new Worker object with a + // connection to a spawned isolate. + throw UnimplementedError(); + } + + Worker._(this._commands, this._responses) { + // TODO: Initialize main isolate receive port listener. + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _startRemoteIsolate(SendPort sp) { + // TODO: Initialize worker isolate's ports. + } +} +// #enddocregion diff --git a/examples/concurrency/lib/robust_ports_example/step_4.dart b/examples/concurrency/lib/robust_ports_example/step_4.dart new file mode 100644 index 0000000000..233b14f7c5 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/step_4.dart @@ -0,0 +1,80 @@ +// ignore_for_file: unused_field, body_might_complete_normally_nullable, unused_element + +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +// #docregion constructor +class Worker { + final SendPort _commands; + final ReceivePort _responses; +// #enddocregion constructor + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(receivePort, sendPort); + } + +// #docregion parse-json + Future parseJson(String message) async { + _commands.send(message); + } +// #enddocregion parse-json + +// #docregion constructor + Worker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } +// #enddocregion constructor + +// #docregion handle-response + void _handleResponsesFromIsolate(dynamic message) { + if (message is RemoteError) { + throw message; + } else { + print(message); + } + } +// #enddocregion handle-response + +// #docregion handle-commands + static void _handleCommandsToIsolate( + ReceivePort receivePort, SendPort sendPort) { + receivePort.listen((message) { + try { + final jsonData = jsonDecode(message as String); + sendPort.send(jsonData); + } catch (e) { + sendPort.send(RemoteError(e.toString(), '')); + } + }); + } +// #enddocregion handle-commands + +// #docregion start-isolate + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } +// #enddocregion start-isolate +} diff --git a/examples/concurrency/lib/robust_ports_example/step_5_add_completers.dart b/examples/concurrency/lib/robust_ports_example/step_5_add_completers.dart new file mode 100644 index 0000000000..b39edb26d6 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/step_5_add_completers.dart @@ -0,0 +1,91 @@ +// ignore_for_file: unused_field, body_might_complete_normally_nullable, unused_element + +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +// #docregion vars +class Worker { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; +// #enddocregion vars + + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(receivePort, sendPort); + } + +// #docregion parse-json + Future parseJson(String message) async { + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, message)); + return await completer.future; + } +// #enddocregion parse-json + +// #docregion constructor + Worker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } +// #enddocregion constructor + +// #docregion handle-response + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); // New + final completer = _activeRequests.remove(id)!; // New + + if (response is RemoteError) { + completer.completeError(response); // Updated + } else { + completer.complete(response); // Updated + } + } +// #enddocregion handle-response + +// #docregion handle-commands + static void _handleCommandsToIsolate( + ReceivePort receivePort, SendPort sendPort) { + receivePort.listen((message) { + final (int id, String jsonText) = message as (int, String); // New + try { + final jsonData = jsonDecode(jsonText); + sendPort.send((id, jsonData)); // Updated + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); + } +// #enddocregion handle-commands + +// #docregion start-isolate + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } +// #enddocregion start-isolate +} diff --git a/examples/concurrency/lib/robust_ports_example/step_6_close_ports.dart b/examples/concurrency/lib/robust_ports_example/step_6_close_ports.dart new file mode 100644 index 0000000000..759c70dc94 --- /dev/null +++ b/examples/concurrency/lib/robust_ports_example/step_6_close_ports.dart @@ -0,0 +1,105 @@ +// ignore_for_file: unused_field, body_might_complete_normally_nullable, unused_element +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +// #docregion close +class Worker { + bool _closed = false; +// #enddocregion close + + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(receivePort, sendPort); + } + + // #docregion parse-json + Future parseJson(String message) async { + if (_closed) throw StateError('Closed'); // New + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, message)); + return await completer.future; + } + // #enddocregion parse-json + + Worker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + } + + // #docregion handle-commands + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + receivePort.listen((message) { + // New if-block. + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, String jsonText) = message as (int, String); + try { + final jsonData = jsonDecode(jsonText); + sendPort.send((id, jsonData)); + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); + } + // #enddocregion handle-commands + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + +// #docregion close + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + print('--- port closed --- '); + } + } +// #enddocregion close +} diff --git a/examples/concurrency/lib/simple_isolate_closure.dart b/examples/concurrency/lib/simple_isolate_closure.dart index 444b144be6..223efe1592 100644 --- a/examples/concurrency/lib/simple_isolate_closure.dart +++ b/examples/concurrency/lib/simple_isolate_closure.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; -// #docregion main +// #docregion const String filename = 'with_keys.json'; void main() async { @@ -16,4 +16,4 @@ void main() async { // Use that data. print('Number of JSON keys: ${jsonData.length}'); } -// #enddocregion main +// #enddocregion diff --git a/examples/concurrency/pubspec.yaml b/examples/concurrency/pubspec.yaml index 18e5e6459e..8b3e7fa484 100644 --- a/examples/concurrency/pubspec.yaml +++ b/examples/concurrency/pubspec.yaml @@ -6,6 +6,9 @@ publish_to: none environment: sdk: ^3.2.0 +dependencies: + http: any + dev_dependencies: lints: ^3.0.0 test: ^1.24.6 diff --git a/examples/misc/lib/language_tour/classes/point_this.dart b/examples/misc/lib/language_tour/classes/point_this.dart new file mode 100644 index 0000000000..a2135e9737 --- /dev/null +++ b/examples/misc/lib/language_tour/classes/point_this.dart @@ -0,0 +1,16 @@ +// ignore_for_file: invalid_reference_to_this, unnecessary_this +double initialX = 1.5; + +class Point { + // OK, can access declarations that do not depend on `this`: + double? x = initialX; + + // ERROR, can't access `this` in non-`late` initializer: + double? y = this.x; + + // OK, can access `this` in `late` initializer: + late double? z = this.x; + + // OK, `this.fieldName` is a parameter declaration, not an expression: + Point(this.x, this.y); +} diff --git a/firebase.json b/firebase.json index a6108a9741..7a9d5b84c6 100644 --- a/firebase.json +++ b/firebase.json @@ -170,7 +170,8 @@ { "source": "/go/non-promo-public-field", "destination": "/tools/non-promotion-reasons#private", "type": 301 }, { "source": "/go/non-promo-this", "destination": "/tools/non-promotion-reasons#this", "type": 301 }, { "source": "/go/non-promo-write", "destination": "/tools/non-promotion-reasons#write", "type": 301 }, - + + { "source": "/go/next-gen-js-interop", "destination": "/interop/js-interop#next-generation-js-interop-preview", "type": 301 }, { "source": "/go/null-safety-migration", "destination": "/null-safety/migration-guide", "type": 301 }, { "source": "/go/package-discontinue", "destination": "/tools/pub/publishing#discontinue", "type": 301 }, { "source": "/go/package-retraction", "destination": "/tools/pub/publishing#retract", "type": 301 }, diff --git a/src/_data/side-nav.yml b/src/_data/side-nav.yml index ced1ae9972..513a3e155d 100644 --- a/src/_data/side-nav.yml +++ b/src/_data/side-nav.yml @@ -101,6 +101,8 @@ permalink: /language/concurrency - title: Asynchronous support permalink: /language/async + - title: Isolates + permalink: /language/isolates - title: Null safety expanded: false children: diff --git a/src/assets/img/language/concurrency/ports-passing-messages.png b/src/assets/img/language/concurrency/ports-passing-messages.png new file mode 100644 index 0000000000..aef5a58c67 Binary files /dev/null and b/src/assets/img/language/concurrency/ports-passing-messages.png differ diff --git a/src/assets/img/language/concurrency/ports-setup.png b/src/assets/img/language/concurrency/ports-setup.png new file mode 100644 index 0000000000..f42781fc0e Binary files /dev/null and b/src/assets/img/language/concurrency/ports-setup.png differ diff --git a/src/assets/img/language/event-handling.png b/src/assets/img/language/event-handling.png new file mode 100644 index 0000000000..4a5c83cab0 Binary files /dev/null and b/src/assets/img/language/event-handling.png differ diff --git a/src/codelabs/null-safety.md b/src/codelabs/null-safety.md index 519394f435..fc67d1f6b4 100644 --- a/src/codelabs/null-safety.md +++ b/src/codelabs/null-safety.md @@ -89,10 +89,11 @@ void main() { } ``` -## The null assertion operator (!) + +## The non-null assertion operator (!) If you're sure an expression with a nullable type doesn't equal `null`, -you can use the [null assertion operator](/null-safety/understanding-null-safety#null-assertion-operator) +you can use the [non-null assertion operator][] (`!`) to make Dart treat it as non-nullable. By adding `!` after the expression, you assert two conditions to Dart about the expression: @@ -106,6 +107,8 @@ you assert two conditions to Dart about the expression: Don't use it unless you have no doubt the expression can't equal `null`. {{site.alert.end}} +[non-null assertion operator]: /null-safety/understanding-null-safety#non-null-assertion-operator + ### Exercise: Null assertion In the following code, try adding exclamation points to correct the @@ -139,7 +142,7 @@ You can also use null-aware operators to handle nullable values. Sometimes the flow of the program tells you that the value of an expression cannot be `null`. To force Dart to treat that expression as non-nullable, -add the [null assertion operator](#the-null-assertion-operator-) (`!`). +add the [non-null assertion operator](#the-non-null-assertion-operator-) (`!`). If the value does equal `null`, using this operator throws an exception. To handle potential `null` values, use the conditional property access diff --git a/src/effective-dart/usage.md b/src/effective-dart/usage.md index db2375008d..91e810b7a4 100644 --- a/src/effective-dart/usage.md +++ b/src/effective-dart/usage.md @@ -416,7 +416,7 @@ value. Sometimes it's best to simply [use `!`][] on the field. [private]: /effective-dart/design#prefer-making-declarations-private [final]: /effective-dart/design#prefer-making-fields-and-top-level-variables-final [`final`]: /effective-dart/usage#do-follow-a-consistent-rule-for-var-and-final-on-local-variables -[use `!`]: /null-safety/understanding-null-safety#null-assertion-operator +[use `!`]: /null-safety/understanding-null-safety#non-null-assertion-operator ## Strings diff --git a/src/get-dart/_linux.md b/src/get-dart/_linux.md index 94853111b2..3e8e7ba3d9 100644 --- a/src/get-dart/_linux.md +++ b/src/get-dart/_linux.md @@ -27,18 +27,3 @@ $ sudo apt-get install dart Alternatively, download Dart SDK [as a Debian package](#){:.debian-link-stable} in the `.deb` package format. - -#### Modify PATH for access to all Dart binaries - -After installing the SDK, **add its `bin` directory to your `PATH`**. For example, -use the following command to change `PATH` in your active terminal session: - -```terminal -$ export PATH="$PATH:/usr/lib/dart/bin" -``` - -To change the PATH for future terminal sessions, use a command like this: - -```terminal -$ echo 'export PATH="$PATH:/usr/lib/dart/bin"' >> ~/.profile -``` diff --git a/src/guides/language/coming-from/swift-to-dart.md b/src/guides/language/coming-from/swift-to-dart.md index 5b99a680cd..d642347d95 100644 --- a/src/guides/language/coming-from/swift-to-dart.md +++ b/src/guides/language/coming-from/swift-to-dart.md @@ -2407,7 +2407,7 @@ and won't be covered here. Each isolate has its own event loop. For more information, see [How isolates work][]. -[How isolates work]: /language/concurrency +[How isolates work]: /language/concurrency#isolates ### Futures diff --git a/src/guides/whats-new.md b/src/guides/whats-new.md index 1dde22b6f2..5f24f2f689 100644 --- a/src/guides/whats-new.md +++ b/src/guides/whats-new.md @@ -454,7 +454,7 @@ we made the following changes to this site: as the underlying compilers of tools like [`dart compile js`][] and [`webdev`][]. * Increased documentation coverage of null safety: - * Documented the null assertion operator (`!`) as part of + * Documented the non-null assertion operator (`!`) as part of the [Other operators][] section of the language tour. * Migrated the [Low-level HTML tutorials][] to support null safety and discuss how to interact with web APIs while using it. diff --git a/src/interop/js-interop.md b/src/interop/js-interop.md index 6deaee1e3d..226a9f8862 100644 --- a/src/interop/js-interop.md +++ b/src/interop/js-interop.md @@ -30,7 +30,7 @@ For help using the `js` package, see the following: Dart's JS interop story is currently evolving. Many of the features that enable future JS interop -are ready to experiment with as of Dart version 2.19. +are ready to experiment with as of Dart version 3.2. These features support the existing production and development web compilers, as well as Dart's in-progress Wasm compiler ([`dart2wasm`][]). @@ -44,7 +44,7 @@ However, the features available for preview are much closer to future JS interop than any pattern supported today. So, there are a few reasons to try them out now: -* New JS interop developers can learn and build with future JS interop +* New JS interop developers can learn and build with future JS interop, so they won't have to unlearn obsolete patterns in a few months. * Existing JS interop developers eager to experiment with the latest features in JS interop @@ -57,18 +57,16 @@ expected to work across compilers for JS interop. *Requirements:* -* Dart SDK constraint: `>= 2.19` -* [`package:js`][] constraint: `>= 0.6.6` +* Dart SDK constraint: `>= 3.2` [`dart2wasm`]: https://github.com/dart-lang/sdk/blob/main/pkg/dart2wasm#running-dart2wasm -[Dart 3]: https://medium.com/dartlang/dart-3-alpha-f1458fb9d232 -[`package:js`]: {{site.pub-pkg}}/js -### `package:js` +### `dart:js_interop` -The key feature of next-generation JS interop is [static interop][]. -We recommend using static interop as the default for `package:js`, -as it is more declarative, more likely to be optimized, +The key feature of next-generation JS interop is static interop. +We recommend using static interop through [`dart:js_interop`][] +as the default choice for interoping with JavaScript. +It is more declarative and explicit, more likely to be optimized, more likely to perform better, and required for `dart2wasm`. Static interop addresses several gaps in the existing JS interop story: @@ -85,10 +83,10 @@ Static interop addresses several gaps in the existing JS interop story: and making the boundary between the two languages more visible. For example, it enforces that JS classes are not meant to be mixed with Dart (dynamic calls aren't allowed, - and JS interop types cannot be implemented as a Dart class). + and JS interop types can't be implemented by a Dart class). You can implement static interop using -the `package:js` annotation [`@staticInterop`][]. +the `dart:js_interop` annotation [`@staticInterop`][]. The set of features for future static interop currently includes: * `@staticInterop` interfaces @@ -100,24 +98,26 @@ The set of features for future static interop currently includes: * Top-level external members * [`@JSExport`][] for mocking and exports -To learn how to implement static interop and see examples, -visit the [static interop][] specification. +For examples that showcase how to use static interop, +check out the [implementation of `package:web`][package-web], +which provides bindings to browser APIs using static interop. -[`@staticInterop`]: {{site.pub-api}}/js/latest/js/staticInterop-constant.html -[static interop]: {{site.pub-pkg}}/js#staticinterop -[`@JSExport`]: {{site.pub-pkg}}/js#jsexport-and-js_utilcreatedartexport +[`@staticInterop`]: {{site.dart-api}}/dart-js_interop/staticInterop-constant.html +[`dart:js_interop`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-js_interop/dart-js_interop-library.html +[`@JSExport`]: {{site.pub-pkg}}/dart-js_interop/JSExport-class.html +[package-web]: https://github.com/dart-lang/web -### `dart:js_util` +### `dart:js_interop_unsafe` -[`dart:js_util`][] provides low-level interop API +[`dart:js_interop_unsafe`][] provides low-level interop API and is supported by the JS and `dart2wasm` backends. -`dart:js_util` can provide more flexibility, +`dart:js_interop_unsafe` can provide more flexibility, for example, in potential, rare edge cases we haven't yet accounted for where static interop is not expressive enough. However, it is not as ergonomic, and we do not plan to optimize it in the same way as static interop. As a result, we highly recommend using static interop over -`dart:js_util` whenever it's possible. +`dart:js_interop_unsafe` whenever it's possible. -[`dart:js_util`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-js_util/dart-js_util-library.html +[`dart:js_interop_unsafe`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-js_interop_unsafe/dart-js_interop_unsafe-library.html diff --git a/src/language/async.md b/src/language/async.md index 0ddf15cb35..8b22390a82 100644 --- a/src/language/async.md +++ b/src/language/async.md @@ -6,8 +6,8 @@ prevpage: url: /language/concurrency title: Concurrency nextpage: - url: /null-safety - + url: /language/isolates + title: Isolates --- diff --git a/src/language/classes.md b/src/language/classes.md index db31a20ca9..3f7cca1e0d 100644 --- a/src/language/classes.md +++ b/src/language/classes.md @@ -167,7 +167,9 @@ class Point { } ``` -All uninitialized instance variables have the value `null`. +An uninitialized instance variable declared with a +[nullable type][] has the value `null`. +Non-nullable instance variables [must be initialized][] at declaration. All instance variables generate an implicit *getter* method. Non-final instance variables and @@ -175,11 +177,6 @@ Non-final instance variables and an implicit *setter* method. For details, check out [Getters and setters][]. -If you initialize a non-`late` instance variable where it's declared, -the value is set when the instance is created, -which is before the constructor and its initializer list execute. -As a result, non-`late` instance variable initializers can't access `this`. - ```dart class Point { @@ -195,6 +192,31 @@ void main() { } ``` +Initializing a non-`late` instance variable where it's declared +sets the value when the instance is created, +before the constructor and its initializer list execute. +As a result, the initializing expression (after the `=`) +of a non-`late` instance variable can't access `this`. + + +```dart +double initialX = 1.5; + +class Point { + // OK, can access declarations that do not depend on `this`: + double? x = initialX; + + // ERROR, can't access `this` in non-`late` initializer: + double? y = this.x; + + // OK, can access `this` in `late` initializer: + late double? z = this.x; + + // OK, `this.fieldName` is a parameter declaration, not an expression: + Point(this.x, this.y); +} +``` + Instance variables can be `final`, in which case they must be set exactly once. Initialize `final`, non-`late` instance variables @@ -350,3 +372,5 @@ can pass a static method as a parameter to a constant constructor. [initializer list]: /language/constructors#initializer-list [factory constructor]: /language/constructors#factory-constructors [late-final-ivar]: /effective-dart/design#avoid-public-late-final-fields-without-initializers +[nullable type]: /null-safety/understanding-null-safety#using-nullable-types +[must be initialized]: /null-safety/understanding-null-safety#uninitialized-variables \ No newline at end of file diff --git a/src/language/concurrency.md b/src/language/concurrency.md index 63ce3bc40d..920d7bb5cb 100644 --- a/src/language/concurrency.md +++ b/src/language/concurrency.md @@ -21,8 +21,9 @@ nextpage: This page contains a conceptual overview of how concurrent programming works in Dart. It explains the event-loop, async language features, and isolates from -a high-level. For more practical code examples of using async features, -read the [Asynchrony support](/language/async). +a high-level. For more practical code examples of using concurrency in Dart, +read the [Asynchrony support](/language/async) page and +[Isolates](/language/isolates) page. Concurrent programming in Dart refers to both asynchronous APIs, like `Future` and `Stream`, and *isolates*, which allow you to move processes to separate @@ -345,8 +346,9 @@ block without affecting other isolates. There are two ways to work with isolates in Dart, depending on the use-case: * Use [`Isolate.run()`][] to perform a single computation on a separate thread. -* Use [`Isolate.spawn()`][] to create an isolate that will handle - multiple messages over time, or a background worker. +* Use [`Isolate.spawn()`][] to create an isolate that will handle multiple + messages over time, or a background worker. For more information on working + with long-lived isolates, read the [Isolates](/language/isolates) page. In most cases, `Isolate.run` is the recommended API to run processes in the background. diff --git a/src/language/error-handling.md b/src/language/error-handling.md index 7c9cb8134e..577673b0b4 100644 --- a/src/language/error-handling.md +++ b/src/language/error-handling.md @@ -222,7 +222,7 @@ the arguments to `assert` aren't evaluated. [`dart run`]: /tools/dart-run [`dart compile js`]: /tools/dart-compile#js -[isolate]: /language/concurrency +[isolate]: /language/concurrency#isolates [`Error`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-core/Error-class.html [`Exception`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-core/Exception-class.html [`StackTrace`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-core/StackTrace-class.html diff --git a/src/language/isolates.md b/src/language/isolates.md new file mode 100644 index 0000000000..1482f37a07 --- /dev/null +++ b/src/language/isolates.md @@ -0,0 +1,1108 @@ +--- +title: Isolates +description: Information on writing isolates in Dart. +short-title: Isolates +prevpage: + url: /language/async + title: Asynchronous support +nextpage: + url: /null-safety + title: Sound Null Safety +--- + + + + + +This page discusses some examples that use the `Isolate` API to implement +isolates. + +You should use isolates whenever your application is handling computations that +are large enough to temporarily block other computations. +The most common example is in [Flutter][] applications, when you +need to perform large computations that might otherwise cause the +UI to become unresponsive. + +There aren't any rules about when you _must_ use isolates, +but here are some more situations where they can be useful: + +- Parsing and decoding exceptionally large JSON blobs. +- Processing and compressing photos, audio and video. +- Converting audio and video files. +- Performing complex searching and filtering on large lists or within + file systems. +- Performing I/O, such as communicating with a database. +- Handling a large volume of network requests. + +[Flutter]: {{site.flutter}}/perf/isolates + +## Implementing a simple worker isolate + +These examples implement a main isolate +that spawns a simple worker isolate. +[`Isolate.run()`][] simplifies the steps behind +setting up and managing worker isolates: + +1. Spawns (starts and creates) an isolate. +2. Runs a function on the spawned isolate. +3. Captures the result. +4. Returns the result to the main isolate. +5. Terminates the isolate once work is complete. +6. Checks, captures, and throws exceptions and errors back to the main isolate. + +[`Isolate.run()`]: {{site.dart-api}}/dev/dart-isolate/Isolate/run.html + +{{site.alert.flutter-note}} +If you're using Flutter, you can use [Flutter's `compute` function][] +instead of `Isolate.run()`. +{{site.alert.end}} + +[Flutter's `compute` function]: {{site.flutter-api}}/flutter/foundation/compute.html + +### Running an existing method in a new isolate + +1. Call `run()` to spawn a new isolate (a [background worker][]), + directly in the [main isolate][] while `main()` waits for the result: + + +```dart +const String filename = 'with_keys.json'; + +void main() async { + // Read some data. + final jsonData = await Isolate.run(_readAndParseJson); + + // Use that data. + print('Number of JSON keys: ${jsonData.length}'); +} +``` + +2. Pass the worker isolate the function you want it to execute + as its first argument. In this example, it's the existing function `_readAndParseJson()`: + + +```dart +Future> _readAndParseJson() async { + final fileData = await File(filename).readAsString(); + final jsonData = jsonDecode(fileData) as Map; + return jsonData; +} +``` + +3. `Isolate.run()` takes the result `_readAndParseJson()` returns + and sends the value back to the main isolate, + shutting down the worker isolate. + +4. The worker isolate *transfers* the memory holding the result + to the main isolate. It *does not copy* the data. + The worker isolate performs a verification pass to ensure + the objects are allowed to be transferred. + +`_readAndParseJson()` is an existing, +asynchronous function that could just as easily +run directly in the main isolate. +Using `Isolate.run()` to run it instead enables concurrency. +The worker isolate completely abstracts the computations +of `_readAndParseJson()`. It can complete without blocking the main isolate. + +The result of `Isolate.run()` is always a Future, +because code in the main isolate continues to run. +Whether the computation the worker isolate executes +is synchronous or asynchronous doesn't impact the +main isolate, because it's running concurrently either way. + +For the complete program, check out the [send_and_receive.dart][] sample. + +[send_and_receive.dart]: https://github.com/dart-lang/samples/blob/main/isolates/bin/send_and_receive.dart +[background worker]: /language/concurrency#background-workers +[main isolate]: /language/concurrency#the-main-isolate + +### Sending closures with isolates + +You can also create a simple worker isolate with `run()` using a +function literal, or closure, directly in the main isolate. + + +```dart +const String filename = 'with_keys.json'; + +void main() async { + // Read some data. + final jsonData = await Isolate.run(() async { + final fileData = await File(filename).readAsString(); + final jsonData = jsonDecode(fileData) as Map; + return jsonData; + }); + + // Use that data. + print('Number of JSON keys: ${jsonData.length}'); +} +``` + +This example accomplishes the same as the previous. +A new isolate spawns, computes something, and sends back the result. + +However, now the isolate sends a [closure][]. +Closures are less limited than typical named functions, +both in how they function and how they're written into the code. +In this example, `Isolate.run()` executes what looks like local code, +concurrently. In that sense, you can imagine `run()` to work like a +control flow operator for "run in parallel". + +[closure]: /language/functions#anonymous-functions + +## Sending multiple messages between isolates with ports + +Short-lived isolates are convenient to use, +but require performance overhead to spawn new isolates +and to copy objects from one isolate to another. +If your code relies on repeatedly running the same computation +using `Isolate.run`, you might improve performance by instead creating +long-lived isolates that don’t exit immediately. + +To do this, you can use some of the low-level isolate APIs that +`Isolate.run` abstracts: + +* [`Isolate.spawn()`][] and [`Isolate.exit()`][] +* [`ReceivePort`][] and [`SendPort`][] +* [`SendPort.send()` method][] + + +This section goes over the steps required to establish +2-way communication between a newly spawned isolate +and the [main isolate][]. +The first example, [Basic ports](#basic-ports-example), introduces the process +at a high-level. The second example, [Robust ports](#robust-ports-example), +gradually adds more practical, real-world functionality to the first. + +[`Isolate.exit()`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/Isolate/exit.html +[`Isolate.spawn()`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/Isolate/spawn.html +[`ReceivePort`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/ReceivePort-class.html +[`SendPort`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/SendPort-class.html +[`SendPort.send()` method]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/SendPort/send.html +[main isolate]: /language/concurrency#isolates + + +### `ReceivePort` and `SendPort` + +Setting up long-lived communication between isolates requires +two classes (in addition to `Isolate`): `ReceivePort` and `SendPort`. +These ports are the only way isolates can communicate with each other. + +A `ReceivePort` is an object that handles messages that are sent from other +isolates. Those messages are sent via a `SendPort`. + +{{site.alert.note}} +A `SendPort` object is associated with exactly one `ReceivePort`, +but a single `ReceivePort` can have many `SendPorts`. +When you create a `ReceivePort`, it creates a `SendPort` for itself. +You can create additional `SendPorts` that +can send messages to an existing `ReceivePort`. +{{site.alert.end}} + +Ports behave similarly to [`Stream`][] objects +(in fact, receive ports implement `Stream`!) +You can think of a `SendPort` and `ReceivePort` like +Stream's `StreamController` and listeners, respectively. +A `SendPort` is like a `StreamController` because you "add" messages to them +with the [`SendPort.send()` method][], and those messages are handled by a listener, +in this case the `ReceivePort`. The `RecievePort` then handles the messages it +receives by passing them as arguments to a callback that you provide. + +#### Setting up ports + +A newly spawned isolate only has the information it receives through the +`Isolate.spawn` call. If you need the main isolate to continue to communicate +with a spawned isolate past its initial creation, you must set up a +communication channel where the spawned isolate can send messages to the +main isolate. Isolates can only communicate via message passing. +They can’t “see” inside each others’ memory, +which is where the name “isolate” comes from. + +To set up this 2-way communication, first create a [`ReceivePort`][] +in the main isolate, then pass its [`SendPort`][] as an argument to the +new isolate when spawning it with `Isolate.spawn`. +The new isolate then creates its own `ReceivePort`, and sends _its_ `SendPort` +back on the `SendPort` it was passed by the main isolate. +The main isolate receives this `SendPort`, and +now both sides have an open channel to send and receive messages. + +{{site.alert.note}} +The diagrams in this section are high-level and intended to convey the +_concept_ of using ports for isolates. Actual implementation requires +a bit more code, which you will find +[later on this page](#basic-ports-example). +{{site.alert.end}} + +![A figure showing events being fed, one by one, into the event loop](/assets/img/language/concurrency/ports-setup.png) + +1. Create a `ReceivePort` in the main isolate. The `SendPort` is created + automatically as a property on the `ReceivePort`. +2. Spawn the worker isolate with `Isolate.spawn()` +3. Pass a reference to `ReceivePort.sendPort` as the first message to the + worker isolate. +4. Create another new `ReceivePort` in the worker isolate. +5. Pass a reference to the worker isolate's `ReceivePort.sendPort` as the + first message _back_ to the main isolate. + +Along with creating the ports and setting up communication, you’ll also need +to tell the ports what to do when they receive messages. This is done using +the `listen` method on each respective `ReceivePort`. + +![A figure showing events being fed, one by one, into the event loop](/assets/img/language/concurrency/ports-passing-messages.png) + +1. Send a message via the main isolate’s reference to the worker isolate's + `SendPort`. +2. Receive and handle the message via a listener on the worker isolate's + `ReceivePort`. This is where the computation you want to move off the + main isolate is executed. +3. Send a return message via the worker isolate's reference to the main + isolate's `SendPort`. +4. Receive the message via a listener on the main isolate's `ReceivePort`. + +### Basic ports example + +This example demonstrates how you can set up a long-lived worker isolate +with 2-way communication between it and the main isolate. +The code uses the example of sending JSON text to a new isolate, +where the JSON will be parsed and decoded, +before being sent back to the main isolate. + +{{site.alert.warn}} +This example is meant to teach the _bare minimum_ needed to +spawn a new isolate that can send and receive multiple messages over time. + +It does not cover important pieces of functionality that are expected +in production software, like error handling, shutting down ports, +and message sequencing. + +The [Robust ports example][] in the next section covers this functionality and +discusses some of the issues that can arise without it. +{{site.alert.end}} + +[robust ports example]: #robust-ports-example + +#### Step 1: Define the worker class + +First, create a class for your background worker isolate. +This class contains all the functionality you need to: + +- Spawn an isolate. +- Send messages to that isolate. +- Have the isolate decode some JSON. +- Send the decoded JSON back to the main isolate. + +The class exposes two public methods: one that spawns the worker +isolate, and one that handles sending messages to that worker isolate. + +The remaining sections in this example will show you +how to fill in the class methods, one-by-one. + + +```dart +class Worker { + Future spawn() async { + // TODO: Add functionality to spawn a worker isolate. + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Define code that should be executed on the worker isolate. + } + + static void _startRemoteIsolate(SendPort port) { + // TODO: Handle messages sent back from the worker isolate. + } + + Future parseJson(String message) async { + // TODO: Define a public method that can + // be used to send messages to the worker isolate. + } +} +``` + +#### Step 2: Spawn a worker isolate + +The `Worker.spawn` method is where you will group the code for creating the +worker isolate and ensuring it can receive and send messages. + +- First, create a `ReceivePort`. This allows the main isolate to receive + messages sent from the newly spawned worker isolate. +- Next, add a listener to the receive port to handle messages the worker isolate + will send back. The callback passed to the + listener, `_handleMessagesFromIsolate`, will be covered + in [step 4](#step-4-handle-messages-on-the-main-isolate). +- Finally, spawn the worker isolate with `Isolate.spawn`. It expects two + arguments: a function to be executed on the worker isolate + (covered in [step 3](#step-3-execute-code-on-the-worker-isolate)), + and the `sendPort` property of the receive port. + + +```dart +Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); +} +``` + +The `receivePort.sendPort` argument will be passed to the +callback (`_handleMessagesFromIsolate`) as an argument when it’s called on the +worker isolate. This is the first step in ensuring that the worker isolate has a +way to send messages back to the main isolate. + +#### Step 3: Execute code on the worker isolate + +In this step, you define the method `_isolateEntryPoint` that is sent to the +worker isolate to be executed when it spawns. This method is like the “main” +method for the worker isolate. + +- First, create another new `ReceivePort`. This port receives + future messages from the main isolate. +- Next, send that port’s `SendPort` back to the main isolate. +- Finally, add a listener to the new `ReceivePort`. This listener handles + messages the main isolate sends to the worker isolate. + + +```dart +static void _startRemoteIsolate(SendPort port) { + final receivePort = ReceivePort(); + port.send(receivePort.sendPort); + + receivePort.listen((dynamic message) async { + if (message is String) { + final transformed = jsonDecode(message); + port.send(transformed); + } + }); +} +``` + +The listener on the worker’s `ReceivePort` decodes the JSON passed from the main +isolate, and then sends the decoded JSON back to the main isolate. + +This listener is the entry point for messages sent from the main isolate to the +worker isolate. **This is the only chance you have to tell the worker isolate what code +to execute in the future.** + +#### Step 4: Handle messages on the main isolate + +Finally, you need to tell the main isolate how to handle messages sent from the +worker isolate back to the main isolate. To do so, you need to fill in +the `_handleResponsesFromIsolate` method. Recall that this method is passed to +the `Worker.spawn` method, as described +in [step 2](#step-2-spawn-a-worker-isolate): + + +```dart +Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); +} +``` + +Also recall that you sent a `SendPort` back to the main isolate +in [step 3](#step-3-execute-code-on-the-worker-isolate). This method handles the +receipt of that `SendPort`, as well as handling future messages (which will be +decoded JSON). + +- First, check if the message is a `SendPort`. If so, assign that port to the + class's `_sendPort` property so it can be used to send messages later. +- Next, check if the message is of type `Map`, the expected + type of decoded JSON. If so, handle that message with your + application-specific logic. In this example, the message is printed. + + +```dart +void _handleResponsesFromIsolate(dynamic message) { + if (message is SendPort) { + _sendPort = message; + _isolateReady.complete(); + } else if (message is Map) { + print(message); + } +} +``` + +#### Step 5: Add a completer to ensure your isolate is set-up + +To complete the class, define a public method called `parseJson`, which is +responsible for sending messages to the worker isolate. It also needs to ensure +that messages can be sent before the isolate is fully set up. +To handle this, use a [`Completer`][]. + +- First, add a class-level property called a `Completer` and name + it `_isolateReady`. +- Next, add a call to `complete()` on the completer in + the `_handleResponsesFromIsolate` method (created in [step 4](#step-4-handle-messages-on-the-main-isolate)) if the message is + a `SendPort`. +- Finally, in the `parseJson` method, add `await _isolateReady.future` before + adding `_sendPort.send`. This ensures that no message can be sent to the + worker isolate until it is spawned _and_ has sent its `SendPort` back to the + main isolate. + + +```dart +Future parseJson(String message) async { + await _isolateReady.future; + _sendPort.send(message); +} +``` + +#### Complete example + +
+Expand to see the complete example + + +```dart +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +void main() async { + final worker = Worker(); + await worker.spawn(); + await worker.parseJson('{"key":"value"}'); +} + +class Worker { + late SendPort _sendPort; + final Completer _isolateReady = Completer.sync(); + + Future spawn() async { + final receivePort = ReceivePort(); + receivePort.listen(_handleResponsesFromIsolate); + await Isolate.spawn(_startRemoteIsolate, receivePort.sendPort); + } + + void _handleResponsesFromIsolate(dynamic message) { + if (message is SendPort) { + _sendPort = message; + _isolateReady.complete(); + } else if (message is Map) { + print(message); + } + } + + static void _startRemoteIsolate(SendPort port) { + final receivePort = ReceivePort(); + port.send(receivePort.sendPort); + + receivePort.listen((dynamic message) async { + if (message is String) { + final transformed = jsonDecode(message); + port.send(transformed); + } + }); + } + + Future parseJson(String message) async { + await _isolateReady.future; + _sendPort.send(message); + } +} +``` +
+ +### Robust ports example + +The [previous example][] explained the basic building blocks needed to set up a +long-lived isolate with two-way communication. As mentioned, that example lacks +some important features, such as error handling, the ability to close the +ports when they’re no longer in use, and inconsistencies around message ordering +in some situations. + +This example expands on the information in the first example by creating a +long-lived worker isolate that has these additional features and more, and +follows better design patterns. Although this code has similarities to the first +example, it is not an extension of that example. + +{{site.alert.note}} +This example assumes that you are already familiar with +establishing communication between isolates with `Isolate.spawn` and ports, +which was covered in the [previous example][]. +{{site.alert.end}} + +#### Step 1: Define the worker class + +First, create a class for your background worker isolate. This class contains +all the functionality you need to: + +- Spawn an isolate. +- Send messages to that isolate. +- Have the isolate decode some JSON. +- Send the decoded JSON back to the main isolate. + +The class exposes three public methods: one that creates the worker +isolate, one that handles sending messages to that worker isolate, and one +that can shut down the ports when they’re no longer in use. + + +```dart +class Worker { + final SendPort _commands; + final ReceivePort _responses; + + Future parseJson(String message) async { + // TODO: Ensure the port is still open. + _commands.send(message); + } + + static Future spawn() async { + // TODO: Add functionality to create a new Worker object with a + // connection to a spawned isolate. + throw UnimplementedError(); + } + + Worker._(this._commands, this._responses) { + // TODO: Initialize main isolate receive port listener. + } + + void _handleResponsesFromIsolate(dynamic message) { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _handleCommandsToIsolate(ReceivePort rp, SendPort sp) async { + // TODO: Handle messages sent back from the worker isolate. + } + + static void _startRemoteIsolate(SendPort sp) { + // TODO: Initialize worker isolate's ports. + } +} +``` + +{{site.alert.note}} +In this example, `SendPort` and `ReceivePort` instances +follow a best practice naming convention, in which they are named in relation to +the main isolate. The messages sent through the `SendPort` from the main isolate +to the worker isolate are called _commands_, and the messages sent back to the +main isolate are called _responses_. +{{site.alert.end}} + +#### Step 2: Create a `RawReceivePort` in the `Worker.spawn` method + +Before spawning an isolate, you need to create a [`RawReceivePort`][], which is +a lower-level `ReceivePort`. Using `RawReceivePort` is a preferred pattern +because it allows you to separate your isolate startup logic from logic that +handles message passing on the isolate. + +In the `Worker.spawn` method: + +- First, create the `RawReceivePort`. This `ReceivePort` is only responsible for + receiving the initial message from the worker isolate, which will be + a `SendPort`. +- Next, create a `Completer` that will indicate when the isolate is ready to + receive messages. When this completes, it will return a record with + a `ReceivePort` and a `SendPort`. +- Next, define the `RawReceivePort.handler` property. This property is + a `Function?` that behaves like `ReceivePort.listener`. The function is called + when a message is received by this port. +- Within the handler function, call `isolateReady.complete()`. This expects + a [record][] with a `ReceivePort` and a `SendPort` as an argument. + The `SendPort` is the initial message sent from the worker isolate, which will + be assigned in the next step to the class level `SendPort` named `_commands`. +- Then, create a new `ReceivePort` with + the `ReceivePort.fromRawReceivePort` constructor, and pass in + the `startupPort`. + + +```dart +class Worker { + final SendPort _commands; + final ReceivePort _responses; + + static Future spawn() async { + // Create a receive port and add its initial message handler. + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; +// ··· + } +``` + +By creating a `RawReceivePort` first, and then a `ReceivePort`, you’ll be able +to add a new callback to `ReceivePort.listen` later on. Conversely, if you were +to create a `ReceivePort` straight away, you’d only be able to add +one `listener`, because `ReceivePort` implements [`Stream`][], rather +than [`BroadcastStream`][]. + +Effectively, this allows you to separate your isolate start-up logic from the +logic that handles receiving messages after setting up communication is +complete. This benefit will become more obvious as the logic in the other +methods grows. + +#### Step 3: Spawn a worker isolate with `Isolate.spawn` + +This step continues to fill in the `Worker.spawn` method. You’ll add the code +needed to spawn an isolate, and return an instance of `Worker` from this class. +In this example, the call to `Isolate.spawn` is wrapped in +a [`try`/`catch` block][], which ensures that, if the isolate fails to start up, +the `startupPort` will be closed, and the `Worker` object won’t be created. + +- First, declare a variable called `isolate`, and attempt to spawn a worker + isolate in a `try`/`catch` block. If spawning a worker isolate fails, close + the receive port that was created in the previous step. The method passed + to `Isolate.spawn` will be covered in a later step. +- Next, await the `isolateReady.future`, and destructure the send port and + receive port from the record it returns. +- Finally, return an instance of `Worker` by calling its private constructor, + and passing in the ports from that completer. + + +```dart +class Worker { + final SendPort _commands; + final ReceivePort _responses; + + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(sendPort, receivePort); + } +``` + +Note that in this example (compared to the [previous example][]), `Worker.spawn` +acts as an asynchronous static constructor for this class and is the only way to +create an instance of `Worker`. This simplifies the API, making the code that +creates an instance of `Worker` cleaner. + +#### Step 4: Complete the isolate setup process + +In this step, you will complete the basic isolate setup process. This correlates +almost entirely to the [previous example][], and there are no new concepts. +There is a slight change in that the code is broken into more methods, which +is a design practice that +sets you up for adding more functionality through the remainder of this example. +For an in-depth walkthrough of the basic process of setting up an isolate, see +the [basic ports example](#basic-ports-example). + +First, create the private constructor that is returned from the `Worker.spawn` +method. In the constructor body, add a listener to the receive port used by the +main isolate, and pass an as-yet undefined method to that listener +called `_handleResponsesFromIsolate`. + + +```dart +class Worker { + final SendPort _commands; + final ReceivePort _responses; +// ··· + Worker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } +``` + +Next, add the code to `_startRemoteIsolate` that is responsible for initializing +the ports on the worker +isolate. [Recall](#step-3-spawn-a-worker-isolate-with-isolatespawn) that this +method was passed to `Isolate.spawn` in the `Worker.spawn` method, and it will +be passed the main isolate’s `SendPort` as an argument. + +- Create a new `ReceivePort`. +- Send that port’s `SendPort` back to the main isolate. +- Call a new method called `_handleCommandsToIsolate`, and pass both the + new `ReceivePort` and `SendPort` from the main isolate as arguments. + + +```dart +static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); +} +``` + +Next, add the `_handleCommandsToIsolate` method, which is responsible for +receiving messages from the main isolate, decoding json on the worker isolate, +and sending the decoded json back as a response. + +- First, declare a listener on the worker isolate’s `ReceivePort`. +- Within the callback added to the listener, attempt to decode the JSON passed + from the main isolate within a [`try`/`catch` block][]. If decoding is + successful, send the decoded JSON back to the main isolate. +- If there is an error, send back a [`RemoteError`][]. + + +```dart +static void _handleCommandsToIsolate( + ReceivePort receivePort, SendPort sendPort) { + receivePort.listen((message) { + try { + final jsonData = jsonDecode(message as String); + sendPort.send(jsonData); + } catch (e) { + sendPort.send(RemoteError(e.toString(), '')); + } + }); +} +``` + +Next, add the code for the `_handleResponsesFromIsolate` method. + +- First, check if the message is a `RemoteError`, in which case you + should `throw` that error. +- Otherwise, print the message. In future steps, you will update this code to + return messages rather than print them. + + +```dart +void _handleResponsesFromIsolate(dynamic message) { + if (message is RemoteError) { + throw message; + } else { + print(message); + } +} +``` + +Finally, add the `parseJson` method, which is a public method that allows +outside code to send JSON to the worker isolate to be decoded. + + +```dart +Future parseJson(String message) async { + _commands.send(message); +} +``` + +You will update this method in the next step. + +#### Step 5: Handle multiple messages at the same time + +Currently, if you rapidly send messages to the worker isolate, the isolate will +send the decoded json response in _the order that they complete_, rather than +the order that they’re sent. You have no way to determine which response +corresponds to which message. + +In this step, you’ll fix this problem by giving each message an id, and +using `Completer` objects to ensure that when outside code calls `parseJson` the +response that is returned to that caller is the correct response. + +First, add two class-level properties to `Worker`: + +- `Map> _activeRequests` +- `int _idCounter` + + +```dart +class Worker { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; +``` + +The `_activeRequests` map associates a message sent to the worker isolate +with a `Completer`. The keys used in `_activeRequests` are taken +from `_idCounter`, which will be increased as more messages are sent. + +Next, update the `parseJson` method to create completers before it sends +messages to the worker isolate. + +- First create a `Completer`. +- Next, increment `_idCounter`, so that each `Completer` is associated with a + unique number. +- Add an entry to the `_activeRequests` map in which the key is the current + number of `_idCounter`, and the completer is the value. +- Send the message to the worker isolate, along with the id. Because you can + only send one value through the `SendPort`, wrap the id and message in a + [record][]. +- Finally, return the completer’s future, which will eventually contain the + response from the worker isolate. + + +```dart +Future parseJson(String message) async { + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, message)); + return await completer.future; +} +``` + +You also need to update `_handleResponsesFromIsolate` +and `_handleCommandsToIsolate` to handle this system. + +In `_handleCommandsToIsolate`, you need to account for the `message` being a +record with two values, rather than just the json text. Do so by destructuring +the values from `message`. + +Then, after decoding the json, update the call to `sendPort.send` to pass both +the id and the decoded json back to the main isolate, again using a record. + + +```dart +static void _handleCommandsToIsolate( + ReceivePort receivePort, SendPort sendPort) { + receivePort.listen((message) { + final (int id, String jsonText) = message as (int, String); // New + try { + final jsonData = jsonDecode(jsonText); + sendPort.send((id, jsonData)); // Updated + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); +} +``` + +Finally, update the `_handleResponsesFromIsolate`. + +- First, destructure the id and the response from the message argument again. +- Then, remove the completer that corresponds to this request from + the `_activeRequests` map. +- Lastly, rather than throwing an error or printing the decoded json, complete + the completer, passing in the response. When this completes, the response will + be returned to the code that called `parseJson` on the main isolate. + + +```dart +void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); // New + final completer = _activeRequests.remove(id)!; // New + + if (response is RemoteError) { + completer.completeError(response); // Updated + } else { + completer.complete(response); // Updated + } +} +``` + +#### Step 6: Add functionality to close the ports + +When the isolate is no longer being used by your code, you should close the +ports on the main isolate and the worker isolate. + +- First, add a class-level boolean that tracks if the ports are closed. +- Then, add the `Worker.close` method. Within this method: + - Update `_closed` to be true. + - Send a final message to the worker isolate. + This message is a `String` that reads “shutdown”, + but it could be any object you’d like. + You will use it in the next code snippet. +- Finally, check if `_activeRequests` is empty. If it is, close down the main + isolate’s `ReceivePort` named `_responses`. + + +```dart +class Worker { + bool _closed = false; +// ··· + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + print('--- port closed --- '); + } + } +``` + +- Next, you need to handle the “shutdown” message in the worker isolate. Add the + following code to the `_handleCommandsToIsolate` method. This code will check if + the message is a `String` that reads “shutdown”. If it is, it will close the + worker isolate’s `ReceivePort`, and return. + + +```dart +static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, +) { + receivePort.listen((message) { + // New if-block. + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, String jsonText) = message as (int, String); + try { + final jsonData = jsonDecode(jsonText); + sendPort.send((id, jsonData)); + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); +} +``` + +- Finally, you should add code to check if the ports are closed before trying to + send messages. Add one line in the `Worker.parseJson` method. + + +```dart +Future parseJson(String message) async { + if (_closed) throw StateError('Closed'); // New + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, message)); + return await completer.future; +} +``` + +#### Complete example + +
+ Expand here to see the full example + + +```dart +import 'dart:async'; +import 'dart:convert'; +import 'dart:isolate'; + +void main() async { + final worker = await Worker.spawn(); + print(await worker.parseJson('{"key":"value"}')); + print(await worker.parseJson('"banana"')); + print(await worker.parseJson('[true, false, null, 1, "string"]')); + print( + await Future.wait([worker.parseJson('"yes"'), worker.parseJson('"no"')])); + worker.close(); +} + +class Worker { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + Future parseJson(String message) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + _commands.send((id, message)); + return await completer.future; + } + + static Future spawn() async { + // Create a receive port and add its initial message handler + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = + await connection.future; + + return Worker._(receivePort, sendPort); + } + + Worker._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + receivePort.listen((message) { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, String jsonText) = message as (int, String); + try { + final jsonData = jsonDecode(jsonText); + sendPort.send((id, jsonData)); + } catch (e) { + sendPort.send((id, RemoteError(e.toString(), ''))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + print('--- port closed --- '); + } + } +} +``` + +
+ +[`Isolate.exit()`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/Isolate/exit.html +[`Isolate.spawn()`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/Isolate/spawn.html +[`ReceivePort`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/ReceivePort-class.html +[`SendPort`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/SendPort-class.html +[`SendPort.send()` method]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/SendPort/send.html +[main isolate]: /language/concurrency#isolates +[`Stream`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-async/Stream-class.html +[`BroadcastStream`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-async/BroadcastStream-class.html +[`Completer`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-async/Completer-class.html +[`RawReceivePort`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/RawReceivePort-class.html +[record]: /language/records +[previous example]: #basic-ports-example +[`try`/`catch` block]: /language/error-handling#catch +[`RemoteError`]: {{site.dart-api}}/{{site.data.pkg-vers.SDK.channel}}/dart-isolate/RemoteError-class.html diff --git a/src/language/operators.md b/src/language/operators.md index 3b7996f854..b6ab052758 100644 --- a/src/language/operators.md +++ b/src/language/operators.md @@ -509,7 +509,7 @@ You've seen most of the remaining operators in other examples: | `?[]` | Conditional subscript access | Like `[]`, but the leftmost operand can be null; example: `fooList?[1]` passes the int `1` to `fooList` to access the element at index `1` unless `fooList` is null (in which case the expression evaluates to null) | `.` | Member access | Refers to a property of an expression; example: `foo.bar` selects property `bar` from expression `foo` | `?.` | Conditional member access | Like `.`, but the leftmost operand can be null; example: `foo?.bar` selects property `bar` from expression `foo` unless `foo` is null (in which case the value of `foo?.bar` is null) -| `!` | Null assertion operator | Casts an expression to its underlying non-nullable type, throwing a runtime exception if the cast fails; example: `foo!.bar` asserts `foo` is non-null and selects the property `bar`, unless `foo` is null in which case a runtime exception is thrown +| `!` | Non-null assertion operator | Casts an expression to its underlying non-nullable type, throwing a runtime exception if the cast fails; example: `foo!.bar` asserts `foo` is non-null and selects the property `bar`, unless `foo` is null in which case a runtime exception is thrown {:.table .table-striped} For more information about the `.`, `?.`, and `..` operators, see diff --git a/src/null-safety/understanding-null-safety.md b/src/null-safety/understanding-null-safety.md index 2754313bb2..eb844dcad8 100644 --- a/src/null-safety/understanding-null-safety.md +++ b/src/null-safety/understanding-null-safety.md @@ -692,7 +692,7 @@ String makeCommand(String executable, [List? arguments]) { The language is also smarter about what kinds of expressions cause promotion. An explicit `== null` or `!= null` of course works. But explicit casts using `as`, or assignments, or the postfix `!` operator -(which we'll cover [later on](#null-assertion-operator)) also cause +(which we'll cover [later on](#non-null-assertion-operator)) also cause promotion. The general goal is that if the code is dynamically correct and it's reasonable to figure that out statically, the analysis should be clever enough to do so. @@ -873,7 +873,8 @@ There isn't a null-aware function call operator, but you can write: function?.call(arg1, arg2); ``` -### Null assertion operator + +### Non-null assertion operator The great thing about using flow analysis to move a nullable variable to the non-nullable side of the world is that doing so is provably safe. You get to diff --git a/src/tools/dartaotruntime.md b/src/tools/dartaotruntime.md index 2c82056152..70fc9f0ad8 100644 --- a/src/tools/dartaotruntime.md +++ b/src/tools/dartaotruntime.md @@ -4,25 +4,45 @@ description: Command-line tool for running AOT-compiled snapshots of Dart code. toc: false --- -Use the `dartaotruntime` command to run AOT (ahead-of-time) compiled programs -called **AOT snapshots**. -This tool is supported on Windows, macOS, and Linux. +With Dart, you can create pre-compiled Dart applications called *AOT snapshots*. + +## Create AOT snapshot app + To produce AOT snapshots, use the `aot-snapshot` subcommand of the [`dart compile` command][dart compile]. +## Run AOT snapshot app + +To run AOT programs, use the `dartaotruntime` command. +This tool supports Windows, macOS, and Linux. + +{{site.alert.note}} + To run use the `dartaotruntime` command, + add the path to your Dart `bin` directory to your `PATH` environment variable. +{{site.alert.end}} + [dart compile]: /tools/dart-compile +## Review an example + Here's an example of creating and running an AOT snapshot: ```terminal $ dart compile aot-snapshot bin/myapp.dart +``` + +```terminal Generated: /Users/me/simpleapp/bin/myapp.aot +``` + +```terminal $ dartaotruntime bin/simpleapp.aot ``` -For information on command-line options, use the `--help` flag: +## Learn more options + +To learn more about command-line options, use the `--help` flag: ```terminal $ dartaotruntime --help ``` - diff --git a/src/tutorials/server/fetch-data.md b/src/tutorials/server/fetch-data.md index b7b4b4636d..26c7ea609b 100644 --- a/src/tutorials/server/fetch-data.md +++ b/src/tutorials/server/fetch-data.md @@ -634,7 +634,7 @@ to another [isolate][] as a background worker to prevent your interface from becoming unresponsive. [Concurrency in Dart]: /language/concurrency -[isolate]: /language/concurrency +[isolate]: /language/concurrency#isolates [URI]: https://wikipedia.org/wiki/Uniform_Resource_Identifier [Using JSON]: /guides/json diff --git a/src/web/deployment.md b/src/web/deployment.md index 9d1feb6c9f..2279e1ff88 100644 --- a/src/web/deployment.md +++ b/src/web/deployment.md @@ -31,18 +31,10 @@ with `webdev build`. The following steps are optional. They can help make your app more reliable and responsive. -* [Use the pwa package to make your app work offline](#use-the-pwa-package-to-make-your-app-work-offline) * [Use deferred loading to reduce your app's initial size](#use-deferred-loading-to-reduce-your-apps-initial-size) * [Follow best practices for web apps](#follow-best-practices-for-web-apps) * [Remove unneeded build files](#remove-unneeded-build-files) -#### Use the pwa package to make your app work offline - -The [pwa package]({{site.pub-pkg}}/pwa) simplifies the task of -making your app work with limited or no connectivity. -To learn more about using this package, see -[Making a Dart web app offline-capable: 3 lines of code.](https://medium.com/dartlang/making-a-dart-web-app-offline-capable-3-lines-of-code-e980010a7815) - #### Use deferred loading to reduce your app's initial size You can use Dart's support for deferred loading to