Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ Rewrite in TypeScript #4

Merged
merged 12 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Compiled JavaScript
dist

# Logs
logs
*.log
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
talker.js
=========
# talker.js

A tiny (<4kB minified, <1kB min+gzip), promise-based library for cross-origin communication between frames and windows.
A small (<13kB minified, <6kB min+gzip), promise-based library for cross-origin communication between frames and windows.

Documentation
-------------
## Documentation

Please see [drive.secondstreet.com/introducing-talker](http://drive.secondstreet.com/introducing-talker/) for instructions and examples for using Talker.js in your own projects.

Building
--------
## Building

```
npm install -g grunt-cli
npm install
grunt
yarn install
yarn run build
```
15 changes: 9 additions & 6 deletions dist/amd/talker.min.js

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions dist/common_js/talker.min.js

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions dist/named_amd/talker.min.js

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions dist/talker.min.js

Large diffs are not rendered by default.

27 changes: 19 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
{
"name": "talker.js",
"version": "1.0.1",
"version": "1.1.0",
"description": "A tiny, promise-based library for cross-origin communication between frames and windows.",
"main": "src/talker.js",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-uglify": "^0.5.0"
},
"main": "dist/talker.js",
"types": "dist/index.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsc",
"test": "tslint src/"
},
"repository": {
"type": "git",
Expand All @@ -26,5 +24,18 @@
"bugs": {
"url": "https://github.com/secondstreet/talker.js/issues"
},
"homepage": "https://github.com/secondstreet/talker.js"
"homepage": "https://github.com/secondstreet/talker.js",
"devDependencies": {
"prettier": "^1.15.2",
"ts-loader": "^5.3.1",
"tslint": "5.7.0",
"tslint-config-prettier": "1.9.0",
"tslint-plugin-prettier": "^2.0.1",
Kerrick marked this conversation as resolved.
Show resolved Hide resolved
"typescript": "3.1.2",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2"
},
"dependencies": {
"es6-promise": "4.2.4"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed that this and a few of the ones in devDeps don't have a ^ in front of the version; any particular reason? Would be nice to have them all defined the same way if not. (I noticed because the yarn lock update in ss-embed was listing both ^4.2.4 and 4.2.4 for es6-promise separately).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For devDependencies, it mostly came down to the ones I don't trust to follow semver. TypeScript introduces breaking changes outside of major version releases, so I didn't allow it (or tslint+friends) to auto-upgrade.

For dependencies, it's because this is a public library and I wanted to pin very specific dependency versions as "actually tested to work".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After talking further offsite, let's go with ~ -- it should be safe enough.

}
}
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const TALKER_CONTENT_TYPE: string = "application/x-talkerjs-v1+json";
export const TALKER_ERR_MSG_TIMEOUT: string =
"Talker.js message timed out waiting for a response.";
235 changes: 235 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/*!
* @overview talker.js - a tiny promise-based wrapper for HTML5 postMessage
* @copyright Copyright (c) 2018 Second Street and contributors
* @license Licensed under MIT License https://github.com/secondstreet/talker.js/blob/master/LICENSE
*/

import createManipulablePromise, {
ManipulablePromise
} from "./utils/manipulable-promise";
import {
IncomingMessage,
OutgoingMessage,
JSONableMessage,
Stringifyable
} from "./message";
import { TALKER_CONTENT_TYPE, TALKER_ERR_MSG_TIMEOUT } from "./constants";

interface SentMessages {
[id: number]: ManipulablePromise<IncomingMessage | Error>;
}

/**
* Talker
* Opens a communication line between this window and a remote window via postMessage.
*/
class Talker {
/*
* @property timeout - The number of milliseconds to wait before assuming no response will be received.
*/
public timeout: number = 3000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: it's always helpful to have the units as part of the variable name:

Suggested change
public timeout: number = 3000;
public timeoutMs: number = 3000;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to avoid bumping the major version number and this would be changing the public API.


/**
* @property onMessage - Will be called with every non-handshake, non-response message from the remote window
*/
onMessage?: (message: IncomingMessage) => void;

// Will be resolved when a handshake is newly established with the remote window.
private readonly handshake: ManipulablePromise<
boolean
> = createManipulablePromise();
// Whether we've received a handshake from the remote window
private handshaken: boolean = false;
// The ID of the latest OutgoingMessage
private latestId: number = 0;
private readonly queue: OutgoingMessage[] = [];
private readonly sent: SentMessages = {};

/**
* @param remoteWindow - The remote `window` object to post/receive messages to/from
* @param remoteOrigin - The protocol, host, and port you expect the remoteWindow to be
* @param localWindow - The local `window` object
*/
constructor(
private readonly remoteWindow: Window,
private readonly remoteOrigin: string,
private readonly localWindow: Window = window
) {
this.localWindow.addEventListener(
"message",
(messageEvent: MessageEvent) => this.receiveMessage(messageEvent),
false
);
this.sendHandshake();
}

/**
* @param namespace - The namespace the message is in
* @param data - The data to send
* @param responseToId - If this is a response to a previous message, its ID.
*/
send(
namespace: string,
data: Stringifyable,
responseToId: number | null = null
): ManipulablePromise<IncomingMessage | Error> {
const message = new OutgoingMessage(this, namespace, data, responseToId);

const promise = createManipulablePromise<IncomingMessage | Error>();

this.sent[message.id] = promise;
this.queue.push(message);
this.flushQueue();

setTimeout(() => {
if (!promise.__reject__) {
return;
}
promise.__reject__(new Error(TALKER_ERR_MSG_TIMEOUT));
}, this.timeout);

return promise;
}

/**
* This is not marked private because other Talker-related classes need access to it,
* but your application code should probably avoid calling this method.
*/
nextId(): number {
return (this.latestId += 1);
}

private receiveMessage(messageEvent: MessageEvent): void {
let object: JSONableMessage;
try {
object = JSON.parse(messageEvent.data);
} catch (err) {
object = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that familiar with talker; can you explain why we'd ever want to keep going here if the message data failed JSON parsing? (And preferably add a comment to the code.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a feature of the library that data-less messages can be sent via Talker. This handles that scenario (e.g. JSON.parse(undefined), JSON.parse(''), etc.).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in the future it should be a falsey check to fallback to this + actually error on malformed JSON? Probably not something to change now though.

namespace: "",
data: {},
id: this.nextId(),
type: TALKER_CONTENT_TYPE
};
}
if (
!this.isSafeMessage(messageEvent.source, messageEvent.origin, object.type)
) {
return;
}

const isHandshake = object.handshake || object.handshakeConfirmation;
return isHandshake
? this.handleHandshake(object)
: this.handleMessage(object);
}

/**
* Determines whether it is safe and appropriate to parse a postMessage messageEvent
* @param source - "source" property from the postMessage event
* @param origin - Protocol, host, and port
* @param type - Internet Media Type
*/
private isSafeMessage(
source: Window | MessagePort | ServiceWorker | null,
origin: string,
type: string
): boolean {
const isSourceSafe = source === this.remoteWindow;
const isOriginSafe =
this.remoteOrigin === "*" || origin === this.remoteOrigin;
const isTypeSafe = type === TALKER_CONTENT_TYPE;
return isSourceSafe && isOriginSafe && isTypeSafe;
}

private handleHandshake(object: JSONableMessage): void {
if (object.handshake) {
// One last handshake in case the remote window (which we now know is ready) hasn't seen ours yet
this.sendHandshake(this.handshaken);
}
if (!this.handshaken) {
this.handshaken = true;
if (this.handshake.__resolve__) {
this.handshake.__resolve__(this.handshaken);
}
this.flushQueue();
}
}

private handleMessage(rawObject: JSONableMessage): void {
const message = new IncomingMessage(
this,
rawObject.namespace,
rawObject.data,
rawObject.id
);
const responseId = rawObject.responseToId;
return responseId
? this.respondToMessage(responseId, message)
: this.broadcastMessage(message);
}

/**
* @param id - Message ID of the waiting promise
* @param message - Message that is responding to that ID
*/
private respondToMessage(id: number, message: IncomingMessage): void {
const sent = this.sent[id];
if (sent && sent.__resolve__) {
sent.__resolve__(message);
delete this.sent[id];
}
}

/**
* Send a non-response message to awaiting hooks/callbacks
* @param message - Message that arrived
*/
private broadcastMessage(message: IncomingMessage): void {
if (this.onMessage) {
this.onMessage.call(this, message);
}
}

/**
* Send a handshake message to the remote window
* @param confirmation - Is this a confirmation handshake?
*/
private sendHandshake(confirmation: boolean = false): void {
return this.postMessage({
type: TALKER_CONTENT_TYPE,
[confirmation ? "handshakeConfirmation" : "handshake"]: true
});
}

/**
* Wrapper around window.postMessage to only send if we have the necessary objects
*/
private postMessage(data: OutgoingMessage | JSONableMessage): void {
const message = JSON.stringify(data);
if (this.remoteWindow && this.remoteOrigin) {
try {
this.remoteWindow.postMessage(message, this.remoteOrigin);
} catch (e) {
// no-op
}
}
}

/**
* Flushes the internal queue of outgoing messages, sending each one.
* Does nothing if Talker has not handshaken with the remote.
*/
private flushQueue(): void {
if (this.handshaken) {
while (this.queue.length > 0) {
const message = this.queue.shift();
if (message) {
this.postMessage(message);
}
}
}
}
}

export { IncomingMessage, OutgoingMessage };
export default Talker;
Loading