Skip to content

Commit

Permalink
Add typings for tree-shakable version of library
Browse files Browse the repository at this point in the history
I wasn’t sure of the best way to approach the typings of the modules
(`Rest` etc). They should be opaque to the user (they shouldn’t be
interacting with them directly and we want be free to change their
interface in the future).

There is an open issue to add support for opaque types to TypeScript
[1], and people have suggested various sorts of ways of approximating
them, which revolve around the use of `unique symbol` declarations.
However, I don’t fully understand these solutions and so thought it best
not to include them in our public API. So, for now, let’s just use
`unknown`, the same way as we do for `CipherParams.key`.

Resolves #1442.

[1] microsoft/TypeScript#202
  • Loading branch information
lawrence-forooghian committed Nov 14, 2023
1 parent be44206 commit b839715
Show file tree
Hide file tree
Showing 15 changed files with 202 additions and 95 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ module.exports = {
},
},
{
files: 'ably.d.ts',
files: ['ably.d.ts', 'modules.d.ts'],
extends: [
'plugin:jsdoc/recommended',
],
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@ jobs:
- run: npm ci
- run: npm run lint
- run: npm run format:check
- run: npx tsc --noEmit ably.d.ts build/ably-webworker.min.d.ts
- run: npx tsc --noEmit ably.d.ts modules.d.ts build/ably-webworker.min.d.ts
- run: npm audit --production
107 changes: 54 additions & 53 deletions ably.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1700,32 +1700,7 @@ declare namespace Types {
/**
* A client that offers a simple stateless API to interact directly with Ably's REST API.
*/
class Rest {
/**
* Construct a client object using an Ably {@link Types.ClientOptions} object.
*
* @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably.
*/
constructor(options: Types.ClientOptions);
/**
* Constructs a client object using an Ably API key or token string.
*
* @param keyOrToken - The Ably API key or token string used to validate the client.
*/
constructor(keyOrToken: string);
/**
* The cryptographic functions available in the library.
*/
static Crypto: Types.Crypto;
/**
* Static utilities related to messages.
*/
static Message: Types.MessageStatic;
/**
* Static utilities related to presence messages.
*/
static PresenceMessage: Types.PresenceMessageStatic;

abstract class Rest {
/**
* An {@link Types.Auth} object.
*/
Expand Down Expand Up @@ -1799,31 +1774,7 @@ declare namespace Types {
/**
* A client that extends the functionality of {@link Rest} and provides additional realtime-specific features.
*/
class Realtime {
/**
* Construct a client object using an Ably {@link Types.ClientOptions} object.
*
* @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably.
*/
constructor(options: Types.ClientOptions);
/**
* Constructs a client object using an Ably API key or token string.
*
* @param keyOrToken - The Ably API key or token string used to validate the client.
*/
constructor(keyOrToken: string);
/**
* The cryptographic functions available in the library.
*/
static Crypto: Types.Crypto;
/**
* Static utilities related to messages.
*/
static Message: Types.MessageStatic;
/**
* Static utilities related to presence messages.
*/
static PresenceMessage: Types.PresenceMessageStatic;
abstract class Realtime {
/**
* A client ID, used for identifying this client when publishing messages or for presence purposes. The `clientId` can be any non-empty string, except it cannot contain a `*`. This option is primarily intended to be used in situations where the library is instantiated with a key. A `clientId` may also be implicit in a token used to instantiate the library; an error will be raised if a `clientId` specified here conflicts with the `clientId` implicit in the token.
*/
Expand Down Expand Up @@ -2849,12 +2800,62 @@ declare namespace Types {
/**
* A client that offers a simple stateless API to interact directly with Ably's REST API.
*/
export declare class Rest extends Types.Rest {}
export declare class Rest extends Types.Rest {
/**
* Construct a client object using an Ably {@link Types.ClientOptions} object.
*
* @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably.
*/
constructor(options: Types.ClientOptions);
/**
* Constructs a client object using an Ably API key or token string.
*
* @param keyOrToken - The Ably API key or token string used to validate the client.
*/
constructor(keyOrToken: string);
/**
* The cryptographic functions available in the library.
*/
static Crypto: Types.Crypto;
/**
* Static utilities related to messages.
*/
static Message: Types.MessageStatic;
/**
* Static utilities related to presence messages.
*/
static PresenceMessage: Types.PresenceMessageStatic;
}

/**
* A client that extends the functionality of {@link Rest} and provides additional realtime-specific features.
*/
export declare class Realtime extends Types.Realtime {}
export declare class Realtime extends Types.Realtime {
/**
* Construct a client object using an Ably {@link Types.ClientOptions} object.
*
* @param options - A {@link Types.ClientOptions} object to configure the client connection to Ably.
*/
constructor(options: Types.ClientOptions);
/**
* Constructs a client object using an Ably API key or token string.
*
* @param keyOrToken - The Ably API key or token string used to validate the client.
*/
constructor(keyOrToken: string);
/**
* The cryptographic functions available in the library.
*/
static Crypto: Types.Crypto;
/**
* Static utilities related to messages.
*/
static Message: Types.MessageStatic;
/**
* Static utilities related to presence messages.
*/
static PresenceMessage: Types.PresenceMessageStatic;
}

/**
* A generic Ably error object that contains an Ably-specific status code, and a generic status code. Errors returned from the Ably server are compatible with the `ErrorInfo` structure and should result in errors that inherit from `ErrorInfo`.
Expand Down
45 changes: 45 additions & 0 deletions modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Types } from './ably';

export declare const generateRandomKey: Types.Crypto['generateRandomKey'];
export declare const getDefaultCryptoParams: Types.Crypto['getDefaultParams'];
export declare const decodeMessage: Types.MessageStatic['fromEncoded'];
export declare const decodeEncryptedMessage: Types.MessageStatic['fromEncoded'];
export declare const decodeMessages: Types.MessageStatic['fromEncodedArray'];
export declare const decodeEncryptedMessages: Types.MessageStatic['fromEncodedArray'];
export declare const decodePresenceMessage: Types.PresenceMessageStatic['fromEncoded'];
export declare const decodePresenceMessages: Types.PresenceMessageStatic['fromEncodedArray'];
export declare const constructPresenceMessage: Types.PresenceMessageStatic['fromValues'];

export declare const Rest: unknown;
export declare const Crypto: unknown;
export declare const MsgPack: unknown;
export declare const RealtimePresence: unknown;
export declare const WebSocketTransport: unknown;
export declare const XHRPolling: unknown;
export declare const XHRStreaming: unknown;
export declare const XHRRequest: unknown;
export declare const FetchRequest: unknown;
export declare const MessageInteractions: unknown;

export interface ModulesMap {
Rest?: typeof Rest;
Crypto?: typeof Crypto;
MsgPack?: typeof MsgPack;
RealtimePresence?: typeof RealtimePresence;
WebSocketTransport?: typeof WebSocketTransport;
XHRPolling?: typeof XHRPolling;
XHRStreaming?: typeof XHRStreaming;
XHRRequest?: typeof XHRRequest;
FetchRequest?: typeof FetchRequest;
MessageInteractions?: typeof MessageInteractions;
}

export declare class BaseRest extends Types.Rest {
constructor(options: Types.ClientOptions, modules: ModulesMap);
}

export declare class BaseRealtime extends Types.Realtime {
constructor(options: Types.ClientOptions, modules: ModulesMap);
}

export { Types };
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
"default": "./build/ably.js"
},
"./modules": {
"types": "./modules.d.ts",
"default": "./build/modules/index.js"
}
},
"typings": "./ably.d.ts",
"files": [
"build/**",
"ably.d.ts",
"modules.d.ts",
"resources/**",
"src/**",
"react/**"
Expand Down Expand Up @@ -126,8 +128,8 @@
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"prepare": "npm run build",
"format": "prettier --write --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts webpack.config.js Gruntfile.js scripts/*.js docs/chrome-mv3.md",
"format:check": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts webpack.config.js Gruntfile.js scripts/*.js",
"format": "prettier --write --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts modules.d.ts webpack.config.js Gruntfile.js scripts/*.js docs/chrome-mv3.md",
"format:check": "prettier --check --ignore-path .gitignore --ignore-path .prettierignore src test ably.d.ts modules.d.ts webpack.config.js Gruntfile.js scripts/*.js",
"sourcemap": "source-map-explorer build/ably.min.js",
"sourcemap:noencryption": "source-map-explorer build/ably.noencryption.min.js",
"modulereport": "node scripts/moduleReport.js",
Expand Down
11 changes: 8 additions & 3 deletions test/package/browser/template/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ This directory is intended to be used for testing the following aspects of the a
- that its exports are correctly configured and provide access to ably-js’s functionality
- that its TypeScript typings are correctly configured and can be successfully used from a TypeScript-based app that imports the package

The file `src/index.ts` imports the ably-js package and exports a function which briefly exercises its functionality.
It contains two files, each of which import ably-js in different manners, and which export a function which briefly exercises its functionality:

- `src/index-default.ts` imports the default ably-js package (`import { Realtime } from 'ably'`).
- `src/index-modules.ts` imports the tree-shakable ably-js package (`import { BaseRealtime, WebSocketTransport, FetchRequest } from 'ably/modules'`).

## Why is `ably` not in `package.json`?

Expand All @@ -15,6 +18,8 @@ The `ably` dependency gets added when we run the repository’s `test:package` p

This directory exposes three package scripts that are to be used for testing:

- `build`: Uses esbuild to create a bundle containing `src/index.ts` and ably-js.
- `test`: Using the bundle created by `build`, tests that the code that exercises ably-js’s functionality is working correctly in a browser.
- `build`: Uses esbuild to create:
1. a bundle containing `src/index-default.ts` and ably-js;
2. a bundle containing `src/index-modules.ts` and ably-js.
- `test`: Using the bundles created by `build`, tests that the code that exercises ably-js’s functionality is working correctly in a browser.
- `typecheck`: Type-checks the code that imports ably-js.
2 changes: 1 addition & 1 deletion test/package/browser/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "esbuild --bundle src/index.ts --outdir=dist",
"build": "esbuild --bundle src/index-default.ts --outdir=dist && esbuild --bundle src/index-modules.ts --outdir=dist",
"typecheck": "tsc -noEmit",
"test-support:server": "ts-node server/server.ts",
"test": "playwright test",
Expand Down
11 changes: 11 additions & 0 deletions test/package/browser/template/server/resources/index-default.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ably NPM package test (default export)</title>
</head>
<body>
<script type="text/javascript" src="index-default.js"></script>
<script type="text/javascript" src="runTest.js"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions test/package/browser/template/server/resources/index-modules.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Ably NPM package test (tree-shakable export)</title>
</head>
<body>
<script type="text/javascript" src="index-modules.js"></script>
<script type="text/javascript" src="runTest.js"></script>
</body>
</html>
21 changes: 0 additions & 21 deletions test/package/browser/template/server/resources/index.html

This file was deleted.

9 changes: 9 additions & 0 deletions test/package/browser/template/server/resources/runTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(async () => {
try {
await testAblyPackage();
onResult(null);
} catch (error) {
console.log('Caught error', error);
onResult(error);
}
})();
5 changes: 4 additions & 1 deletion test/package/browser/template/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import path from 'node:path';

async function startWebServer(listenPort: number) {
const server = express();
server.get('/', (req, res) => res.send('OK'));
server.use(express.static(path.join(__dirname, '/resources')));
server.use('/index.js', express.static(path.join(__dirname, '..', 'dist', 'index.js')));
for (const filename of ['index-default.js', 'index-modules.js']) {
server.use(`/${filename}`, express.static(path.join(__dirname, '..', 'dist', filename)));
}

server.listen(listenPort);
}
Expand Down
File renamed without changes.
34 changes: 34 additions & 0 deletions test/package/browser/template/src/index-modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { BaseRealtime, Types, WebSocketTransport, FetchRequest, generateRandomKey } from 'ably/modules';
import { createSandboxAblyAPIKey } from './sandbox';

// This function exists to check that we can import the Types namespace and refer to its types.
async function attachChannel(channel: Types.RealtimeChannel) {
await channel.attach();
}

// This function exists to check that one of the free-standing functions (arbitrarily chosen) can be imported and does something vaguely sensible.
async function checkStandaloneFunction() {
const generatedKey = await generateRandomKey();
if (!(generatedKey instanceof ArrayBuffer)) {
throw new Error('Expected to get an ArrayBuffer from generateRandomKey');
}
}

globalThis.testAblyPackage = async function () {
const key = await createSandboxAblyAPIKey();

const realtime = new BaseRealtime({ key, environment: 'sandbox' }, { WebSocketTransport, FetchRequest });

const channel = realtime.channels.get('channel');
await attachChannel(channel);

const receivedMessagePromise = new Promise<void>((resolve) => {
channel.subscribe(() => {
resolve();
});
});

await channel.publish('message', { foo: 'bar' });
await receivedMessagePromise;
await checkStandaloneFunction();
};
31 changes: 19 additions & 12 deletions test/package/browser/template/test/package.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { test, expect } from '@playwright/test';

test.describe('NPM package', () => {
test('can be imported and provides access to Ably functionality', async ({ page }) => {
const pageResultPromise = new Promise<void>((resolve, reject) => {
page.exposeFunction('onResult', (error: Error | null) => {
if (error) {
reject(error);
} else {
resolve();
}
for (const scenario of [
{ name: 'default export', path: '/index-default.html' },
{ name: 'modular export', path: '/index-modules.html' },
]) {
test.describe(scenario.name, () => {
test('can be imported and provides access to Ably functionality', async ({ page }) => {
const pageResultPromise = new Promise<void>((resolve, reject) => {
page.exposeFunction('onResult', (error: Error | null) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});

await page.goto(scenario.path);
await pageResultPromise;
});
});

await page.goto('/');
await pageResultPromise;
});
}
});

0 comments on commit b839715

Please sign in to comment.