├── .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 |
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 |
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 |
--------------------------------------------------------------------------------