-
Notifications
You must be signed in to change notification settings - Fork 9
/
index.ts
176 lines (156 loc) · 6.02 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import Logger from "../app/src/collector/logger"
import { TreeCollector } from "../app/src/instrumentation/rxjs-5.x.x/collector"
import Instrumentation from "../app/src/instrumentation/rxjs-5.x.x/instrumentation"
import * as WebSocketType from "ws"
// Optional ws import
const WebSocket: typeof WebSocketType = (() => { try { return require("ws") } catch(e) { } })()
// Unused, but will support multi-version instrumention in the future maybe
// interface RxFiddleOptions {
// version?: 4 | 5 // default "detects"
// targets?: any[] // default "all available"
// serve?: { port: number, networkInterface?: string } // default off
// }
export type TeardownLogic = Function
export type OnNext = (m: any) => void
export type PartialObserver = OnNext | { next: OnNext } | { onNext: OnNext }
export default class RxFiddle {
constructor(private targets: { [name: string]: any } = {}) {
if("Rx" in targets) {
Object.assign(targets, {
Observable: targets.Rx.Observable,
Subscriber: targets.Rx.Subscriber,
})
}
Object.keys(targets).forEach(name => {
if(
(name === "Observable" || name === "Subscriber") &&
Object.keys(targets).map(name => targets[name]).indexOf(targets[name].prototype) < 0
) {
targets[name + "Proto"] = targets[name].prototype
}
})
this.targets = targets
}
/**
* Setup instrumentation and forward all messages to the provided Observer
* @param observer
*/
public subscribe(observer: PartialObserver): TeardownLogic {
let next: OnNext = typeof observer === "function" ? observer : () => { /* */ }
if (typeof observer === "object") {
next = ((observer as any).next || (observer as any).onNext).bind(observer)
}
let logger = new Logger(m => next(m))
return this.instrumentRx5(logger)
}
/**
* Auto detect the environment
* @param param Optional, options to pass to openWindow or serve.
*/
public auto(options?: any): TeardownLogic {
if(typeof window === "object") {
console.log("RxFiddle detected a web browser.")
return this.openWindow(options)
} else if(typeof process === "object") {
console.log("RxFiddle detected Node.")
return this.serve(options)
} else {
console.log("RxFiddle could not detect the JavaScript environment. Please use either RxFiddle.serve or RxFiddle.openWindow.")
}
}
/**
* Setup instrumentation and open a window, publish all messages there
* @param param Optional, specify which RxFiddle instance to use. Defaults to rxfiddle.net,
* but you can also run your own RxFiddle on your own localhost.
*/
public openWindow({ address, origin, window: frame }: { address?: string, origin?: string, window?: Window } = {}): TeardownLogic {
if(typeof window !== "object") {
if(typeof process === "object") {
console.warn("To use RxFiddle.openWindow, you need to run your app in the web browser. Consider using RxFiddle.serve since you're running in NodeJS.")
} else {
console.warn("To use RxFiddle.openWindow, you need to run your app in the web browser.")
}
}
if(!address) {
address = `https://rxfiddle.net/#type=postMessage`
origin = "https://rxfiddle.net"
}
// Open RxFiddle window and prepare to clean it up on reload or unload
let w = frame || window.open(address, '_blank', 'toolbar=0,location=0,menubar=0')
window.addEventListener("unload", () => !w.closed && w.close())
let first = true
let replayQueue = [] as any[]
// Ping until the RxFiddle window replies
let interval = setInterval(() => w.postMessage({ type: "ping" }, origin), 100)
function ready() {
clearInterval(interval)
window.removeEventListener("message", detectPong)
replayQueue.forEach(m => w.postMessage(m, origin))
replayQueue = null
}
let detectPong = (m: MessageEvent) => { m.origin === origin && "pong" === m.data.type && ready() }
window.addEventListener("message", detectPong)
let teardown = this.subscribe((m: any) => {
if(first) {
console.log("RxFiddle detected Observables and now publishes the data.")
first = false
}
if(replayQueue === null) {
w.postMessage(m, origin)
} else {
replayQueue.push(m)
}
})
return () => {
teardown()
!w.closed && w.close()
}
}
/**
* Setup instrumentation and a WebSocketServer and publish all messages there
* @param param Specify a port
*/
public serve({ port }: { port?: number, networkInterface?: string } = {}): TeardownLogic {
if(!WebSocket) {
console.warn("To use RxFiddle.serve, install the websocket library ws using:\n npm i ws")
return () => {}
}
if(!port) {
port = 8080
}
let replayQueue = [] as any[]
let wss = new WebSocket.Server({ perMessageDeflate: false, port })
console.log(`RxFiddle server is serving at port ${port}. Surf to https://rxfiddle.net/#type=ws&url=ws://127.0.0.1:${port}.`)
let first = true
// Subscribe and send to all clients
let teardown = this.subscribe((m: any) => {
if(first) {
console.log("RxFiddle detected Observables and now publishes the data.")
first = false
}
let json = JSON.stringify(m)
wss.clients.forEach(ws => ws.send(json))
replayQueue.push(json)
})
// Replay for new connections
wss.on("connection", (ws) => {
console.log("RxFiddle client connected.")
ws.send(`{ "version": "1.0.0" }`)
replayQueue.forEach(m => ws.send(m))
})
// Cleanup
return () => {
teardown()
wss.close((err) => console.warn("Error while closing RxFiddle WebSocket server.", err))
}
}
private instrumentRx5(logger: Logger): TeardownLogic {
// Attach intrumentation
let instrumentation = new Instrumentation(new TreeCollector(logger, this.targets as any), this.targets as any)
instrumentation.setup()
Object.keys(this.targets).forEach(name =>
instrumentation.setup(this.targets[name], name)
)
return instrumentation.teardown
}
}