diff --git a/ably/ably-tests.ts b/ably/ably-tests.ts new file mode 100644 index 0000000000..dbe82bc054 --- /dev/null +++ b/ably/ably-tests.ts @@ -0,0 +1,239 @@ +import * as Ably from 'ably'; + +const ApiKey = 'appId.keyId:secret'; +const client = Ably.Realtime; +const restClient = Ably.Rest; + +// Connection +// Successful connection: + +client.connection.on('connected', function() { + // successful connection +}); + +// Failed connection: + +client.connection.on('failed', function() { + // failed connection +}); + + +// Subscribing to a channel + +var channel = client.channels.get('test'); +channel.subscribe(function(message) { + message.name; // 'greeting' + message.data; // 'Hello World!' +}); + +// Only certain events: + +channel.subscribe('myEvent', function(message) { + message.name; // 'myEvent' + message.data; // 'myData' +}); + +// Publishing to a channel + +// Publish a single message with name and data +channel.publish('greeting', 'Hello World!'); + +// Optionally, you can use a callback to be notified of success or failure +channel.publish('greeting', 'Hello World!', function(err) { + if(err) { + console.log('publish failed with error ' + err); + } else { + console.log('publish succeeded'); + } +}) + +// Publish several messages at once +channel.publish([{name: 'greeting', data: 'Hello World!'}], function() { }); + +// Querying the History + +channel.history(function(err, messagesPage) { + messagesPage.items; // array of Message + messagesPage.items[0].data; // payload for first message + messagesPage.items.length; // number of messages in the current page of history + messagesPage.hasNext(); // true if there are further pages + messagesPage.isLast(); // true if this page is the last page + messagesPage.next(function(nextPage) { nextPage; }); // retrieves the next page as PaginatedResult +}); + +// Can optionally take an options param, see https://www.ably.io/documentation/rest-api/#message-history +channel.history({ start: Date.now()-10000, end: Date.now(), limit: 100, direction: 'forwards'}, function(err, messagesPage) { + console.log(messagesPage.items.length); +}); + + +// Presence on a channel +// Getting presence: + +channel.presence.get(function(err, presenceSet) { + presenceSet; // array of PresenceMessages +}); + +// Note that presence#get on a realtime channel does not return a PaginatedResult, as the library maintains a local copy of the presence set. + +// Entering (and leaving) the presence set: + +channel.presence.enter('my status', function(err) { + // now I am entered +}); + +channel.presence.update('new status', function(err) { + // my presence data is updated +}); + +channel.presence.leave(function(err) { + // I've left the presence set +}); + +channel.presence.enterClient('myClientId', 'status', function(err) { +}); + +// and similiarly, updateClient and leaveClient +// Querying the Presence History + +channel.presence.history(function(err, messagesPage) { // PaginatedResult + messagesPage.items; // array of PresenceMessage + messagesPage.items[0].data; // payload for first message + messagesPage.items.length; // number of messages in the current page of history + messagesPage.hasNext(); // true if there are further pages + messagesPage.isLast(); // true if this page is the last page + messagesPage.next(function(nextPage) { }); // retrieves the next page as PaginatedResult +}); + +// Can optionally take an options param, see https://www.ably.io/documentation/rest-api/#message-history +channel.history({ start: Date.now()-10000, end: Date.now(), limit: 100, direction: 'forwards' }, function(err, messagesPage) {}); + +// Symmetrical end-to-end encrypted payloads on a channel + +// When a 128 bit or 256 bit key is provided to the library, the data attributes of all messages are encrypted and decrypted automatically using that key. The secret key is never transmitted to Ably. See https://www.ably.io/documentation/realtime/encryption + +// Generate a random 256-bit key for demonstration purposes (in +// practice you need to create one and distribute it to clients yourselves) +Ably.Realtime.Crypto.generateRandomKey(function(err, key) { + var channel = client.channels.get('channelName', { cipher: { key: key } }); + + channel.subscribe(function(message) { + message.name; // 'name is not encrypted' + message.data; // 'sensitive data is encrypted' + }); + + channel.publish('name is not encrypted', 'sensitive data is encrypted'); +}) + +// You can also change the key on an existing channel using setOptions (which takes a callback which is called after the new encryption settings have taken effect): + +channel.setOptions({cipher: {key: ''}}, function() { + // New encryption settings are in effect +}); + +// Using the REST API + +var channel = restClient.channels.get('test'); + +// Publishing to a channel + +// Publish a single message with name and data +channel.publish('greeting', 'Hello World!'); + +// Optionally, you can use a callback to be notified of success or failure +channel.publish('greeting', 'Hello World!', function(err) { + if(err) { + console.log('publish failed with error ' + err); + } else { + console.log('publish succeeded'); + } +}) + +// Publish several messages at once +channel.publish([{name: 'greeting', data: 'Hello World!'}], function() {}); + +// Querying the History + +channel.history(function(err, messagesPage) { + messagesPage; // PaginatedResult + messagesPage.items; // array of Message + messagesPage.items[0].data; // payload for first message + messagesPage.items.length; // number of messages in the current page of history + messagesPage.hasNext(); // true if there are further pages + messagesPage.isLast(); // true if this page is the last page + messagesPage.next(function(nextPage) {}); // retrieves the next page as PaginatedResult +}); + +// Can optionally take an options param, see https://www.ably.io/documentation/rest-api/#message-history +channel.history({ start: Date.now()-10000, end: Date.now(), limit: 100, direction: 'forwards' }, function(err, messagesPage) {}); + +// Presence on a channel + +channel.presence.get(function(err, presencePage) { // PaginatedResult + presencePage.items; // array of PresenceMessage + presencePage.items[0].data; // payload for first message + presencePage.items.length; // number of messages in the current page of members + presencePage.hasNext(); // true if there are further pages + presencePage.isLast(); // true if this page is the last page + presencePage.next(function(nextPage) {}); // retrieves the next page as PaginatedResult +}); + +// Querying the Presence History + +channel.presence.history(function(err, messagesPage) { // PaginatedResult + messagesPage.items; // array of PresenceMessage + messagesPage.items[0].data; // payload for first message + messagesPage.items.length; // number of messages in the current page of history + messagesPage.hasNext(); // true if there are further pages + messagesPage.isLast(); // true if this page is the last page + messagesPage.next(function(nextPage) { }); // retrieves the next page as PaginatedResult +}); + +// Can optionally take an options param, see https://www.ably.io/documentation/rest-api/#message-history +channel.history({ start: Date.now()-10000, end: Date.now(), limit: 100, direction: 'forwards' }, function(err, messagesPage) {}); + + +// Generate Token and Token Request +// See https://www.ably.io/documentation/general/authentication for an explanation of Ably's authentication mechanism. + +// Requesting a token: + +client.auth.requestToken(function(err, tokenDetails) { + // tokenDetails is instance of TokenDetails + // see https://www.ably.io/documentation/rest/authentication/#token-details for its properties + + // Now we have the token, we can send it to someone who can instantiate a client with it: + var clientUsingToken = new Ably.Realtime(tokenDetails.token); +}); + +// requestToken can take two optional params +// tokenParams: https://www.ably.io/documentation/rest/authentication/#token-params +// authOptions: https://www.ably.io/documentation/rest/authentication/#auth-options +client.auth.requestToken({}, {}, function(err, tokenDetails) { }); + +// Creating a token request (for example, on a server in response to a request by a client using the authCallback or authUrl mechanisms): + +client.auth.createTokenRequest(function(err, tokenRequest) { + // now send the tokenRequest back to the client, which will + // use it to request a token and connect to Ably +}); + +// createTokenRequest can take two optional params +// tokenParams: https://www.ably.io/documentation/rest/authentication/#token-params +// authOptions: https://www.ably.io/documentation/rest/authentication/#auth-options +client.auth.createTokenRequest({}, {}, function(err, tokenRequest) { }); + +// Fetching your application's stats + +client.stats(function(err, statsPage) { // statsPage as PaginatedResult + statsPage.items; // array of Stats + statsPage.items[0].data; // payload for first message + statsPage.items.length; // number of messages in the current page of history + statsPage.hasNext(); // true if there are further pages + statsPage.isLast(); // true if this page is the last page + statsPage.next(function(nextPage) {}); // retrieves the next page as PaginatedResult +}); + +// Fetching the Ably service time + +client.time(function(err, time) {}); // time is in ms since epoch diff --git a/ably/index.d.ts b/ably/index.d.ts new file mode 100644 index 0000000000..e42b65d199 --- /dev/null +++ b/ably/index.d.ts @@ -0,0 +1,443 @@ +// Type definitions for Ably Realtime and Rest client library v0.9.0 +// Project: https://www.ably.io/ +// Definitions by: Ably +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare namespace ChannelState { + export type INITIALIZED = 'initialized'; + export type ATTACHING = 'attaching'; + export type ATTACHED = "attached"; + export type DETACHING = "detaching"; + export type DETACHED = "detached"; + export type SUSPENDED = "suspended"; + export type FAILED = "failed"; +} +type ChannelState = ChannelState.FAILED | ChannelState.INITIALIZED | ChannelState.SUSPENDED | ChannelState.ATTACHED | ChannelState.ATTACHING | ChannelState.DETACHED | ChannelState.DETACHING; + +declare namespace ConnectionState { + export type INITIALIZED = "initialized"; + export type CONNECTING = "connecting"; + export type CONNECTED = "connected"; + export type DISCONNECTED = "disconnected"; + export type SUSPENDED = "suspended"; + export type CLOSING = "closing"; + export type CLOSED = "closed"; + export type FAILED = "failed"; +} +type ConnectionState = ConnectionState.INITIALIZED | ConnectionState.CONNECTED | ConnectionState.CONNECTING | ConnectionState.DISCONNECTED | ConnectionState.SUSPENDED | ConnectionState.CLOSED | ConnectionState.CLOSING | ConnectionState.FAILED; + +declare namespace ConnectionEvent { + export type INITIALIZED = "initialized"; + export type CONNECTING = "connecting"; + export type CONNECTED = "connected"; + export type DISCONNECTED = "disconnected"; + export type SUSPENDED = "suspended"; + export type CLOSING = "closing"; + export type CLOSED = "closed"; + export type FAILED = "failed"; + export type UPDATE = "update"; +} +type ConnectionEvent = ConnectionEvent.INITIALIZED | ConnectionEvent.CONNECTED | ConnectionEvent.CONNECTING | ConnectionEvent.DISCONNECTED | ConnectionEvent.SUSPENDED | ConnectionEvent.CLOSED | ConnectionEvent.CLOSING | ConnectionEvent.FAILED | ConnectionEvent.UPDATE; + +declare namespace PresenceAction { + export type ABSENT = "absent"; + export type PRESENT = "present"; + export type ENTER = "enter"; + export type LEAVE = "leave"; + export type UPDATE = "update"; +} +type PresenceAction = PresenceAction.ABSENT | PresenceAction.PRESENT | PresenceAction.ENTER | PresenceAction.LEAVE | PresenceAction.UPDATE; + +declare namespace StatsIntervalGranularity { + export type MINUTE = "minute"; + export type HOUR = "hour"; + export type DAY = "day"; + export type MONTH = "month"; +} +type StatsIntervalGranularity = StatsIntervalGranularity.MINUTE | StatsIntervalGranularity.HOUR | StatsIntervalGranularity.DAY | StatsIntervalGranularity.MONTH; + +declare namespace HTTPMethods { + export type POST = "POST"; + export type GET = "GET"; +} +type HTTPMethods = HTTPMethods.GET | HTTPMethods.POST; + +// Interfaces +declare interface ClientOptions extends AuthOptions { + /** + * When true will automatically connect to Ably when library is instanced. This is true by default + */ + autoConnect?: boolean; + + /** + * Optional clientId that can be used to specify the identity for this client. In most cases + * it is preferable to instead specift a clientId in the token issued to this client. + */ + clientId?: string; + + defaultTokenParams?: TokenParams; + + /** + * When true, messages published on channels by this client will be echoed back to this client. + * This is true by default + */ + echoMessages?: boolean; + + /** + * Use this only if you have been provided a dedicated environment by Ably + */ + environment?: string; + + /** + * Logger configuration + */ + log?: LogInfo; + port?: number; + + /** + * When true, messages will be queued whilst the connection is disconnected. True by default. + */ + queueMessages?: boolean; + + restHost?: string; + realtimeHost?: string; + fallbackHosts?: Array; + + /** + * Can be used to explicitly recover a connection. + * See https://www.ably.io/documentation/realtime/connection#connection-state-recovery + */ + recover?: standardCallback | string; + + /** + * Use a non-secure connection connection. By default, a TLS connection is used to connect to Ably + */ + tls?: boolean; + tlsPort?: number; + + /** + * When true, the more efficient MsgPack binary encoding is used. + * When false, JSON text encoding is used. + */ + useBinaryProtocol?: boolean; +} + +declare interface AuthOptions { + /** + * A function which is called when a new token is required. + * The role of the callback is to either generate a signed TokenRequest which may then be submitted automatically + * by the library to the Ably REST API requestToken; or to provide a valid token in as a TokenDetails object. + **/ + authCallback?: (data: TokenParams,callback: (error: ErrorInfo | string, tokenRequestOrDetails: TokenDetails | TokenRequest | string) => void) => void; + authHeaders?: { [index: string]: string }; + authMethod?: HTTPMethods; + authParams?: { [index: string]: string }; + + /** + * A URL that the library may use to obtain a token string (in plain text format), or a signed TokenRequest or TokenDetails (in JSON format). + **/ + authUrl?: string; + key?: string; + queryTime?: boolean; + token?: TokenDetails | string; + tokenDetails?: TokenDetails; + useTokenAuth?: boolean; +} + +declare interface TokenParams { + capability?: string; + clientId?: string; + nonce?: string; + timestamp?: number; + ttl?: number; +} + +declare interface CipherParams { + algorithm: string; + key: any; + keyLength: number; + mode: string; +} + +declare interface ErrorInfo { + code: number; + message: string; + statusCode: number; +} + +declare interface StatsMessageCount { + count: number; + data: number; +} + +declare interface StatsMessageTypes { + all: StatsMessageCount; + messages: StatsMessageCount; + presence: StatsMessageCount; +} + +declare interface StatsRequestCount { + failed: number; + refused: number; + succeeded: number; +} + +declare interface StatsResourceCount { + mean: number; + min: number; + opened: number; + peak: number; + refused: number; +} + +declare interface StatsConnectionTypes { + all: StatsResourceCount; + plain: StatsResourceCount; + tls: StatsResourceCount; +} + +declare interface StatsMessageTraffic { + all: StatsMessageTypes, + realtime: StatsMessageTypes, + rest: StatsMessageTypes, + webhook: StatsMessageTypes +} + +declare interface TokenDetails { + capability: string; + clientId?: string; + expires: number; + issued: number; + token: string; +} + +declare interface TokenRequest { + capability: string; + clientId?: string; + keyName: string; + mac: string; + nonce: string; + timestamp: number; + ttl?: number; +} + +declare interface ChannelOptions { + cipher: any; +} + +declare interface RestPresenceHistoryParams { + start?: number; + end?: number; + direction?: string; + limit?: number; +} + +declare interface RestPresenceParams { + limit?: number; + clientId?: string; + connectionId?: string; +} + +declare interface RealtimePresenceParams { + waitForSync?: boolean; + clientId?: string; + connectionId?: string; +} + +declare interface RealtimePresenceHistoryParams { + start?: number; + end?: number; + direction?: string; + limit?: number; + untilAttach?: boolean +} + +declare interface LogInfo { + /** + * A number controlling the verbosity of the output. Valid values are: 0 (no logs), 1 (errors only), + * 2 (errors plus connection and channel state changes), 3 (high-level debug output), and 4 (full debug output). + **/ + level?: number; + + /** + * A function to handle each line of log output. If handler is not specified, console.log is used. + **/ + handler?: (...args) => void; +} + +declare interface ChannelEvent { + state: ChannelState; +} + +declare interface ChannelStateChange { + current: ChannelState; + previous: ChannelState; + reason?: ErrorInfo; + resumed: boolean; +} + +declare interface ConnectionStateChange { + current: ConnectionState; + previous: ConnectionState; + reason?: ErrorInfo; + retryIn?: number; +} + +// Common Listeners +type PaginatedResultCallback = (error: ErrorInfo, results: PaginatedResult ) => void; +type standardCallback = (error: ErrorInfo, results: any) => void; +type messageCallback = (message: T) => void; +type errorCallback = (error: ErrorInfo) => void; +type channelEventCallback = (channelEvent: ChannelEvent, changeStateChange: ChannelStateChange) => void; +type connectionEventCallback = (connectionEvent: ConnectionEvent, connectionStateChange: ConnectionStateChange) => void; +type timeCallback = (error: ErrorInfo, time: number) => void; + + +// Internal Classes +declare class EventEmitter { + on: (eventOrCallback: string | T, callback?: T) => void; + once: (eventOrCallback: string | T, callback?: T) => void; + off: (eventOrCallback?: string | T, callback?: T) => void; +} + +// Classes +export declare class Auth { + clientId: string; + authorize: (tokenParams?: TokenParams, authOptions?: AuthOptions, callback?: (error: ErrorInfo, Results: TokenDetails) => void) => void; + createTokenRequest: (tokenParams?: TokenParams, authOptions?: AuthOptions, callback?: (error: ErrorInfo, Results: TokenRequest) => void) => void; + requestToken: (TokenParams?: TokenParams, authOptions?: AuthOptions, callback?: (error: ErrorInfo, Results: TokenDetails) => void) => void; +} + +export declare class Presence { + get: (params: RestPresenceParams, callback: PaginatedResultCallback) => void; + history: (params: RestPresenceHistoryParams, callback: PaginatedResultCallback) => void; +} + +export declare class RealtimePresence { + syncComplete: () => boolean; + get: (Params: RealtimePresenceParams, callback?: (error: ErrorInfo, messages: Array) => void) => void; + history: (ParamsOrCallback: RealtimePresenceHistoryParams | PaginatedResultCallback, callback?: PaginatedResultCallback) => void; + subscribe: (presenceOrCallback: PresenceAction | messageCallback, listener?: messageCallback) => void; + unsubscribe: (presence?: PresenceAction, listener?: messageCallback) => void; + enter: (data: any, callback?: errorCallback) => void; + update: (data: any, callback?: errorCallback) => void; + leave: (data: any, callback?: errorCallback) => void; + enterClient: (clientId: string, data: any, callback?: errorCallback) => void; + updateClient: (clientId: string, data: any, callback?: errorCallback) => void; + leaveClient: (clientId: string, data: any, callback?: errorCallback) => void; +} + +export declare class Channel { + name: string; + presence: Presence; + history: (paramsOrCallback?: RestPresenceHistoryParams | PaginatedResultCallback, callback?: PaginatedResultCallback) => void; + publish: (messagesOrName: any, messagedataOrCallback?: errorCallback | any, callback?: errorCallback) => void; +} + +export declare class RealtimeChannel extends EventEmitter { + name: string; + errorReason: ErrorInfo; + state: ChannelState; + presence: RealtimePresence; + attach: (callback?: standardCallback) => void; + detach:(callback?: standardCallback) => void; + history: (paramsOrCallback?: RealtimePresenceHistoryParams | PaginatedResultCallback, callback?: PaginatedResultCallback) => void; + subscribe: (eventOrCallback: messageCallback | string, listener?: messageCallback) => void; + unsubscribe: (eventOrCallback?: messageCallback | string, listener?: messageCallback) => void; + publish: (messagesOrName: any, messageDataOrCallback?: errorCallback | any, callback?: errorCallback) => void; +} + +export declare class Channels { + get: (name: string, channelOptions?: ChannelOptions) => T; + release: (name: string) => void; +} + +export declare class Message { + constructor(); + fromEncoded: (JsonObject: string, channelOptions: ChannelOptions) => Message; + fromEncodedArray: (JsonArray: string, channelOptions: ChannelOptions) => Array; + clientId: string; + connectionId: string; + data: any; + encoding: string; + extras: any; + id: string; + name: string; + timestamp: number; +} + +export declare class PresenceMessage { + fromEncoded: (JsonObject: any, channelOptions?: ChannelOptions) => PresenceMessage; + fromEncodedArray: (JsonArray: Array, channelOptions?: ChannelOptions) => Array; + action: PresenceAction; + clientId: string; + connectionId: string; + data: any; + encoding: string; + id: string; + timestamp: number; +} + +export declare class Rest { + constructor(options: ClientOptions | string); + auth: Auth; + channels: Channels; + request: (method: string, path: string, params?: any, body?: Array | any, headers?: any, callback?: (error: ErrorInfo, response: HttpPaginatedResponse) => void) => void; + stats: (paramsOrCallback?: PaginatedResultCallback | any, callback?: PaginatedResultCallback) => void; + time: (paramsOrCallback?: timeCallback | any, callback?: timeCallback) => void; +} + +export declare class Realtime { + constructor(options: ClientOptions | string); + auth: Auth; + channels: Channels; + clientId: string; + connection: Connection; + request: (method: string, path: string, params?: any, body?: Array | any, headers?: any, callback?: (error: ErrorInfo, response: HttpPaginatedResponse) => void) => void; + stats: (paramsOrCallback?: PaginatedResultCallback | any, callback?: PaginatedResultCallback) => void; + close: () => void; + connect: () => void; + time: (paramsOrCallback?: timeCallback | any, callback?: timeCallback) => void; +} + +export declare class Connection extends EventEmitter { + errorReason: ErrorInfo; + id: string; + key: string; + recoveryKey: string; + serial: number; + state: ConnectionState; + close: () => void; + connect: () => void; + ping: (callback?: (error: ErrorInfo, responseTime: number ) => void ) => void; +} + +export declare class Stats { + all: StatsMessageTypes; + apiRequests: StatsRequestCount; + channels: StatsResourceCount; + connections: StatsConnectionTypes; + inbound: StatsMessageTraffic; + intervalId: string; + outbound: StatsMessageTraffic; + persisted: StatsMessageTypes; + tokenRequests: StatsRequestCount; +} + +export declare class PaginatedResult { + items: Array; + first: (results: PaginatedResultCallback) => void; + next: (results: PaginatedResultCallback) => void; + current: (results: PaginatedResultCallback) => void; + hasNext: () => boolean; + isLast: () => boolean; +} + +export declare class HttpPaginatedResponse extends PaginatedResult { + items: Array; + statusCode: number; + success: boolean; + errorCode: number; + errorMessage: string; + headers: any; +} diff --git a/ably/tsconfig.json b/ably/tsconfig.json new file mode 100644 index 0000000000..b13a7a2ea5 --- /dev/null +++ b/ably/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "ably-test.ts" + ] +}