├── .ably └── capabilities.yaml ├── .editorconfig ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── bundle-report.yml │ ├── check.yml │ ├── docs.yml │ ├── features.yml │ ├── publish-cdn.yml │ ├── react.yml │ ├── spec-coverage-report.yml │ ├── test-browser.yml │ ├── test-node.yml │ └── test-package.yml ├── .gitignore ├── .gitmodules ├── .mocharc.js ├── .prettierignore ├── .prettierrc.json ├── .vscode └── launch.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── Gruntfile.js ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── ably.d.ts ├── docs ├── migration-guides │ ├── v1 │ │ └── react-hooks.md │ └── v2 │ │ ├── lib.md │ │ └── react-hooks.md └── react.md ├── grunt └── esbuild │ ├── build.js │ └── strip-logs.js ├── images └── JavaScriptSDK-github.png ├── modular.d.ts ├── objects.d.ts ├── package.json ├── push.d.ts ├── resources ├── Browserstack-logo@2x.png └── typescript-demo.gif ├── scripts ├── README.md ├── cdn_deploy.js ├── dox.d.ts ├── moduleReport.ts ├── processPrivateApiData │ ├── dto.ts │ ├── exclusions.ts │ ├── grouping.ts │ ├── load.ts │ ├── output.ts │ ├── run.ts │ ├── runtimeContext.ts │ ├── staticContext.ts │ ├── utils.ts │ └── withoutPrivateAPIUsage.ts └── specCoverageReport.ts ├── src ├── common │ ├── constants │ │ ├── HttpMethods.ts │ │ ├── HttpStatusCodes.ts │ │ ├── TransportName.ts │ │ └── XHRStates.ts │ ├── lib │ │ ├── client │ │ │ ├── auth.ts │ │ │ ├── baseclient.ts │ │ │ ├── baserealtime.ts │ │ │ ├── baserest.ts │ │ │ ├── channelstatechange.ts │ │ │ ├── connection.ts │ │ │ ├── connectionstatechange.ts │ │ │ ├── defaultrealtime.ts │ │ │ ├── defaultrest.ts │ │ │ ├── filteredsubscriptions.ts │ │ │ ├── modularplugins.ts │ │ │ ├── paginatedresource.ts │ │ │ ├── presencemap.ts │ │ │ ├── push.ts │ │ │ ├── realtimeannotations.ts │ │ │ ├── realtimechannel.ts │ │ │ ├── realtimepresence.ts │ │ │ ├── resource.ts │ │ │ ├── rest.ts │ │ │ ├── restannotations.ts │ │ │ ├── restchannel.ts │ │ │ ├── restchannelmixin.ts │ │ │ ├── restpresence.ts │ │ │ └── restpresencemixin.ts │ │ ├── transport │ │ │ ├── comettransport.ts │ │ │ ├── connectionerrors.ts │ │ │ ├── connectionmanager.ts │ │ │ ├── messagequeue.ts │ │ │ ├── protocol.ts │ │ │ ├── transport.ts │ │ │ └── websockettransport.ts │ │ ├── types │ │ │ ├── annotation.ts │ │ │ ├── basemessage.ts │ │ │ ├── defaultannotation.ts │ │ │ ├── defaultmessage.ts │ │ │ ├── defaultpresencemessage.ts │ │ │ ├── devicedetails.ts │ │ │ ├── errorinfo.ts │ │ │ ├── message.ts │ │ │ ├── presencemessage.ts │ │ │ ├── protocolmessage.ts │ │ │ ├── protocolmessagecommon.ts │ │ │ ├── pushchannelsubscription.ts │ │ │ └── stats.ts │ │ └── util │ │ │ ├── defaults.ts │ │ │ ├── eventemitter.ts │ │ │ ├── logger.ts │ │ │ ├── multicaster.ts │ │ │ └── utils.ts │ ├── platform.ts │ └── types │ │ ├── ClientOptions.ts │ │ ├── IBufferUtils.ts │ │ ├── ICipher.ts │ │ ├── ICryptoStatic.ts │ │ ├── IDefaults.d.ts │ │ ├── IPlatformConfig.d.ts │ │ ├── IWebStorage.ts │ │ ├── IXHRRequest.d.ts │ │ ├── channel.d.ts │ │ ├── cryptoDataTypes.ts │ │ ├── globals.d.ts │ │ ├── http.ts │ │ ├── msgpack.ts │ │ └── utils.d.ts ├── fragments │ ├── ably.d.ts │ └── license.js ├── platform │ ├── nativescript │ │ ├── config.js │ │ ├── index.ts │ │ └── lib │ │ │ └── util │ │ │ └── webstorage.js │ ├── nodejs │ │ ├── config.ts │ │ ├── index.ts │ │ └── lib │ │ │ ├── transport │ │ │ ├── index.ts │ │ │ ├── nodecomettransport.d.ts │ │ │ └── nodecomettransport.js │ │ │ └── util │ │ │ ├── bufferutils.ts │ │ │ ├── crypto.ts │ │ │ ├── defaults.ts │ │ │ └── http.ts │ ├── react-hooks │ │ ├── .eslintrc.json │ │ ├── .gitignore │ │ ├── CHANGELOG.md │ │ ├── res │ │ │ ├── package.cjs.json │ │ │ ├── package.mjs.json │ │ │ └── package.react.json │ │ ├── sample-app │ │ │ ├── index.html │ │ │ ├── public │ │ │ │ └── robots.txt │ │ │ └── src │ │ │ │ ├── App.css │ │ │ │ ├── App.tsx │ │ │ │ ├── logo.svg │ │ │ │ ├── script.tsx │ │ │ │ └── vite-env.d.ts │ │ ├── src │ │ │ ├── AblyContext.tsx │ │ │ ├── AblyProvider.tsx │ │ │ ├── AblyReactHooks.ts │ │ │ ├── ChannelProvider.tsx │ │ │ ├── fakes │ │ │ │ └── ably.ts │ │ │ ├── hooks │ │ │ │ ├── constants.ts │ │ │ │ ├── useAbly.ts │ │ │ │ ├── useChannel.test.tsx │ │ │ │ ├── useChannel.ts │ │ │ │ ├── useChannelAttach.test.tsx │ │ │ │ ├── useChannelAttach.ts │ │ │ │ ├── useChannelInstance.ts │ │ │ │ ├── useChannelStateListener.test.tsx │ │ │ │ ├── useChannelStateListener.ts │ │ │ │ ├── useConnectionStateListener.test.tsx │ │ │ │ ├── useConnectionStateListener.ts │ │ │ │ ├── useEventListener.ts │ │ │ │ ├── usePresence.test.tsx │ │ │ │ ├── usePresence.ts │ │ │ │ ├── usePresenceListener.test.tsx │ │ │ │ ├── usePresenceListener.ts │ │ │ │ └── useStateErrors.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── tsconfig.cjs.json │ │ ├── tsconfig.json │ │ ├── tsconfig.mjs.json │ │ └── vite.config.ts │ ├── react-native │ │ ├── config.ts │ │ └── index.ts │ └── web │ │ ├── config.ts │ │ ├── index.ts │ │ ├── lib │ │ ├── http │ │ │ ├── http.ts │ │ │ └── request │ │ │ │ ├── fetchrequest.ts │ │ │ │ ├── index.ts │ │ │ │ └── xhrrequest.ts │ │ ├── transport │ │ │ ├── index.ts │ │ │ └── xhrpollingtransport.ts │ │ └── util │ │ │ ├── bufferutils.ts │ │ │ ├── crypto.ts │ │ │ ├── defaults.ts │ │ │ ├── domevent.js │ │ │ ├── hmac-sha256.ts │ │ │ ├── msgpack.ts │ │ │ └── webstorage.ts │ │ ├── modular.ts │ │ └── modular │ │ ├── annotations.ts │ │ ├── crypto.ts │ │ ├── http.ts │ │ ├── message.ts │ │ ├── msgpack.ts │ │ ├── presencemessage.ts │ │ ├── realtimepresence.ts │ │ └── transports.ts └── plugins │ ├── index.d.ts │ ├── objects │ ├── batchcontext.ts │ ├── batchcontextlivecounter.ts │ ├── batchcontextlivemap.ts │ ├── defaults.ts │ ├── index.ts │ ├── livecounter.ts │ ├── livemap.ts │ ├── liveobject.ts │ ├── objectid.ts │ ├── objectmessage.ts │ ├── objects.ts │ ├── objectspool.ts │ └── syncobjectsdatapool.ts │ └── push │ ├── getW3CDeviceDetails.ts │ ├── index.ts │ ├── pushactivation.ts │ └── pushchannel.ts ├── test ├── browser │ ├── connection.test.js │ ├── http.test.js │ ├── modular.test.js │ ├── push.test.js │ └── simple.test.js ├── common │ ├── globals │ │ ├── environment.js │ │ └── named_dependencies.js │ └── modules │ │ ├── client_module.js │ │ ├── objects_helper.js │ │ ├── private_api_recorder.js │ │ ├── shared_helper.js │ │ ├── testapp_manager.js │ │ └── testapp_module.js ├── mocha.html ├── package │ └── browser │ │ └── template │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── playwright-hooks.config.ts │ │ ├── playwright-lib.config.js │ │ ├── playwright │ │ ├── index.html │ │ └── index.tsx │ │ ├── server │ │ ├── resources │ │ │ ├── index-default.html │ │ │ ├── index-modular.html │ │ │ ├── index-objects.html │ │ │ └── runTest.js │ │ └── server.ts │ │ ├── src │ │ ├── ReactApp.tsx │ │ ├── index-default.ts │ │ ├── index-modular.ts │ │ ├── index-objects.ts │ │ ├── sandbox.ts │ │ └── tsconfig.json │ │ ├── test │ │ ├── hooks │ │ │ └── ReactApp.spec.tsx │ │ └── lib │ │ │ └── package.test.ts │ │ └── tsconfig.json ├── playwright.html ├── realtime │ ├── annotations.test.js │ ├── api.test.js │ ├── auth.test.js │ ├── channel.test.js │ ├── connection.test.js │ ├── connectivity.test.js │ ├── crypto.test.js │ ├── delta.test.js │ ├── encoding.test.js │ ├── event_emitter.test.js │ ├── failure.test.js │ ├── history.test.js │ ├── init.test.js │ ├── message.test.js │ ├── objects.test.js │ ├── presence.test.js │ ├── reauth.test.js │ ├── resume.test.js │ ├── sync.test.js │ ├── transports.test.js │ └── utils.test.js ├── rest │ ├── api.test.js │ ├── auth.test.js │ ├── batch.test.js │ ├── bufferutils.test.js │ ├── capability.test.js │ ├── defaults.test.js │ ├── fallbacks.test.js │ ├── history.test.js │ ├── http.test.js │ ├── init.test.js │ ├── message.test.js │ ├── presence.test.js │ ├── push.test.js │ ├── request.test.js │ ├── stats.test.js │ ├── status.test.js │ ├── time.test.js │ └── updates-deletes.test.js ├── support │ ├── browser_file_list.js │ ├── browser_setup.js │ ├── environment.vars.js │ ├── mocha_junit_reporter │ │ ├── index.js │ │ └── shims │ │ │ └── fs.js │ ├── mocha_reporter.js │ ├── modules_helper.js │ ├── output_directory_paths.js │ ├── playwrightSetup.js │ ├── push_channel_transport.js │ ├── push_sw.js │ ├── root_hooks.js │ ├── runPlaywrightTests.js │ └── test_helper.js ├── unit │ └── presencemap.test.js └── web_server.js ├── tools ├── .gitignore └── crypto │ └── generate-test-data.js ├── tsconfig.json ├── typedoc.json ├── typedoc └── landing-page.md ├── vite.config.ts ├── vitest.config.ts └── webpack.config.js /.ably/capabilities.yaml: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | common-version: 1.2.1 4 | compliance: 5 | Agent Identifier: 6 | Agents: 7 | Runtime: 8 | Authentication: 9 | API Key: 10 | Token: 11 | Callback: 12 | Literal: 13 | URL: 14 | Query Time: 15 | Debugging: 16 | Error Information: 17 | Logs: 18 | Protocol: 19 | JSON: 20 | Maximum Message Size: 21 | MessagePack: 22 | Realtime: 23 | Authentication: 24 | Get Confirmed Client Identifier: 25 | Channel: 26 | Attach: 27 | Encryption: 28 | History: 29 | Mode: 30 | Presence: 31 | Enter: 32 | Client: 33 | Get: 34 | History: 35 | Subscribe: 36 | Update: 37 | Client: 38 | Publish: 39 | Retry Timeout: 40 | State Events: 41 | Subscribe: 42 | Deltas: 43 | Rewind: 44 | Connection: 45 | Disconnected Retry Timeout: 46 | Get Identifier: 47 | Incremental Backoff: 48 | Lifecycle Control: 49 | OS Connectivity Events: 50 | Ping: 51 | Recovery: 52 | State Events: 53 | Suspended Retry Timeout: 54 | Message Echoes: 55 | Message Queuing: 56 | Transport Parameters: 57 | REST: 58 | Authentication: 59 | Authorize: 60 | Create Token Request: 61 | Get Client Identifier: 62 | Request Token: 63 | Channel: 64 | Encryption: 65 | Existence Check: 66 | Get: 67 | History: 68 | Name: 69 | Presence: 70 | History: 71 | Member List: 72 | Publish: 73 | Idempotence: 74 | Parameters for Query String: 75 | Release: 76 | Status: 77 | Channel Details: 78 | Opaque Request: 79 | Push Notifications Administration: 80 | Channel Subscription: 81 | List: 82 | List Channels: 83 | Remove: 84 | Save: 85 | Device Registration: 86 | Get: 87 | List: 88 | Remove: 89 | Save: 90 | Publish: 91 | Request Timeout: 92 | Service: 93 | Get Time: 94 | Statistics: 95 | Query: 96 | Support Hyperlink on Request Failure: 97 | Service: 98 | Environment: 99 | Fallbacks: 100 | Hosts: 101 | Internet Up Check: 102 | REST Follows Realtime: 103 | Retry Count: 104 | Retry Duration: 105 | Retry Timeout: 106 | Host: 107 | Testing: 108 | Disable TLS: 109 | TCP Insecure Port: 110 | TCP Secure Port: 111 | Transport: 112 | Connection Open Timeout: 113 | HTTP/2: 114 | .caveats: 'Supported in browser' 115 | Maximum Frame Size: 116 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | 10 | [*.{js,ts,.ts.d}] 11 | indent_style = space 12 | indent_size = 2 13 | # IntelliJ Specific 14 | ij_javascript_spaces_within_imports = true 15 | ij_typescript_spaces_within_imports = true 16 | ij_typescript_spaces_around_arrow_function_operator = true 17 | ij_javascript_spaces_around_arrow_function_operator = true 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | browser: true, 7 | }, 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | sourceType: 'module', 11 | }, 12 | plugins: ['@typescript-eslint', 'security', 'jsdoc'], 13 | extends: ['eslint:recommended', 'plugin:security/recommended'], 14 | rules: { 15 | 'eol-last': 'error', 16 | // security/detect-object-injection just gives a lot of false positives 17 | // see https://github.com/nodesecurity/eslint-plugin-security/issues/21 18 | 'security/detect-object-injection': 'off', 19 | '@typescript-eslint/no-var-requires': 'error', 20 | // Use typescript-eslint’s version of the no-redeclare rule, which isn’t triggered by overload signatures. 21 | // TODO remove this once we start using the full @typescript-eslint/recommended ruleset in #958 22 | 'no-redeclare': 'off', 23 | '@typescript-eslint/no-redeclare': 'error', 24 | 'jsdoc/multiline-blocks': [ 25 | 'warn', 26 | { 27 | noZeroLineText: false, 28 | noFinalLineText: false, 29 | }, 30 | ], 31 | }, 32 | overrides: [ 33 | { 34 | files: ['**/*.{ts,tsx}'], 35 | rules: { 36 | '@typescript-eslint/no-unused-vars': ['error', { varsIgnorePattern: '^_' }], 37 | // TypeScript already enforces these rules better than any eslint setup can 38 | 'no-undef': 'off', 39 | 'no-dupe-class-members': 'off', 40 | 'no-unused-vars': 'off', 41 | }, 42 | }, 43 | { 44 | files: ['ably.d.ts', 'modular.d.ts'], 45 | extends: ['plugin:jsdoc/recommended'], 46 | rules: { 47 | 'jsdoc/check-tag-names': ['warn', { definedTags: ['experimental'] }], 48 | }, 49 | }, 50 | ], 51 | ignorePatterns: ['build', 'test', 'tools', 'scripts', 'typedoc/generated', 'react', 'Gruntfile.js', 'grunt'], 52 | settings: { 53 | jsdoc: { 54 | tagNamePreference: { 55 | default: 'defaultValue', 56 | }, 57 | }, 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | labels: [] # prevent the default `dependencies` label from being added to pull requests 8 | # Disable @dependabot (except for security updates) because it pollutes a list of PRs, we'll update CI to use @renovate instead 9 | open-pull-requests-limit: 0 10 | -------------------------------------------------------------------------------- /.github/workflows/bundle-report.yml: -------------------------------------------------------------------------------- 1 | name: Bundle size report 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | bundle-report: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | deployments: write 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Reconfigure git to use HTTP authentication 17 | run: > 18 | git config --global url."https://github.com/".insteadOf 19 | ssh://git@github.com/ 20 | - name: Use Node.js 20.x 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 20.x 24 | - run: npm ci 25 | - name: Build bundle reports 26 | run: | 27 | mkdir bundle-reports 28 | npm run sourcemap -- --html bundle-reports/index.html 29 | - uses: aws-actions/configure-aws-credentials@v1 30 | with: 31 | aws-region: eu-west-2 32 | role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-ably-js 33 | role-session-name: '${{ github.run_id }}-${{ github.run_number }}' 34 | - uses: ably/sdk-upload-action@v1 35 | with: 36 | sourcePath: bundle-reports 37 | githubToken: ${{ secrets.GITHUB_TOKEN }} 38 | artifactName: bundle-report 39 | # This step performs some validation and may fail, so it should come after the steps that we want to always execute. 40 | - run: npm run modulereport 41 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Reconfigure git to use HTTP authentication 14 | run: > 15 | git config --global url."https://github.com/".insteadOf 16 | ssh://git@github.com/ 17 | - name: Use Node.js 20.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 20.x 21 | - run: npm ci 22 | - run: npm run lint 23 | - run: npm run format:check 24 | - run: npx tsc --noEmit ably.d.ts modular.d.ts 25 | # for some reason, this doesn't work in CI using `npx attw --pack .` 26 | - run: npm pack 27 | - run: npx attw ably-$(node -e "console.log(require('./package.json').version)").tgz --summary --exclude-entrypoints 'ably/modular' 28 | # see https://github.com/ably/ably-js/issues/1546 for why we ignore 'false-cjs' currently. 29 | # should remove when switched to auto-generated type declaration files for modular variant of the library. 30 | - run: npx attw ably-$(node -e "console.log(require('./package.json').version)").tgz --summary --entrypoints 'ably/modular' --ignore-rules false-cjs 31 | - run: npm audit --production 32 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: API Reference 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | deployments: write 15 | id-token: write 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Use Node.js 20.x 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 20.x 23 | 24 | - name: Install Package Dependencies 25 | run: npm ci 26 | 27 | - name: Build Documentation 28 | run: npm run docs 29 | 30 | - name: Configure AWS Credentials 31 | uses: aws-actions/configure-aws-credentials@v1 32 | with: 33 | aws-region: eu-west-2 34 | role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/ably-sdk-builds-ably-js 35 | role-session-name: '${{ github.run_id }}-${{ github.run_number }}' 36 | 37 | - name: Upload Documentation 38 | uses: ably/sdk-upload-action@v1 39 | with: 40 | sourcePath: typedoc/generated 41 | githubToken: ${{ secrets.GITHUB_TOKEN }} 42 | artifactName: typedoc 43 | -------------------------------------------------------------------------------- /.github/workflows/features.yml: -------------------------------------------------------------------------------- 1 | name: Features 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | uses: ably/features/.github/workflows/sdk-features.yml@main 12 | with: 13 | repository-name: ably-js 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /.github/workflows/publish-cdn.yml: -------------------------------------------------------------------------------- 1 | name: Publish to CDN 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Release tag' 7 | required: true 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | # These permissions are necessary to run the configure-aws-credentials action 13 | permissions: 14 | id-token: write 15 | contents: read 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | ref: ${{ github.event.inputs.version }} 20 | - name: Configure AWS Credentials 21 | uses: aws-actions/configure-aws-credentials@v1 22 | with: 23 | aws-region: us-east-1 24 | role-to-assume: arn:aws:iam::${{ secrets.ABLY_AWS_ACCOUNT_ID_SDK }}:role/prod-ably-sdk-cdn 25 | role-session-name: '${{ github.run_id }}-${{ github.run_number }}' 26 | - name: Use Node.js 20.x 27 | uses: actions/setup-node@v1 28 | with: 29 | node-version: 20.x 30 | - run: npm ci 31 | - run: node scripts/cdn_deploy.js --skipCheckout --tag=${{ github.event.inputs.version }} 32 | -------------------------------------------------------------------------------- /.github/workflows/react.yml: -------------------------------------------------------------------------------- 1 | name: Test (react hooks) 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 20 18 | - run: npm ci 19 | - run: npm run test:react 20 | -------------------------------------------------------------------------------- /.github/workflows/spec-coverage-report.yml: -------------------------------------------------------------------------------- 1 | name: Spec coverage report 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | spec-coverage-report: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | deployments: write 13 | id-token: write 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Reconfigure git to use HTTP authentication 17 | run: > 18 | git config --global url."https://github.com/".insteadOf 19 | ssh://git@github.com/ 20 | - name: Use Node.js 20.x 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 20.x 24 | - run: npm ci 25 | - run: npm run speccoveragereport 26 | -------------------------------------------------------------------------------- /.github/workflows/test-browser.yml: -------------------------------------------------------------------------------- 1 | name: Test browser 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test-browser: 11 | runs-on: ubuntu-22.04 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | browser: [chromium, firefox, webkit] 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | submodules: 'recursive' 20 | - name: Reconfigure git to use HTTP authentication 21 | run: > 22 | git config --global url."https://github.com/".insteadOf 23 | ssh://git@github.com/ 24 | - name: Use Node.js 20.x 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: 20.x 28 | - run: npm ci 29 | - name: Install Playwright browsers and dependencies 30 | run: npx playwright install --with-deps 31 | - env: 32 | PLAYWRIGHT_BROWSER: ${{ matrix.browser }} 33 | run: npm run test:playwright 34 | - name: Generate private API usage reports 35 | run: npm run process-private-api-data private-api-usage/*.json 36 | - name: Save private API usage data 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: private-api-usage-${{ matrix.browser }} 40 | path: | 41 | private-api-usage 42 | private-api-usage-reports 43 | - name: Upload test results 44 | if: always() 45 | uses: ably/test-observability-action@v1 46 | with: 47 | server-auth: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} 48 | path: './junit' 49 | -------------------------------------------------------------------------------- /.github/workflows/test-node.yml: -------------------------------------------------------------------------------- 1 | name: Test NodeJS 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test-node: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | node-version: [16.x, 18.x, 20.x] 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | submodules: 'recursive' 20 | - name: Reconfigure git to use HTTP authentication 21 | run: > 22 | git config --global url."https://github.com/".insteadOf 23 | ssh://git@github.com/ 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run test:node 30 | env: 31 | CI: true 32 | - name: Generate private API usage reports 33 | run: npm run process-private-api-data private-api-usage/*.json 34 | - name: Save private API usage data 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: private-api-usage-${{ matrix.node-version }} 38 | path: | 39 | private-api-usage 40 | private-api-usage-reports 41 | - name: Upload test results 42 | if: always() 43 | uses: ably/test-observability-action@v1 44 | with: 45 | server-auth: ${{ secrets.TEST_OBSERVABILITY_SERVER_AUTH_KEY }} 46 | path: './junit' 47 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Test NPM package 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test-npm-package: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | submodules: true 15 | - name: Use Node.js 20.x 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 20.x 19 | - run: npm ci 20 | - run: npm run test:package 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | ably-js.iml 3 | .idea 4 | node_modules 5 | npm-debug.log 6 | .tool-versions 7 | build/ 8 | react/ 9 | typedoc/generated/ 10 | junit/ 11 | private-api-usage/ 12 | private-api-usage-reports/ 13 | test/support/mocha_junit_reporter/build/ 14 | .claude 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/common/ably-common"] 2 | path = test/common/ably-common 3 | url = https://github.com/ably/ably-common.git 4 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | require: ['source-map-support/register', 'test/support/modules_helper.js', 'test/support/test_helper.js'], 3 | file: ['test/support/root_hooks.js'], 4 | reporter: 'test/support/mocha_reporter.js', 5 | }; 6 | 7 | // mocha has a ridiculous issue (https://github.com/mochajs/mocha/issues/4100) that command line 8 | // specs don't override config specs; they are merged instead, so you can't run a single test file 9 | // if you've defined specs in your config. therefore we work around it by only adding specs to the 10 | // config if none are passed as arguments 11 | if (!process.argv.slice(2).some(isTestFile)) { 12 | config.spec = ['test/realtime/*.test.js', 'test/rest/*.test.js', 'test/unit/*.test.js']; 13 | } 14 | 15 | function isTestFile(arg) { 16 | return arg.match(/\.test.js$/); 17 | } 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | test/common/ably-common/ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Test", 11 | "env": { 12 | "ABLY_ENDPOINT": "nonprod:sandbox" 13 | }, 14 | "cwd": "${workspaceFolder}", 15 | "runtimeExecutable": "node", 16 | "runtimeArgs": ["--inspect-brk=9229", "node_modules/.bin/grunt", "test:node", "--test=${relativeFile}"], 17 | "port": 9229 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2015-2022 Ably Real-time Ltd (ably.com) 2 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | This repository is owned by the Ably SDK team. 2 | -------------------------------------------------------------------------------- /docs/migration-guides/v1/react-hooks.md: -------------------------------------------------------------------------------- 1 | # React hooks upgrade / migration guide 2 | 3 | ## `@ably-labs/react-hooks` to `ably 1.x` 4 | 5 | ### Hooks now return object 6 | 7 | In previous versions of our react hooks, the `useChannel` and `usePresence` hooks returned arrays. 8 | Since these hooks now return more values we've opted to change them to return objects. 9 | You can still access the return values using simple destructuring syntax like in the below example: 10 | 11 | ```jsx 12 | const { channel, ably } = useChannel('your-channel-name', (message) => { 13 | /* ... */ 14 | }); 15 | 16 | const { presenceData, updateStatus } = usePresence('your-channel-name'); 17 | ``` 18 | 19 | ### Replacing `configureAbly` with `AblyProvider` 20 | 21 | In versions 1 and 2 of our react-hooks, we exported a function called `configureAbly` which was used to register an Ably client instance to global state. 22 | This caused a few issues (most notably it made the hooks difficult to use with hot module reloading), so we have replaced the global configuration function with a context provider (`AblyProvider`) 23 | The simplest way to use the context provider is to create your own ably-js client outside and then pass it as a prop to the `AblyProvider`. 24 | All child components of the `AblyProvider` will then be able to use the hooks, making use of the provided Ably client instance. For this reason, we recommend putting the `AblyProvider` high up in your component tree, surrounding all components which may need to use Ably hooks. 25 | 26 | For example, replace this: 27 | 28 | ```jsx 29 | configureAbly(options); 30 | ``` 31 | 32 | With this: 33 | 34 | ```jsx 35 | const client = new Ably.Realtime(options); 36 | 37 | return {children}; 38 | ``` 39 | 40 | If you were already using multiple Ably clients in the same react application, you may pass in an optional `id` prop to the provider, which you can then pass to the hooks to specify which Ably client instance the hook should use: 41 | 42 | ```jsx 43 | const client = new Ably.Realtime(options); 44 | 45 | return ( 46 | 47 | {children} 48 | 49 | ); 50 | 51 | // in a child component: 52 | useChannel({ channelName: 'my_channel', id: 'foo' }, (msg) => { 53 | console.log(msg); 54 | }); 55 | ``` 56 | -------------------------------------------------------------------------------- /images/JavaScriptSDK-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably/ably-js/HEAD/images/JavaScriptSDK-github.png -------------------------------------------------------------------------------- /objects.d.ts: -------------------------------------------------------------------------------- 1 | // The ESLint warning is triggered because we only use these types in a documentation comment. 2 | /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ 3 | import { RealtimeClient } from './ably'; 4 | import { BaseRealtime } from './modular'; 5 | /* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */ 6 | 7 | /** 8 | * Provides a {@link RealtimeClient} instance with the ability to use Objects functionality. 9 | * 10 | * To create a client that includes this plugin, include it in the client options that you pass to the {@link RealtimeClient.constructor}: 11 | * 12 | * ```javascript 13 | * import { Realtime } from 'ably'; 14 | * import Objects from 'ably/objects'; 15 | * const realtime = new Realtime({ ...options, plugins: { Objects } }); 16 | * ``` 17 | * 18 | * The Objects plugin can also be used with a {@link BaseRealtime} client 19 | * 20 | * ```javascript 21 | * import { BaseRealtime, WebSocketTransport, FetchRequest } from 'ably/modular'; 22 | * import Objects from 'ably/objects'; 23 | * const realtime = new BaseRealtime({ ...options, plugins: { WebSocketTransport, FetchRequest, Objects } }); 24 | * ``` 25 | */ 26 | declare const Objects: any; 27 | 28 | export = Objects; 29 | -------------------------------------------------------------------------------- /push.d.ts: -------------------------------------------------------------------------------- 1 | // The ESLint warning is triggered because we only use these types in a documentation comment. 2 | /* eslint-disable no-unused-vars, @typescript-eslint/no-unused-vars */ 3 | import { RealtimeClient, RestClient } from './ably'; 4 | import { BaseRest, BaseRealtime, Rest } from './modular'; 5 | /* eslint-enable no-unused-vars, @typescript-eslint/no-unused-vars */ 6 | 7 | /** 8 | * Provides a {@link RestClient} or {@link RealtimeClient} instance with the ability to be activated as a target for push notifications. 9 | * 10 | * To create a client that includes this plugin, include it in the client options that you pass to the {@link RestClient.constructor} or {@link RealtimeClient.constructor}: 11 | * 12 | * ```javascript 13 | * import { Realtime } from 'ably'; 14 | * import Push from 'ably/push'; 15 | * const realtime = new Realtime({ ...options, plugins: { Push } }); 16 | * ``` 17 | * 18 | * The Push plugin can also be used with a {@link BaseRest} or {@link BaseRealtime} client, with the additional requirement that you must also use the {@link Rest} plugin 19 | * 20 | * ```javascript 21 | * import { BaseRealtime, Rest, WebSocketTransport, FetchRequest } from 'ably/modular'; 22 | * import Push from 'ably/push'; 23 | * const realtime = new BaseRealtime({ ...options, plugins: { Rest, WebSocketTransport, FetchRequest, Push } }); 24 | * ``` 25 | */ 26 | declare const Push: any; 27 | 28 | export = Push; 29 | -------------------------------------------------------------------------------- /resources/Browserstack-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably/ably-js/HEAD/resources/Browserstack-logo@2x.png -------------------------------------------------------------------------------- /resources/typescript-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably/ably-js/HEAD/resources/typescript-demo.gif -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | This directory is for scripts used by the CI for deployment, etc. 2 | 3 | ### cdn_deploy.js 4 | 5 | Deploys correctly versioned code and source maps to the CDN. 6 | 7 | Arguments: 8 | 9 | - **--bucket**: The S3 bucket name to deploy to, defaults to `prod-cdn.ably.com`. 10 | - **--root**: The base directory inside the bucket to deploy to, defaults to `lib`. 11 | - **--path**: The local path to retrieve source files from. Defaults to `.`. 12 | - **--includeDirs**: A comma separated list of directories to include. Defaults to `.`. 13 | - **--excludeDirs**: A comma separated list of directories to exclude. Defaults to `node_modules,.git`. 14 | - **--fileRegex**: A regular expression to test file names against for upload. Defaults to `^(?!\.).*\.(map|js|html)$`. 15 | - **--skipCheckout**: Optional. Skip checking out the branch before running. 16 | 17 | #### AWS Access 18 | 19 | Expects AWS access to be configured in the surrounding environment. To run 20 | locally, first set some temporary AWS credentials as environment variables 21 | using `ably-env`: 22 | 23 | ``` 24 | source <(ably-env secrets print-aws) 25 | ``` 26 | 27 | See [AWS Access](https://ably.atlassian.net/wiki/spaces/ENG/pages/665190401/AWS+Access) 28 | for more information about gaining access to AWS. 29 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/dto.ts: -------------------------------------------------------------------------------- 1 | export type TestPrivateApiContextDto = { 2 | type: 'test'; 3 | title: string; 4 | /** 5 | * null means that either the test isn’t parameterised or that this usage is unique to the specific parameter 6 | */ 7 | parameterisedTestTitle: string | null; 8 | helperStack: string[]; 9 | file: string; 10 | suite: string[]; 11 | }; 12 | 13 | export type HookPrivateApiContextDto = { 14 | type: 'hook'; 15 | title: string; 16 | helperStack: string[]; 17 | file: string; 18 | suite: string[]; 19 | }; 20 | 21 | export type RootHookPrivateApiContextDto = { 22 | type: 'hook'; 23 | title: string; 24 | helperStack: string[]; 25 | file: null; 26 | suite: null; 27 | }; 28 | 29 | export type TestDefinitionPrivateApiContextDto = { 30 | type: 'definition'; 31 | label: string; 32 | helperStack: string[]; 33 | file: string; 34 | suite: string[]; 35 | }; 36 | 37 | export type PrivateApiContextDto = 38 | | TestPrivateApiContextDto 39 | | HookPrivateApiContextDto 40 | | RootHookPrivateApiContextDto 41 | | TestDefinitionPrivateApiContextDto; 42 | 43 | export type PrivateApiUsageDto = { 44 | context: PrivateApiContextDto; 45 | privateAPIIdentifier: string; 46 | }; 47 | 48 | export type TestStartRecord = { 49 | context: TestPrivateApiContextDto; 50 | privateAPIIdentifier: null; 51 | }; 52 | 53 | export type Record = PrivateApiUsageDto | TestStartRecord; 54 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/exclusions.ts: -------------------------------------------------------------------------------- 1 | import { PrivateApiUsageDto } from './dto'; 2 | 3 | type ExclusionRule = { 4 | privateAPIIdentifier: string; 5 | // i.e. only ignore when called from within this helper 6 | helper?: string; 7 | }; 8 | 9 | /** 10 | * This exclusions mechanism is not currently being used on `main`, but I will use it on a separate unified test suite branch in order to exclude some private API usage that can currently be disregarded in the context of the unified test suite. 11 | */ 12 | export function applyingExclusions(usageDtos: PrivateApiUsageDto[]) { 13 | const exclusionRules: ExclusionRule[] = []; 14 | 15 | return usageDtos.filter( 16 | (usageDto) => 17 | !exclusionRules.some( 18 | (exclusionRule) => 19 | exclusionRule.privateAPIIdentifier === usageDto.privateAPIIdentifier && 20 | (!('helper' in exclusionRule) || usageDto.context.helperStack.includes(exclusionRule.helper!)), 21 | ), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/grouping.ts: -------------------------------------------------------------------------------- 1 | import { PrivateApiUsageDto } from './dto'; 2 | 3 | export type Group = { 4 | key: Key; 5 | values: Value[]; 6 | }; 7 | 8 | export function grouped( 9 | values: Value[], 10 | keyForValue: (value: Value) => Key, 11 | areKeysEqual: (key1: Key, key2: Key) => boolean, 12 | ) { 13 | const result: Group[] = []; 14 | 15 | for (const value of values) { 16 | const key = keyForValue(value); 17 | 18 | let existingGroup = result.find((group) => areKeysEqual(group.key, key)); 19 | 20 | if (existingGroup === undefined) { 21 | existingGroup = { key, values: [] }; 22 | result.push(existingGroup); 23 | } 24 | 25 | existingGroup.values.push(value); 26 | } 27 | 28 | return result; 29 | } 30 | 31 | /** 32 | * Makes sure that each private API is only listed once in a given context. 33 | */ 34 | function dedupeUsages(contextGroups: Group[]) { 35 | for (const contextGroup of contextGroups) { 36 | const newUsages: typeof contextGroup.values = []; 37 | 38 | for (const usage of contextGroup.values) { 39 | const existing = newUsages.find((otherUsage) => otherUsage.privateAPIIdentifier === usage.privateAPIIdentifier); 40 | if (existing === undefined) { 41 | newUsages.push(usage); 42 | } 43 | } 44 | 45 | contextGroup.values = newUsages; 46 | } 47 | } 48 | 49 | export function groupedAndDeduped( 50 | usages: PrivateApiUsageDto[], 51 | keyForUsage: (usage: PrivateApiUsageDto) => Key, 52 | areKeysEqual: (key1: Key, key2: Key) => boolean, 53 | ) { 54 | const result = grouped(usages, keyForUsage, areKeysEqual); 55 | dedupeUsages(result); 56 | return result; 57 | } 58 | 59 | /** 60 | * Return value is sorted in decreasing order of usage of a given private API identifer 61 | */ 62 | export function groupedAndSortedByPrivateAPIIdentifier( 63 | groupedByKey: Group[], 64 | ): Group[] { 65 | const flattened = groupedByKey.flatMap((group) => group.values.map((value) => ({ key: group.key, value }))); 66 | 67 | const groupedByPrivateAPIIdentifier = grouped( 68 | flattened, 69 | (value) => value.value.privateAPIIdentifier, 70 | (id1, id2) => id1 === id2, 71 | ).map((group) => ({ key: group.key, values: group.values.map((value) => value.key) })); 72 | 73 | groupedByPrivateAPIIdentifier.sort((group1, group2) => group2.values.length - group1.values.length); 74 | 75 | return groupedByPrivateAPIIdentifier; 76 | } 77 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/load.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { applyingExclusions } from './exclusions'; 3 | import { splittingRecords, stripFilePrefix } from './utils'; 4 | import { Record } from './dto'; 5 | 6 | export function load(jsonFilePath: string) { 7 | let records = JSON.parse(readFileSync(jsonFilePath).toString('utf-8')) as Record[]; 8 | 9 | stripFilePrefix(records); 10 | 11 | let { usageDtos, testStartRecords } = splittingRecords(records); 12 | 13 | usageDtos = applyingExclusions(usageDtos); 14 | 15 | return { usageDtos, testStartRecords }; 16 | } 17 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/run.ts: -------------------------------------------------------------------------------- 1 | import { 2 | writeByPrivateAPIIdentifierCSV, 3 | writeNoPrivateAPIUsageCSV, 4 | writeRuntimePrivateAPIUsageCSV, 5 | writeStaticPrivateAPIUsageCSV, 6 | writeSummary, 7 | } from './output'; 8 | import { groupedAndDedupedByRuntimeContext } from './runtimeContext'; 9 | import { groupedAndDedupedByStaticContext } from './staticContext'; 10 | import { load } from './load'; 11 | import { percentageString, sortStaticContextUsages } from './utils'; 12 | import { splitTestsByPrivateAPIUsageRequirement } from './withoutPrivateAPIUsage'; 13 | import { groupedAndSortedByPrivateAPIIdentifier } from './grouping'; 14 | 15 | if (process.argv.length > 3) { 16 | throw new Error('Expected a single argument (path to private API usages JSON file'); 17 | } 18 | 19 | const jsonFilePath = process.argv[2]; 20 | 21 | if (!jsonFilePath) { 22 | throw new Error('Path to private API usages JSON file not specified'); 23 | } 24 | 25 | const { usageDtos, testStartRecords } = load(jsonFilePath); 26 | 27 | const usagesGroupedByRuntimeContext = groupedAndDedupedByRuntimeContext(usageDtos); 28 | 29 | const usagesGroupedByStaticContext = groupedAndDedupedByStaticContext(usageDtos); 30 | sortStaticContextUsages(usagesGroupedByStaticContext); 31 | 32 | const { 33 | requiringPrivateAPIUsage: testsThatRequirePrivateAPIUsage, 34 | notRequiringPrivateAPIUsage: testsThatDoNotRequirePrivateAPIUsage, 35 | } = splitTestsByPrivateAPIUsageRequirement(testStartRecords, usagesGroupedByRuntimeContext); 36 | 37 | const totalNumberOfTests = testStartRecords.length; 38 | const numberOfTestsThatRequirePrivateApiUsage = testsThatRequirePrivateAPIUsage.length; 39 | const numberOfTestsThatDoNotRequirePrivateAPIUsage = testsThatDoNotRequirePrivateAPIUsage.length; 40 | 41 | const summary = `Total number of tests: ${totalNumberOfTests} 42 | Number of tests that require private API usage: ${numberOfTestsThatRequirePrivateApiUsage} (${percentageString( 43 | numberOfTestsThatRequirePrivateApiUsage, 44 | totalNumberOfTests, 45 | )}) 46 | Number of tests that do not require private API usage: ${numberOfTestsThatDoNotRequirePrivateAPIUsage} (${percentageString( 47 | numberOfTestsThatDoNotRequirePrivateAPIUsage, 48 | totalNumberOfTests, 49 | )}) 50 | `; 51 | console.log(summary); 52 | 53 | const byPrivateAPIIdentifier = groupedAndSortedByPrivateAPIIdentifier(usagesGroupedByStaticContext); 54 | 55 | writeRuntimePrivateAPIUsageCSV(usagesGroupedByRuntimeContext); 56 | writeStaticPrivateAPIUsageCSV(usagesGroupedByStaticContext); 57 | writeNoPrivateAPIUsageCSV(testsThatDoNotRequirePrivateAPIUsage); 58 | writeByPrivateAPIIdentifierCSV(byPrivateAPIIdentifier); 59 | writeSummary(summary); 60 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/runtimeContext.ts: -------------------------------------------------------------------------------- 1 | import { PrivateApiContextDto, PrivateApiUsageDto } from './dto'; 2 | import { Group, groupedAndDeduped } from './grouping'; 3 | import { joinComponents } from './utils'; 4 | 5 | /** 6 | * Used for determining whether two contexts are equal. 7 | */ 8 | export function runtimeContextIdentifier(context: RuntimeContext) { 9 | return joinComponents([ 10 | { key: 'type', value: context.type }, 11 | { key: 'file', value: context.file ?? 'null' }, 12 | { key: 'suite', value: context.suite?.join(',') ?? 'null' }, 13 | { key: 'title', value: context.type === 'definition' ? context.label : context.title }, 14 | { key: 'helperStack', value: 'helperStack' in context ? context.helperStack.join(',') : 'null' }, 15 | { 16 | key: 'parameterisedTestTitle', 17 | value: ('parameterisedTestTitle' in context ? context.parameterisedTestTitle : null) ?? 'null', 18 | }, 19 | ]); 20 | } 21 | 22 | export type RuntimeContext = PrivateApiContextDto; 23 | 24 | export function groupedAndDedupedByRuntimeContext( 25 | usages: PrivateApiUsageDto[], 26 | ): Group[] { 27 | return groupedAndDeduped( 28 | usages, 29 | (usage) => usage.context, 30 | (context1, context2) => runtimeContextIdentifier(context1) === runtimeContextIdentifier(context2), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/utils.ts: -------------------------------------------------------------------------------- 1 | import { PrivateApiUsageDto, Record, TestStartRecord } from './dto'; 2 | import { Group } from './grouping'; 3 | import { StaticContext } from './staticContext'; 4 | 5 | export function stripFilePrefix(records: Record[]) { 6 | for (const record of records) { 7 | if (record.context.file !== null) { 8 | record.context.file = record.context.file.replace('/home/runner/work/ably-js/ably-js/', ''); 9 | } 10 | } 11 | } 12 | 13 | export function splittingRecords(records: Record[]) { 14 | return { 15 | testStartRecords: records.filter((record) => record.privateAPIIdentifier == null) as TestStartRecord[], 16 | usageDtos: records.filter((record) => record.privateAPIIdentifier !== null) as PrivateApiUsageDto[], 17 | }; 18 | } 19 | 20 | export function percentageString(value: number, total: number) { 21 | return `${((100 * value) / total).toLocaleString(undefined, { maximumFractionDigits: 1 })}%`; 22 | } 23 | 24 | /** 25 | * Puts the miscHelper usages (i.e. stuff that doesn’t have file info) first. 26 | */ 27 | export function sortStaticContextUsages(contextGroups: Group[]) { 28 | const original = [...contextGroups]; 29 | 30 | contextGroups.sort((a, b) => { 31 | if (a.key.type === 'miscHelper' && b.key.type !== 'miscHelper') { 32 | return -1; 33 | } 34 | 35 | if (a.key.type !== 'miscHelper' && b.key.type === 'miscHelper') { 36 | return 1; 37 | } 38 | 39 | return original.indexOf(a) - original.indexOf(b); 40 | }); 41 | } 42 | 43 | export function joinComponents(components: { key: string; value: string }[]) { 44 | return components.map((pair) => `${pair.key}=${pair.value}`).join(';'); 45 | } 46 | -------------------------------------------------------------------------------- /scripts/processPrivateApiData/withoutPrivateAPIUsage.ts: -------------------------------------------------------------------------------- 1 | import { PrivateApiUsageDto, TestStartRecord } from './dto'; 2 | import { Group } from './grouping'; 3 | import { RuntimeContext } from './runtimeContext'; 4 | 5 | function areStringArraysEqual(arr1: string[], arr2: string[]) { 6 | if (arr1.length !== arr2.length) { 7 | return false; 8 | } 9 | 10 | for (let i = 0; i < arr1.length; i++) { 11 | if (arr1[i] !== arr2[i]) { 12 | return false; 13 | } 14 | } 15 | 16 | return true; 17 | } 18 | 19 | function mustSuiteHierarchyBeExecutedToRunTest(test: TestStartRecord, suites: string[]) { 20 | // i.e. is `suites` a prefix of `test.context.suite`? 21 | return areStringArraysEqual(test.context.suite.slice(0, suites.length), suites); 22 | } 23 | 24 | function mustRuntimeContextBeExecutedToRunTest(test: TestStartRecord, runtimeContext: RuntimeContext) { 25 | if (runtimeContext.type === 'hook') { 26 | if (runtimeContext.file === null) { 27 | // root hook; must be executed to run _any_ test 28 | return true; 29 | } 30 | if ( 31 | runtimeContext.file === test.context.file && 32 | mustSuiteHierarchyBeExecutedToRunTest(test, runtimeContext.suite) 33 | ) { 34 | // the hook must be executed to run this test 35 | return true; 36 | } 37 | } 38 | 39 | // otherwise, return true if and only if it’s the same test 40 | return ( 41 | runtimeContext.type === 'test' && 42 | runtimeContext.file === test.context.file && 43 | areStringArraysEqual(runtimeContext.suite, test.context.suite) && 44 | test.context.title === runtimeContext.title 45 | ); 46 | } 47 | 48 | /** 49 | * This extracts all of the test start records for the tests that can be run without any private API usage. That is, neither the test itself, nor any of the hooks that the test requires, call a private API. It does not consider whether private APIs are required in order to define the test (that is, contexts of type `testDefinition`). 50 | */ 51 | export function splitTestsByPrivateAPIUsageRequirement( 52 | testStartRecords: TestStartRecord[], 53 | groupedUsages: Group[], 54 | ): { requiringPrivateAPIUsage: TestStartRecord[]; notRequiringPrivateAPIUsage: TestStartRecord[] } { 55 | const result: { requiringPrivateAPIUsage: TestStartRecord[]; notRequiringPrivateAPIUsage: TestStartRecord[] } = { 56 | requiringPrivateAPIUsage: [], 57 | notRequiringPrivateAPIUsage: [], 58 | }; 59 | 60 | for (const testStartRecord of testStartRecords) { 61 | if ( 62 | groupedUsages.some((contextGroup) => mustRuntimeContextBeExecutedToRunTest(testStartRecord, contextGroup.key)) 63 | ) { 64 | result.requiringPrivateAPIUsage.push(testStartRecord); 65 | } else { 66 | result.notRequiringPrivateAPIUsage.push(testStartRecord); 67 | } 68 | } 69 | 70 | return result; 71 | } 72 | -------------------------------------------------------------------------------- /src/common/constants/HttpMethods.ts: -------------------------------------------------------------------------------- 1 | enum HttpMethods { 2 | Get = 'get', 3 | Delete = 'delete', 4 | Post = 'post', 5 | Put = 'put', 6 | Patch = 'patch', 7 | } 8 | 9 | export default HttpMethods; 10 | -------------------------------------------------------------------------------- /src/common/constants/HttpStatusCodes.ts: -------------------------------------------------------------------------------- 1 | enum HttpStatusCodes { 2 | Success = 200, 3 | NoContent = 204, 4 | BadRequest = 400, 5 | Unauthorized = 401, 6 | Forbidden = 403, 7 | RequestTimeout = 408, 8 | InternalServerError = 500, 9 | } 10 | 11 | export function isSuccessCode(statusCode: number) { 12 | return statusCode >= HttpStatusCodes.Success && statusCode < HttpStatusCodes.BadRequest; 13 | } 14 | 15 | export default HttpStatusCodes; 16 | -------------------------------------------------------------------------------- /src/common/constants/TransportName.ts: -------------------------------------------------------------------------------- 1 | export namespace TransportNames { 2 | export const WebSocket = 'web_socket' as const; 3 | export const Comet = 'comet' as const; 4 | export const XhrPolling = 'xhr_polling' as const; 5 | } 6 | 7 | type TransportName = typeof TransportNames.WebSocket | typeof TransportNames.Comet | typeof TransportNames.XhrPolling; 8 | 9 | export default TransportName; 10 | -------------------------------------------------------------------------------- /src/common/constants/XHRStates.ts: -------------------------------------------------------------------------------- 1 | enum XHRStates { 2 | REQ_SEND = 0, 3 | REQ_RECV = 1, 4 | REQ_RECV_POLL = 2, 5 | REQ_RECV_STREAM = 3, 6 | } 7 | 8 | export default XHRStates; 9 | -------------------------------------------------------------------------------- /src/common/lib/client/baserest.ts: -------------------------------------------------------------------------------- 1 | import BaseClient from './baseclient'; 2 | import ClientOptions from '../../types/ClientOptions'; 3 | import { Rest } from './rest'; 4 | import Defaults from '../util/defaults'; 5 | import Logger from '../util/logger'; 6 | 7 | /** 8 | `BaseRest` is an export of the tree-shakable version of the SDK, and acts as the base class for the `DefaultRest` class exported by the non tree-shakable version. 9 | 10 | It always includes the `Rest` plugin. 11 | */ 12 | export class BaseRest extends BaseClient { 13 | /* 14 | * The public typings declare that this only accepts an object, but since we want to emit a good error message in the case where a non-TypeScript user does one of these things: 15 | * 16 | * 1. passes a string (which is quite likely if they’re e.g. migrating from the default variant to the modular variant) 17 | * 2. passes no argument at all 18 | * 19 | * tell the compiler that these cases are possible so that it forces us to handle them. 20 | */ 21 | constructor(options?: ClientOptions | string) { 22 | super(Defaults.objectifyOptions(options, false, 'BaseRest', Logger.defaultLogger, { Rest })); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/common/lib/client/channelstatechange.ts: -------------------------------------------------------------------------------- 1 | import ErrorInfo from '../types/errorinfo'; 2 | 3 | class ChannelStateChange { 4 | previous: string; 5 | current: string; 6 | resumed?: boolean; 7 | reason?: string | Error | ErrorInfo; 8 | hasBacklog?: boolean; 9 | 10 | constructor( 11 | previous: string, 12 | current: string, 13 | resumed?: boolean, 14 | hasBacklog?: boolean, 15 | reason?: string | Error | ErrorInfo | null, 16 | ) { 17 | this.previous = previous; 18 | this.current = current; 19 | if (current === 'attached') { 20 | this.resumed = resumed; 21 | this.hasBacklog = hasBacklog; 22 | } 23 | if (reason) this.reason = reason; 24 | } 25 | } 26 | 27 | export default ChannelStateChange; 28 | -------------------------------------------------------------------------------- /src/common/lib/client/connection.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from '../util/eventemitter'; 2 | import ConnectionManager from '../transport/connectionmanager'; 3 | import Logger from '../util/logger'; 4 | import ConnectionStateChange from './connectionstatechange'; 5 | import ErrorInfo from '../types/errorinfo'; 6 | import { NormalisedClientOptions } from '../../types/ClientOptions'; 7 | import BaseRealtime from './baserealtime'; 8 | import Platform from 'common/platform'; 9 | 10 | class Connection extends EventEmitter { 11 | ably: BaseRealtime; 12 | connectionManager: ConnectionManager; 13 | state: string; 14 | key?: string; 15 | id?: string; 16 | errorReason: ErrorInfo | null; 17 | 18 | constructor(ably: BaseRealtime, options: NormalisedClientOptions) { 19 | super(ably.logger); 20 | this.ably = ably; 21 | this.connectionManager = new ConnectionManager(ably, options); 22 | this.state = this.connectionManager.state.state; 23 | this.key = undefined; 24 | this.id = undefined; 25 | this.errorReason = null; 26 | 27 | this.connectionManager.on('connectionstate', (stateChange: ConnectionStateChange) => { 28 | const state = (this.state = stateChange.current as string); 29 | Platform.Config.nextTick(() => { 30 | this.emit(state, stateChange); 31 | }); 32 | }); 33 | this.connectionManager.on('update', (stateChange: ConnectionStateChange) => { 34 | Platform.Config.nextTick(() => { 35 | this.emit('update', stateChange); 36 | }); 37 | }); 38 | } 39 | 40 | whenState = ((state: string) => { 41 | return EventEmitter.prototype.whenState.call(this, state, this.state); 42 | }) as any; 43 | 44 | connect(): void { 45 | Logger.logAction(this.logger, Logger.LOG_MINOR, 'Connection.connect()', ''); 46 | this.connectionManager.requestState({ state: 'connecting' }); 47 | } 48 | 49 | async ping(): Promise { 50 | Logger.logAction(this.logger, Logger.LOG_MINOR, 'Connection.ping()', ''); 51 | return this.connectionManager.ping(); 52 | } 53 | 54 | close(): void { 55 | Logger.logAction(this.logger, Logger.LOG_MINOR, 'Connection.close()', 'connectionKey = ' + this.key); 56 | this.connectionManager.requestState({ state: 'closing' }); 57 | } 58 | 59 | get recoveryKey(): string | null { 60 | this.logger.deprecationWarning( 61 | 'The `Connection.recoveryKey` attribute has been replaced by the `Connection.createRecoveryKey()` method. Replace your usage of `recoveryKey` with the return value of `createRecoveryKey()`. `recoveryKey` will be removed in a future version.', 62 | ); 63 | return this.createRecoveryKey(); 64 | } 65 | 66 | createRecoveryKey(): string | null { 67 | return this.connectionManager.createRecoveryKey(); 68 | } 69 | } 70 | 71 | export default Connection; 72 | -------------------------------------------------------------------------------- /src/common/lib/client/connectionstatechange.ts: -------------------------------------------------------------------------------- 1 | import { IPartialErrorInfo } from '../types/errorinfo'; 2 | 3 | class ConnectionStateChange { 4 | previous?: string; 5 | current?: string; 6 | retryIn?: number; 7 | reason?: IPartialErrorInfo; 8 | 9 | constructor(previous?: string, current?: string, retryIn?: number | null, reason?: IPartialErrorInfo) { 10 | this.previous = previous; 11 | this.current = current; 12 | if (retryIn) this.retryIn = retryIn; 13 | if (reason) this.reason = reason; 14 | } 15 | } 16 | 17 | export default ConnectionStateChange; 18 | -------------------------------------------------------------------------------- /src/common/lib/client/defaultrest.ts: -------------------------------------------------------------------------------- 1 | import { BaseRest } from './baserest'; 2 | import ClientOptions from '../../types/ClientOptions'; 3 | import { allCommonModularPlugins } from './modularplugins'; 4 | import Platform from 'common/platform'; 5 | import { DefaultMessage } from '../types/defaultmessage'; 6 | import { MsgPack } from 'common/types/msgpack'; 7 | import { DefaultPresenceMessage } from '../types/defaultpresencemessage'; 8 | import { DefaultAnnotation } from '../types/defaultannotation'; 9 | import { Http } from 'common/types/http'; 10 | import RealtimeAnnotations from './realtimeannotations'; 11 | import RestAnnotations from './restannotations'; 12 | import Annotation, { WireAnnotation } from '../types/annotation'; 13 | import Defaults from '../util/defaults'; 14 | import Logger from '../util/logger'; 15 | 16 | /** 17 | `DefaultRest` is the class that the non tree-shakable version of the SDK exports as `Rest`. It ensures that this version of the SDK includes all of the functionality which is optionally available in the tree-shakable version. 18 | */ 19 | export class DefaultRest extends BaseRest { 20 | // The public typings declare that this requires an argument to be passed, but since we want to emit a good error message in the case where a non-TypeScript user does not pass an argument, tell the compiler that this is possible so that it forces us to handle it. 21 | constructor(options?: ClientOptions | string) { 22 | const MsgPack = DefaultRest._MsgPack; 23 | if (!MsgPack) { 24 | throw new Error('Expected DefaultRest._MsgPack to have been set'); 25 | } 26 | 27 | super( 28 | Defaults.objectifyOptions(options, true, 'Rest', Logger.defaultLogger, { 29 | ...allCommonModularPlugins, 30 | Crypto: DefaultRest.Crypto ?? undefined, 31 | MsgPack: DefaultRest._MsgPack ?? undefined, 32 | Annotations: { 33 | Annotation, 34 | WireAnnotation, 35 | RealtimeAnnotations, 36 | RestAnnotations, 37 | }, 38 | }), 39 | ); 40 | } 41 | 42 | private static _Crypto: typeof Platform.Crypto = null; 43 | static get Crypto() { 44 | if (this._Crypto === null) { 45 | throw new Error('Encryption not enabled; use ably.encryption.js instead'); 46 | } 47 | 48 | return this._Crypto; 49 | } 50 | static set Crypto(newValue: typeof Platform.Crypto) { 51 | this._Crypto = newValue; 52 | } 53 | 54 | static Message = DefaultMessage; 55 | static PresenceMessage = DefaultPresenceMessage; 56 | static Annotation = DefaultAnnotation; 57 | 58 | static _MsgPack: MsgPack | null = null; 59 | 60 | // Used by tests 61 | static _Http = Http; 62 | } 63 | -------------------------------------------------------------------------------- /src/common/lib/client/modularplugins.ts: -------------------------------------------------------------------------------- 1 | import { Rest } from './rest'; 2 | import { IUntypedCryptoStatic } from '../../types/ICryptoStatic'; 3 | import { MsgPack } from 'common/types/msgpack'; 4 | import RealtimePresence from './realtimepresence'; 5 | import RealtimeAnnotations from './realtimeannotations'; 6 | import RestAnnotations from './restannotations'; 7 | import XHRRequest from 'platform/web/lib/http/request/xhrrequest'; 8 | import fetchRequest from 'platform/web/lib/http/request/fetchrequest'; 9 | import { FilteredSubscriptions } from './filteredsubscriptions'; 10 | import PresenceMessage, { WirePresenceMessage } from '../types/presencemessage'; 11 | import Annotation, { WireAnnotation } from '../types/annotation'; 12 | import { TransportCtor } from '../transport/transport'; 13 | import type * as PushPlugin from 'plugins/push'; 14 | import type * as ObjectsPlugin from 'plugins/objects'; 15 | 16 | export interface PresenceMessagePlugin { 17 | PresenceMessage: typeof PresenceMessage; 18 | WirePresenceMessage: typeof WirePresenceMessage; 19 | } 20 | 21 | export type RealtimePresencePlugin = PresenceMessagePlugin & { 22 | RealtimePresence: typeof RealtimePresence; 23 | }; 24 | 25 | export type AnnotationsPlugin = { 26 | Annotation: typeof Annotation; 27 | WireAnnotation: typeof WireAnnotation; 28 | RealtimeAnnotations: typeof RealtimeAnnotations; 29 | RestAnnotations: typeof RestAnnotations; 30 | }; 31 | 32 | export interface ModularPlugins { 33 | Rest?: typeof Rest; 34 | Crypto?: IUntypedCryptoStatic; 35 | MsgPack?: MsgPack; 36 | RealtimePresence?: RealtimePresencePlugin; 37 | Annotations?: AnnotationsPlugin; 38 | WebSocketTransport?: TransportCtor; 39 | XHRPolling?: TransportCtor; 40 | XHRRequest?: typeof XHRRequest; 41 | FetchRequest?: typeof fetchRequest; 42 | MessageInteractions?: typeof FilteredSubscriptions; 43 | Push?: typeof PushPlugin; 44 | Objects?: typeof ObjectsPlugin; // PC5, PT2b 45 | } 46 | 47 | export const allCommonModularPlugins: ModularPlugins = { Rest }; 48 | -------------------------------------------------------------------------------- /src/common/lib/client/restpresence.ts: -------------------------------------------------------------------------------- 1 | import * as Utils from '../util/utils'; 2 | import Logger from '../util/logger'; 3 | import PaginatedResource, { PaginatedResult } from './paginatedresource'; 4 | import PresenceMessage, { WirePresenceMessage, _fromEncodedArray } from '../types/presencemessage'; 5 | import RestChannel from './restchannel'; 6 | import Defaults from '../util/defaults'; 7 | 8 | class RestPresence { 9 | channel: RestChannel; 10 | 11 | constructor(channel: RestChannel) { 12 | this.channel = channel; 13 | } 14 | 15 | get logger(): Logger { 16 | return this.channel.logger; 17 | } 18 | 19 | async get(params: any): Promise> { 20 | Logger.logAction(this.logger, Logger.LOG_MICRO, 'RestPresence.get()', 'channel = ' + this.channel.name); 21 | const client = this.channel.client, 22 | format = client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, 23 | envelope = this.channel.client.http.supportsLinkHeaders ? undefined : format, 24 | headers = Defaults.defaultGetHeaders(client.options); 25 | 26 | Utils.mixin(headers, client.options.headers); 27 | 28 | return new PaginatedResource( 29 | client, 30 | this.channel.client.rest.presenceMixin.basePath(this), 31 | headers, 32 | envelope, 33 | async (body, headers, unpacked) => { 34 | const decoded = ( 35 | unpacked ? body : Utils.decodeBody(body, client._MsgPack, format) 36 | ) as Utils.Properties[]; 37 | 38 | return _fromEncodedArray(decoded, this.channel); 39 | }, 40 | ).get(params); 41 | } 42 | 43 | async history(params: any): Promise> { 44 | Logger.logAction(this.logger, Logger.LOG_MICRO, 'RestPresence.history()', 'channel = ' + this.channel.name); 45 | return this.channel.client.rest.presenceMixin.history(this, params); 46 | } 47 | } 48 | 49 | export default RestPresence; 50 | -------------------------------------------------------------------------------- /src/common/lib/client/restpresencemixin.ts: -------------------------------------------------------------------------------- 1 | import RestPresence from './restpresence'; 2 | import RealtimePresence from './realtimepresence'; 3 | import * as Utils from '../util/utils'; 4 | import Defaults from '../util/defaults'; 5 | import PaginatedResource, { PaginatedResult } from './paginatedresource'; 6 | import PresenceMessage, { WirePresenceMessage, _fromEncodedArray } from '../types/presencemessage'; 7 | import { RestChannelMixin } from './restchannelmixin'; 8 | 9 | export class RestPresenceMixin { 10 | static basePath(presence: RestPresence | RealtimePresence) { 11 | return RestChannelMixin.basePath(presence.channel) + '/presence'; 12 | } 13 | 14 | static async history( 15 | presence: RestPresence | RealtimePresence, 16 | params: any, 17 | ): Promise> { 18 | const client = presence.channel.client, 19 | format = client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, 20 | envelope = presence.channel.client.http.supportsLinkHeaders ? undefined : format, 21 | headers = Defaults.defaultGetHeaders(client.options); 22 | 23 | Utils.mixin(headers, client.options.headers); 24 | 25 | return new PaginatedResource( 26 | client, 27 | this.basePath(presence) + '/history', 28 | headers, 29 | envelope, 30 | async (body, headers, unpacked) => { 31 | const decoded = ( 32 | unpacked ? body : Utils.decodeBody(body, client._MsgPack, format) 33 | ) as Utils.Properties[]; 34 | 35 | return _fromEncodedArray(decoded, presence.channel); 36 | }, 37 | ).get(params); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/common/lib/transport/connectionerrors.ts: -------------------------------------------------------------------------------- 1 | import ErrorInfo from '../types/errorinfo'; 2 | 3 | const ConnectionErrorCodes = { 4 | DISCONNECTED: 80003, 5 | SUSPENDED: 80002, 6 | FAILED: 80000, 7 | CLOSING: 80017, 8 | CLOSED: 80017, 9 | UNKNOWN_CONNECTION_ERR: 50002, 10 | UNKNOWN_CHANNEL_ERR: 50001, 11 | }; 12 | 13 | const ConnectionErrors = { 14 | disconnected: () => 15 | ErrorInfo.fromValues({ 16 | statusCode: 400, 17 | code: ConnectionErrorCodes.DISCONNECTED, 18 | message: 'Connection to server temporarily unavailable', 19 | }), 20 | suspended: () => 21 | ErrorInfo.fromValues({ 22 | statusCode: 400, 23 | code: ConnectionErrorCodes.SUSPENDED, 24 | message: 'Connection to server unavailable', 25 | }), 26 | failed: () => 27 | ErrorInfo.fromValues({ 28 | statusCode: 400, 29 | code: ConnectionErrorCodes.FAILED, 30 | message: 'Connection failed or disconnected by server', 31 | }), 32 | closing: () => 33 | ErrorInfo.fromValues({ 34 | statusCode: 400, 35 | code: ConnectionErrorCodes.CLOSING, 36 | message: 'Connection closing', 37 | }), 38 | closed: () => 39 | ErrorInfo.fromValues({ 40 | statusCode: 400, 41 | code: ConnectionErrorCodes.CLOSED, 42 | message: 'Connection closed', 43 | }), 44 | unknownConnectionErr: () => 45 | ErrorInfo.fromValues({ 46 | statusCode: 500, 47 | code: ConnectionErrorCodes.UNKNOWN_CONNECTION_ERR, 48 | message: 'Internal connection error', 49 | }), 50 | unknownChannelErr: () => 51 | ErrorInfo.fromValues({ 52 | statusCode: 500, 53 | code: ConnectionErrorCodes.UNKNOWN_CONNECTION_ERR, 54 | message: 'Internal channel error', 55 | }), 56 | }; 57 | 58 | export function isRetriable(err: ErrorInfo) { 59 | if (!err.statusCode || !err.code || err.statusCode >= 500) { 60 | return true; 61 | } 62 | return Object.values(ConnectionErrorCodes).includes(err.code); 63 | } 64 | 65 | export default ConnectionErrors; 66 | -------------------------------------------------------------------------------- /src/common/lib/transport/messagequeue.ts: -------------------------------------------------------------------------------- 1 | import ErrorInfo from '../types/errorinfo'; 2 | import EventEmitter from '../util/eventemitter'; 3 | import Logger from '../util/logger'; 4 | import { PendingMessage } from './protocol'; 5 | 6 | class MessageQueue extends EventEmitter { 7 | messages: Array; 8 | 9 | constructor(logger: Logger) { 10 | super(logger); 11 | this.messages = []; 12 | } 13 | 14 | count(): number { 15 | return this.messages.length; 16 | } 17 | 18 | push(message: PendingMessage): void { 19 | this.messages.push(message); 20 | } 21 | 22 | shift(): PendingMessage | undefined { 23 | return this.messages.shift(); 24 | } 25 | 26 | last(): PendingMessage { 27 | return this.messages[this.messages.length - 1]; 28 | } 29 | 30 | copyAll(): PendingMessage[] { 31 | return this.messages.slice(); 32 | } 33 | 34 | append(messages: Array): void { 35 | this.messages.push.apply(this.messages, messages); 36 | } 37 | 38 | prepend(messages: Array): void { 39 | this.messages.unshift.apply(this.messages, messages); 40 | } 41 | 42 | completeMessages(serial: number, count: number, err?: ErrorInfo | null): void { 43 | Logger.logAction( 44 | this.logger, 45 | Logger.LOG_MICRO, 46 | 'MessageQueue.completeMessages()', 47 | 'serial = ' + serial + '; count = ' + count, 48 | ); 49 | err = err || null; 50 | const messages = this.messages; 51 | if (messages.length === 0) { 52 | throw new Error('MessageQueue.completeMessages(): completeMessages called on any empty MessageQueue'); 53 | } 54 | const first = messages[0]; 55 | if (first) { 56 | const startSerial = first.message.msgSerial as number; 57 | const endSerial = serial + count; /* the serial of the first message that is *not* the subject of this call */ 58 | if (endSerial > startSerial) { 59 | const completeMessages = messages.splice(0, endSerial - startSerial); 60 | for (const message of completeMessages) { 61 | (message.callback as Function)(err); 62 | } 63 | } 64 | if (messages.length == 0) this.emit('idle'); 65 | } 66 | } 67 | 68 | completeAllMessages(err: ErrorInfo): void { 69 | this.completeMessages(0, Number.MAX_SAFE_INTEGER || Number.MAX_VALUE, err); 70 | } 71 | 72 | resetSendAttempted(): void { 73 | for (let msg of this.messages) { 74 | msg.sendAttempted = false; 75 | } 76 | } 77 | 78 | clear(): void { 79 | Logger.logAction( 80 | this.logger, 81 | Logger.LOG_MICRO, 82 | 'MessageQueue.clear()', 83 | 'clearing ' + this.messages.length + ' messages', 84 | ); 85 | this.messages = []; 86 | this.emit('idle'); 87 | } 88 | } 89 | 90 | export default MessageQueue; 91 | -------------------------------------------------------------------------------- /src/common/lib/types/defaultannotation.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../../ably'; 2 | import Logger from '../util/logger'; 3 | import Annotation, { fromEncoded, fromEncodedArray, WireAnnotation } from './annotation'; 4 | import type { Properties } from '../util/utils'; 5 | 6 | /** 7 | `DefaultAnnotation` is the class returned by `DefaultRest` and `DefaultRealtime`’s `Annotation` static property. It introduces the static methods described in the `AnnotationStatic` interface of the public API of the non tree-shakable version of the library. 8 | */ 9 | export class DefaultAnnotation extends Annotation { 10 | static async fromEncoded(encoded: unknown, inputOptions?: API.ChannelOptions): Promise { 11 | return fromEncoded(Logger.defaultLogger, encoded as WireAnnotation, inputOptions); 12 | } 13 | 14 | static async fromEncodedArray(encodedArray: Array, options?: API.ChannelOptions): Promise { 15 | return fromEncodedArray(Logger.defaultLogger, encodedArray as WireAnnotation[], options); 16 | } 17 | 18 | static fromValues(values: Properties): Annotation { 19 | return Annotation.fromValues(values); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/common/lib/types/defaultmessage.ts: -------------------------------------------------------------------------------- 1 | import Message, { WireMessage, fromEncoded, fromEncodedArray } from './message'; 2 | import * as API from '../../../../ably'; 3 | import Platform from 'common/platform'; 4 | import Logger from '../util/logger'; 5 | import type { Properties } from '../util/utils'; 6 | 7 | /** 8 | `DefaultMessage` is the class returned by `DefaultRest` and `DefaultRealtime`’s `Message` static property. It introduces the static methods described in the `MessageStatic` interface of the public API of the non tree-shakable version of the library. 9 | */ 10 | export class DefaultMessage extends Message { 11 | static async fromEncoded(encoded: unknown, inputOptions?: API.ChannelOptions): Promise { 12 | return fromEncoded(Logger.defaultLogger, Platform.Crypto, encoded as WireMessage, inputOptions); 13 | } 14 | 15 | static async fromEncodedArray(encodedArray: Array, options?: API.ChannelOptions): Promise { 16 | return fromEncodedArray(Logger.defaultLogger, Platform.Crypto, encodedArray as WireMessage[], options); 17 | } 18 | 19 | static fromValues(values: Properties): Message { 20 | return Message.fromValues(values); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/lib/types/defaultpresencemessage.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../../ably'; 2 | import Logger from '../util/logger'; 3 | import PresenceMessage, { fromEncoded, fromEncodedArray, WirePresenceMessage } from './presencemessage'; 4 | import Platform from 'common/platform'; 5 | import type { Properties } from '../util/utils'; 6 | 7 | /** 8 | `DefaultPresenceMessage` is the class returned by `DefaultRest` and `DefaultRealtime`’s `PresenceMessage` static property. It introduces the static methods described in the `PresenceMessageStatic` interface of the public API of the non tree-shakable version of the library. 9 | */ 10 | export class DefaultPresenceMessage extends PresenceMessage { 11 | static async fromEncoded(encoded: unknown, inputOptions?: API.ChannelOptions): Promise { 12 | return fromEncoded(Logger.defaultLogger, Platform.Crypto, encoded as WirePresenceMessage, inputOptions); 13 | } 14 | 15 | static async fromEncodedArray( 16 | encodedArray: Array, 17 | options?: API.ChannelOptions, 18 | ): Promise { 19 | return fromEncodedArray(Logger.defaultLogger, Platform.Crypto, encodedArray as WirePresenceMessage[], options); 20 | } 21 | 22 | static fromValues(values: Properties): PresenceMessage { 23 | return PresenceMessage.fromValues(values); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/lib/types/protocolmessagecommon.ts: -------------------------------------------------------------------------------- 1 | // constant definitions that can be imported by anyone without worrying about circular 2 | // deps 3 | 4 | // TR2 5 | export const actions = { 6 | HEARTBEAT: 0, 7 | ACK: 1, 8 | NACK: 2, 9 | CONNECT: 3, 10 | CONNECTED: 4, 11 | DISCONNECT: 5, 12 | DISCONNECTED: 6, 13 | CLOSE: 7, 14 | CLOSED: 8, 15 | ERROR: 9, 16 | ATTACH: 10, 17 | ATTACHED: 11, 18 | DETACH: 12, 19 | DETACHED: 13, 20 | PRESENCE: 14, 21 | MESSAGE: 15, 22 | SYNC: 16, 23 | AUTH: 17, 24 | ACTIVATE: 18, 25 | OBJECT: 19, 26 | OBJECT_SYNC: 20, 27 | ANNOTATION: 21, 28 | }; 29 | 30 | export const ActionName: string[] = []; 31 | Object.keys(actions).forEach(function (name) { 32 | ActionName[(actions as { [key: string]: number })[name]] = name; 33 | }); 34 | 35 | // TR3 36 | export const flags: { [key: string]: number } = { 37 | /* Channel attach state flags */ 38 | HAS_PRESENCE: 1 << 0, 39 | HAS_BACKLOG: 1 << 1, 40 | RESUMED: 1 << 2, 41 | TRANSIENT: 1 << 4, 42 | ATTACH_RESUME: 1 << 5, 43 | HAS_OBJECTS: 1 << 7, 44 | /* Channel mode flags */ 45 | PRESENCE: 1 << 16, 46 | PUBLISH: 1 << 17, 47 | SUBSCRIBE: 1 << 18, 48 | PRESENCE_SUBSCRIBE: 1 << 19, 49 | ANNOTATION_PUBLISH: 1 << 21, 50 | ANNOTATION_SUBSCRIBE: 1 << 22, 51 | OBJECT_SUBSCRIBE: 1 << 24, 52 | OBJECT_PUBLISH: 1 << 25, 53 | }; 54 | 55 | export const flagNames = Object.keys(flags); 56 | 57 | flags.MODE_ALL = 58 | flags.PRESENCE | 59 | flags.PUBLISH | 60 | flags.SUBSCRIBE | 61 | flags.PRESENCE_SUBSCRIBE | 62 | flags.ANNOTATION_PUBLISH | 63 | flags.ANNOTATION_SUBSCRIBE | 64 | flags.OBJECT_SUBSCRIBE | 65 | flags.OBJECT_PUBLISH; 66 | 67 | export const channelModes = [ 68 | 'PRESENCE', 69 | 'PUBLISH', 70 | 'SUBSCRIBE', 71 | 'PRESENCE_SUBSCRIBE', 72 | 'ANNOTATION_PUBLISH', 73 | 'ANNOTATION_SUBSCRIBE', 74 | 'OBJECT_SUBSCRIBE', 75 | 'OBJECT_PUBLISH', 76 | ]; 77 | -------------------------------------------------------------------------------- /src/common/lib/types/pushchannelsubscription.ts: -------------------------------------------------------------------------------- 1 | import { MsgPack } from 'common/types/msgpack'; 2 | import * as Utils from '../util/utils'; 3 | 4 | type PushChannelSubscriptionObject = { 5 | channel: string; 6 | deviceId?: string; 7 | clientId?: string; 8 | }; 9 | 10 | class PushChannelSubscription { 11 | /** PCS4, the channel name associated with this subscription */ 12 | channel!: string; 13 | /** PCS2, optional, populated for subscriptions made for a specific device registration */ 14 | deviceId?: string; 15 | /** PCS3, optional, populated for subscriptions made for a specific clientId */ 16 | clientId?: string; 17 | 18 | /** 19 | * Overload toJSON() to intercept JSON.stringify() 20 | * @return {*} 21 | */ 22 | toJSON(): PushChannelSubscriptionObject { 23 | return { 24 | channel: this.channel, 25 | deviceId: this.deviceId, 26 | clientId: this.clientId, 27 | }; 28 | } 29 | 30 | toString(): string { 31 | let result = '[PushChannelSubscription'; 32 | if (this.channel) result += '; channel=' + this.channel; 33 | if (this.deviceId) result += '; deviceId=' + this.deviceId; 34 | if (this.clientId) result += '; clientId=' + this.clientId; 35 | result += ']'; 36 | return result; 37 | } 38 | 39 | static toRequestBody = Utils.encodeBody; 40 | 41 | static fromResponseBody( 42 | body: Array> | Record, 43 | MsgPack: MsgPack | null, 44 | format?: Utils.Format, 45 | ): PushChannelSubscription | PushChannelSubscription[] { 46 | if (format) { 47 | body = Utils.decodeBody(body, MsgPack, format) as Record; 48 | } 49 | 50 | if (Array.isArray(body)) { 51 | return PushChannelSubscription.fromValuesArray(body); 52 | } else { 53 | return PushChannelSubscription.fromValues(body); 54 | } 55 | } 56 | 57 | static fromValues(values: Record): PushChannelSubscription { 58 | return Object.assign(new PushChannelSubscription(), values); 59 | } 60 | 61 | static fromValuesArray(values: Array>): PushChannelSubscription[] { 62 | const count = values.length, 63 | result = new Array(count); 64 | for (let i = 0; i < count; i++) result[i] = PushChannelSubscription.fromValues(values[i]); 65 | return result; 66 | } 67 | } 68 | 69 | export default PushChannelSubscription; 70 | -------------------------------------------------------------------------------- /src/common/lib/types/stats.ts: -------------------------------------------------------------------------------- 1 | type StatsValues = { 2 | entries?: Partial>; 3 | schema?: string; 4 | appId?: string; 5 | inProgress?: never; 6 | unit?: never; 7 | intervalId?: never; 8 | }; 9 | 10 | class Stats { 11 | entries?: Partial>; 12 | schema?: string; 13 | appId?: string; 14 | inProgress?: never; 15 | unit?: never; 16 | intervalId?: never; 17 | 18 | constructor(values?: StatsValues) { 19 | this.entries = (values && values.entries) || undefined; 20 | this.schema = (values && values.schema) || undefined; 21 | this.appId = (values && values.appId) || undefined; 22 | this.inProgress = (values && values.inProgress) || undefined; 23 | this.unit = (values && values.unit) || undefined; 24 | this.intervalId = (values && values.intervalId) || undefined; 25 | } 26 | 27 | static fromValues(values: StatsValues): Stats { 28 | return new Stats(values); 29 | } 30 | } 31 | 32 | export default Stats; 33 | -------------------------------------------------------------------------------- /src/common/lib/util/multicaster.ts: -------------------------------------------------------------------------------- 1 | import { StandardCallback } from 'common/types/utils'; 2 | import ErrorInfo from 'common/lib/types/errorinfo'; 3 | import Logger from './logger'; 4 | 5 | export interface MulticasterInstance extends Function { 6 | (err?: ErrorInfo | null, result?: T): void; 7 | push: (fn: StandardCallback) => void; 8 | /** 9 | * Creates a promise that will be resolved or rejected when this instance is called. 10 | */ 11 | createPromise: () => Promise; 12 | /** 13 | * Syntatic sugar for when working in a context that uses promises; equivalent to calling as a function with arguments (null, result). 14 | */ 15 | resolveAll(result: T): void; 16 | /** 17 | * Syntatic sugar for when working in a context that uses promises; equivalent to calling as a function with arguments (err). 18 | */ 19 | rejectAll(err: ErrorInfo): void; 20 | } 21 | 22 | class Multicaster { 23 | members: Array>; 24 | 25 | // Private constructor; use static Multicaster.create instead 26 | private constructor( 27 | private readonly logger: Logger, 28 | members?: Array | undefined>, 29 | ) { 30 | this.members = (members as Array>) || []; 31 | } 32 | 33 | private call(err?: ErrorInfo | null, result?: T): void { 34 | for (const member of this.members) { 35 | if (member) { 36 | try { 37 | member(err, result); 38 | } catch (e) { 39 | Logger.logAction( 40 | this.logger, 41 | Logger.LOG_ERROR, 42 | 'Multicaster multiple callback handler', 43 | 'Unexpected exception: ' + e + '; stack = ' + (e as Error).stack, 44 | ); 45 | } 46 | } 47 | } 48 | } 49 | 50 | push(...args: Array>): void { 51 | this.members.push(...args); 52 | } 53 | 54 | createPromise(): Promise { 55 | return new Promise((resolve, reject) => { 56 | this.push((err, result) => { 57 | err ? reject(err) : resolve(result!); 58 | }); 59 | }); 60 | } 61 | 62 | resolveAll(result: T) { 63 | this.call(null, result); 64 | } 65 | 66 | rejectAll(err: ErrorInfo) { 67 | this.call(err); 68 | } 69 | 70 | static create(logger: Logger, members?: Array | undefined>): MulticasterInstance { 71 | const instance = new Multicaster(logger, members); 72 | return Object.assign((err?: ErrorInfo | null, result?: T) => instance.call(err, result), { 73 | push: (fn: StandardCallback) => instance.push(fn), 74 | createPromise: () => instance.createPromise(), 75 | resolveAll: (result: T) => instance.resolveAll(result), 76 | rejectAll: (err: ErrorInfo) => instance.rejectAll(err), 77 | }); 78 | } 79 | } 80 | 81 | export default Multicaster; 82 | -------------------------------------------------------------------------------- /src/common/platform.ts: -------------------------------------------------------------------------------- 1 | import { IPlatformConfig } from './types/IPlatformConfig'; 2 | import { IPlatformHttpStatic } from './types/http'; 3 | import IDefaults from './types/IDefaults'; 4 | import IWebStorage from './types/IWebStorage'; 5 | import IBufferUtils from './types/IBufferUtils'; 6 | import * as WebBufferUtils from '../platform/web/lib/util/bufferutils'; 7 | import * as NodeBufferUtils from '../platform/nodejs/lib/util/bufferutils'; 8 | import { IUntypedCryptoStatic } from '../common/types/ICryptoStatic'; 9 | import TransportName from './constants/TransportName'; 10 | import { TransportCtor } from './lib/transport/transport'; 11 | 12 | export type Bufferlike = WebBufferUtils.Bufferlike | NodeBufferUtils.Bufferlike; 13 | type BufferUtilsOutput = WebBufferUtils.Output | NodeBufferUtils.Output; 14 | type ToBufferOutput = WebBufferUtils.ToBufferOutput | NodeBufferUtils.ToBufferOutput; 15 | 16 | export type TransportImplementations = Partial>; 17 | 18 | export default class Platform { 19 | static Config: IPlatformConfig; 20 | /* 21 | What we actually _want_ is for Platform to be a generic class 22 | parameterised by Bufferlike etc, but that requires far-reaching changes to 23 | components that make use of Platform. So instead we have to advertise a 24 | BufferUtils object that accepts a broader range of data types than it 25 | can in reality handle. 26 | */ 27 | static BufferUtils: IBufferUtils; 28 | /* 29 | We’d like this to be ICryptoStatic with the correct generic arguments, 30 | but Platform doesn’t currently allow that, as described in the BufferUtils 31 | comment above. 32 | */ 33 | static Crypto: IUntypedCryptoStatic | null; 34 | static Http: IPlatformHttpStatic; 35 | static Transports: { 36 | order: TransportName[]; 37 | // Transport implementations that always come with this platform 38 | bundledImplementations: TransportImplementations; 39 | }; 40 | static Defaults: IDefaults; 41 | static WebStorage: IWebStorage | null; 42 | } 43 | -------------------------------------------------------------------------------- /src/common/types/ClientOptions.ts: -------------------------------------------------------------------------------- 1 | import { Modify } from './utils'; 2 | import * as API from '../../../ably'; 3 | import { ModularPlugins } from 'common/lib/client/modularplugins'; 4 | import { StandardPlugins } from 'plugins'; 5 | 6 | export type RestAgentOptions = { 7 | keepAlive: boolean; 8 | maxSockets: number; 9 | }; 10 | 11 | export default interface ClientOptions extends API.ClientOptions { 12 | restAgentOptions?: RestAgentOptions; 13 | pushFullWait?: boolean; 14 | agents?: Record; 15 | } 16 | 17 | export type NormalisedClientOptions = Modify< 18 | ClientOptions, 19 | { 20 | primaryDomain: string; 21 | keyName?: string; 22 | keySecret?: string; 23 | timeouts: Record; 24 | maxMessageSize: number; 25 | connectivityCheckParams: Record | null; 26 | headers: Record; 27 | } 28 | >; 29 | -------------------------------------------------------------------------------- /src/common/types/IBufferUtils.ts: -------------------------------------------------------------------------------- 1 | export default interface IBufferUtils { 2 | base64CharSet: string; 3 | hexCharSet: string; 4 | isBuffer: (buffer: unknown) => buffer is Bufferlike; 5 | /** 6 | * On browser this returns a Uint8Array, on node a Buffer 7 | */ 8 | toBuffer: (buffer: Bufferlike) => ToBufferOutput; 9 | toArrayBuffer: (buffer: Bufferlike) => ArrayBuffer; 10 | base64Encode: (buffer: Bufferlike) => string; 11 | base64UrlEncode: (buffer: Bufferlike) => string; 12 | base64Decode: (string: string) => Output; 13 | hexEncode: (buffer: Bufferlike) => string; 14 | hexDecode: (string: string) => Output; 15 | utf8Encode: (string: string) => Output; 16 | utf8Decode: (buffer: Bufferlike) => string; 17 | areBuffersEqual: (buffer1: Bufferlike, buffer2: Bufferlike) => boolean; 18 | byteLength: (buffer: Bufferlike) => number; 19 | /** 20 | * Returns ArrayBuffer on browser and Buffer on Node.js 21 | */ 22 | arrayBufferViewToBuffer: (arrayBufferView: ArrayBufferView) => Bufferlike; 23 | concat(buffers: Bufferlike[]): Output; 24 | sha256(message: Bufferlike): Output; 25 | hmacSha256(message: Bufferlike, key: Bufferlike): Output; 26 | } 27 | -------------------------------------------------------------------------------- /src/common/types/ICipher.ts: -------------------------------------------------------------------------------- 1 | export default interface ICipher { 2 | algorithm: string; 3 | encrypt: (plaintext: InputPlaintext) => Promise; 4 | decrypt: (ciphertext: InputCiphertext) => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/common/types/ICryptoStatic.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../ably'; 2 | import ICipher from './ICipher'; 3 | import Logger from '../../common/lib/util/logger'; 4 | 5 | export type IGetCipherParams = (API.CipherParams | API.CipherParamOptions) & { iv?: IV }; 6 | export interface IGetCipherReturnValue { 7 | cipher: Cipher; 8 | cipherParams: API.CipherParams; 9 | } 10 | 11 | export default interface ICryptoStatic 12 | extends API.Crypto { 13 | getCipher( 14 | params: IGetCipherParams, 15 | logger: Logger, 16 | ): IGetCipherReturnValue>; 17 | } 18 | 19 | /* 20 | A less strongly typed version of ICryptoStatic to use until we 21 | can make Platform a generic type (see comment there). 22 | */ 23 | export interface IUntypedCryptoStatic extends API.Crypto { 24 | getCipher(params: any, logger: Logger): any; 25 | } 26 | -------------------------------------------------------------------------------- /src/common/types/IDefaults.d.ts: -------------------------------------------------------------------------------- 1 | import TransportName from '../constants/TransportName'; 2 | import { RestAgentOptions } from './ClientOptions'; 3 | 4 | export default interface IDefaults { 5 | connectivityCheckUrl: string; 6 | wsConnectivityCheckUrl: string; 7 | defaultTransports: TransportName[]; 8 | restAgentOptions?: RestAgentOptions; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/types/IPlatformConfig.d.ts: -------------------------------------------------------------------------------- 1 | import { DeviceFormFactor, DevicePlatform } from 'common/lib/types/devicedetails'; 2 | 3 | /** 4 | * Interface for common config properties shared between all platforms and that are relevant for all platforms. 5 | * 6 | * These properties must always be required and set for each platform. 7 | */ 8 | export interface ICommonPlatformConfig { 9 | agent: string; 10 | logTimestamps: boolean; 11 | binaryType: BinaryType; 12 | WebSocket: typeof WebSocket | typeof import('ws'); 13 | useProtocolHeartbeats: boolean; 14 | supportsBinary: boolean; 15 | preferBinary: boolean; 16 | nextTick: process.nextTick; 17 | inspect: (value: unknown) => string; 18 | stringByteSize: Buffer.byteLength; 19 | getRandomArrayBuffer: (byteLength: number) => Promise; 20 | push?: IPlatformPushConfig; 21 | } 22 | 23 | /** 24 | * Interface for platform specific config properties that do make sense on some platforms but not on others. 25 | * 26 | * These properties should always be optional, so that only relevant platforms would set them. 27 | */ 28 | export interface ISpecificPlatformConfig { 29 | addEventListener?: typeof window.addEventListener | typeof global.addEventListener | null; 30 | userAgent?: string | null; 31 | inherits?: typeof import('util').inherits; 32 | currentUrl?: string; 33 | fetchSupported?: boolean; 34 | xhrSupported?: boolean; 35 | allowComet?: boolean; 36 | ArrayBuffer?: typeof ArrayBuffer | false; 37 | atob?: typeof atob | null; 38 | TextEncoder?: typeof TextEncoder; 39 | TextDecoder?: typeof TextDecoder; 40 | isWebworker?: boolean; 41 | } 42 | 43 | export interface IPlatformPushStorage { 44 | get(name: string): string; 45 | set(name: string, value: string); 46 | remove(name: string); 47 | } 48 | 49 | export interface IPlatformPushConfig { 50 | platform: DevicePlatform; 51 | formFactor: DeviceFormFactor; 52 | storage: IPlatformPushStorage; 53 | getPushDeviceDetails?(machine: any); 54 | } 55 | 56 | export type IPlatformConfig = ICommonPlatformConfig & ISpecificPlatformConfig; 57 | -------------------------------------------------------------------------------- /src/common/types/IWebStorage.ts: -------------------------------------------------------------------------------- 1 | export default interface IWebStorage { 2 | sessionSupported: boolean; 3 | localSupported: boolean; 4 | set(name: string, value: string, ttl?: number): void; 5 | get(name: string): any; 6 | remove(name: string): void; 7 | setSession(name: string, value: string, ttl?: number): void; 8 | getSession(name: string): any; 9 | removeSession(name: string): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/types/IXHRRequest.d.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from '../lib/util/eventemitter'; 2 | 3 | /** 4 | * A common interface shared by the browser and NodeJS XHRRequest implementations 5 | */ 6 | export default interface IXHRRequest extends EventEmitter { 7 | exec(): void; 8 | abort(): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/types/channel.d.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../ably'; 2 | 3 | export interface ChannelOptions extends API.ChannelOptions { 4 | channelCipher?: { 5 | algorithm: string; 6 | encrypt: Function; 7 | decrypt: Function; 8 | } | null; 9 | updateOnAttached?: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /src/common/types/cryptoDataTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Allows us to derive the generic type arguments for a platform’s `ICryptoStatic` implementation, given other properties of the platform. 3 | */ 4 | export namespace CryptoDataTypes { 5 | /** 6 | The type of initialization vector that the platform is expected to be able to use when creating a cipher. 7 | */ 8 | export type IV = BufferUtilsOutput; 9 | 10 | /** 11 | The type of plaintext that the platform is expected to be able to encrypt. 12 | 13 | - `Bufferlike`: The `Bufferlike` of the platform’s `IBufferUtils` implementation. 14 | - `BufferUtilsOutput`: The `Output` of the platform’s `IBufferUtils` implementation. 15 | */ 16 | export type InputPlaintext = Bufferlike | BufferUtilsOutput; 17 | 18 | /** 19 | The type of ciphertext that the platform is expected to be able to decrypt. 20 | 21 | - `MessagePackBinaryType`: The type to which this platform’s MessagePack implementation deserializes elements of the `bin` or `ext` type. 22 | - `BufferUtilsOutput`: The `Output` of the platform’s `IBufferUtils` implementation. 23 | */ 24 | export type InputCiphertext = MessagePackBinaryType | BufferUtilsOutput; 25 | } 26 | -------------------------------------------------------------------------------- /src/common/types/globals.d.ts: -------------------------------------------------------------------------------- 1 | // The signature of clearTimeout varies between browser and NodeJS. This typing essentially just merges the two for compatibility. 2 | declare function clearTimeout(timer?: NodeJS.Timeout | number | null): void; 3 | -------------------------------------------------------------------------------- /src/common/types/msgpack.ts: -------------------------------------------------------------------------------- 1 | export interface MsgPack { 2 | encode(value: any, sparse?: boolean): Buffer | ArrayBuffer | undefined; 3 | decode(buffer: Buffer): any; 4 | } 5 | -------------------------------------------------------------------------------- /src/common/types/utils.d.ts: -------------------------------------------------------------------------------- 1 | export type StandardCallback = (err?: ErrorInfo | null, result?: T) => void; 2 | export type ErrCallback = (err?: ErrorInfo | null) => void; 3 | export type PaginatedResultCallback = StandardCallback>; 4 | /** 5 | * Use this to override specific property typings on an existing object type 6 | */ 7 | export type Modify = Omit & R; 8 | -------------------------------------------------------------------------------- /src/fragments/ably.d.ts: -------------------------------------------------------------------------------- 1 | import Ably = require('../ably'); 2 | export = Ably; 3 | -------------------------------------------------------------------------------- /src/fragments/license.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { version } = require('../../package.json'); 3 | 4 | module.exports = `@license Copyright 2015-2022 Ably Real-time Ltd (ably.com) 5 | 6 | Ably JavaScript Library v${version} 7 | https://github.com/ably/ably-js 8 | 9 | Released under the Apache Licence v2.0`; 10 | -------------------------------------------------------------------------------- /src/platform/nativescript/config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | require('nativescript-websockets'); 3 | 4 | var randomBytes; 5 | if (global.android) { 6 | randomBytes = function (size) { 7 | var sr = new java.security.SecureRandom(); 8 | var buffer = Array.create('byte', size); 9 | sr.nextBytes(buffer); 10 | return android.util.Base64.encodeToString(buffer, android.util.Base64.DEFAULT); 11 | }; 12 | } else { 13 | randomBytes = function (size) { 14 | var bytes = NSMutableData.dataWithLength(size); 15 | SecRandomCopyBytes(kSecRandomDefault, size, bytes.mutableBytes()); 16 | return bytes.base64EncodedStringWithOptions(0); 17 | }; 18 | } 19 | 20 | var Config = { 21 | agent: 'nativescript', 22 | logTimestamps: true, 23 | binaryType: 'arraybuffer', 24 | WebSocket: WebSocket, 25 | xhrSupported: XMLHttpRequest, 26 | allowComet: true, 27 | useProtocolHeartbeats: true, 28 | supportsBinary: typeof TextDecoder !== 'undefined' && TextDecoder, 29 | preferBinary: false, // Motivation as on web; see `preferBinary` comment there. 30 | ArrayBuffer: ArrayBuffer, 31 | atob: null, 32 | nextTick: function (f) { 33 | setTimeout(f, 0); 34 | }, 35 | addEventListener: null, 36 | inspect: JSON.stringify, 37 | stringByteSize: function (str) { 38 | /* str.length will be an underestimate for non-ascii strings. But if we're 39 | * in a browser too old to support TextDecoder, not much we can do. Better 40 | * to underestimate, so if we do go over-size, the server will reject the 41 | * message */ 42 | return (typeof TextDecoder !== 'undefined' && new TextEncoder().encode(str).length) || str.length; 43 | }, 44 | TextEncoder: global.TextEncoder, 45 | TextDecoder: global.TextDecoder, 46 | getRandomArrayBuffer: async function (byteLength) { 47 | var bytes = randomBytes(byteLength); 48 | return bytes; 49 | }, 50 | }; 51 | 52 | export default Config; 53 | -------------------------------------------------------------------------------- /src/platform/nativescript/index.ts: -------------------------------------------------------------------------------- 1 | // Common 2 | import { DefaultRest } from '../../common/lib/client/defaultrest'; 3 | import { DefaultRealtime } from '../../common/lib/client/defaultrealtime'; 4 | import Platform from '../../common/platform'; 5 | import ErrorInfo from '../../common/lib/types/errorinfo'; 6 | import { makeFromDeserializedWithDependencies as makeProtocolMessageFromDeserialized } from '../../common/lib/types/protocolmessage'; 7 | 8 | // Platform Specific 9 | import BufferUtils from '../web/lib/util/bufferutils'; 10 | // @ts-ignore 11 | import { createCryptoClass } from '../web/lib/util/crypto'; 12 | import Http from '../web/lib/http/http'; 13 | // @ts-ignore 14 | import Config from './config'; 15 | // @ts-ignore 16 | import Transports from '../web/lib/transport'; 17 | import Logger from '../../common/lib/util/logger'; 18 | import { getDefaults } from '../../common/lib/util/defaults'; 19 | // @ts-ignore 20 | import WebStorage from './lib/util/webstorage'; 21 | import PlatformDefaults from '../web/lib/util/defaults'; 22 | import msgpack from '../web/lib/util/msgpack'; 23 | import { defaultBundledRequestImplementations } from '../web/lib/http/request'; 24 | 25 | const Crypto = createCryptoClass(Config, BufferUtils); 26 | 27 | Platform.Crypto = Crypto; 28 | Platform.BufferUtils = BufferUtils; 29 | Platform.Http = Http; 30 | Platform.Config = Config; 31 | Platform.Transports = Transports; 32 | Platform.WebStorage = WebStorage; 33 | 34 | for (const clientClass of [DefaultRest, DefaultRealtime]) { 35 | clientClass.Crypto = Crypto; 36 | clientClass._MsgPack = msgpack; 37 | } 38 | 39 | Http.bundledRequestImplementations = defaultBundledRequestImplementations; 40 | 41 | Logger.initLogHandlers(); 42 | 43 | Platform.Defaults = getDefaults(PlatformDefaults); 44 | 45 | if (Platform.Config.agent) { 46 | // @ts-ignore 47 | Platform.Defaults.agent += ' ' + Platform.Config.agent; 48 | } 49 | 50 | export default { 51 | ErrorInfo, 52 | Rest: DefaultRest, 53 | Realtime: DefaultRealtime, 54 | msgpack, 55 | makeProtocolMessageFromDeserialized, 56 | }; 57 | -------------------------------------------------------------------------------- /src/platform/nativescript/lib/util/webstorage.js: -------------------------------------------------------------------------------- 1 | import appSettings from '@nativescript/core/application-settings'; 2 | 3 | var WebStorage = (function () { 4 | function WebStorage() {} 5 | 6 | function set(name, value, ttl) { 7 | var wrappedValue = { value: value }; 8 | if (ttl) { 9 | wrappedValue.expires = Date.now() + ttl; 10 | } 11 | return appSettings.setString(name, JSON.stringify(wrappedValue)); 12 | } 13 | 14 | function get(name) { 15 | var rawItem = appSettings.getString(name); 16 | if (!rawItem) return null; 17 | var wrappedValue = JSON.parse(rawItem); 18 | if (wrappedValue.expires && wrappedValue.expires < Date.now()) { 19 | appSettings.remove(name); 20 | return null; 21 | } 22 | return wrappedValue.value; 23 | } 24 | 25 | WebStorage.set = function (name, value, ttl) { 26 | return set(name, value, ttl); 27 | }; 28 | WebStorage.get = function (name) { 29 | return get(name); 30 | }; 31 | WebStorage.remove = function (name) { 32 | return appSettings.remove(name); 33 | }; 34 | 35 | return WebStorage; 36 | })(); 37 | 38 | export default WebStorage; 39 | -------------------------------------------------------------------------------- /src/platform/nodejs/config.ts: -------------------------------------------------------------------------------- 1 | import { IPlatformConfig } from '../../common/types/IPlatformConfig'; 2 | import crypto from 'crypto'; 3 | import WebSocket from 'ws'; 4 | import util from 'util'; 5 | 6 | const Config: IPlatformConfig = { 7 | agent: 'nodejs/' + process.versions.node, 8 | logTimestamps: true, 9 | userAgent: null, 10 | binaryType: 'nodebuffer' as BinaryType, 11 | WebSocket, 12 | useProtocolHeartbeats: false, 13 | supportsBinary: true, 14 | preferBinary: true, 15 | nextTick: process.nextTick, 16 | inspect: util.inspect, 17 | stringByteSize: Buffer.byteLength, 18 | inherits: util.inherits, 19 | addEventListener: null, 20 | getRandomArrayBuffer: async function (byteLength: number): Promise { 21 | return util.promisify(crypto.randomBytes)(byteLength); 22 | }, 23 | }; 24 | 25 | export default Config; 26 | -------------------------------------------------------------------------------- /src/platform/nodejs/index.ts: -------------------------------------------------------------------------------- 1 | // Common 2 | import { DefaultRest } from '../../common/lib/client/defaultrest'; 3 | import { DefaultRealtime } from '../../common/lib/client/defaultrealtime'; 4 | import Platform from '../../common/platform'; 5 | import ErrorInfo from '../../common/lib/types/errorinfo'; 6 | import { makeFromDeserializedWithDependencies as makeProtocolMessageFromDeserialized } from '../../common/lib/types/protocolmessage'; 7 | 8 | // Platform Specific 9 | import BufferUtils from './lib/util/bufferutils'; 10 | // @ts-ignore 11 | import { createCryptoClass } from './lib/util/crypto'; 12 | import Http from './lib/util/http'; 13 | import Config from './config'; 14 | // @ts-ignore 15 | import Transports from './lib/transport'; 16 | import Logger from '../../common/lib/util/logger'; 17 | import { getDefaults } from '../../common/lib/util/defaults'; 18 | import PlatformDefaults from './lib/util/defaults'; 19 | import msgpack = require('@ably/msgpack-js'); 20 | 21 | const Crypto = createCryptoClass(BufferUtils); 22 | 23 | Platform.Crypto = Crypto; 24 | Platform.BufferUtils = BufferUtils as typeof Platform.BufferUtils; 25 | Platform.Http = Http; 26 | Platform.Config = Config; 27 | Platform.Transports = Transports; 28 | Platform.WebStorage = null; 29 | 30 | for (const clientClass of [DefaultRest, DefaultRealtime]) { 31 | clientClass.Crypto = Crypto; 32 | clientClass._MsgPack = msgpack; 33 | } 34 | 35 | Logger.initLogHandlers(); 36 | 37 | Platform.Defaults = getDefaults(PlatformDefaults); 38 | 39 | if (Platform.Config.agent) { 40 | // @ts-ignore 41 | Platform.Defaults.agent += ' ' + Platform.Config.agent; 42 | } 43 | 44 | module.exports = { 45 | ErrorInfo, 46 | Rest: DefaultRest, 47 | Realtime: DefaultRealtime, 48 | msgpack: null, 49 | makeProtocolMessageFromDeserialized, 50 | }; 51 | -------------------------------------------------------------------------------- /src/platform/nodejs/lib/transport/index.ts: -------------------------------------------------------------------------------- 1 | import { TransportNames } from 'common/constants/TransportName'; 2 | import NodeCometTransport from './nodecomettransport'; 3 | import { default as WebSocketTransport } from '../../../../common/lib/transport/websockettransport'; 4 | import { TransportCtor } from 'common/lib/transport/transport'; 5 | 6 | export default { 7 | order: [TransportNames.Comet], 8 | bundledImplementations: { 9 | [TransportNames.WebSocket]: WebSocketTransport as TransportCtor, 10 | [TransportNames.Comet]: NodeCometTransport as unknown as TransportCtor, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/platform/nodejs/lib/transport/nodecomettransport.d.ts: -------------------------------------------------------------------------------- 1 | import Transport from '../../../../common/lib/transport/transport'; 2 | 3 | declare class NodeCometTransport extends Transport { 4 | static isAvailable(): boolean; 5 | } 6 | 7 | export default NodeCometTransport; 8 | -------------------------------------------------------------------------------- /src/platform/nodejs/lib/util/defaults.ts: -------------------------------------------------------------------------------- 1 | import IDefaults from '../../../../common/types/IDefaults'; 2 | import { TransportNames } from '../../../../common/constants/TransportName'; 3 | 4 | const Defaults: IDefaults = { 5 | connectivityCheckUrl: 'https://internet-up.ably-realtime.com/is-the-internet-up.txt', 6 | wsConnectivityCheckUrl: 'wss://ws-up.ably-realtime.com', 7 | /* Note: order matters here: the base transport is the leftmost one in the 8 | * intersection of baseTransportOrder and the transports clientOption that's supported. */ 9 | defaultTransports: [TransportNames.WebSocket], 10 | restAgentOptions: { maxSockets: 40, keepAlive: true }, 11 | }; 12 | 13 | export default Defaults; 14 | -------------------------------------------------------------------------------- /src/platform/react-hooks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended", 11 | "plugin:react-hooks/recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": "latest", 16 | "sourceType": "module" 17 | }, 18 | "plugins": ["@typescript-eslint", "react", "react-hooks", "import"], 19 | "rules": { 20 | // we should remove these at some point 21 | "@typescript-eslint/no-explicit-any": 0 22 | }, 23 | "settings": { 24 | "react": { 25 | "version": "detect" 26 | } 27 | }, 28 | "overrides": [ 29 | { 30 | "files": ["**/*.{ts,tsx}"], 31 | "rules": { 32 | // see: 33 | // https://github.com/microsoft/TypeScript/issues/16577#issuecomment-703190339 34 | "import/extensions": [ 35 | "error", 36 | "always", 37 | { 38 | "ignorePackages": true 39 | } 40 | ] 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/platform/react-hooks/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | -------------------------------------------------------------------------------- /src/platform/react-hooks/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v2.1.1](https://github.com/ably-labs/react-hooks/tree/v2.1.1) 4 | 5 | [Full Changelog](https://github.com/ably-labs/react-hooks/compare/934c32cb9af7e0b0aa4732c373294c632423f584...v2.1.1) 6 | 7 | - fix: wrap `updateStatus` in `useCallback` [\#37](https://github.com/ably-labs/react-hooks/pull/37) 8 | - fix: correct ably-agent format [\#34](https://github.com/ably-labs/react-hooks/pull/34) 9 | -------------------------------------------------------------------------------- /src/platform/react-hooks/res/package.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /src/platform/react-hooks/res/package.mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /src/platform/react-hooks/res/package.react.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./cjs/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /src/platform/react-hooks/sample-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/platform/react-hooks/sample-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/platform/react-hooks/sample-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App p { 6 | margin: 0.4rem; 7 | } 8 | -------------------------------------------------------------------------------- /src/platform/react-hooks/sample-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/platform/react-hooks/sample-app/src/script.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 4 | // @ts-ignore 5 | import * as Ably from 'ably'; 6 | import App from './App.js'; 7 | import { AblyProvider, ChannelProvider } from '../../src/index.js'; 8 | 9 | const rootId = 'root'; 10 | const container = document.getElementById(rootId); 11 | 12 | if (!container) { 13 | throw new Error(`No element found with id #${rootId} found`); 14 | } 15 | 16 | function generateRandomId() { 17 | return Math.random().toString(36).substr(2, 9); 18 | } 19 | 20 | const client = new Ably.Realtime({ 21 | key: import.meta.env.VITE_ABLY_API_KEY, 22 | clientId: generateRandomId(), 23 | }); 24 | 25 | const root = createRoot(container); 26 | root.render( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | , 52 | ); 53 | -------------------------------------------------------------------------------- /src/platform/react-hooks/sample-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/AblyContext.tsx: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import React from 'react'; 3 | 4 | // We need to make sure we never create more than one Ably React context. 5 | // This might happen when exporting a context directly from a module - 6 | // there's a risk of creating multiple instances of the same context 7 | // if there are misconfigurations in module bundler or package manager on the consumer side of Ably Context. 8 | // This can lead to problems like having an Ably Channel instance added 9 | // in one context, and then attempting to retrieve it from another different context. 10 | // This is why a single Ably context is created and stored in the global state. 11 | const contextKey = Symbol.for('__ABLY_CONTEXT__'); 12 | 13 | const globalObjectForContext: { [contextKey]?: React.Context } = 14 | typeof globalThis !== 'undefined' ? (globalThis as any) : {}; 15 | 16 | // Ably context contains an object which stores all provider options indexed by provider id, 17 | // which is used to get options set by specific `AblyProvider` after calling `React.useContext`. 18 | export type AblyContextValue = Record; 19 | 20 | export interface AblyContextProviderProps { 21 | client: Ably.RealtimeClient; 22 | _channelNameToChannelContext: Record; 23 | } 24 | 25 | export interface ChannelContextProps { 26 | channel: Ably.RealtimeChannel; 27 | derived?: boolean; 28 | } 29 | 30 | function getContext(): React.Context { 31 | let context = globalObjectForContext[contextKey]; 32 | if (!context) { 33 | context = globalObjectForContext[contextKey] = React.createContext({}); 34 | } 35 | 36 | return context; 37 | } 38 | 39 | export const AblyContext = getContext(); 40 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/AblyProvider.tsx: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import React, { useMemo } from 'react'; 3 | import { AblyContext, AblyContextValue } from './AblyContext.js'; 4 | 5 | interface AblyProviderProps { 6 | children?: React.ReactNode | React.ReactNode[] | null; 7 | client?: Ably.RealtimeClient; 8 | ablyId?: string; 9 | } 10 | 11 | export const AblyProvider = ({ client, children, ablyId = 'default' }: AblyProviderProps) => { 12 | if (!client) { 13 | throw new Error('AblyProvider: the `client` prop is required'); 14 | } 15 | 16 | const context = React.useContext(AblyContext); 17 | 18 | const value: AblyContextValue = useMemo(() => { 19 | return { ...context, [ablyId]: { client, _channelNameToChannelContext: {} } }; 20 | }, [context, client, ablyId]); 21 | 22 | return {children}; 23 | }; 24 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/AblyReactHooks.ts: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | 3 | export type ChannelNameAndOptions = { 4 | channelName: string; 5 | ablyId?: string; 6 | skip?: boolean; 7 | 8 | onConnectionError?: (error: Ably.ErrorInfo) => unknown; 9 | onChannelError?: (error: Ably.ErrorInfo) => unknown; 10 | }; 11 | 12 | export type ChannelNameAndAblyId = Pick; 13 | export type ChannelParameters = string | ChannelNameAndOptions; 14 | 15 | export const version = '2.15.0'; 16 | 17 | /** 18 | * channel options for react-hooks 19 | */ 20 | export function channelOptionsForReactHooks(options?: Ably.ChannelOptions): Ably.ChannelOptions { 21 | return { 22 | ...options, 23 | params: { 24 | ...options?.params, 25 | agent: `react-hooks/${version}`, 26 | }, 27 | // we explicitly attach channels in React hooks (useChannel, usePresence, usePresenceListener) 28 | // to avoid situations where implicit attachment could cause errors (when connection state is failed or disconnected) 29 | attachOnSubscribe: false, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/ChannelProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useMemo } from 'react'; 2 | import * as Ably from 'ably'; 3 | import { type AblyContextValue, AblyContext } from './AblyContext.js'; 4 | import { channelOptionsForReactHooks } from './AblyReactHooks.js'; 5 | 6 | interface ChannelProviderProps { 7 | ablyId?: string; 8 | channelName: string; 9 | options?: Ably.ChannelOptions; 10 | deriveOptions?: Ably.DeriveOptions; 11 | children?: React.ReactNode | React.ReactNode[] | null; 12 | } 13 | 14 | export const ChannelProvider = ({ 15 | ablyId = 'default', 16 | channelName, 17 | options, 18 | deriveOptions, 19 | children, 20 | }: ChannelProviderProps) => { 21 | const context = React.useContext(AblyContext); 22 | const { client, _channelNameToChannelContext } = context[ablyId]; 23 | 24 | if (_channelNameToChannelContext[channelName]) { 25 | throw new Error('You can not use more than one `ChannelProvider` with the same channel name'); 26 | } 27 | 28 | const derived = Boolean(deriveOptions); 29 | const channel = derived ? client.channels.getDerived(channelName, deriveOptions) : client.channels.get(channelName); 30 | 31 | const value: AblyContextValue = useMemo(() => { 32 | return { 33 | ...context, 34 | [ablyId]: { 35 | client, 36 | _channelNameToChannelContext: { 37 | ..._channelNameToChannelContext, 38 | [channelName]: { 39 | channel, 40 | derived, 41 | }, 42 | }, 43 | }, 44 | }; 45 | }, [derived, client, channel, channelName, _channelNameToChannelContext, ablyId, context]); 46 | 47 | useLayoutEffect(() => { 48 | channel.setOptions(channelOptionsForReactHooks(options)); 49 | }, [channel, options]); 50 | 51 | return {children}; 52 | }; 53 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/constants.ts: -------------------------------------------------------------------------------- 1 | import type * as Ably from 'ably'; 2 | 3 | export const INACTIVE_CONNECTION_STATES: Ably.ConnectionState[] = ['suspended', 'closing', 'closed', 'failed']; 4 | export const INACTIVE_CHANNEL_STATES: Ably.ChannelState[] = ['failed', 'suspended', 'detaching']; 5 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useAbly.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AblyContext } from '../AblyContext.js'; 3 | import * as API from 'ably'; 4 | 5 | export function useAbly(ablyId = 'default'): API.RealtimeClient { 6 | const client = React.useContext(AblyContext)[ablyId].client; 7 | 8 | if (!client) { 9 | throw new Error( 10 | 'Could not find ably client in context. ' + 'Make sure your ably hooks are called inside an ', 11 | ); 12 | } 13 | 14 | return client; 15 | } 16 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useChannelAttach.test.tsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { beforeEach, describe, expect, it, vi } from 'vitest'; 3 | import { useChannelAttach } from './useChannelAttach.js'; 4 | 5 | interface LocalTestContext { 6 | useChannelAttach: typeof useChannelAttach; 7 | } 8 | 9 | describe('useChannelAttach', () => { 10 | const fakeAblyClientRef: any = {}; 11 | 12 | beforeEach(async (context) => { 13 | vi.doMock('./useConnectionStateListener.js', () => ({ 14 | useConnectionStateListener: vi.fn(), 15 | })); 16 | 17 | vi.doMock('./useAbly.js', () => ({ 18 | useAbly: () => fakeAblyClientRef.current, 19 | })); 20 | 21 | context.useChannelAttach = (await import('./useChannelAttach.js')).useChannelAttach; 22 | fakeAblyClientRef.current = { connection: { state: 'initialized' } }; 23 | }); 24 | 25 | it('should call attach on render', ({ useChannelAttach }) => { 26 | const channel = { attach: vi.fn(() => Promise.resolve()) }; 27 | const { result } = renderHook(() => useChannelAttach(channel, undefined, false)); 28 | 29 | expect(result.current.connectionState).toBe('initialized'); 30 | expect(channel.attach).toHaveBeenCalled(); 31 | }); 32 | 33 | it('should not call attach when skipped', ({ useChannelAttach }) => { 34 | const channel = { attach: vi.fn(() => Promise.resolve()) }; 35 | const { result } = renderHook(() => useChannelAttach(channel, undefined, true)); 36 | 37 | expect(result.current.connectionState).toBe('initialized'); 38 | expect(channel.attach).not.toHaveBeenCalled(); 39 | }); 40 | 41 | it('should not call attach when in failed state', ({ useChannelAttach }) => { 42 | fakeAblyClientRef.current = { connection: { state: 'failed' } }; 43 | const channel = { attach: vi.fn(() => Promise.resolve()) }; 44 | const { result } = renderHook(() => useChannelAttach(channel, undefined, false)); 45 | 46 | expect(result.current.connectionState).toBe('failed'); 47 | expect(channel.attach).not.toHaveBeenCalled(); 48 | }); 49 | 50 | it('should call attach when go back to the connected state', async ({ useChannelAttach }) => { 51 | fakeAblyClientRef.current = { connection: { state: 'suspended' } }; 52 | const channel = { attach: vi.fn(() => Promise.resolve()) }; 53 | const { result, rerender } = renderHook(() => useChannelAttach(channel, undefined, false)); 54 | 55 | expect(result.current.connectionState).toBe('suspended'); 56 | expect(channel.attach).not.toHaveBeenCalled(); 57 | 58 | fakeAblyClientRef.current = { connection: { state: 'connected' } }; 59 | rerender(); 60 | 61 | expect(result.current.connectionState).toBe('connected'); 62 | expect(channel.attach).toHaveBeenCalled(); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useChannelAttach.ts: -------------------------------------------------------------------------------- 1 | import type * as Ably from 'ably'; 2 | import { useEffect, useState } from 'react'; 3 | import { useConnectionStateListener } from './useConnectionStateListener.js'; 4 | import { useAbly } from './useAbly.js'; 5 | import { INACTIVE_CONNECTION_STATES } from './constants.js'; 6 | import { logError } from '../utils.js'; 7 | 8 | interface ChannelAttachResult { 9 | connectionState: Ably.ConnectionState; 10 | } 11 | 12 | export function useChannelAttach( 13 | channel: Ably.RealtimeChannel, 14 | ablyId: string | undefined, 15 | skip: boolean, 16 | ): ChannelAttachResult { 17 | const ably = useAbly(ablyId); 18 | 19 | // we need to listen for the current connection state in order to react to it. 20 | // for example, we should attach when first connected, re-enter when reconnected, 21 | // and be able to prevent attaching when the connection is in an inactive state. 22 | // all of that can be achieved by using the useConnectionStateListener hook. 23 | const [connectionState, setConnectionState] = useState(ably.connection.state); 24 | useConnectionStateListener((stateChange) => { 25 | setConnectionState(stateChange.current); 26 | }, ablyId); 27 | 28 | if (ably.connection.state !== connectionState) { 29 | setConnectionState(ably.connection.state); 30 | } 31 | 32 | const shouldAttachToTheChannel = !skip && !INACTIVE_CONNECTION_STATES.includes(connectionState); 33 | 34 | useEffect(() => { 35 | if (shouldAttachToTheChannel) { 36 | channel.attach().catch((reason) => { 37 | // we use a fire-and-forget approach for attaching, but calling detach during the attaching process or while 38 | // suspending can cause errors that will be automatically resolved 39 | logError(ably, reason.toString()); 40 | }); 41 | } 42 | }, [shouldAttachToTheChannel, channel]); 43 | 44 | // we expose `connectionState` here for reuse in the usePresence hook, where we need to prevent 45 | // entering and leaving presence in a similar manner 46 | return { connectionState }; 47 | } 48 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useChannelInstance.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AblyContext, ChannelContextProps } from '../AblyContext.js'; 3 | 4 | export function useChannelInstance(ablyId = 'default', channelName: string): ChannelContextProps { 5 | const channelContext = React.useContext(AblyContext)[ablyId]._channelNameToChannelContext[channelName]; 6 | 7 | if (!channelContext) { 8 | throw new Error( 9 | `Could not find a parent ChannelProvider in the component tree for channelName="${channelName}". Make sure your channel based hooks (usePresence, useChannel, useChannelStateListener) are called inside a component`, 10 | ); 11 | } 12 | 13 | return channelContext; 14 | } 15 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useChannelStateListener.ts: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import { ChannelNameAndAblyId } from '../AblyReactHooks.js'; 3 | import { useEventListener } from './useEventListener.js'; 4 | import { useChannelInstance } from './useChannelInstance.js'; 5 | 6 | type ChannelStateListener = (stateChange: Ably.ChannelStateChange) => any; 7 | 8 | export function useChannelStateListener(channelName: string, listener?: ChannelStateListener); 9 | 10 | export function useChannelStateListener(options: ChannelNameAndAblyId, listener?: ChannelStateListener); 11 | 12 | export function useChannelStateListener( 13 | options: ChannelNameAndAblyId | string, 14 | state?: Ably.ChannelState | Ably.ChannelState[], 15 | listener?: ChannelStateListener, 16 | ); 17 | 18 | export function useChannelStateListener( 19 | channelNameOrNameAndAblyId: ChannelNameAndAblyId | string, 20 | stateOrListener?: Ably.ChannelState | Ably.ChannelState[] | ChannelStateListener, 21 | listener?: (stateChange: Ably.ChannelStateChange) => any, 22 | ) { 23 | const channelHookOptions = 24 | typeof channelNameOrNameAndAblyId === 'object' 25 | ? channelNameOrNameAndAblyId 26 | : { channelName: channelNameOrNameAndAblyId }; 27 | 28 | const { ablyId, channelName } = channelHookOptions; 29 | 30 | const { channel } = useChannelInstance(ablyId, channelName); 31 | 32 | const _listener = typeof listener === 'function' ? listener : (stateOrListener as ChannelStateListener); 33 | 34 | const state = typeof stateOrListener !== 'function' ? stateOrListener : undefined; 35 | 36 | useEventListener(channel, _listener, state); 37 | } 38 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useConnectionStateListener.ts: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import { useAbly } from './useAbly.js'; 3 | import { useEventListener } from './useEventListener.js'; 4 | 5 | type ConnectionStateListener = (stateChange: Ably.ConnectionStateChange) => any; 6 | 7 | export function useConnectionStateListener(listener: ConnectionStateListener, ablyId?: string); 8 | 9 | export function useConnectionStateListener( 10 | state: Ably.ConnectionState | Ably.ConnectionState[], 11 | listener: ConnectionStateListener, 12 | ablyId?: string, 13 | ); 14 | 15 | export function useConnectionStateListener( 16 | stateOrListener?: Ably.ConnectionState | Ably.ConnectionState[] | ConnectionStateListener, 17 | listenerOrAblyId?: string | ConnectionStateListener, 18 | ablyId = 'default', 19 | ) { 20 | const _ablyId = typeof listenerOrAblyId === 'string' ? listenerOrAblyId : ablyId; 21 | const ably = useAbly(_ablyId); 22 | 23 | const listener = 24 | typeof listenerOrAblyId === 'function' ? listenerOrAblyId : (stateOrListener as ConnectionStateListener); 25 | const state = typeof stateOrListener !== 'function' ? stateOrListener : undefined; 26 | 27 | useEventListener(ably.connection, listener, state); 28 | } 29 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useEventListener.ts: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | type EventListener = (stateChange: T) => any; 5 | 6 | export function useEventListener< 7 | S extends Ably.ConnectionState | Ably.ChannelState, 8 | C extends Ably.ConnectionStateChange | Ably.ChannelStateChange, 9 | >(emitter: Ably.EventEmitter, C, S>, listener: EventListener, event?: S | S[]) { 10 | const savedListener = useRef(listener); 11 | 12 | useEffect(() => { 13 | savedListener.current = listener; 14 | }, [listener]); 15 | 16 | useEffect(() => { 17 | if (event) { 18 | emitter.on(event as S, savedListener.current); 19 | } else { 20 | emitter.on(listener); 21 | } 22 | 23 | return () => { 24 | if (event) { 25 | emitter.off(event as S, listener); 26 | } else { 27 | emitter.off(listener); 28 | } 29 | }; 30 | }, [emitter, event, listener]); 31 | } 32 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/usePresenceListener.ts: -------------------------------------------------------------------------------- 1 | import type * as Ably from 'ably'; 2 | import { useCallback, useEffect, useRef, useState } from 'react'; 3 | import { ChannelParameters } from '../AblyReactHooks.js'; 4 | import { useChannelInstance } from './useChannelInstance.js'; 5 | import { useStateErrors } from './useStateErrors.js'; 6 | import { useChannelAttach } from './useChannelAttach.js'; 7 | 8 | interface PresenceMessage extends Ably.PresenceMessage { 9 | data: T; 10 | } 11 | 12 | export interface PresenceListenerResult { 13 | presenceData: PresenceMessage[]; 14 | connectionError: Ably.ErrorInfo | null; 15 | channelError: Ably.ErrorInfo | null; 16 | } 17 | 18 | export type OnPresenceMessageReceived = (presenceData: PresenceMessage) => void; 19 | 20 | export function usePresenceListener( 21 | channelNameOrNameAndOptions: ChannelParameters, 22 | onPresenceMessageReceived?: OnPresenceMessageReceived, 23 | ): PresenceListenerResult { 24 | const params = 25 | typeof channelNameOrNameAndOptions === 'object' 26 | ? channelNameOrNameAndOptions 27 | : { channelName: channelNameOrNameAndOptions }; 28 | const skip = params.skip; 29 | 30 | const { channel } = useChannelInstance(params.ablyId, params.channelName); 31 | const { connectionError, channelError } = useStateErrors(params); 32 | const [presenceData, updatePresenceData] = useState>>([]); 33 | 34 | const onPresenceMessageReceivedRef = useRef(onPresenceMessageReceived); 35 | useEffect(() => { 36 | onPresenceMessageReceivedRef.current = onPresenceMessageReceived; 37 | }, [onPresenceMessageReceived]); 38 | 39 | const updatePresence = useCallback( 40 | async (message?: Ably.PresenceMessage) => { 41 | const snapshot = await channel.presence.get(); 42 | updatePresenceData(snapshot); 43 | 44 | onPresenceMessageReceivedRef.current?.(message); 45 | }, 46 | [channel.presence], 47 | ); 48 | 49 | const onMount = useCallback(async () => { 50 | channel.presence.subscribe(['enter', 'leave', 'update'], updatePresence); 51 | const snapshot = await channel.presence.get(); 52 | updatePresenceData(snapshot); 53 | }, [channel.presence, updatePresence]); 54 | 55 | const onUnmount = useCallback(async () => { 56 | channel.presence.unsubscribe(['enter', 'leave', 'update'], updatePresence); 57 | }, [channel.presence, updatePresence]); 58 | 59 | useEffect(() => { 60 | if (skip) return; 61 | 62 | onMount(); 63 | return () => { 64 | onUnmount(); 65 | }; 66 | }, [skip, onMount, onUnmount]); 67 | 68 | useChannelAttach(channel, params.ablyId, skip); 69 | 70 | return { presenceData, connectionError, channelError }; 71 | } 72 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/hooks/useStateErrors.ts: -------------------------------------------------------------------------------- 1 | import { ErrorInfo } from 'ably'; 2 | import { useState } from 'react'; 3 | import { useConnectionStateListener } from './useConnectionStateListener.js'; 4 | import { useChannelStateListener } from './useChannelStateListener.js'; 5 | import { ChannelNameAndOptions } from '../AblyReactHooks.js'; 6 | 7 | export function useStateErrors(params: ChannelNameAndOptions) { 8 | const [connectionError, setConnectionError] = useState(null); 9 | const [channelError, setChannelError] = useState(null); 10 | 11 | useConnectionStateListener( 12 | ['suspended', 'failed', 'disconnected'], 13 | (stateChange) => { 14 | if (stateChange.reason) { 15 | params.onConnectionError?.(stateChange.reason); 16 | setConnectionError(stateChange.reason); 17 | } 18 | }, 19 | params.ablyId, 20 | ); 21 | 22 | useConnectionStateListener( 23 | ['connected', 'closed'], 24 | () => { 25 | setConnectionError(null); 26 | }, 27 | params.ablyId, 28 | ); 29 | 30 | useChannelStateListener(params, ['suspended', 'failed', 'detached'], (stateChange) => { 31 | if (stateChange.reason) { 32 | params.onChannelError?.(stateChange.reason); 33 | setChannelError(stateChange.reason); 34 | } 35 | }); 36 | 37 | useChannelStateListener(params, ['attached'], () => { 38 | setChannelError(null); 39 | }); 40 | 41 | return { connectionError, channelError }; 42 | } 43 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AblyReactHooks.js'; 2 | export * from './hooks/useChannel.js'; 3 | export * from './hooks/usePresence.js'; 4 | export * from './hooks/usePresenceListener.js'; 5 | export * from './hooks/useAbly.js'; 6 | export * from './AblyProvider.js'; 7 | export * from './hooks/useChannelStateListener.js'; 8 | export * from './hooks/useConnectionStateListener.js'; 9 | export { ChannelProvider } from './ChannelProvider.js'; 10 | export * from './AblyContext.js'; 11 | -------------------------------------------------------------------------------- /src/platform/react-hooks/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * In rare cases when we need to access core logger to log error messages 3 | * 4 | * @param ablyClient ably core SDK client, it has any type because we access internal Logger class 5 | * @param message message to log 6 | */ 7 | export const logError = (ablyClient: any, message: string) => { 8 | try { 9 | ablyClient.Logger.logAction(ablyClient.logger, ablyClient.Logger.LOG_ERROR, `[react-hooks] ${message}`); 10 | } catch (error) { 11 | // we don't want to fail on logger if something change 12 | console.error(`Unable to access ably-js logger, while sending ${message}`); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/platform/react-hooks/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "../../../react/cjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/platform/react-hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*.ts", "./ably.d.ts"], 3 | "exclude": ["./src/**/*.test.tsx", "./src/fakes/**/*.ts"], 4 | "compilerOptions": { 5 | "target": "ES2017", 6 | "rootDir": "./src", 7 | "sourceMap": true, 8 | "strict": false, 9 | "esModuleInterop": true, 10 | "declaration": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "allowJs": true, 15 | "jsx": "react-jsx", 16 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 17 | "types": ["vitest/globals"], 18 | "paths": { 19 | "ably": ["../../../"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/platform/react-hooks/tsconfig.mjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES6", 5 | "outDir": "../../../react/mjs" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/platform/react-hooks/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | export default defineConfig({ 5 | root: 'sample-app', 6 | server: { 7 | port: 8080, 8 | strictPort: true, 9 | host: true, 10 | }, 11 | plugins: [react()], 12 | test: { 13 | globals: true, 14 | environment: 'jsdom', 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/platform/react-native/config.ts: -------------------------------------------------------------------------------- 1 | import { IPlatformConfig } from '../../common/types/IPlatformConfig'; 2 | import BufferUtils from '../web/lib/util/bufferutils'; 3 | 4 | type RNRandomBytes = { 5 | randomBytes: (byteLength: number, cb: (err: Error | null, base64String: string | null) => void) => void; 6 | }; 7 | 8 | export default function (bufferUtils: typeof BufferUtils): IPlatformConfig { 9 | return { 10 | agent: 'reactnative', 11 | logTimestamps: true, 12 | binaryType: 'arraybuffer', 13 | WebSocket: WebSocket, 14 | xhrSupported: true, 15 | allowComet: true, 16 | useProtocolHeartbeats: true, 17 | supportsBinary: !!(typeof TextDecoder !== 'undefined' && TextDecoder), 18 | preferBinary: false, // Motivation as on web; see `preferBinary` comment there. 19 | ArrayBuffer: typeof ArrayBuffer !== 'undefined' && ArrayBuffer, 20 | atob: global.atob, 21 | nextTick: 22 | typeof global.queueMicrotask === 'function' 23 | ? (f: () => void) => global.queueMicrotask(f) 24 | : (f: () => void) => Promise.resolve().then(f), 25 | addEventListener: null, 26 | inspect: JSON.stringify, 27 | stringByteSize: function (str: string) { 28 | /* str.length will be an underestimate for non-ascii strings. But if we're 29 | * in a browser too old to support TextDecoder, not much we can do. Better 30 | * to underestimate, so if we do go over-size, the server will reject the 31 | * message */ 32 | return (typeof TextDecoder !== 'undefined' && new TextEncoder().encode(str).length) || str.length; 33 | }, 34 | TextEncoder: global.TextEncoder, 35 | TextDecoder: global.TextDecoder, 36 | getRandomArrayBuffer: (function (RNRandomBytes: RNRandomBytes) { 37 | return async function (byteLength: number) { 38 | return new Promise((resolve, reject) => { 39 | RNRandomBytes.randomBytes(byteLength, (err, base64String) => { 40 | err ? reject(err) : resolve(bufferUtils.toArrayBuffer(bufferUtils.base64Decode(base64String!))); 41 | }); 42 | }); 43 | }; 44 | // Installing @types/react-native would fix this but conflicts with @types/node 45 | // See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/15960 46 | // eslint-disable-next-line @typescript-eslint/no-var-requires 47 | })(require('react-native').NativeModules.RNRandomBytes), 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /src/platform/react-native/index.ts: -------------------------------------------------------------------------------- 1 | // Common 2 | import { DefaultRest } from '../../common/lib/client/defaultrest'; 3 | import { DefaultRealtime } from '../../common/lib/client/defaultrealtime'; 4 | import Platform from '../../common/platform'; 5 | import ErrorInfo from '../../common/lib/types/errorinfo'; 6 | import { makeFromDeserializedWithDependencies as makeProtocolMessageFromDeserialized } from '../../common/lib/types/protocolmessage'; 7 | 8 | // Platform Specific 9 | import BufferUtils from '../web/lib/util/bufferutils'; 10 | // @ts-ignore 11 | import { createCryptoClass } from '../web/lib/util/crypto'; 12 | import Http from '../web/lib/http/http'; 13 | import configFactory from './config'; 14 | // @ts-ignore 15 | import Transports from '../web/lib/transport'; 16 | import Logger from '../../common/lib/util/logger'; 17 | import { getDefaults } from '../../common/lib/util/defaults'; 18 | import WebStorage from '../web/lib/util/webstorage'; 19 | import PlatformDefaults from '../web/lib/util/defaults'; 20 | import msgpack from '../web/lib/util/msgpack'; 21 | import { defaultBundledRequestImplementations } from '../web/lib/http/request'; 22 | 23 | // lightweight polyfill for TextEncoder/TextDecoder 24 | import 'fastestsmallesttextencoderdecoder'; 25 | 26 | const Config = configFactory(BufferUtils); 27 | 28 | const Crypto = createCryptoClass(Config, BufferUtils); 29 | 30 | Platform.Crypto = Crypto; 31 | Platform.BufferUtils = BufferUtils; 32 | Platform.Http = Http; 33 | Platform.Config = Config; 34 | Platform.Transports = Transports; 35 | Platform.WebStorage = WebStorage; 36 | 37 | for (const clientClass of [DefaultRest, DefaultRealtime]) { 38 | clientClass.Crypto = Crypto; 39 | clientClass._MsgPack = msgpack; 40 | } 41 | 42 | Http.bundledRequestImplementations = defaultBundledRequestImplementations; 43 | 44 | Logger.initLogHandlers(); 45 | 46 | Platform.Defaults = getDefaults(PlatformDefaults); 47 | 48 | if (Platform.Config.agent) { 49 | // @ts-ignore 50 | Platform.Defaults.agent += ' ' + Platform.Config.agent; 51 | } 52 | 53 | export default { 54 | ErrorInfo, 55 | Rest: DefaultRest, 56 | Realtime: DefaultRealtime, 57 | msgpack, 58 | makeProtocolMessageFromDeserialized, 59 | }; 60 | -------------------------------------------------------------------------------- /src/platform/web/index.ts: -------------------------------------------------------------------------------- 1 | // Common 2 | import { DefaultRest } from '../../common/lib/client/defaultrest'; 3 | import { DefaultRealtime } from '../../common/lib/client/defaultrealtime'; 4 | import Platform from '../../common/platform'; 5 | import ErrorInfo from '../../common/lib/types/errorinfo'; 6 | import { makeFromDeserializedWithDependencies as makeProtocolMessageFromDeserialized } from '../../common/lib/types/protocolmessage'; 7 | 8 | // Platform Specific 9 | import BufferUtils from './lib/util/bufferutils'; 10 | // @ts-ignore 11 | import { createCryptoClass } from './lib/util/crypto'; 12 | import Http from './lib/http/http'; 13 | import Config from './config'; 14 | // @ts-ignore 15 | import Transports from './lib/transport'; 16 | import Logger from '../../common/lib/util/logger'; 17 | import { getDefaults } from '../../common/lib/util/defaults'; 18 | import WebStorage from './lib/util/webstorage'; 19 | import PlatformDefaults from './lib/util/defaults'; 20 | import msgpack from './lib/util/msgpack'; 21 | import { defaultBundledRequestImplementations } from './lib/http/request'; 22 | 23 | const Crypto = createCryptoClass(Config, BufferUtils); 24 | 25 | Platform.Crypto = Crypto; 26 | Platform.BufferUtils = BufferUtils; 27 | Platform.Http = Http; 28 | Platform.Config = Config; 29 | Platform.Transports = Transports; 30 | Platform.WebStorage = WebStorage; 31 | 32 | for (const clientClass of [DefaultRest, DefaultRealtime]) { 33 | clientClass.Crypto = Crypto; 34 | clientClass._MsgPack = msgpack; 35 | } 36 | 37 | Http.bundledRequestImplementations = defaultBundledRequestImplementations; 38 | 39 | Logger.initLogHandlers(); 40 | 41 | Platform.Defaults = getDefaults(PlatformDefaults); 42 | 43 | if (Platform.Config.agent) { 44 | // @ts-ignore 45 | Platform.Defaults.agent += ' ' + Platform.Config.agent; 46 | } 47 | 48 | export { DefaultRest as Rest, DefaultRealtime as Realtime, msgpack, makeProtocolMessageFromDeserialized, ErrorInfo }; 49 | 50 | export default { 51 | ErrorInfo, 52 | Rest: DefaultRest, 53 | Realtime: DefaultRealtime, 54 | msgpack, 55 | makeProtocolMessageFromDeserialized, 56 | }; 57 | -------------------------------------------------------------------------------- /src/platform/web/lib/http/request/index.ts: -------------------------------------------------------------------------------- 1 | import { HTTPRequestImplementations } from '../http'; 2 | import XHRRequest from './xhrrequest'; 3 | import fetchRequest from './fetchrequest'; 4 | 5 | export const defaultBundledRequestImplementations: HTTPRequestImplementations = { 6 | XHRRequest: XHRRequest, 7 | FetchRequest: fetchRequest, 8 | }; 9 | 10 | export const modularBundledRequestImplementations: HTTPRequestImplementations = {}; 11 | -------------------------------------------------------------------------------- /src/platform/web/lib/transport/index.ts: -------------------------------------------------------------------------------- 1 | import TransportName from 'common/constants/TransportName'; 2 | import Platform from 'common/platform'; 3 | import XhrPollingTransport from './xhrpollingtransport'; 4 | import WebSocketTransport from '../../../../common/lib/transport/websockettransport'; 5 | 6 | // For reasons that I don’t understand, if we use [TransportNames.XhrPolling] for the keys in defaultTransports’s, then defaultTransports does not get tree-shaken. Hence using literals instead. They’re still correctly type-checked. 7 | 8 | const order: TransportName[] = ['xhr_polling']; 9 | 10 | const defaultTransports: (typeof Platform)['Transports'] = { 11 | order, 12 | bundledImplementations: { 13 | web_socket: WebSocketTransport, 14 | xhr_polling: XhrPollingTransport, 15 | }, 16 | }; 17 | 18 | export default defaultTransports; 19 | 20 | export const ModularTransports: (typeof Platform)['Transports'] = { 21 | order, 22 | bundledImplementations: {}, 23 | }; 24 | -------------------------------------------------------------------------------- /src/platform/web/lib/transport/xhrpollingtransport.ts: -------------------------------------------------------------------------------- 1 | import Platform from '../../../../common/platform'; 2 | import CometTransport from '../../../../common/lib/transport/comettransport'; 3 | import XHRRequest from '../http/request/xhrrequest'; 4 | import ConnectionManager, { TransportParams } from 'common/lib/transport/connectionmanager'; 5 | import Auth from 'common/lib/client/auth'; 6 | import { RequestBody, RequestParams } from 'common/types/http'; 7 | import { TransportNames } from 'common/constants/TransportName'; 8 | 9 | var shortName = TransportNames.XhrPolling; 10 | class XHRPollingTransport extends CometTransport { 11 | shortName = shortName; 12 | constructor(connectionManager: ConnectionManager, auth: Auth, params: TransportParams) { 13 | super(connectionManager, auth, params); 14 | params.stream = false; 15 | this.shortName = shortName; 16 | } 17 | 18 | static isAvailable() { 19 | return !!(Platform.Config.xhrSupported && Platform.Config.allowComet); 20 | } 21 | 22 | toString() { 23 | return 'XHRPollingTransport; uri=' + this.baseUri + '; isConnected=' + this.isConnected; 24 | } 25 | 26 | createRequest( 27 | uri: string, 28 | headers: Record, 29 | params: RequestParams, 30 | body: RequestBody | null, 31 | requestMode: number, 32 | ) { 33 | return XHRRequest.createRequest(uri, headers, params, body, requestMode, this.timeouts, this.logger); 34 | } 35 | } 36 | 37 | export default XHRPollingTransport; 38 | -------------------------------------------------------------------------------- /src/platform/web/lib/util/defaults.ts: -------------------------------------------------------------------------------- 1 | import IDefaults from 'common/types/IDefaults'; 2 | import { TransportNames } from 'common/constants/TransportName'; 3 | 4 | const Defaults: IDefaults = { 5 | connectivityCheckUrl: 'https://internet-up.ably-realtime.com/is-the-internet-up.txt', 6 | wsConnectivityCheckUrl: 'wss://ws-up.ably-realtime.com', 7 | /* Order matters here: the base transport is the leftmost one in the 8 | * intersection of baseTransportOrder and the transports clientOption that's 9 | * supported. */ 10 | defaultTransports: [TransportNames.XhrPolling, TransportNames.WebSocket], 11 | }; 12 | 13 | export default Defaults; 14 | -------------------------------------------------------------------------------- /src/platform/web/lib/util/domevent.js: -------------------------------------------------------------------------------- 1 | var DomEvent = (function () { 2 | function DomEvent() {} 3 | 4 | DomEvent.addListener = function (target, event, listener) { 5 | if (target.addEventListener) { 6 | target.addEventListener(event, listener, false); 7 | } else { 8 | target.attachEvent('on' + event, function () { 9 | listener.apply(target, arguments); 10 | }); 11 | } 12 | }; 13 | 14 | DomEvent.removeListener = function (target, event, listener) { 15 | if (target.removeEventListener) { 16 | target.removeEventListener(event, listener, false); 17 | } else { 18 | target.detachEvent('on' + event, function () { 19 | listener.apply(target, arguments); 20 | }); 21 | } 22 | }; 23 | 24 | DomEvent.addMessageListener = function (target, listener) { 25 | DomEvent.addListener(target, 'message', listener); 26 | }; 27 | 28 | DomEvent.removeMessageListener = function (target, listener) { 29 | DomEvent.removeListener(target, 'message', listener); 30 | }; 31 | 32 | DomEvent.addUnloadListener = function (listener) { 33 | DomEvent.addListener(global, 'unload', listener); 34 | }; 35 | 36 | return DomEvent; 37 | })(); 38 | 39 | export default DomEvent; 40 | -------------------------------------------------------------------------------- /src/platform/web/modular.ts: -------------------------------------------------------------------------------- 1 | // Common 2 | import { BaseRest } from '../../common/lib/client/baserest'; 3 | import BaseRealtime from '../../common/lib/client/baserealtime'; 4 | import Platform from '../../common/platform'; 5 | import ErrorInfo from '../../common/lib/types/errorinfo'; 6 | 7 | // Platform Specific 8 | import BufferUtils from './lib/util/bufferutils'; 9 | // @ts-ignore 10 | import Http from './lib/http/http'; 11 | import Config from './config'; 12 | // @ts-ignore 13 | import { ModularTransports } from './lib/transport'; 14 | import Logger from '../../common/lib/util/logger'; 15 | import { getDefaults } from '../../common/lib/util/defaults'; 16 | import WebStorage from './lib/util/webstorage'; 17 | import PlatformDefaults from './lib/util/defaults'; 18 | import { modularBundledRequestImplementations } from './lib/http/request'; 19 | 20 | Platform.BufferUtils = BufferUtils; 21 | Platform.Http = Http; 22 | Platform.Config = Config; 23 | Platform.Transports = ModularTransports; 24 | Platform.WebStorage = WebStorage; 25 | 26 | Http.bundledRequestImplementations = modularBundledRequestImplementations; 27 | 28 | Logger.initLogHandlers(); 29 | 30 | Platform.Defaults = getDefaults(PlatformDefaults); 31 | 32 | if (Platform.Config.agent) { 33 | // @ts-ignore 34 | Platform.Defaults.agent += ' ' + Platform.Config.agent; 35 | } 36 | 37 | export * from './modular/crypto'; 38 | export * from './modular/message'; 39 | export * from './modular/presencemessage'; 40 | export * from './modular/msgpack'; 41 | export * from './modular/realtimepresence'; 42 | export * from './modular/annotations'; 43 | export * from './modular/transports'; 44 | export * from './modular/http'; 45 | export { Rest } from '../../common/lib/client/rest'; 46 | export { FilteredSubscriptions as MessageInteractions } from '../../common/lib/client/filteredsubscriptions'; 47 | export { BaseRest, BaseRealtime, ErrorInfo }; 48 | -------------------------------------------------------------------------------- /src/platform/web/modular/annotations.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../../ably'; 2 | import Logger from '../../../common/lib/util/logger'; 3 | import { AnnotationsPlugin } from 'common/lib/client/modularplugins'; 4 | import RealtimeAnnotations from '../../../common/lib/client/realtimeannotations'; 5 | import RestAnnotations from '../../../common/lib/client/restannotations'; 6 | import Annotation, { WireAnnotation, fromEncoded, fromEncodedArray } from '../../../common/lib/types/annotation'; 7 | 8 | export const Annotations: AnnotationsPlugin = { 9 | Annotation, 10 | WireAnnotation, 11 | RealtimeAnnotations, 12 | RestAnnotations, 13 | }; 14 | 15 | export const decodeAnnotation = ((obj, options) => { 16 | return fromEncoded(Logger.defaultLogger, obj, options); 17 | }) as API.AnnotationStatic['fromEncoded']; 18 | 19 | export const decodeAnnotations = ((obj, options) => { 20 | return fromEncodedArray(Logger.defaultLogger, obj, options); 21 | }) as API.AnnotationStatic['fromEncodedArray']; 22 | -------------------------------------------------------------------------------- /src/platform/web/modular/crypto.ts: -------------------------------------------------------------------------------- 1 | import BufferUtils from '../lib/util/bufferutils'; 2 | import { createCryptoClass } from '../lib/util/crypto'; 3 | import Config from '../config'; 4 | import * as API from '../../../../ably'; 5 | 6 | export const Crypto = /* @__PURE__@ */ createCryptoClass(Config, BufferUtils); 7 | 8 | export const generateRandomKey: API.Crypto['generateRandomKey'] = (keyLength) => { 9 | return Crypto.generateRandomKey(keyLength); 10 | }; 11 | 12 | export const getDefaultCryptoParams: API.Crypto['getDefaultParams'] = (params) => { 13 | return Crypto.getDefaultParams(params); 14 | }; 15 | -------------------------------------------------------------------------------- /src/platform/web/modular/http.ts: -------------------------------------------------------------------------------- 1 | export { default as XHRRequest } from '../lib/http/request/xhrrequest'; 2 | export { default as FetchRequest } from '../lib/http/request/fetchrequest'; 3 | -------------------------------------------------------------------------------- /src/platform/web/modular/message.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../../ably'; 2 | import { Crypto } from './crypto'; 3 | import { fromEncoded, fromEncodedArray } from '../../../common/lib/types/message'; 4 | import Logger from '../../../common/lib/util/logger'; 5 | 6 | // The type assertions for the decode* functions below are due to https://github.com/ably/ably-js/issues/1421 7 | 8 | export const decodeMessage = ((obj, options) => { 9 | return fromEncoded(Logger.defaultLogger, null, obj, options); 10 | }) as API.MessageStatic['fromEncoded']; 11 | 12 | export const decodeEncryptedMessage = ((obj, options) => { 13 | return fromEncoded(Logger.defaultLogger, Crypto, obj, options); 14 | }) as API.MessageStatic['fromEncoded']; 15 | 16 | export const decodeMessages = ((obj, options) => { 17 | return fromEncodedArray(Logger.defaultLogger, null, obj, options); 18 | }) as API.MessageStatic['fromEncodedArray']; 19 | 20 | export const decodeEncryptedMessages = ((obj, options) => { 21 | return fromEncodedArray(Logger.defaultLogger, Crypto, obj, options); 22 | }) as API.MessageStatic['fromEncodedArray']; 23 | -------------------------------------------------------------------------------- /src/platform/web/modular/msgpack.ts: -------------------------------------------------------------------------------- 1 | export { default as MsgPack } from '../lib/util/msgpack'; 2 | -------------------------------------------------------------------------------- /src/platform/web/modular/presencemessage.ts: -------------------------------------------------------------------------------- 1 | import * as API from '../../../../ably'; 2 | import { fromValues, fromEncoded, fromEncodedArray } from '../../../common/lib/types/presencemessage'; 3 | import { Crypto } from './crypto'; 4 | import Logger from '../../../common/lib/util/logger'; 5 | 6 | // The type assertions for the functions below are due to https://github.com/ably/ably-js/issues/1421 7 | 8 | export const decodePresenceMessage = ((obj, options) => { 9 | return fromEncoded(Logger.defaultLogger, null, obj, options); 10 | }) as API.PresenceMessageStatic['fromEncoded']; 11 | 12 | export const decodeEncryptedPresenceMessage = ((obj, options) => { 13 | return fromEncoded(Logger.defaultLogger, Crypto, obj, options); 14 | }) as API.PresenceMessageStatic['fromEncoded']; 15 | 16 | export const decodePresenceMessages = ((obj, options) => { 17 | return fromEncodedArray(Logger.defaultLogger, null, obj, options); 18 | }) as API.PresenceMessageStatic['fromEncodedArray']; 19 | 20 | export const decodeEncryptedPresenceMessages = ((obj, options) => { 21 | return fromEncodedArray(Logger.defaultLogger, Crypto, obj, options); 22 | }) as API.PresenceMessageStatic['fromEncodedArray']; 23 | 24 | export const constructPresenceMessage = fromValues as API.PresenceMessageStatic['fromValues']; 25 | -------------------------------------------------------------------------------- /src/platform/web/modular/realtimepresence.ts: -------------------------------------------------------------------------------- 1 | import { RealtimePresencePlugin } from 'common/lib/client/modularplugins'; 2 | import { default as realtimePresenceClass } from '../../../common/lib/client/realtimepresence'; 3 | import PresenceMessage, { WirePresenceMessage } from '../../../common/lib/types/presencemessage'; 4 | 5 | const RealtimePresence: RealtimePresencePlugin = { 6 | RealtimePresence: realtimePresenceClass, 7 | PresenceMessage, 8 | WirePresenceMessage, 9 | }; 10 | 11 | export { RealtimePresence }; 12 | -------------------------------------------------------------------------------- /src/platform/web/modular/transports.ts: -------------------------------------------------------------------------------- 1 | export { default as XHRPolling } from '../lib/transport/xhrpollingtransport'; 2 | export { default as WebSocketTransport } from '../../../common/lib/transport/websockettransport'; 3 | -------------------------------------------------------------------------------- /src/plugins/index.d.ts: -------------------------------------------------------------------------------- 1 | import Objects from './objects'; 2 | import Push from './push'; 3 | 4 | export interface StandardPlugins { 5 | Objects?: typeof Objects; 6 | Push?: typeof Push; 7 | } 8 | -------------------------------------------------------------------------------- /src/plugins/objects/batchcontextlivecounter.ts: -------------------------------------------------------------------------------- 1 | import type BaseClient from 'common/lib/client/baseclient'; 2 | import { BatchContext } from './batchcontext'; 3 | import { LiveCounter } from './livecounter'; 4 | import { Objects } from './objects'; 5 | 6 | export class BatchContextLiveCounter { 7 | private _client: BaseClient; 8 | 9 | constructor( 10 | private _batchContext: BatchContext, 11 | private _objects: Objects, 12 | private _counter: LiveCounter, 13 | ) { 14 | this._client = this._objects.getClient(); 15 | } 16 | 17 | value(): number { 18 | this._objects.throwIfInvalidAccessApiConfiguration(); 19 | this._batchContext.throwIfClosed(); 20 | return this._counter.value(); 21 | } 22 | 23 | increment(amount: number): void { 24 | this._objects.throwIfInvalidWriteApiConfiguration(); 25 | this._batchContext.throwIfClosed(); 26 | const msg = LiveCounter.createCounterIncMessage(this._objects, this._counter.getObjectId(), amount); 27 | this._batchContext.queueMessage(msg); 28 | } 29 | 30 | decrement(amount: number): void { 31 | this._objects.throwIfInvalidWriteApiConfiguration(); 32 | this._batchContext.throwIfClosed(); 33 | // do an explicit type safety check here before negating the amount value, 34 | // so we don't unintentionally change the type sent by a user 35 | if (typeof amount !== 'number') { 36 | throw new this._client.ErrorInfo('Counter value decrement should be a number', 40003, 400); 37 | } 38 | 39 | this.increment(-amount); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/objects/batchcontextlivemap.ts: -------------------------------------------------------------------------------- 1 | import type * as API from '../../../ably'; 2 | import { BatchContext } from './batchcontext'; 3 | import { LiveMap } from './livemap'; 4 | import { LiveObject } from './liveobject'; 5 | import { Objects } from './objects'; 6 | 7 | export class BatchContextLiveMap { 8 | constructor( 9 | private _batchContext: BatchContext, 10 | private _objects: Objects, 11 | private _map: LiveMap, 12 | ) {} 13 | 14 | get(key: TKey): T[TKey] | undefined { 15 | this._objects.throwIfInvalidAccessApiConfiguration(); 16 | this._batchContext.throwIfClosed(); 17 | const value = this._map.get(key); 18 | if (value instanceof LiveObject) { 19 | return this._batchContext.getWrappedObject(value.getObjectId()) as T[TKey]; 20 | } else { 21 | return value; 22 | } 23 | } 24 | 25 | size(): number { 26 | this._objects.throwIfInvalidAccessApiConfiguration(); 27 | this._batchContext.throwIfClosed(); 28 | return this._map.size(); 29 | } 30 | 31 | *entries(): IterableIterator<[TKey, T[TKey]]> { 32 | this._objects.throwIfInvalidAccessApiConfiguration(); 33 | this._batchContext.throwIfClosed(); 34 | yield* this._map.entries(); 35 | } 36 | 37 | *keys(): IterableIterator { 38 | this._objects.throwIfInvalidAccessApiConfiguration(); 39 | this._batchContext.throwIfClosed(); 40 | yield* this._map.keys(); 41 | } 42 | 43 | *values(): IterableIterator { 44 | this._objects.throwIfInvalidAccessApiConfiguration(); 45 | this._batchContext.throwIfClosed(); 46 | yield* this._map.values(); 47 | } 48 | 49 | set(key: TKey, value: T[TKey]): void { 50 | this._objects.throwIfInvalidWriteApiConfiguration(); 51 | this._batchContext.throwIfClosed(); 52 | const msg = LiveMap.createMapSetMessage(this._objects, this._map.getObjectId(), key, value); 53 | this._batchContext.queueMessage(msg); 54 | } 55 | 56 | remove(key: TKey): void { 57 | this._objects.throwIfInvalidWriteApiConfiguration(); 58 | this._batchContext.throwIfClosed(); 59 | const msg = LiveMap.createMapRemoveMessage(this._objects, this._map.getObjectId(), key); 60 | this._batchContext.queueMessage(msg); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/plugins/objects/defaults.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULTS = { 2 | gcInterval: 1000 * 60 * 5, // 5 minutes 3 | /** 4 | * The SDK will attempt to use the `objectsGCGracePeriod` value provided by the server in the `connectionDetails` object of the `CONNECTED` event. 5 | * If the server does not provide this value, the SDK will fall back to this default value. 6 | * 7 | * Must be > 2 minutes to ensure we keep tombstones long enough to avoid the possibility of receiving an operation 8 | * with an earlier serial that would not have been applied if the tombstone still existed. 9 | * 10 | * Applies both for map entries tombstones and object tombstones. 11 | */ 12 | gcGracePeriod: 1000 * 60 * 60 * 24, // 24 hours 13 | }; 14 | -------------------------------------------------------------------------------- /src/plugins/objects/index.ts: -------------------------------------------------------------------------------- 1 | import { ObjectMessage, WireObjectMessage } from './objectmessage'; 2 | import { Objects } from './objects'; 3 | 4 | export { Objects, ObjectMessage, WireObjectMessage }; 5 | 6 | export default { 7 | Objects, 8 | ObjectMessage, 9 | WireObjectMessage, 10 | }; 11 | -------------------------------------------------------------------------------- /src/plugins/objects/objectid.ts: -------------------------------------------------------------------------------- 1 | import type BaseClient from 'common/lib/client/baseclient'; 2 | import type Platform from 'common/platform'; 3 | 4 | export type LiveObjectType = 'map' | 'counter'; 5 | 6 | /** 7 | * Represents a parsed object id. 8 | * 9 | * @internal 10 | */ 11 | export class ObjectId { 12 | private constructor( 13 | readonly type: LiveObjectType, 14 | readonly hash: string, 15 | readonly msTimestamp: number, 16 | ) {} 17 | 18 | static fromInitialValue( 19 | platform: typeof Platform, 20 | objectType: LiveObjectType, 21 | initialValue: string, 22 | nonce: string, 23 | msTimestamp: number, 24 | ): ObjectId { 25 | const valueForHashBuffer = platform.BufferUtils.concat([ 26 | platform.BufferUtils.utf8Encode(initialValue), 27 | platform.BufferUtils.utf8Encode(':'), 28 | platform.BufferUtils.utf8Encode(nonce), 29 | ]); 30 | const hashBuffer = platform.BufferUtils.sha256(valueForHashBuffer); 31 | const hash = platform.BufferUtils.base64UrlEncode(hashBuffer); 32 | 33 | return new ObjectId(objectType, hash, msTimestamp); 34 | } 35 | 36 | /** 37 | * Create ObjectId instance from hashed object id string. 38 | */ 39 | static fromString(client: BaseClient, objectId: string | null | undefined): ObjectId { 40 | if (client.Utils.isNil(objectId)) { 41 | throw new client.ErrorInfo('Invalid object id string', 92000, 500); 42 | } 43 | 44 | // RTO6b1 45 | const [type, rest] = objectId.split(':'); 46 | if (!type || !rest) { 47 | throw new client.ErrorInfo('Invalid object id string', 92000, 500); 48 | } 49 | 50 | if (!['map', 'counter'].includes(type)) { 51 | throw new client.ErrorInfo(`Invalid object type in object id: ${objectId}`, 92000, 500); 52 | } 53 | 54 | const [hash, msTimestamp] = rest.split('@'); 55 | if (!hash || !msTimestamp) { 56 | throw new client.ErrorInfo('Invalid object id string', 92000, 500); 57 | } 58 | 59 | if (!Number.isInteger(Number.parseInt(msTimestamp))) { 60 | throw new client.ErrorInfo('Invalid object id string', 92000, 500); 61 | } 62 | 63 | return new ObjectId(type as LiveObjectType, hash, Number.parseInt(msTimestamp)); 64 | } 65 | 66 | toString(): string { 67 | return `${this.type}:${this.hash}@${this.msTimestamp}`; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/plugins/push/index.ts: -------------------------------------------------------------------------------- 1 | import PushChannel from './pushchannel'; 2 | import { getW3CPushDeviceDetails } from './getW3CDeviceDetails'; 3 | import { ActivationStateMachine, CalledActivate, CalledDeactivate, localDeviceFactory } from './pushactivation'; 4 | 5 | export { 6 | ActivationStateMachine, 7 | localDeviceFactory, 8 | CalledActivate, 9 | CalledDeactivate, 10 | PushChannel, 11 | getW3CPushDeviceDetails, 12 | }; 13 | 14 | export default { 15 | ActivationStateMachine, 16 | localDeviceFactory, 17 | CalledActivate, 18 | CalledDeactivate, 19 | PushChannel, 20 | getW3CPushDeviceDetails, 21 | }; 22 | -------------------------------------------------------------------------------- /test/browser/http.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['ably', 'shared_helper', 'chai'], function (Ably, Helper, chai) { 4 | var rest; 5 | var expect = chai.expect; 6 | 7 | describe('rest/http/fetch', function () { 8 | this.timeout(60 * 1000); 9 | let initialXhrSupported; 10 | before(function (done) { 11 | const helper = Helper.forHook(this); 12 | initialXhrSupported = Ably.Rest.Platform.Config.xhrSupported; 13 | Ably.Rest.Platform.Config.xhrSupported = false; 14 | helper.setupApp(function () { 15 | rest = helper.AblyRest(); 16 | done(); 17 | }); 18 | }); 19 | 20 | after((done) => { 21 | Ably.Rest.Platform.Config.xhrSupported = initialXhrSupported; 22 | done(); 23 | }); 24 | 25 | /** @nospec */ 26 | it('Should use fetch when XHR is not supported', function (done) { 27 | let oldFetch = window.fetch; 28 | window.fetch = () => { 29 | done(); 30 | window.fetch = oldFetch; 31 | }; 32 | const channel = rest.channels.get('http_test_channel'); 33 | channel.publish('test', 'Testing fetch support'); 34 | }); 35 | 36 | /** @nospec */ 37 | it('Should succeed in using fetch to publish a message', function (done) { 38 | const channel = rest.channels.get('http_test_channel'); 39 | Helper.whenPromiseSettles(channel.publish('test', 'Testing fetch support'), (err) => { 40 | expect(err).to.not.exist; 41 | done(); 42 | }); 43 | }); 44 | 45 | /** 46 | * RTL6b talks about a callback which should receive an error object (which what we're doing here), but suggests to test other things. 47 | * This test simply tests that with fetch API we're still receiving an error, so it's probably @nospec. 48 | * 49 | * @nospec 50 | */ 51 | it('Should pass errors correctly', function (done) { 52 | const channel = rest.channels.get(''); 53 | Helper.whenPromiseSettles(channel.publish('test', 'Invalid message'), (err) => { 54 | expect(err).to.exist; 55 | done(); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/common/globals/environment.js: -------------------------------------------------------------------------------- 1 | /* Assumes process.env defined, or window.__env__ or populated via globals.env.js and karam-env-preprocessor plugin */ 2 | 3 | define(function (require) { 4 | var defaultLogLevel = 4, 5 | environment = isBrowser ? window.__env__ || {} : process.env, 6 | ablyEndpoint = environment.ABLY_ENDPOINT || 'nonprod:sandbox', 7 | port = environment.ABLY_PORT || 80, 8 | tlsPort = environment.ABLY_TLS_PORT || 443, 9 | tls = 'ABLY_USE_TLS' in environment ? environment.ABLY_USE_TLS.toLowerCase() !== 'false' : true, 10 | logLevel = environment.ABLY_LOG_LEVEL || defaultLogLevel; 11 | 12 | let logLevelSet = environment.ABLY_LOG_LEVEL !== undefined; 13 | 14 | if (isBrowser) { 15 | var url = window.location.href, 16 | keysValues = url.split(/[\?&]+/), 17 | query = {}; 18 | 19 | for (i = 0; i < keysValues.length; i++) { 20 | var keyValue = keysValues[i].split('='); 21 | query[keyValue[0]] = keyValue[1]; 22 | } 23 | 24 | if (query['endpoint']) ablyEndpoint = query['endpoint']; 25 | if (query['port']) port = query['port']; 26 | if (query['tls_port']) tlsPort = query['tls_port']; 27 | if (query['tls']) tls = query['tls'].toLowerCase() !== 'false'; 28 | if (query['log_level']) { 29 | logLevel = Number(query['log_level']); 30 | logLevelSet = true; 31 | } 32 | } else if (process) { 33 | process.on('uncaughtException', function (err) { 34 | console.error(err.stack); 35 | }); 36 | } 37 | 38 | function getLogTimestamp() { 39 | const time = new Date(); 40 | return ( 41 | time.getHours().toString().padStart(2, '0') + 42 | ':' + 43 | time.getMinutes().toString().padStart(2, '0') + 44 | ':' + 45 | time.getSeconds().toString().padStart(2, '0') + 46 | '.' + 47 | time.getMilliseconds().toString().padStart(3, '0') 48 | ); 49 | } 50 | 51 | let clientLogs = []; 52 | 53 | function getLogs() { 54 | return clientLogs; 55 | } 56 | 57 | function flushLogs() { 58 | clientLogs = []; 59 | } 60 | 61 | return (module.exports = { 62 | endpoint: ablyEndpoint, 63 | port: port, 64 | tlsPort: tlsPort, 65 | tls: tls, 66 | logLevel: logLevel, 67 | getLogs, 68 | flushLogs, 69 | 70 | logHandler: function (msg) { 71 | if (logLevelSet) { 72 | console.log(getLogTimestamp(), msg); 73 | } else { 74 | clientLogs.push([getLogTimestamp(), msg]); 75 | } 76 | }, 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/common/globals/named_dependencies.js: -------------------------------------------------------------------------------- 1 | /* These modules are paths to common modules loaded by requireJs in the browser or Node */ 2 | define(function () { 3 | return (module.exports = { 4 | // Ably modules 5 | ably: { browser: 'build/ably', node: 'build/ably-node' }, 6 | 'vcdiff-decoder': { 7 | browser: 'node_modules/@ably/vcdiff-decoder/dist/vcdiff-decoder', 8 | node: 'node_modules/@ably/vcdiff-decoder', 9 | }, 10 | push: { 11 | browser: 'build/push', 12 | node: 'build/push', 13 | }, 14 | objects: { 15 | browser: 'build/objects', 16 | node: 'build/objects', 17 | }, 18 | 19 | // test modules 20 | globals: { browser: 'test/common/globals/environment', node: 'test/common/globals/environment' }, 21 | shared_helper: { browser: 'test/common/modules/shared_helper', node: 'test/common/modules/shared_helper' }, 22 | async: { browser: 'node_modules/async/lib/async' }, 23 | chai: { browser: 'node_modules/chai/chai', node: 'node_modules/chai/chai' }, 24 | ulid: { browser: 'node_modules/ulid/dist/index.umd', node: 'node_modules/ulid/dist/index.umd' }, 25 | dequal: { browser: 'node_modules/dequal/dist/index.min', node: 'node_modules/dequal/dist/index' }, 26 | private_api_recorder: { 27 | browser: 'test/common/modules/private_api_recorder', 28 | node: 'test/common/modules/private_api_recorder', 29 | }, 30 | objects_helper: { 31 | browser: 'test/common/modules/objects_helper', 32 | node: 'test/common/modules/objects_helper', 33 | }, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/common/modules/client_module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Shared test helper used for creating Rest and Real-time clients */ 4 | 5 | define(['ably', 'globals', 'test/common/modules/testapp_module'], function (Ably, ablyGlobals, testAppHelper) { 6 | var utils = Ably.Realtime.Utils; 7 | 8 | function ablyClientOptions(helper, options) { 9 | helper = helper.addingHelperFunction('ablyClientOptions'); 10 | helper.recordPrivateApi('call.Utils.copy'); 11 | var clientOptions = utils.copy(ablyGlobals); 12 | helper.recordPrivateApi('call.Utils.mixin'); 13 | utils.mixin(clientOptions, options); 14 | var authMethods = ['authUrl', 'authCallback', 'token', 'tokenDetails', 'key']; 15 | 16 | /* Use a default api key if no auth methods provided */ 17 | if ( 18 | authMethods.every(function (method) { 19 | return !(method in clientOptions); 20 | }) 21 | ) { 22 | clientOptions.key = testAppHelper.getTestApp().keys[0].keyStr; 23 | } 24 | 25 | return clientOptions; 26 | } 27 | 28 | function ablyRest(helper, options) { 29 | helper = helper.addingHelperFunction('ablyRest'); 30 | return new Ably.Rest(ablyClientOptions(helper, options)); 31 | } 32 | 33 | function ablyRealtime(helper, options) { 34 | helper = helper.addingHelperFunction('ablyRealtime'); 35 | return new Ably.Realtime(ablyClientOptions(helper, options)); 36 | } 37 | 38 | function ablyRealtimeWithoutEndpoint(helper, options) { 39 | helper = helper.addingHelperFunction('ablyRealtime'); 40 | const clientOptions = ablyClientOptions(helper, options); 41 | delete clientOptions.endpoint; 42 | return new Ably.Realtime(clientOptions); 43 | } 44 | 45 | return (module.exports = { 46 | Ably: Ably, 47 | AblyRest: ablyRest, 48 | AblyRealtime: ablyRealtime, 49 | AblyRealtimeWithoutEndpoint: ablyRealtimeWithoutEndpoint, 50 | ablyClientOptions, 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/mocha.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/package/browser/template/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules/ 3 | /test-results/ 4 | /playwright-report/ 5 | /blob-report/ 6 | /playwright/.cache/ 7 | -------------------------------------------------------------------------------- /test/package/browser/template/README.md: -------------------------------------------------------------------------------- 1 | # ably-js NPM package test (for browser) 2 | 3 | This directory is intended to be used for testing the following aspects of the ably-js NPM package when used in a browser-based app: 4 | 5 | - that its exports are correctly configured and provide access to ably-js’s functionality 6 | - that its TypeScript typings are correctly configured and can be successfully used from a TypeScript-based app that imports the package 7 | 8 | It contains three files, each of which import ably-js in different manners, and provide a way to briefly exercise its functionality: 9 | 10 | - `src/index-default.ts` imports the default ably-js package (`import { Realtime } from 'ably'`). 11 | - `src/index-objects.ts` imports the Objects ably-js plugin (`import Objects from 'ably/objects'`). 12 | - `src/index-modular.ts` imports the tree-shakable ably-js package (`import { BaseRealtime, WebSocketTransport, FetchRequest } from 'ably/modular'`). 13 | - `src/ReactApp.tsx` imports React hooks from the ably-js package (`import { useChannel } from 'ably/react'`). 14 | 15 | ## Why is `ably` not in `package.json`? 16 | 17 | The `ably` dependency gets added when we run the repository’s `test:package` package script. That script copies the contents of this `template` directory to a new temporary directory, and then adds the `ably` dependency to the copy. We do this so that we can check this directory’s `package-lock.json` into Git, without needing to modify it whenever ably-js’s dependencies change. 18 | 19 | ## React hooks tests 20 | 21 | To test hooks imported from `ably/react` in React components, we used [Playwright for components](https://playwright.dev/docs/test-components). The main logic sits in `src/ReactApp.tsx`, and `AblyProvider` is configured in `playwright/index.tsx` file based on [this guide](https://playwright.dev/docs/test-components#hooks). 22 | 23 | ## Package scripts 24 | 25 | This directory exposes three package scripts that are to be used for testing: 26 | 27 | - `build`: Uses esbuild to create: 28 | 1. a bundle containing `src/index-default.ts` and ably-js; 29 | 2. a bundle containing `src/index-objects.ts` and ably-js. 30 | 3. a bundle containing `src/index-modular.ts` and ably-js. 31 | - `test`: Using the bundles created by `build` and playwright components setup, tests that the code that exercises ably-js’s functionality is working correctly in a browser. 32 | - `typecheck`: Type-checks the code that imports ably-js. 33 | -------------------------------------------------------------------------------- /test/package/browser/template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "esbuild --bundle src/index-default.ts --outdir=dist && esbuild --bundle src/index-objects.ts --outdir=dist && esbuild --bundle src/index-modular.ts --outdir=dist", 8 | "typecheck": "tsc --project src -noEmit", 9 | "test-support:server": "ts-node server/server.ts", 10 | "test": "npm run test:lib && npm run test:hooks", 11 | "test:lib": "playwright test -c playwright-lib.config.js", 12 | "test:hooks": "playwright test -c playwright-hooks.config.ts", 13 | "test:install-deps": "playwright install chromium" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@playwright/experimental-ct-react": "^1.39.0", 19 | "@playwright/test": "^1.39.0", 20 | "@tsconfig/node16": "^16.1.1", 21 | "@types/express": "^4.17.20", 22 | "@types/node": "^20.11.19", 23 | "esbuild": "^0.18.20", 24 | "express": "^4.18.2", 25 | "ts-node": "^10.9.1", 26 | "typescript": "^5.2.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/package/browser/template/playwright-hooks.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/experimental-ct-react'; 2 | 3 | /** 4 | * Playwright config for running ably-js react hooks NPM package tests. 5 | * Based on https://playwright.dev/docs/test-components. 6 | * 7 | * See config options: https://playwright.dev/docs/test-configuration. 8 | */ 9 | export default defineConfig({ 10 | testDir: './test/hooks', 11 | /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ 12 | snapshotDir: './__snapshots__', 13 | /* Maximum time one test can run for. */ 14 | timeout: 10 * 1000, 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 28 | trace: 'on-first-retry', 29 | 30 | /* Port to use for Playwright component endpoint. */ 31 | ctPort: 3100, 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /test/package/browser/template/playwright-lib.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@playwright/test'; 2 | 3 | /** 4 | * Playwright config for running ably-js lib NPM package tests. 5 | * 6 | * See config options: https://playwright.dev/docs/test-configuration. 7 | */ 8 | export default defineConfig({ 9 | testDir: './test/lib', 10 | webServer: { 11 | command: 'npm run test-support:server', 12 | url: 'http://localhost:4567', 13 | }, 14 | use: { 15 | baseURL: 'http://localhost:4567', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /test/package/browser/template/playwright/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ably NPM package test (react export) 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/package/browser/template/playwright/index.tsx: -------------------------------------------------------------------------------- 1 | import { beforeMount } from '@playwright/experimental-ct-react/hooks'; 2 | import * as Ably from 'ably'; 3 | import { AblyProvider, ChannelProvider } from 'ably/react'; 4 | 5 | import { createSandboxAblyAPIKey } from '../src/sandbox'; 6 | 7 | beforeMount(async ({ App }) => { 8 | const key = await createSandboxAblyAPIKey(); 9 | 10 | const client = new Ably.Realtime({ 11 | key, 12 | endpoint: 'nonprod:sandbox', 13 | }); 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /test/package/browser/template/server/resources/index-default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ably NPM package test (default export) 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/package/browser/template/server/resources/index-modular.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ably NPM package test (tree-shakable export) 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/package/browser/template/server/resources/index-objects.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ably NPM package test (Objects plugin export) 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/package/browser/template/server/resources/runTest.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | try { 3 | await testAblyPackage(); 4 | onResult(null); 5 | } catch (error) { 6 | console.log('Caught error', error); 7 | onResult(error); 8 | } 9 | })(); 10 | -------------------------------------------------------------------------------- /test/package/browser/template/server/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'node:path'; 3 | 4 | async function startWebServer(listenPort: number) { 5 | const server = express(); 6 | server.get('/', (req, res) => res.send('OK')); 7 | server.use(express.static(path.join(__dirname, '/resources'))); 8 | for (const filename of ['index-default.js', 'index-objects.js', 'index-modular.js']) { 9 | server.use(`/${filename}`, express.static(path.join(__dirname, '..', 'dist', filename))); 10 | } 11 | 12 | server.listen(listenPort); 13 | } 14 | 15 | startWebServer(4567); 16 | -------------------------------------------------------------------------------- /test/package/browser/template/src/ReactApp.tsx: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import { useChannel } from 'ably/react'; 3 | import { useEffect, useState } from 'react'; 4 | 5 | export function App() { 6 | // check that we can refer to the types exported by Ably. 7 | const [messages, updateMessages] = useState([]); 8 | 9 | // check that we can use ably/react exported members 10 | const { channel, ably } = useChannel({ channelName: 'channel' }, (message) => { 11 | updateMessages((prev) => [...prev, message]); 12 | }); 13 | 14 | const messagePreviews = messages.map((message, idx) => ); 15 | 16 | useEffect(() => { 17 | async function publishMessages() { 18 | try { 19 | // Check that we can use the TypeScript overload that accepts name and data as separate arguments 20 | await channel.publish('message', { foo: 'bar' }); 21 | 22 | // Check that we can use the TypeScript overload that accepts a Message object 23 | await channel.publish({ name: 'message', data: { foo: 'baz' } }); 24 | (window as any).onResult(); 25 | } catch (error) { 26 | (window as any).onResult(error); 27 | } 28 | } 29 | 30 | publishMessages(); 31 | }, [channel]); 32 | 33 | return ( 34 |
35 |
Ably NPM package test (react export)
36 |
37 |

Messages

38 |
    {messagePreviews}
39 |
40 |
41 | ); 42 | } 43 | 44 | function MessagePreview({ message }: { message: Ably.Message }) { 45 | return ( 46 |
  • 47 | {message.name}: {message.data.foo} 48 |
  • 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /test/package/browser/template/src/index-default.ts: -------------------------------------------------------------------------------- 1 | import * as Ably from 'ably'; 2 | import { createSandboxAblyAPIKey } from './sandbox'; 3 | 4 | // Fix for "type 'typeof globalThis' has no index signature" error: 5 | // https://stackoverflow.com/questions/68481686/type-typeof-globalthis-has-no-index-signature 6 | declare module globalThis { 7 | var testAblyPackage: () => Promise; 8 | } 9 | 10 | // This function exists to check that we can refer to the types exported by Ably. 11 | async function attachChannel(channel: Ably.RealtimeChannel) { 12 | await channel.attach(); 13 | } 14 | 15 | globalThis.testAblyPackage = async function () { 16 | const key = await createSandboxAblyAPIKey(); 17 | 18 | const realtime = new Ably.Realtime({ key, endpoint: 'nonprod:sandbox' }); 19 | 20 | const channel = realtime.channels.get('channel'); 21 | await attachChannel(channel); 22 | 23 | const receivedMessagePromise = new Promise((resolve) => { 24 | channel.subscribe(resolve); 25 | }); 26 | 27 | // Check that we can use the TypeScript overload that accepts name and data as separate arguments 28 | await channel.publish('message', { foo: 'bar' }); 29 | const receivedMessage = await receivedMessagePromise; 30 | 31 | // Check that id and timestamp of a message received from Ably can be assigned to non-optional types 32 | const { id: string, timestamp: number } = receivedMessage; 33 | 34 | channel.unsubscribe(); 35 | 36 | // Check that we can use the TypeScript overload that accepts a Message object 37 | await channel.publish({ name: 'message', data: { foo: 'bar' } }); 38 | }; 39 | -------------------------------------------------------------------------------- /test/package/browser/template/src/index-modular.ts: -------------------------------------------------------------------------------- 1 | import { BaseRealtime, WebSocketTransport, FetchRequest, generateRandomKey } from 'ably/modular'; 2 | import { InboundMessage, RealtimeChannel } from 'ably'; 3 | import { createSandboxAblyAPIKey } from './sandbox'; 4 | 5 | // Fix for "type 'typeof globalThis' has no index signature" error: 6 | // https://stackoverflow.com/questions/68481686/type-typeof-globalthis-has-no-index-signature 7 | declare module globalThis { 8 | var testAblyPackage: () => Promise; 9 | } 10 | 11 | // This function exists to check that we can refer to the types exported by Ably. 12 | async function attachChannel(channel: RealtimeChannel) { 13 | await channel.attach(); 14 | } 15 | 16 | // This function exists to check that one of the free-standing functions (arbitrarily chosen) can be imported and does something vaguely sensible. 17 | async function checkStandaloneFunction() { 18 | const generatedKey = await generateRandomKey(); 19 | if (!(generatedKey instanceof ArrayBuffer)) { 20 | throw new Error('Expected to get an ArrayBuffer from generateRandomKey'); 21 | } 22 | } 23 | 24 | globalThis.testAblyPackage = async function () { 25 | const key = await createSandboxAblyAPIKey(); 26 | 27 | const realtime = new BaseRealtime({ 28 | key, 29 | endpoint: 'nonprod:sandbox', 30 | plugins: { WebSocketTransport, FetchRequest }, 31 | }); 32 | 33 | const channel = realtime.channels.get('channel'); 34 | await attachChannel(channel); 35 | 36 | const receivedMessagePromise = new Promise((resolve) => { 37 | channel.subscribe(resolve); 38 | }); 39 | 40 | // Check that we can use the TypeScript overload that accepts name and data as separate arguments 41 | await channel.publish('message', { foo: 'bar' }); 42 | const receivedMessage = await receivedMessagePromise; 43 | 44 | // Check that id and timestamp of a message received from Ably can be assigned to non-optional types 45 | const { id: string, timestamp: number } = receivedMessage; 46 | 47 | await checkStandaloneFunction(); 48 | 49 | channel.unsubscribe(); 50 | 51 | // Check that we can use the TypeScript overload that accepts a Message object 52 | await channel.publish({ name: 'message', data: { foo: 'bar' } }); 53 | }; 54 | -------------------------------------------------------------------------------- /test/package/browser/template/src/sandbox.ts: -------------------------------------------------------------------------------- 1 | import testAppSetup from '../../../../common/ably-common/test-resources/test-app-setup.json'; 2 | 3 | export async function createSandboxAblyAPIKey(withOptions?: object) { 4 | const postData = { 5 | ...testAppSetup.post_apps, 6 | ...(withOptions ?? {}), 7 | }; 8 | const response = await fetch('https://sandbox.realtime.ably-nonprod.net/apps', { 9 | method: 'POST', 10 | headers: { 'Content-Type': 'application/json' }, 11 | body: JSON.stringify(postData), 12 | }); 13 | 14 | if (!response.ok) { 15 | throw new Error(`Response not OK (${response.status})`); 16 | } 17 | 18 | const testApp = await response.json(); 19 | return testApp.keys[0].keyStr; 20 | } 21 | -------------------------------------------------------------------------------- /test/package/browser/template/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "resolveJsonModule": true, 6 | "esModuleInterop": true, 7 | "module": "esnext", 8 | "moduleResolution": "bundler", 9 | "jsx": "react-jsx" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/package/browser/template/test/hooks/ReactApp.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/experimental-ct-react'; 2 | 3 | import { App } from '../../src/ReactApp'; 4 | 5 | test.describe('NPM package', () => { 6 | for (const scenario of [{ name: 'react export' }]) { 7 | test.describe(scenario.name, () => { 8 | /** @nospec */ 9 | test('can be imported and provides access to Ably functionality', async ({ mount, page }) => { 10 | page.on('console', (message) => { 11 | if (['error', 'warning'].includes(message.type())) { 12 | console.log(`Console ${message.type()}:`, message); 13 | } 14 | }); 15 | 16 | page.on('pageerror', (err) => { 17 | console.log('Uncaught exception:', err); 18 | }); 19 | 20 | const pageResultPromise = new Promise((resolve, reject) => { 21 | page.exposeFunction('onResult', (error: Error | null) => { 22 | if (error) { 23 | reject(error); 24 | } else { 25 | resolve(); 26 | } 27 | }); 28 | }); 29 | 30 | const component = await mount(); 31 | await expect(pageResultPromise).resolves.not.toThrow(); 32 | await expect(component).toContainText('Ably NPM package test (react export)'); 33 | }); 34 | }); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /test/package/browser/template/test/lib/package.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('NPM package', () => { 4 | for (const scenario of [ 5 | { name: 'default export', path: '/index-default.html' }, 6 | { name: 'Objects plugin export', path: '/index-objects.html' }, 7 | { name: 'modular export', path: '/index-modular.html' }, 8 | ]) { 9 | test.describe(scenario.name, () => { 10 | /** @nospec */ 11 | test('can be imported and provides access to Ably functionality', async ({ page }) => { 12 | const pageResultPromise = new Promise((resolve, reject) => { 13 | page.exposeFunction('onResult', (error: Error | null) => { 14 | if (error) { 15 | reject(error); 16 | } else { 17 | resolve(); 18 | } 19 | }); 20 | }); 21 | 22 | await page.goto(scenario.path); 23 | await pageResultPromise; 24 | }); 25 | }); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /test/package/browser/template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react-jsx" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/playwright.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mocha Tests 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/realtime/api.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['ably', 'chai'], function (Ably, chai) { 4 | var expect = chai.expect; 5 | 6 | describe('realtime/api', function () { 7 | /** 8 | * Spec does not have an explicit item that states that Realtime should be a class, it is shown in types instead. 9 | * 10 | * @nospec 11 | */ 12 | it('Client constructors', function () { 13 | expect(typeof Ably.Realtime).to.equal('function'); 14 | }); 15 | 16 | /** 17 | * It actually tests RSC1b, but in the context of Realtime class 18 | * 19 | * @specpartial RTC12 20 | */ 21 | it('constructor without any arguments', function () { 22 | expect(() => new Ably.Realtime()).to.throw( 23 | 'must be initialized with either a client options object, an Ably API key, or an Ably Token', 24 | ); 25 | }); 26 | 27 | /** 28 | * @spec REC1b1 29 | * @spec REC1c1 30 | */ 31 | it('constructor with conflict client options', function () { 32 | expect( 33 | () => 34 | new Ably.Realtime({ 35 | endpoint: 'nonprod:sandbox', 36 | environment: 'sandbox', 37 | }), 38 | ) 39 | .to.throw() 40 | .with.property('code', 40106); 41 | 42 | expect( 43 | () => 44 | new Ably.Realtime({ 45 | environment: 'nonprod:sandbox', 46 | restHost: 'localhost', 47 | }), 48 | ) 49 | .to.throw() 50 | .with.property('code', 40106); 51 | 52 | expect( 53 | () => 54 | new Ably.Realtime({ 55 | endpoint: 'nonprod:sandbox', 56 | restHost: 'localhost', 57 | }), 58 | ) 59 | .to.throw() 60 | .with.property('code', 40106); 61 | }); 62 | 63 | /** 64 | * @spec RSE1 65 | * @spec RSE2 66 | */ 67 | it('Crypto', function () { 68 | expect(typeof Ably.Realtime.Crypto).to.equal('function'); 69 | expect(typeof Ably.Realtime.Crypto.getDefaultParams).to.equal('function'); 70 | expect(typeof Ably.Realtime.Crypto.generateRandomKey).to.equal('function'); 71 | }); 72 | 73 | /** @specpartial TM3 - tests only functions exist */ 74 | it('Message', function () { 75 | expect(typeof Ably.Realtime.Message).to.equal('function'); 76 | expect(typeof Ably.Realtime.Message.fromEncoded).to.equal('function'); 77 | expect(typeof Ably.Realtime.Message.fromEncodedArray).to.equal('function'); 78 | }); 79 | 80 | /** @specpartial TP4 - tests only functions exist */ 81 | it('PresenceMessage', function () { 82 | expect(typeof Ably.Realtime.PresenceMessage).to.equal('function'); 83 | expect(typeof Ably.Realtime.PresenceMessage.fromEncoded).to.equal('function'); 84 | expect(typeof Ably.Realtime.PresenceMessage.fromEncodedArray).to.equal('function'); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/realtime/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['shared_helper', 'chai'], function (Helper, chai) { 4 | var expect = chai.expect; 5 | 6 | describe('incremental backoff and jitter', function () { 7 | /** @spec RTB1 */ 8 | it('should calculate retry timeouts using incremental backoff and jitter', function () { 9 | const helper = this.test.helper; 10 | var retryAttempts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; 11 | var initialTimeout = 15; 12 | 13 | helper.recordPrivateApi('call.Utils.getRetryTime'); 14 | var retryTimeouts = retryAttempts.map((attempt) => helper.Utils.getRetryTime(initialTimeout, attempt)); 15 | expect(retryTimeouts.filter((timeout) => timeout >= 30).length).to.equal(0); 16 | 17 | function checkIsBetween(value, min, max) { 18 | expect(value).to.be.above(min); 19 | expect(value).to.be.below(max); 20 | } 21 | 22 | // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout 23 | // Lower bound = 0.8 * Upper bound 24 | checkIsBetween(retryTimeouts[0], 12, 15); 25 | checkIsBetween(retryTimeouts[1], 16, 20); 26 | checkIsBetween(retryTimeouts[2], 20, 25); 27 | 28 | for (var i = 3; i < retryTimeouts.length; i++) { 29 | checkIsBetween(retryTimeouts[i], 24, 30); 30 | } 31 | 32 | function calculateBounds(retryAttempt, initialTimeout) { 33 | var upperBound = Math.min((retryAttempt + 2) / 3, 2) * initialTimeout; 34 | var lowerBound = 0.8 * upperBound; 35 | return { lower: lowerBound, upper: upperBound }; 36 | } 37 | 38 | for (var i = 0; i < retryTimeouts.length; i++) { 39 | var bounds = calculateBounds(retryAttempts[i], initialTimeout); 40 | checkIsBetween(retryTimeouts[i], bounds.lower, bounds.upper); 41 | } 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/rest/api.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['ably', 'chai'], function (Ably, chai) { 4 | var expect = chai.expect; 5 | 6 | describe('rest/api', function () { 7 | /** 8 | * Spec does not have an explicit item that states that Rest should be a class, it is shown in types instead. 9 | * @nospec 10 | */ 11 | it('Client constructors', function () { 12 | expect(typeof Ably.Rest).to.equal('function'); 13 | }); 14 | 15 | /** @spec RSC1b */ 16 | it('constructor without any arguments', function () { 17 | expect(() => new Ably.Rest()).to.throw( 18 | 'must be initialized with either a client options object, an Ably API key, or an Ably Token', 19 | ); 20 | }); 21 | 22 | /** 23 | * @spec RSE1 24 | * @spec RSE2 25 | */ 26 | it('Crypto', function () { 27 | expect(typeof Ably.Rest.Crypto).to.equal('function'); 28 | expect(typeof Ably.Rest.Crypto.getDefaultParams).to.equal('function'); 29 | expect(typeof Ably.Rest.Crypto.generateRandomKey).to.equal('function'); 30 | }); 31 | 32 | /** @specpartial TM3 - tests only functions exist */ 33 | it('Message', function () { 34 | expect(typeof Ably.Rest.Message).to.equal('function'); 35 | expect(typeof Ably.Rest.Message.fromEncoded).to.equal('function'); 36 | expect(typeof Ably.Rest.Message.fromEncodedArray).to.equal('function'); 37 | }); 38 | 39 | /** @specpartial TP4 - tests only functions exist */ 40 | it('PresenceMessage', function () { 41 | expect(typeof Ably.Rest.PresenceMessage).to.equal('function'); 42 | expect(typeof Ably.Rest.PresenceMessage.fromEncoded).to.equal('function'); 43 | expect(typeof Ably.Rest.PresenceMessage.fromEncodedArray).to.equal('function'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/rest/bufferutils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['ably', 'chai'], function (Ably, chai) { 4 | var expect = chai.expect; 5 | var BufferUtils = Ably.Realtime.Platform.BufferUtils; 6 | var testString = 'test'; 7 | var testBase64 = 'dGVzdA=='; 8 | var testHex = '74657374'; 9 | 10 | describe('rest/bufferutils', function () { 11 | /** @nospec */ 12 | it('Basic encoding and decoding', function () { 13 | /* base64 */ 14 | expect(BufferUtils.base64Encode(BufferUtils.utf8Encode(testString))).to.equal(testBase64); 15 | expect(BufferUtils.utf8Decode(BufferUtils.base64Decode(testBase64))).to.equal(testString); 16 | 17 | /* hex */ 18 | expect(BufferUtils.hexEncode(BufferUtils.utf8Encode(testString))).to.equal(testHex); 19 | expect(BufferUtils.utf8Decode(BufferUtils.hexDecode(testHex))).to.equal(testString); 20 | 21 | /* compare */ 22 | expect( 23 | BufferUtils.areBuffersEqual(BufferUtils.utf8Encode(testString), BufferUtils.utf8Encode(testString)), 24 | ).to.equal(true); 25 | expect( 26 | BufferUtils.areBuffersEqual(BufferUtils.utf8Encode(testString), BufferUtils.utf8Encode('other')), 27 | ).to.not.equal(true); 28 | }); 29 | 30 | /** 31 | * In node it's idiomatic for most methods dealing with binary data to 32 | * return Buffers. In the browser it's more idiomatic to return ArrayBuffers. 33 | * 34 | * @nospec 35 | */ 36 | it('BufferUtils return correct types', function () { 37 | if (typeof Buffer !== 'undefined') { 38 | /* node */ 39 | expect(BufferUtils.utf8Encode(testString).constructor).to.equal(Buffer); 40 | expect(BufferUtils.hexDecode(testHex).constructor).to.equal(Buffer); 41 | expect(BufferUtils.base64Decode(testBase64).constructor).to.equal(Buffer); 42 | expect(BufferUtils.toBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(Buffer); 43 | expect(BufferUtils.toArrayBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(ArrayBuffer); 44 | } else { 45 | /* modern browsers */ 46 | expect(BufferUtils.utf8Encode(testString).constructor).to.equal(ArrayBuffer); 47 | expect(BufferUtils.hexDecode(testHex).constructor).to.equal(ArrayBuffer); 48 | expect(BufferUtils.base64Decode(testBase64).constructor).to.equal(ArrayBuffer); 49 | expect(BufferUtils.toBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(Uint8Array); 50 | expect(BufferUtils.toArrayBuffer(BufferUtils.utf8Encode(testString)).constructor).to.equal(ArrayBuffer); 51 | } 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/rest/status.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['shared_helper', 'chai'], function (Helper, chai) { 4 | var rest; 5 | var expect = chai.expect; 6 | 7 | describe('rest/status', function () { 8 | this.timeout(30 * 1000); 9 | 10 | before(function (done) { 11 | const helper = Helper.forHook(this); 12 | helper.setupApp(function (err) { 13 | if (err) { 14 | done(err); 15 | return; 16 | } 17 | rest = helper.AblyRest(); 18 | done(); 19 | }); 20 | }); 21 | 22 | /** 23 | * @spec RSL8 24 | * @spec RSL8a 25 | * @spec CHD2a 26 | * @spec CHD2b 27 | * @spec CHS2a 28 | * @spec CHS2b 29 | * @spec CHO2a 30 | * @spec CHM2a 31 | * @spec CHM2b 32 | * @spec CHM2c 33 | * @spec CHM2d 34 | * @spec CHM2e 35 | * @spec CHM2f 36 | */ 37 | Helper.testOnJsonMsgpack('status0', async function (options, _, helper) { 38 | const rest = helper.AblyRest(options); 39 | var channel = rest.channels.get('status0'); 40 | var channelDetails = await channel.status(); 41 | expect(channelDetails.channelId).to.equal('status0'); 42 | expect(channelDetails.status.isActive).to.be.a('boolean'); 43 | var metrics = channelDetails.status.occupancy.metrics; 44 | expect(metrics.connections).to.be.a('number'); 45 | expect(metrics.presenceConnections).to.be.a('number'); 46 | expect(metrics.presenceMembers).to.be.a('number'); 47 | expect(metrics.presenceSubscribers).to.be.a('number'); 48 | expect(metrics.publishers).to.be.a('number'); 49 | expect(metrics.subscribers).to.be.a('number'); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/rest/time.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['shared_helper', 'chai'], function (Helper, chai) { 4 | var rest; 5 | var expect = chai.expect; 6 | 7 | describe('rest/time', function () { 8 | before(function (done) { 9 | const helper = Helper.forHook(this); 10 | helper.setupApp(function (err) { 11 | if (err) { 12 | done(err); 13 | return; 14 | } 15 | rest = helper.AblyRest(); 16 | done(); 17 | }); 18 | }); 19 | 20 | /** @spec RSC16 */ 21 | it('time0', async function () { 22 | var serverTime = await rest.time(); 23 | var localFiveMinutesAgo = Date.now() - 5 * 60 * 1000; 24 | expect( 25 | serverTime > localFiveMinutesAgo, 26 | 'Verify returned time matches current local time with 5 minute leeway for badly synced local clocks', 27 | ).to.be.ok; 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /test/support/browser_setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var allTestFiles = [], 4 | TEST_HOOKS_REGEXP = /root_hooks\.js$/i, 5 | TEST_REGEXP = /\.test\.js$/i, 6 | // TEST_REGEXP = /simple\.test\.js$/i, 7 | TEAR_DOWN_REGEXP = /tear_down\.js$/i; 8 | 9 | var pathToModule = function (path) { 10 | return path.replace(/^\/base\//, '').replace(/\.js$/, ''); 11 | }; 12 | 13 | var forEachKey = function (object, fn) { 14 | for (var prop in object) { 15 | if (object.hasOwnProperty(prop)) { 16 | fn(prop); 17 | } 18 | } 19 | }; 20 | 21 | // Add the root mocha hooks 22 | forEachKey(window.__testFiles__.files, function (file) { 23 | if (TEST_HOOKS_REGEXP.test(file)) { 24 | // Normalize paths to RequireJS module names. 25 | allTestFiles.push(pathToModule(file)); 26 | } 27 | }); 28 | 29 | // Match all test files 30 | forEachKey(window.__testFiles__.files, function (file) { 31 | if (TEST_REGEXP.test(file)) { 32 | // Normalize paths to RequireJS module names. 33 | allTestFiles.push(pathToModule(file)); 34 | } 35 | }); 36 | 37 | // Add the final tear down 38 | forEachKey(window.__testFiles__.files, function (file) { 39 | if (TEAR_DOWN_REGEXP.test(file)) { 40 | // Normalize paths to RequireJS module names. 41 | allTestFiles.push(pathToModule(file)); 42 | } 43 | }); 44 | var baseUrl = ''; 45 | 46 | require([(baseUrl + '/test/common/globals/named_dependencies.js').replace('//', '/')], function (modules) { 47 | var requireJsPaths = {}; 48 | for (var key in modules) { 49 | if (modules.hasOwnProperty(key) && modules[key].browser) { 50 | requireJsPaths[key] = modules[key].browser; 51 | } 52 | } 53 | 54 | require.config({ 55 | // Karma serves files under /base, which is the basePath from your config file 56 | baseUrl: baseUrl, 57 | 58 | // Ensure changes to these modules are reflected in node_helper.js 59 | paths: requireJsPaths, 60 | 61 | // The following requireJS depdendencies are not requireJS compatible but instead pollute the global namespace 62 | // It is better therefore to grab the global object and provide that to requireJS dependency management 63 | shim: { 64 | ably: { 65 | exports: 'Ably', 66 | }, 67 | 'browser-base64': { 68 | exports: 'Base64', 69 | }, 70 | 'vcdiff-decoder': { 71 | exports: 'vcdiffDecoder', 72 | }, 73 | }, 74 | 75 | // dynamically load all test files 76 | deps: allTestFiles, 77 | 78 | callback: () => { 79 | // (For some reason things don’t work if you return a Promise from this callback, hence the nested async function) 80 | (async () => { 81 | // Let modular.test.js register its tests before we run the test suite 82 | await registerAblyModularTests(); 83 | 84 | // we have to kickoff mocha 85 | mocha.run(); 86 | })(); 87 | }, 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/support/environment.vars.js: -------------------------------------------------------------------------------- 1 | /* This file is processed by the karam-env-preprocessor plugin and injects variables into window.__env__ */ 2 | -------------------------------------------------------------------------------- /test/support/mocha_junit_reporter/index.js: -------------------------------------------------------------------------------- 1 | const MochaJUnitReporter = require('mocha-junit-reporter'); 2 | module.exports = MochaJUnitReporter; 3 | -------------------------------------------------------------------------------- /test/support/mocha_junit_reporter/shims/fs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // mocha-junit-reporter calls this to check whether a report file already 3 | // exists (so it can delete it if so), so just return false since we’re not 4 | // going to write the report to the filesystem anyway 5 | existsSync: () => false, 6 | }; 7 | -------------------------------------------------------------------------------- /test/support/mocha_reporter.js: -------------------------------------------------------------------------------- 1 | const Mocha = require('mocha'); 2 | const MochaJUnitReporter = require('mocha-junit-reporter'); 3 | const path = require('path'); 4 | const outputDirectoryPaths = require('./output_directory_paths'); 5 | 6 | /** 7 | * Logs test results to the console (by extending the default `Spec` reporter) and also emits a JUnit XML file. 8 | */ 9 | class Reporter extends Mocha.reporters.Spec { 10 | jUnitReporter; 11 | 12 | constructor(runner, options) { 13 | super(runner, options); 14 | const jUnitFileName = `node-${process.version.split('.')[0]}.junit`; 15 | const jUnitFilePath = path.join(outputDirectoryPaths.jUnit, jUnitFileName); 16 | this.jUnitReporter = new MochaJUnitReporter(runner, { reporterOptions: { mochaFile: jUnitFilePath } }); 17 | } 18 | } 19 | 20 | module.exports = Reporter; 21 | -------------------------------------------------------------------------------- /test/support/modules_helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Modules helper 5 | -------------- 6 | 7 | RequireJS is used within browser tests to manage dependency loading. 8 | Node.js CommonJS is used within Node.js Jasmine tests, however the RequireJS syntax is not 9 | supported so this library provides compatibility with RequiredJS 10 | 11 | Exports 12 | ------- 13 | 14 | For browsers, RequireJS convention is used returning the exported methods. 15 | For Node.js, public methods are exported using module.exports 16 | 17 | To export for both Node.js & Browser use the follwing at the end of your modules: 18 | return modules.exports = yourObject; 19 | */ 20 | 21 | var isBrowser = typeof window == 'object'; 22 | if (isBrowser) { 23 | window.module = {}; 24 | window.isBrowser = true; 25 | } else { 26 | global.isBrowser = false; 27 | 28 | // Simulate the dependency injection from RequireJS in Node.js 29 | global.define = function (requireModules, callback) { 30 | if (typeof requireModules === 'function') { 31 | // no dependencies were provided, just call the provided callback 32 | requireModules.apply(this, require); 33 | } else { 34 | var namedDependencies = require('../common/globals/named_dependencies'); 35 | 36 | var required = requireModules.map(function (module) { 37 | var modulePath = (namedDependencies[module] || {}).node; 38 | if (modulePath === 'skip') { 39 | return; 40 | } 41 | 42 | if (modulePath) { 43 | return require('../../' + modulePath); 44 | } else { 45 | /* define has used a relative path to the base such as spec/file */ 46 | if (module.indexOf('/') >= 0) { 47 | return require('../../' + module); 48 | } else { 49 | /* requiring a named Node.js module such as async */ 50 | return require(module); 51 | } 52 | } 53 | }); 54 | 55 | callback.apply(this, required); 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /test/support/output_directory_paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | jUnit: path.join(__dirname, '..', '..', 'junit'), 5 | privateApiUsage: path.join(__dirname, '..', '..', 'private-api-usage'), 6 | }; 7 | -------------------------------------------------------------------------------- /test/support/push_channel_transport.js: -------------------------------------------------------------------------------- 1 | define(['ably'], function (Ably) { 2 | var Defaults = Ably.Realtime.Platform.Defaults; 3 | var ErrorInfo = Ably.ErrorInfo; 4 | return (module.exports = { 5 | getPushDeviceDetails: function (machine) { 6 | var channel = machine.client.options.pushRecipientChannel; 7 | if (!channel) { 8 | machine.handleEvent( 9 | new machine.GettingPushDeviceDetailsFailed( 10 | new ErrorInfo('Missing ClientOptions.pushRecipientChannel', 40000, 400), 11 | ), 12 | ); 13 | return; 14 | } 15 | 16 | var ablyKey = machine.client.options.pushAblyKey || machine.client.options.key; 17 | if (!ablyKey) { 18 | machine.handleEvent( 19 | new machine.GettingPushDeviceDetailsFailed(new ErrorInfo('Missing options.pushAblyKey', 40000, 400)), 20 | ); 21 | return; 22 | } 23 | 24 | var ablyUrl = machine.client.baseUri(Defaults.getHosts(machine.client.options)[0]); 25 | 26 | var device = machine.client.device(); 27 | device.push.recipient = { 28 | transportType: 'ablyChannel', 29 | channel: channel, 30 | ablyKey: ablyKey, 31 | ablyUrl: ablyUrl, 32 | }; 33 | device.persist(); 34 | 35 | machine.handleEvent(new machine.GotPushDeviceDetails()); 36 | }, 37 | storage: (function () { 38 | var values = {}; 39 | return { 40 | set: function (name, value) { 41 | values[name] = value; 42 | }, 43 | get: function (name) { 44 | return values[name]; 45 | }, 46 | remove: function (name) { 47 | delete values[name]; 48 | }, 49 | clear: function () { 50 | values = {}; 51 | }, 52 | }; 53 | })(), 54 | platform: 'browser', 55 | formFactor: 'desktop', 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/support/push_sw.js: -------------------------------------------------------------------------------- 1 | let port; 2 | 3 | self.addEventListener('push', (event) => { 4 | const res = event.data.json(); 5 | port.postMessage({ payload: res }); 6 | }); 7 | 8 | self.addEventListener('message', (event) => { 9 | if (event.data.type === 'INIT_PORT') { 10 | port = event.ports[0]; 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /test/support/root_hooks.js: -------------------------------------------------------------------------------- 1 | define(['shared_helper'], function (Helper) { 2 | after(function (done) { 3 | const helper = Helper.forHook(this); 4 | this.timeout(10 * 1000); 5 | helper.tearDownApp(function (err) { 6 | if (err) { 7 | done(err); 8 | return; 9 | } 10 | done(); 11 | }); 12 | helper.dumpPrivateApiUsage(); 13 | }); 14 | 15 | afterEach(function () { 16 | this.currentTest.helper.closeActiveClients(); 17 | }); 18 | afterEach(function () { 19 | this.currentTest.helper.logTestResults(this); 20 | }); 21 | afterEach(function () { 22 | this.currentTest.helper.flushTestLogs(); 23 | }); 24 | beforeEach(function () { 25 | this.currentTest.helper = Helper.forTest(this); 26 | this.currentTest.helper.recordTestStart(); 27 | }); 28 | beforeEach(function () { 29 | this.currentTest.helper.clearTransportPreference(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/support/test_helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Provide a global inspect() method, useful for debugging object properties */ 4 | var isBrowser = typeof window == 'object'; 5 | if (isBrowser) { 6 | window.inspect = function (object) { 7 | return JSON.stringify(object); 8 | }; 9 | } else { 10 | var util = require('util'); 11 | global.inspect = util.inspect; 12 | } 13 | -------------------------------------------------------------------------------- /test/unit/presencemap.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | define(['chai', 'ably'], function (chai, Ably) { 4 | const { assert } = chai; 5 | const PresenceMap = Ably.Realtime._PresenceMap; 6 | 7 | class MockRealtimePresence {} 8 | 9 | describe('PresenceMap', () => { 10 | let presenceMap; 11 | 12 | // Helper function to create a presence message 13 | const createPresenceMessage = (clientId, connectionId, action, timestamp) => ({ 14 | clientId, 15 | connectionId, 16 | timestamp, 17 | action, 18 | }); 19 | 20 | beforeEach(() => { 21 | // Initialize with a simple memberKey function that uses clientId as the key 22 | presenceMap = new PresenceMap( 23 | new MockRealtimePresence(), 24 | (item) => item.clientId + ':' + item.connectionId, 25 | (i, j) => i.timestamp > j.timestamp, 26 | ); 27 | }); 28 | 29 | describe('remove()', () => { 30 | it('should return false when no matching member present', () => { 31 | const incoming = createPresenceMessage('client1', 'conn1', 'leave', 100); 32 | assert.isFalse(presenceMap.remove(incoming)); 33 | }); 34 | 35 | it('should return true when removing an (older) matching member', () => { 36 | const original = createPresenceMessage('client1', 'conn1', 'present', 100); 37 | presenceMap.put(original); 38 | const incoming = createPresenceMessage('client1', 'conn1', 'leave', 150); 39 | assert.isTrue(presenceMap.remove(incoming)); 40 | }); 41 | 42 | it('should return false when trying to remove a newer matching member', () => { 43 | const original = createPresenceMessage('client1', 'conn1', 'present', 100); 44 | presenceMap.put(original); 45 | const incoming = createPresenceMessage('client1', 'conn1', 'leave', 50); 46 | assert.isFalse(presenceMap.remove(incoming)); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/web_server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | cors = require('cors'), 3 | path = require('path'); 4 | 5 | /** 6 | * Runs a simple web server that runs the mocha.html tests 7 | * This is useful if you need to run the mocha tests visually 8 | * via a tunnel to your localhost server 9 | * 10 | * @param playwrightTest - used to let this script know that tests are 11 | * running in a playwright context. When truthy, a different html document is 12 | * returned on 'GET /' and console logging is skipped. 13 | */ 14 | 15 | class MochaServer { 16 | servers = []; 17 | 18 | constructor(playwrightTest) { 19 | this.playwrightTest = playwrightTest; 20 | } 21 | 22 | async listen() { 23 | const app = express(); 24 | app.use((req, res, next) => { 25 | if (!this.playwrightTest) console.log('%s %s %s', req.method, req.url, req.path); 26 | next(); 27 | }); 28 | 29 | app.use(cors()); 30 | 31 | app.get('/', (req, res) => { 32 | if (this.playwrightTest) { 33 | res.redirect('/playwright.html'); 34 | } else { 35 | res.redirect('/mocha.html'); 36 | } 37 | }); 38 | 39 | // service workers have limited scope if not served from the base path 40 | app.get('/push_sw.js', (req, res) => { 41 | res.sendFile(path.join(__dirname, 'support', 'push_sw.js')); 42 | }); 43 | 44 | app.use('/node_modules', express.static(__dirname + '/../node_modules')); 45 | app.use('/test', express.static(__dirname)); 46 | app.use('/browser', express.static(__dirname + '/../src/web')); 47 | app.use('/build', express.static(__dirname + '/../build')); 48 | app.use(express.static(__dirname)); 49 | 50 | const port = process.env.PORT || 3000; 51 | // Explicitly listen on the IPv4 and IPv6 loopback interfaces. If you don’t 52 | // pass an address to `app.listen`, then it will bind in a manner that 53 | // succeeds even if the IPv4 socket address is already in use, as long as 54 | // the IPv6 socket address is free. This can lead to confusion. 55 | await this.startServer(app, '127.0.0.1', port); 56 | await this.startServer(app, '::1', port); 57 | 58 | console.log(`Mocha test server listening on http://localhost:${port}/`); 59 | } 60 | 61 | async startServer(app, address, port) { 62 | await new Promise((resolve, reject) => { 63 | const server = app.listen(port, address, resolve); 64 | this.servers.push(server); 65 | server.once('error', reject); 66 | }); 67 | } 68 | 69 | close() { 70 | for (const server of this.servers) { 71 | server.close(); 72 | } 73 | } 74 | } 75 | 76 | module.exports = MochaServer; 77 | -------------------------------------------------------------------------------- /tools/.gitignore: -------------------------------------------------------------------------------- 1 | closure-compiler 2 | 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "lib": ["ES2017", "DOM", "DOM.Iterable", "webworker"], 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "resolveJsonModule": true, 11 | "importHelpers": true, 12 | "baseUrl": "./src" 13 | }, 14 | "include": ["src/**/*", "test/**/*", "scripts/**/*", "ably.d.ts", "modular.d.ts"], 15 | "exclude": ["src/platform/react-hooks", "test/package"] 16 | } 17 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["ably.d.ts", "modular.d.ts"], 4 | "out": "typedoc/generated", 5 | "readme": "typedoc/landing-page.md", 6 | "treatWarningsAsErrors": true, 7 | "includeVersion": true, 8 | "validation": true, 9 | "requiredToBeDocumented": [ 10 | "Accessor", 11 | "Class", 12 | "Constructor", 13 | "Enum", 14 | "EnumMember", 15 | "Function", 16 | "Interface", 17 | "Method", 18 | "Parameter", 19 | "Property", 20 | "TypeAlias", 21 | "Variable", 22 | "Namespace" 23 | ], 24 | "intentionallyNotExported": ["__global.AblyObjectsTypes"] 25 | } 26 | -------------------------------------------------------------------------------- /typedoc/landing-page.md: -------------------------------------------------------------------------------- 1 | # Ably JavaScript Client Library SDK API Reference 2 | 3 | The JavaScript Client Library SDK supports a realtime and a REST interface. The JavaScript API references are generated from the [Ably JavaScript Client Library SDK source code](https://github.com/ably/ably-js/) using [TypeDoc](https://typedoc.org) and structured by classes. 4 | 5 | The realtime interface enables a client to maintain a persistent connection to Ably and publish, subscribe and be present on channels. The REST interface is stateless and typically implemented server-side. It is used to make requests such as retrieving statistics, token authentication and publishing to a channel. 6 | 7 | There are two variants of the Ably JavaScript Client Library SDK: 8 | 9 | - [Default variant](modules/ably.html): This variant of the SDK always creates a fully-featured Ably client. 10 | - [Modular (tree-shakable) variant](modules/modular.html): Aimed at those who are concerned about their app’s bundle size, this allows you to create a client which has only the functionality that you choose. 11 | 12 | View the [Ably docs](https://ably.com/docs/) for conceptual information on using Ably, and for API references featuring all languages. The combined [API references](https://ably.com/docs/api/) are organized by features and split between the [realtime](https://ably.com/docs/api/realtime-sdk) and [REST](https://ably.com/docs/api/rest-sdk) interfaces. 13 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | root: 'src/platform/react-hooks/sample-app', 7 | server: { 8 | port: 8080, 9 | strictPort: true, 10 | host: true, 11 | }, 12 | plugins: [react() as any], 13 | test: { 14 | globals: true, 15 | environment: 'jsdom', 16 | }, 17 | optimizeDeps: { 18 | include: ['ably'], 19 | }, 20 | resolve: { 21 | alias: { 22 | ably: path.resolve(__dirname, 'build', 'ably.js'), 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | root: 'src/platform/react-hooks/src', 7 | plugins: [react() as any], 8 | test: { 9 | globals: true, 10 | environment: 'jsdom', 11 | }, 12 | resolve: { 13 | alias: { 14 | ably: path.resolve(__dirname, 'build', 'ably.js'), 15 | }, 16 | }, 17 | }); 18 | --------------------------------------------------------------------------------