Skip to content

Commit 7d71712

Browse files
author
Kerrick Long
committed
♻️ Rewrite in TypeScript
1 parent 9e99450 commit 7d71712

16 files changed

+3461
-420
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Compiled JavaScript
2+
dist
3+
14
# Logs
25
logs
36
*.log

dist/amd/talker.min.js

+9-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/common_js/talker.min.js

+8-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/named_amd/talker.min.js

+9-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/talker.min.js

+9-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
{
22
"name": "talker.js",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "A tiny, promise-based library for cross-origin communication between frames and windows.",
5-
"main": "src/talker.js",
6-
"devDependencies": {
7-
"grunt": "^0.4.5",
8-
"grunt-contrib-uglify": "^0.5.0"
9-
},
5+
"main": "dist/talker.js",
6+
"types": "dist/index.d.ts",
107
"scripts": {
11-
"test": "echo \"Error: no test specified\" && exit 1"
8+
"build": "tsc",
9+
"test": "tslint src/"
1210
},
1311
"repository": {
1412
"type": "git",
@@ -26,5 +24,16 @@
2624
"bugs": {
2725
"url": "https://github.com/secondstreet/talker.js/issues"
2826
},
29-
"homepage": "https://github.com/secondstreet/talker.js"
27+
"homepage": "https://github.com/secondstreet/talker.js",
28+
"devDependencies": {
29+
"ts-loader": "^5.3.1",
30+
"tslint": "5.7.0",
31+
"tslint-config-prettier": "1.9.0",
32+
"typescript": "3.1.2",
33+
"webpack": "^4.26.1",
34+
"webpack-cli": "^3.1.2"
35+
},
36+
"dependencies": {
37+
"es6-promise": "4.2.4"
38+
}
3039
}

src/index.ts

+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import createManipulablePromise, {
2+
ManipulablePromise
3+
} from "./utils/manipulable-promise";
4+
import {
5+
IncomingMessage,
6+
OutgoingMessage,
7+
JSONifiedMessage,
8+
Stringifyable
9+
} from "./message";
10+
import { TALKER_TYPE, TALKER_ERR_TIMEOUT } from "./strings";
11+
12+
interface SentMessages {
13+
[id: number]: ManipulablePromise<IncomingMessage | Error>;
14+
}
15+
16+
/**
17+
* Talker
18+
* Opens a communication line between this window and a remote window via postMessage.
19+
*/
20+
class Talker {
21+
/*
22+
* @property timeout - The number of milliseconds to wait before assuming no response will be received.
23+
*/
24+
public timeout: number = 3000;
25+
26+
/**
27+
* @property onMessage - Will be called with every non-handshake, non-response message from the remote window
28+
*/
29+
onMessage?: (message: IncomingMessage) => void;
30+
31+
// Will be resolved when a handshake is newly established with the remote window.
32+
private readonly handshake: ManipulablePromise<boolean>;
33+
// Whether we've received a handshake from the remote window
34+
private handshaken: boolean;
35+
// The ID of the latest OutgoingMessage
36+
private latestId: number = 0;
37+
private readonly queue: OutgoingMessage[] = [];
38+
private readonly sent: SentMessages = {};
39+
40+
/**
41+
* @param remoteWindow - The remote `window` object to post/receive messages to/from
42+
* @param remoteOrigin - The protocol, host, and port you expect the remoteWindow to be
43+
* @param localWindow - The local `window` object
44+
*/
45+
constructor(
46+
private readonly remoteWindow: Window,
47+
private readonly remoteOrigin: string,
48+
private readonly localWindow: Window = window
49+
) {
50+
this.handshaken = false;
51+
this.handshake = createManipulablePromise();
52+
53+
this.localWindow.addEventListener(
54+
"message",
55+
(messageEvent: MessageEvent) => this.receiveMessage(messageEvent),
56+
false
57+
);
58+
this.sendHandshake();
59+
60+
return this;
61+
}
62+
63+
/**
64+
* @param namespace - The namespace the message is in
65+
* @param data - The data to send
66+
* @param responseToId - If this is a response to a previous message, its ID.
67+
*/
68+
send(
69+
namespace: string,
70+
data: Stringifyable,
71+
responseToId: number | null = null
72+
): ManipulablePromise<IncomingMessage | Error> {
73+
const message: OutgoingMessage = new OutgoingMessage(
74+
this,
75+
namespace,
76+
data,
77+
responseToId
78+
);
79+
80+
const promise = createManipulablePromise<IncomingMessage | Error>();
81+
82+
this.sent[message.id] = promise;
83+
this.queue.push(message);
84+
this.flushQueue();
85+
86+
setTimeout(
87+
() =>
88+
promise.__reject__ && promise.__reject__(new Error(TALKER_ERR_TIMEOUT)),
89+
this.timeout
90+
);
91+
92+
return promise;
93+
}
94+
95+
/**
96+
* This is not marked private because other Talker-related classes need access to it,
97+
* but your application code should probably avoid calling this method.
98+
*/
99+
nextId(): number {
100+
return (this.latestId += 1);
101+
}
102+
103+
private receiveMessage(messageEvent: MessageEvent): void {
104+
let object: JSONifiedMessage;
105+
try {
106+
object = JSON.parse(messageEvent.data);
107+
} catch (err) {
108+
object = {
109+
namespace: "",
110+
data: {},
111+
id: this.nextId(),
112+
type: TALKER_TYPE
113+
};
114+
}
115+
if (
116+
!this.isSafeMessage(messageEvent.source, messageEvent.origin, object.type)
117+
) {
118+
return;
119+
}
120+
121+
const isHandshake = object.handshake || object.handshakeConfirmation;
122+
return isHandshake
123+
? this.handleHandshake(object)
124+
: this.handleMessage(object);
125+
}
126+
127+
/**
128+
* Determines whether it is safe and appropriate to parse a postMessage messageEvent
129+
* @param source - Source window object
130+
* @param origin - Protocol, host, and port
131+
* @param type - Internet Media Type
132+
*/
133+
private isSafeMessage(
134+
source: Window | MessagePort | ServiceWorker | null,
135+
origin: string,
136+
type: string
137+
): boolean {
138+
const isSourceSafe = source === this.remoteWindow;
139+
const isOriginSafe =
140+
this.remoteOrigin === "*" || origin === this.remoteOrigin;
141+
const isTypeSafe = type === TALKER_TYPE;
142+
return isSourceSafe && isOriginSafe && isTypeSafe;
143+
}
144+
145+
private handleHandshake(object: JSONifiedMessage): void {
146+
if (object.handshake) {
147+
// One last handshake in case the remote window (which we now know is ready) hasn't seen ours yet
148+
this.sendHandshake(this.handshaken);
149+
}
150+
if (!this.handshaken) {
151+
this.handshaken = true;
152+
if (this.handshake.__resolve__) {
153+
this.handshake.__resolve__(this.handshaken);
154+
}
155+
this.flushQueue();
156+
}
157+
}
158+
159+
private handleMessage(rawObject: JSONifiedMessage): void {
160+
const message = new IncomingMessage(
161+
this,
162+
rawObject.namespace,
163+
rawObject.data,
164+
rawObject.id
165+
);
166+
const responseId = rawObject.responseToId;
167+
return responseId
168+
? this.respondToMessage(responseId, message)
169+
: this.broadcastMessage(message);
170+
}
171+
172+
/**
173+
* @param id - Message ID of the waiting promise
174+
* @param message - Message that is responding to that ID
175+
*/
176+
private respondToMessage(id: number, message: IncomingMessage): void {
177+
const sent = this.sent[id];
178+
if (sent && sent.__resolve__) {
179+
sent.__resolve__(message);
180+
delete this.sent[id];
181+
}
182+
}
183+
184+
/**
185+
* Send a non-response message to awaiting hooks/callbacks
186+
* @param message Message that arrived
187+
*/
188+
private broadcastMessage(message: IncomingMessage): void {
189+
if (this.onMessage) {
190+
this.onMessage.call(this, message);
191+
}
192+
}
193+
194+
/**
195+
* Send a handshake message to the remote window
196+
* @param confirmation - Is this a confirmation handshake?
197+
*/
198+
private sendHandshake(confirmation: boolean = false): void {
199+
return this.postMessage({
200+
type: TALKER_TYPE,
201+
[confirmation ? "handshakeConfirmation" : "handshake"]: true
202+
});
203+
}
204+
205+
/**
206+
* Wrapper around window.postMessage to only send if we have the necessary objects
207+
*/
208+
private postMessage(data: OutgoingMessage | JSONifiedMessage): void {
209+
const message = JSON.stringify(data);
210+
if (this.remoteWindow && this.remoteOrigin) {
211+
try {
212+
this.remoteWindow.postMessage(message, this.remoteOrigin);
213+
} catch (e) {
214+
// no-op
215+
}
216+
}
217+
}
218+
219+
/**
220+
* Flushes the internal queue of outgoing messages, sending each one.
221+
* Does nothing if Talker has not handshaken with the remote.
222+
*/
223+
private flushQueue(): void {
224+
if (this.handshaken) {
225+
while (this.queue.length > 0) {
226+
const message = this.queue.shift();
227+
if (message) {
228+
this.postMessage(message);
229+
}
230+
}
231+
}
232+
}
233+
}
234+
235+
export { IncomingMessage, OutgoingMessage };
236+
export default Talker;

src/message.ts

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { Promise } from 'es6-promise';
2+
import { TALKER_TYPE } from './strings';
3+
import Talker from './index';
4+
5+
abstract class Message {
6+
protected readonly type: string = TALKER_TYPE;
7+
8+
constructor(
9+
/*
10+
* @property talker - A {@link Talker} instance that will be used to send responses
11+
*/
12+
protected readonly talker: Talker,
13+
/*
14+
* @property namespace - A namespace to with which to categorize messages
15+
*/
16+
public readonly namespace: string,
17+
public readonly data: Stringifyable,
18+
public readonly responseToId: number | null = null
19+
) {}
20+
}
21+
22+
export interface JSONifiedMessage {
23+
readonly namespace?: string;
24+
readonly data?: Stringifyable;
25+
readonly id?: number;
26+
readonly responseToId?: number;
27+
readonly type: string;
28+
readonly handshake?: boolean;
29+
readonly handshakeConfirmation?: boolean;
30+
}
31+
32+
export interface Stringifyable {
33+
[index: string]: string | number | Stringifyable | Stringifyable[] | boolean | null | undefined;
34+
}
35+
36+
// Consuming applications will almost never interact with this class.
37+
export class OutgoingMessage extends Message {
38+
public readonly id: number = this.talker.nextId();
39+
40+
/**
41+
* @param talker
42+
* @param namespace
43+
* @param data
44+
* @param responseToId - If this is a response to a previous message, its ID.
45+
*/
46+
constructor(
47+
protected readonly talker: Talker,
48+
public readonly namespace: string,
49+
public readonly data: Stringifyable,
50+
public readonly responseToId: number | null = null
51+
) {
52+
super(talker, namespace, data, responseToId);
53+
}
54+
55+
toJSON(): JSONifiedMessage {
56+
const { id, responseToId, namespace, data, type }: OutgoingMessage = this;
57+
return { id, responseToId: responseToId || undefined, namespace, data, type };
58+
}
59+
}
60+
61+
// Consuming applications will interact with this class, but will almost never manually create an instance.
62+
export class IncomingMessage extends Message {
63+
constructor(
64+
protected readonly talker: Talker,
65+
public readonly namespace: string = '',
66+
public readonly data: Stringifyable = {},
67+
// The ID of the message received from the remoteWindow
68+
public readonly id: number = 0
69+
) {
70+
super(talker, namespace, data);
71+
}
72+
73+
/**
74+
* Please note that this response message will use the same timeout as Talker#send.
75+
*/
76+
respond(data: Stringifyable): Promise<IncomingMessage | Error> {
77+
return this.talker.send(this.namespace, data, this.id);
78+
}
79+
}

src/strings.ts

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const TALKER_TYPE: string = 'application/x-talkerjs-v1+json';
2+
export const TALKER_ERR_TIMEOUT: string = 'Talker.js message timed out waiting for a response.';

0 commit comments

Comments
 (0)