forked from cozy/cozy-client-js
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathintents.js
150 lines (120 loc) · 4.69 KB
/
intents.js
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
import {cozyFetchJSON} from './fetch'
const intentClass = 'coz-intent'
// inject iframe for service in given element
function injectService (url, element, intent, data) {
const document = element.ownerDocument
if (!document) throw new Error('Cannot retrieve document object from given element')
const window = document.defaultView
if (!window) throw new Error('Cannot retrieve window object from document')
const iframe = document.createElement('iframe')
iframe.setAttribute('src', url)
iframe.classList.add(intentClass)
element.appendChild(iframe)
// Keeps only http://domain:port/
const serviceOrigin = url.split('/', 3).join('/')
return new Promise((resolve, reject) => {
let handshaken = false
const messageHandler = (event) => {
if (event.origin !== serviceOrigin) return
if (event.data.type === `intent-${intent._id}:ready`) {
handshaken = true
return event.source.postMessage(data, event.origin)
}
window.removeEventListener('message', messageHandler)
iframe.parentNode.removeChild(iframe)
if (event.data.type === `intent-${intent._id}:error`) {
return reject(new Error('Intent error'))
}
if (handshaken && event.data.type === `intent-${intent._id}:cancel`) {
return resolve(null)
}
if (handshaken && event.data.type === `intent-${intent._id}:done`) {
return resolve(event.data.document)
}
if (!handshaken) {
return reject(new Error('Unexpected handshake message from intent service'))
}
// We may be in a state where the messageHandler is still attached to then
// window, but will not be needed anymore. For example, the service failed
// before adding the `unload` listener, so no `intent:cancel` message has
// never been sent.
// So we simply ignore other messages, and this listener will stay here,
// waiting for a message which will never come, forever (almost).
}
window.addEventListener('message', messageHandler)
})
}
export function create (cozy, action, type, data = {}, permissions = []) {
if (!action) throw new Error(`Misformed intent, "action" property must be provided`)
if (!type) throw new Error(`Misformed intent, "type" property must be provided`)
const createPromise = cozyFetchJSON(cozy, 'POST', '/intents', {
data: {
type: 'io.cozy.intents',
attributes: {
action: action,
type: type,
data: data,
permissions: permissions
}
}
})
createPromise.start = (element) => {
return createPromise.then(intent => {
let service = intent.attributes.services && intent.attributes.services[0]
if (!service) {
return Promise.reject(new Error('Unable to find a service'))
}
return injectService(service.href, element, intent, data)
})
}
return createPromise
}
function listenClientData (intent, window) {
return new Promise((resolve, reject) => {
const messageEventListener = (event) => {
if (event.origin !== intent.attributes.client) return
window.removeEventListener('message', messageEventListener)
resolve(event.data)
}
window.addEventListener('message', messageEventListener)
window.parent.postMessage({
type: `intent-${intent._id}:ready`
}, intent.attributes.client)
})
}
// returns a service to communicate with intent client
export function createService (cozy, intentId, serviceWindow) {
serviceWindow = serviceWindow || typeof window !== 'undefined' && window
if (!serviceWindow) throw new Error('Intent service should be used in browser')
intentId = intentId || serviceWindow.location.search.split('=')[1]
if (!intentId) throw new Error('Cannot retrieve intent from URL')
return cozyFetchJSON(cozy, 'GET', `/intents/${intentId}`)
.then(intent => {
let terminated = false
const terminate = (message) => {
if (terminated) throw new Error('Intent service has already been terminated')
terminated = true
serviceWindow.parent.postMessage(message, intent.attributes.client)
}
const cancel = () => {
terminate({type: `intent-${intent._id}:cancel`})
}
// Prevent unfulfilled client promises when this window unloads for a
// reason or another.
serviceWindow.addEventListener('unload', () => {
if (!terminated) cancel()
})
return listenClientData(intent, serviceWindow)
.then(data => {
return {
getData: () => data,
getIntent: () => intent,
terminate: (doc) => terminate({
type: `intent-${intent._id}:done`,
document: doc
}),
cancel: cancel
}
})
})
}