@knowlearning/agents 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/browser/embedded.js +63 -51
- package/agents/browser/initialize.js +157 -7
- package/agents/generic.js +48 -47
- package/package.json +1 -1
- package/agents/browser/embed.js +0 -146
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v1 as uuid } from 'uuid'
|
|
1
|
+
import { validate as isUUID, v1 as uuid } from 'uuid'
|
|
2
2
|
import MutableProxy from '../../persistence/json.js'
|
|
3
3
|
|
|
4
4
|
export default function EmbeddedAgent() {
|
|
@@ -8,6 +8,12 @@ export default function EmbeddedAgent() {
|
|
|
8
8
|
const responses = {}
|
|
9
9
|
const watchers = {}
|
|
10
10
|
|
|
11
|
+
function removeWatcher(key, fn) {
|
|
12
|
+
const watcherIndex = watchers[key].findIndex(x => x === fn)
|
|
13
|
+
if (watcherIndex > -1) watchers[key].splice(watcherIndex, 1)
|
|
14
|
+
else console.warn('TRIED TO REMOVE WATCHER THAT DOES NOT EXIST')
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
async function send(message) {
|
|
12
18
|
const requestId = message.requestId || uuid()
|
|
13
19
|
|
|
@@ -46,9 +52,8 @@ export default function EmbeddedAgent() {
|
|
|
46
52
|
else resolve(data.response)
|
|
47
53
|
}
|
|
48
54
|
else if (data.ii !== undefined) {
|
|
49
|
-
const {
|
|
50
|
-
|
|
51
|
-
if (watchers[key]) watchers[key].forEach(fn => fn(data))
|
|
55
|
+
const { scope } = data
|
|
56
|
+
if (watchers[scope]) watchers[scope].forEach(fn => fn(data))
|
|
52
57
|
}
|
|
53
58
|
})
|
|
54
59
|
|
|
@@ -60,44 +65,29 @@ export default function EmbeddedAgent() {
|
|
|
60
65
|
// TODO: collapse into 1 patch and 1 interact call
|
|
61
66
|
interact(id, [{ op: 'add', path: ['active_type'], value: active_type }])
|
|
62
67
|
interact(id, [{ op: 'add', path: ['active'], value: active }])
|
|
68
|
+
return id
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
const tagTypeToTargetCache = {}
|
|
72
|
+
async function tagIfNotYetTaggedInSession(tag_type, target) {
|
|
73
|
+
const targetCache = tagTypeToTargetCache[tag_type]
|
|
74
|
+
if (targetCache && targetCache[target]) return
|
|
69
75
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
domain = domain || d
|
|
73
|
-
user = user || u
|
|
74
|
-
scope = scope || s
|
|
75
|
-
resolveKey(`${domain}/${user}/${scope}`)
|
|
76
|
-
resolve(send({ type: 'state', scope, user, domain }))
|
|
77
|
-
})
|
|
76
|
+
// always use absolute referene when tagging
|
|
77
|
+
if (!isUUID(target)) target = (await metadata(target)).id
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.then( async k => {
|
|
82
|
-
if (watchFn) throw new Error('Only one watcher allowed')
|
|
83
|
-
if (!watchers[k]) watchers[k] = []
|
|
84
|
-
watchers[k].push(fn)
|
|
85
|
-
watchFn = fn
|
|
86
|
-
})
|
|
87
|
-
return promise
|
|
88
|
-
}
|
|
79
|
+
if (!tagTypeToTargetCache[tag_type]) tagTypeToTargetCache[tag_type] = {}
|
|
80
|
+
tagTypeToTargetCache[tag_type][target] = true
|
|
89
81
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.then( k => {
|
|
93
|
-
watchers[k] = watchers[k].filter(x => x !== watchFn)
|
|
94
|
-
if (watchers[k].length === 0) delete watchers[k]
|
|
95
|
-
})
|
|
82
|
+
await tag(tag_type, target)
|
|
83
|
+
}
|
|
96
84
|
|
|
97
|
-
return promise
|
|
98
|
-
}
|
|
99
85
|
|
|
100
|
-
|
|
86
|
+
function watch(id, fn) {
|
|
87
|
+
tagIfNotYetTaggedInSession('connected', id)
|
|
88
|
+
if (!watchers[id]) watchers[id] = []
|
|
89
|
+
watchers[id].push(fn)
|
|
90
|
+
return () => removeWatcher(id, fn)
|
|
101
91
|
}
|
|
102
92
|
|
|
103
93
|
async function patch(root, scopes) {
|
|
@@ -105,23 +95,23 @@ export default function EmbeddedAgent() {
|
|
|
105
95
|
return send({ type: 'patch', root, scopes })
|
|
106
96
|
}
|
|
107
97
|
|
|
108
|
-
async function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
98
|
+
async function state(scope) {
|
|
99
|
+
tagIfNotYetTaggedInSession('connected', scope)
|
|
100
|
+
const startState = await send({ type: 'state', scope })
|
|
101
|
+
return new MutableProxy(startState, patch => {
|
|
102
|
+
const activePatch = structuredClone(patch)
|
|
103
|
+
activePatch.forEach(entry => entry.path.unshift('active'))
|
|
104
|
+
interact(scope, activePatch)
|
|
105
|
+
})
|
|
112
106
|
|
|
113
|
-
if (initialize) {
|
|
114
|
-
// TODO: consider setting up watcher and updating state
|
|
115
|
-
return new MutableProxy(await state(scope) || {}, handlePatch)
|
|
116
|
-
}
|
|
117
|
-
else return new MutableProxy({}, handlePatch)
|
|
118
107
|
}
|
|
119
108
|
|
|
120
109
|
function reset(scope) {
|
|
121
|
-
return interact(scope, [{ op: 'add', path:[], value: null }])
|
|
110
|
+
return interact(scope, [{ op: 'add', path:['active'], value: null }])
|
|
122
111
|
}
|
|
123
112
|
|
|
124
113
|
function interact(scope, patch) {
|
|
114
|
+
tagIfNotYetTaggedInSession('mutated', scope)
|
|
125
115
|
return send({ type: 'interact', scope, patch })
|
|
126
116
|
}
|
|
127
117
|
|
|
@@ -181,8 +171,27 @@ export default function EmbeddedAgent() {
|
|
|
181
171
|
return promise
|
|
182
172
|
}
|
|
183
173
|
|
|
184
|
-
function
|
|
185
|
-
return
|
|
174
|
+
function isValidMetadataMutation({ path, op, value }) {
|
|
175
|
+
return (
|
|
176
|
+
['active_type', 'name'].includes(path[0])
|
|
177
|
+
&& path.length === 1
|
|
178
|
+
&& typeof value === 'string' || op === 'remove'
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function metadata(scope) {
|
|
183
|
+
const md = await send({ type: 'metadata', scope })
|
|
184
|
+
return new MutableProxy(md, patch => {
|
|
185
|
+
const activePatch = structuredClone(patch)
|
|
186
|
+
activePatch.forEach(entry => {
|
|
187
|
+
if (!isValidMetadataMutation(entry)) throw new Error('You may only modify the type or name for a scope\'s metadata')
|
|
188
|
+
})
|
|
189
|
+
interact(scope, activePatch)
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function tag(tag_type, target, context=[]) {
|
|
194
|
+
return send({ type: 'tag', tag_type, target, context })
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
function login(provider, username, password) {
|
|
@@ -190,25 +199,28 @@ export default function EmbeddedAgent() {
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
function logout() { return send({ type: 'logout' }) }
|
|
193
|
-
|
|
194
202
|
function disconnect() { return send({ type: 'disconnect' }) }
|
|
195
|
-
|
|
196
203
|
function reconnect() { return send({ type: 'reconnect' }) }
|
|
204
|
+
function synced() { return send({ type: 'synced' }) }
|
|
197
205
|
|
|
198
206
|
return {
|
|
207
|
+
embedded: true,
|
|
208
|
+
uuid,
|
|
199
209
|
environment,
|
|
200
210
|
login,
|
|
201
211
|
logout,
|
|
202
212
|
create,
|
|
203
213
|
state,
|
|
214
|
+
watch,
|
|
204
215
|
upload,
|
|
205
216
|
download,
|
|
206
217
|
interact,
|
|
207
218
|
patch,
|
|
208
|
-
mutate,
|
|
209
219
|
reset,
|
|
210
220
|
metadata,
|
|
211
221
|
disconnect,
|
|
212
|
-
reconnect
|
|
222
|
+
reconnect,
|
|
223
|
+
synced,
|
|
224
|
+
tag
|
|
213
225
|
}
|
|
214
226
|
}
|
|
@@ -1,19 +1,169 @@
|
|
|
1
1
|
import RootAgent from './root.js'
|
|
2
2
|
import EmbeddedAgent from './embedded.js'
|
|
3
|
-
import
|
|
3
|
+
import { v1 as uuid, validate as validateUUID } from 'uuid'
|
|
4
4
|
|
|
5
|
-
let
|
|
5
|
+
let Agent
|
|
6
6
|
|
|
7
|
-
export default function() {
|
|
8
|
-
if (
|
|
7
|
+
export default function browserAgent() {
|
|
8
|
+
if (Agent) return Agent
|
|
9
9
|
|
|
10
10
|
let embedded
|
|
11
11
|
|
|
12
12
|
try { embedded = window.self !== window.top }
|
|
13
13
|
catch (e) { embedded = true }
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
Agent = embedded ? EmbeddedAgent() : RootAgent()
|
|
16
|
+
Agent.embed = embed
|
|
17
17
|
|
|
18
|
-
return
|
|
18
|
+
return Agent
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const copy = x => JSON.parse(JSON.stringify(x))
|
|
22
|
+
|
|
23
|
+
const watchers = {}
|
|
24
|
+
|
|
25
|
+
function embed(environment, iframe) {
|
|
26
|
+
const postMessageQueue = []
|
|
27
|
+
const listeners = {}
|
|
28
|
+
let frameLoaded = false
|
|
29
|
+
let embeddedAgentInitialized = false
|
|
30
|
+
|
|
31
|
+
const session = uuid()
|
|
32
|
+
|
|
33
|
+
const postMessage = m => new Promise((resolve, reject) => {
|
|
34
|
+
const message = { ...copy(m), session }
|
|
35
|
+
postMessageQueue.push({ message, sent: resolve })
|
|
36
|
+
if (frameLoaded) processPostMessageQueue()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const processPostMessageQueue = () => {
|
|
40
|
+
while (iframe.parentNode && postMessageQueue.length) {
|
|
41
|
+
const { message, sent } = postMessageQueue.shift()
|
|
42
|
+
iframe.contentWindow.postMessage(message, '*')
|
|
43
|
+
sent()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleMessage = async message => {
|
|
48
|
+
const { requestId, type } = message
|
|
49
|
+
|
|
50
|
+
const sendDown = response => postMessage({ requestId, response })
|
|
51
|
+
|
|
52
|
+
if (type === 'error') {
|
|
53
|
+
console.error(message)
|
|
54
|
+
sendDown({})
|
|
55
|
+
}
|
|
56
|
+
else if (type === 'close') {
|
|
57
|
+
if (listeners.close) listeners.close()
|
|
58
|
+
}
|
|
59
|
+
else if (type === 'environment') {
|
|
60
|
+
const env = await Agent.environment()
|
|
61
|
+
const context = [...env.context, environment.id]
|
|
62
|
+
Object.assign(env, { ...env, context })
|
|
63
|
+
sendDown(env)
|
|
64
|
+
}
|
|
65
|
+
else if (type === 'interact') {
|
|
66
|
+
const { scope, patch } = message
|
|
67
|
+
await Agent.interact(scope, patch)
|
|
68
|
+
if (listeners.mutate) listeners.mutate({ scope })
|
|
69
|
+
sendDown({}) // TODO: might want to send down the interaction index
|
|
70
|
+
}
|
|
71
|
+
else if (type === 'metadata') {
|
|
72
|
+
sendDown(await Agent.metadata(message.scope))
|
|
73
|
+
}
|
|
74
|
+
else if (type === 'tag') {
|
|
75
|
+
const { tag_type, target, context } = message
|
|
76
|
+
const prependedContext = [environment.id, ...context]
|
|
77
|
+
sendDown(await Agent.tag(tag_type, target, prependedContext))
|
|
78
|
+
}
|
|
79
|
+
else if (type === 'state') {
|
|
80
|
+
const { scope } = message
|
|
81
|
+
|
|
82
|
+
const statePromise = Agent.state(scope)
|
|
83
|
+
|
|
84
|
+
if (!watchers[scope]) watchers[scope] = Agent.watch(scope, postMessage)
|
|
85
|
+
|
|
86
|
+
if (listeners.state) listeners.state({ scope })
|
|
87
|
+
sendDown(await statePromise)
|
|
88
|
+
}
|
|
89
|
+
else if (type === 'patch') {
|
|
90
|
+
const { root, scopes } = message
|
|
91
|
+
sendDown(await Agent.patch(root, scopes))
|
|
92
|
+
}
|
|
93
|
+
else if (type === 'upload') {
|
|
94
|
+
const { name, contentType, id } = message
|
|
95
|
+
sendDown(await Agent.upload(name, contentType, undefined, id))
|
|
96
|
+
}
|
|
97
|
+
else if (type === 'download') {
|
|
98
|
+
sendDown(await Agent.download(message.id).url())
|
|
99
|
+
}
|
|
100
|
+
else if (type === 'login') {
|
|
101
|
+
const { provider, username, password } = message
|
|
102
|
+
sendDown(await Agent.login(provider, username, password))
|
|
103
|
+
}
|
|
104
|
+
else if (type === 'logout') Agent.logout()
|
|
105
|
+
else if (type === 'disconnect') sendDown(await Agent.disconnect())
|
|
106
|
+
else if (type === 'reconnect') sendDown(await Agent.reconnect())
|
|
107
|
+
else if (type === 'synced') sendDown(await Agent.synced())
|
|
108
|
+
else {
|
|
109
|
+
console.log('Unknown message type passed up...', message)
|
|
110
|
+
sendDown({})
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
window.addEventListener('message', ({ data }) => {
|
|
115
|
+
if (data.session === session) {
|
|
116
|
+
embeddedAgentInitialized = true
|
|
117
|
+
// TODO: ensure message index ordering!!!!!!!!!!!!!!!!!!!! (no order guarantee given in postMessage protocol, so we need to make a little buffer here)
|
|
118
|
+
handleMessage(data)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// write in a temporary loading notification while frame loads
|
|
123
|
+
const cw = iframe.contentWindow
|
|
124
|
+
if (cw) cw.document.body.innerHTML = 'Loading...'
|
|
125
|
+
|
|
126
|
+
// TODO: make sure content security policy headers for embedded domain always restrict iframe
|
|
127
|
+
// src to only self for embedded domain
|
|
128
|
+
iframe.onload = () => {
|
|
129
|
+
frameLoaded = true
|
|
130
|
+
processPostMessageQueue()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setUpEmbeddedFrame()
|
|
134
|
+
|
|
135
|
+
async function setUpEmbeddedFrame() {
|
|
136
|
+
browserAgent()
|
|
137
|
+
|
|
138
|
+
const { protocol } = window.location
|
|
139
|
+
const { id } = environment
|
|
140
|
+
if (validateUUID(id)) {
|
|
141
|
+
const { domain } = await Agent.metadata(id)
|
|
142
|
+
iframe.src = `${protocol}//${domain}/${id}`
|
|
143
|
+
}
|
|
144
|
+
else iframe.src = id // TODO: ensure is url
|
|
145
|
+
|
|
146
|
+
while(!embeddedAgentInitialized) {
|
|
147
|
+
postMessage({ type: 'setup', session })
|
|
148
|
+
await new Promise(r => setTimeout(r, 100))
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function remove () {
|
|
153
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function on(event, fn) {
|
|
157
|
+
listeners[event] = fn
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function auth(token, state) {
|
|
161
|
+
postMessage({ type: 'auth', token, state })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
auth,
|
|
166
|
+
remove,
|
|
167
|
+
on
|
|
168
|
+
}
|
|
19
169
|
}
|
package/agents/generic.js
CHANGED
|
@@ -5,6 +5,7 @@ const HEARTBEAT_TIMEOUT = 10000
|
|
|
5
5
|
const UPLOAD_TYPE = 'application/json;type=upload'
|
|
6
6
|
const SUBSCRIPTION_TYPE = 'application/json;type=subscription'
|
|
7
7
|
const POSTGRES_QUERY_TYPE = 'application/json;type=postgres-query'
|
|
8
|
+
const TAG_TYPE = 'application/json;type=tag'
|
|
8
9
|
|
|
9
10
|
// transform our custom path implementation to the standard JSONPatch path
|
|
10
11
|
function standardJSONPatch(patch) {
|
|
@@ -13,6 +14,10 @@ function standardJSONPatch(patch) {
|
|
|
13
14
|
})
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
function isUUID(string) {
|
|
18
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(string)
|
|
19
|
+
}
|
|
20
|
+
|
|
16
21
|
function sanitizeJSONPatchPathSegment(s) {
|
|
17
22
|
if (typeof s === "string") return s.replaceAll('~', '~0').replaceAll('/', '~1')
|
|
18
23
|
else return s
|
|
@@ -22,8 +27,9 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
22
27
|
let ws
|
|
23
28
|
let user
|
|
24
29
|
let domain
|
|
25
|
-
|
|
30
|
+
let session
|
|
26
31
|
let server
|
|
32
|
+
let isSynced = false
|
|
27
33
|
let si = -1
|
|
28
34
|
let authed = false
|
|
29
35
|
const states = {}
|
|
@@ -31,6 +37,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
31
37
|
const watchers = {}
|
|
32
38
|
const keyToSubscriptionId = {}
|
|
33
39
|
const lastInteractionResponse = {}
|
|
40
|
+
const tagTypeToTargetCache = {}
|
|
34
41
|
const messageQueue = []
|
|
35
42
|
let resolveEnvironment
|
|
36
43
|
let disconnected = false
|
|
@@ -41,7 +48,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
41
48
|
let lastHeartbeat
|
|
42
49
|
const syncedPromiseResolutions = []
|
|
43
50
|
|
|
44
|
-
const subscriptions = {}
|
|
45
51
|
const patches = state('patches')
|
|
46
52
|
|
|
47
53
|
log('INITIALIZING AGENT CONNECTION')
|
|
@@ -52,9 +58,8 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
function resolveSyncPromises() {
|
|
55
|
-
while (syncedPromiseResolutions.length)
|
|
56
|
-
|
|
57
|
-
}
|
|
61
|
+
while (syncedPromiseResolutions.length) syncedPromiseResolutions.shift()()
|
|
62
|
+
isSynced = true
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
function removeWatcher(key, fn) {
|
|
@@ -76,6 +81,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
function queueMessage(message) {
|
|
84
|
+
isSynced = false
|
|
79
85
|
si += 1
|
|
80
86
|
const promise = new Promise((resolve, reject) => responses[si] = [[resolve, reject]])
|
|
81
87
|
messageQueue.push({...message, si, ts: Date.now()})
|
|
@@ -136,6 +142,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
136
142
|
authed = true
|
|
137
143
|
if (!user) { // this is the first authed websocket connection
|
|
138
144
|
user = message.auth.user
|
|
145
|
+
session = message.session
|
|
139
146
|
domain = message.domain
|
|
140
147
|
server = message.server
|
|
141
148
|
resolveEnvironment(message)
|
|
@@ -152,14 +159,13 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
152
159
|
else {
|
|
153
160
|
if (message.si !== undefined) {
|
|
154
161
|
if (responses[message.si]) {
|
|
155
|
-
// TODO: remove "acknowledged" messages
|
|
162
|
+
// TODO: remove "acknowledged" messages from queue and do accounting with si
|
|
156
163
|
responses[message.si]
|
|
157
164
|
.forEach(([resolve, reject]) => {
|
|
158
165
|
message.error ? reject(message) : resolve(message)
|
|
159
166
|
})
|
|
160
167
|
delete responses[message.si]
|
|
161
168
|
ws.send(JSON.stringify({ack: message.si})) // acknowledgement that we have received the response for this message
|
|
162
|
-
|
|
163
169
|
if (Object.keys(responses).length === 0) {
|
|
164
170
|
resolveSyncPromises()
|
|
165
171
|
}
|
|
@@ -170,14 +176,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
else {
|
|
173
|
-
const sub = subscriptions[keyToSubscriptionId[message.scope]]
|
|
174
179
|
if (watchers[message.scope]) {
|
|
175
|
-
// TODO: debug case where sub is not defined
|
|
176
|
-
if (sub && sub.ii + 1 !== message.ii) {
|
|
177
|
-
// TODO: this seems to be an error that happens with decent regularity (an answer with a given si was skipped/failed)
|
|
178
|
-
// we should be wary of out-of-order ii being passed down (maybe need to wait for older ones???)
|
|
179
|
-
console.warn('UNEXPECTED UPDATE INTERACTION INDEX!!!!!!!!!!! last index in session', sub, ' passed index ', message.ii)
|
|
180
|
-
}
|
|
181
180
|
states[message.scope] = await states[message.scope]
|
|
182
181
|
|
|
183
182
|
const lastResetPatchIndex = message.patch.findLastIndex(p => p.path.length === 0)
|
|
@@ -190,7 +189,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
190
189
|
const state = structuredClone(states[message.scope].active)
|
|
191
190
|
fn({ ...message, state })
|
|
192
191
|
})
|
|
193
|
-
if (sub) sub.ii = message.ii
|
|
194
192
|
}
|
|
195
193
|
}
|
|
196
194
|
}
|
|
@@ -214,25 +212,39 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
214
212
|
|
|
215
213
|
function create({ id=uuid(), active_type, active }) {
|
|
216
214
|
// TODO: collapse into 1 patch and 1 interact call
|
|
217
|
-
interact(id, [{ op: 'add', path: ['active_type'], value: active_type }])
|
|
218
|
-
interact(id, [{ op: 'add', path: ['active'], value: active }])
|
|
215
|
+
interact(id, [{ op: 'add', path: ['active_type'], value: active_type }], false)
|
|
216
|
+
interact(id, [{ op: 'add', path: ['active'], value: active }], false)
|
|
217
|
+
return id
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function tagIfNotYetTaggedInSession(tag_type, target) {
|
|
221
|
+
const targetCache = tagTypeToTargetCache[tag_type]
|
|
222
|
+
if (targetCache && targetCache[target]) return
|
|
223
|
+
|
|
224
|
+
if (!targetCache) tagTypeToTargetCache[tag_type] = {}
|
|
225
|
+
tagTypeToTargetCache[tag_type][target] = true
|
|
226
|
+
|
|
227
|
+
// always use absolute referene when tagging
|
|
228
|
+
if (!isUUID(target)) target = (await metadata(target)).id
|
|
229
|
+
|
|
230
|
+
await tag(tag_type, target)
|
|
219
231
|
}
|
|
220
232
|
|
|
221
|
-
function environment() {
|
|
233
|
+
async function environment() {
|
|
234
|
+
return { ...(await environmentPromise), context: [] }
|
|
235
|
+
}
|
|
222
236
|
|
|
223
237
|
function state(scope) {
|
|
224
|
-
|
|
225
|
-
|
|
238
|
+
tagIfNotYetTaggedInSession('connected', scope)
|
|
239
|
+
return new Promise(async (resolveState, rejectState) => {
|
|
226
240
|
if (!keyToSubscriptionId[scope]) {
|
|
227
241
|
const id = uuid()
|
|
228
242
|
|
|
229
243
|
keyToSubscriptionId[scope] = id
|
|
230
244
|
watchers[scope] = []
|
|
231
245
|
states[scope] = new Promise(async (resolve, reject) => {
|
|
232
|
-
|
|
233
|
-
const subscriptionId = uuid()
|
|
234
246
|
create({
|
|
235
|
-
id
|
|
247
|
+
id,
|
|
236
248
|
active_type: SUBSCRIPTION_TYPE,
|
|
237
249
|
active: { session, scope, ii: null }
|
|
238
250
|
})
|
|
@@ -240,7 +252,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
240
252
|
try {
|
|
241
253
|
const state = await lastMessageResponse()
|
|
242
254
|
// TODO: replace with editing scope of type
|
|
243
|
-
interact(
|
|
255
|
+
interact(id, [{
|
|
244
256
|
op: 'add',
|
|
245
257
|
path: ['active', 'ii'],
|
|
246
258
|
value: 1 // TODO: use state.ii when is coming down properly...
|
|
@@ -253,7 +265,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
253
265
|
// ii does not apply (we clear out the subscription to not cache value)
|
|
254
266
|
delete states[scope]
|
|
255
267
|
delete keyToSubscriptionId[scope]
|
|
256
|
-
delete subscriptions[id]
|
|
257
268
|
}
|
|
258
269
|
}
|
|
259
270
|
catch (error) {
|
|
@@ -277,26 +288,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
277
288
|
rejectState(error)
|
|
278
289
|
}
|
|
279
290
|
})
|
|
280
|
-
|
|
281
|
-
promise.watch = fn => {
|
|
282
|
-
if (watchFn) throw new Error('Only one watcher allowed')
|
|
283
|
-
if (!watchers[scope]) watchers[scope] = []
|
|
284
|
-
watchers[scope].push(fn)
|
|
285
|
-
watchFn = fn
|
|
286
|
-
|
|
287
|
-
return promise
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
promise.unwatch = async () => {
|
|
291
|
-
removeWatcher(scope, watchFn)
|
|
292
|
-
if (watchers[scope].length === 0) {
|
|
293
|
-
delete subscriptions[keyToSubscriptionId[scope]]
|
|
294
|
-
delete keyToSubscriptionId[scope]
|
|
295
|
-
delete watchers[scope]
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return promise
|
|
300
291
|
}
|
|
301
292
|
|
|
302
293
|
function watch(id, fn) {
|
|
@@ -352,7 +343,9 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
352
343
|
return { swaps }
|
|
353
344
|
}
|
|
354
345
|
|
|
355
|
-
|
|
346
|
+
// TODO: addTag option should probably not be exposed
|
|
347
|
+
async function interact(scope, patch, addTag=true) {
|
|
348
|
+
if (addTag) tagIfNotYetTaggedInSession('mutated', scope)
|
|
356
349
|
// TODO: ensure user is owner of scope
|
|
357
350
|
const response = queueMessage({scope, patch})
|
|
358
351
|
|
|
@@ -409,7 +402,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
409
402
|
}
|
|
410
403
|
|
|
411
404
|
async function synced() {
|
|
412
|
-
return new Promise(resolve => syncedPromiseResolutions.push(resolve))
|
|
405
|
+
return isSynced ? null : new Promise(resolve => syncedPromiseResolutions.push(resolve))
|
|
413
406
|
}
|
|
414
407
|
|
|
415
408
|
function disconnect() {
|
|
@@ -436,6 +429,13 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
436
429
|
return lastMessageResponse()
|
|
437
430
|
}
|
|
438
431
|
|
|
432
|
+
function tag(tag_type, target, context=[]) {
|
|
433
|
+
return create({
|
|
434
|
+
active_type: TAG_TYPE,
|
|
435
|
+
active: { tag_type, target, context }
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
439
439
|
return {
|
|
440
440
|
uuid,
|
|
441
441
|
environment,
|
|
@@ -455,6 +455,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
455
455
|
synced,
|
|
456
456
|
disconnect,
|
|
457
457
|
reconnect,
|
|
458
|
+
tag,
|
|
458
459
|
debug
|
|
459
460
|
}
|
|
460
461
|
}
|
package/package.json
CHANGED
package/agents/browser/embed.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { v1 as uuid, validate as validateUUID } from 'uuid'
|
|
2
|
-
|
|
3
|
-
const copy = x => JSON.parse(JSON.stringify(x))
|
|
4
|
-
|
|
5
|
-
const watchers = {}
|
|
6
|
-
|
|
7
|
-
export default function embed(environment, iframe) {
|
|
8
|
-
const postMessageQueue = []
|
|
9
|
-
const listeners = {}
|
|
10
|
-
let frameLoaded = false
|
|
11
|
-
let embeddedAgentInitialized = false
|
|
12
|
-
|
|
13
|
-
const session = uuid()
|
|
14
|
-
|
|
15
|
-
const postMessage = m => new Promise((resolve, reject) => {
|
|
16
|
-
const message = { ...copy(m), session }
|
|
17
|
-
postMessageQueue.push({ message, sent: resolve })
|
|
18
|
-
if (frameLoaded) processPostMessageQueue()
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const processPostMessageQueue = () => {
|
|
22
|
-
while (iframe.parentNode && postMessageQueue.length) {
|
|
23
|
-
const { message, sent } = postMessageQueue.shift()
|
|
24
|
-
iframe.contentWindow.postMessage(message, '*')
|
|
25
|
-
sent()
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const handleMessage = async message => {
|
|
30
|
-
const { requestId, type } = message
|
|
31
|
-
|
|
32
|
-
const sendDown = response => {
|
|
33
|
-
if (message.type === 'environment') Object.assign(response, environment)
|
|
34
|
-
return postMessage({ requestId, response })
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (type === 'error') {
|
|
38
|
-
console.error(message)
|
|
39
|
-
sendDown({})
|
|
40
|
-
}
|
|
41
|
-
else if (type === 'close') {
|
|
42
|
-
if (listeners.close) listeners.close()
|
|
43
|
-
}
|
|
44
|
-
else if (type === 'environment') {
|
|
45
|
-
sendDown(await Agent.environment())
|
|
46
|
-
}
|
|
47
|
-
else if (type === 'interact') {
|
|
48
|
-
const { scope, patch } = message
|
|
49
|
-
await Agent.interact(scope, patch)
|
|
50
|
-
if (listeners.mutate) listeners.mutate({ scope })
|
|
51
|
-
sendDown({}) // TODO: might want to send down the interaction index
|
|
52
|
-
}
|
|
53
|
-
else if (type === 'state') {
|
|
54
|
-
const { scope, user, domain } = message
|
|
55
|
-
|
|
56
|
-
const statePromise = Agent.state(scope, user, domain)
|
|
57
|
-
|
|
58
|
-
const key = `${session}/${domain}/${user}/${scope}`
|
|
59
|
-
|
|
60
|
-
// only watch once per key per experience session
|
|
61
|
-
if (!watchers[key]) {
|
|
62
|
-
watchers[key] = statePromise.unwatch // TODO: consider how to use this...
|
|
63
|
-
statePromise.watch(postMessage)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (listeners.state) listeners.state({ scope, user, domain })
|
|
67
|
-
sendDown(await statePromise)
|
|
68
|
-
}
|
|
69
|
-
else if (type === 'patch') {
|
|
70
|
-
const { root, scopes } = message
|
|
71
|
-
sendDown(await Agent.patch(root, scopes))
|
|
72
|
-
}
|
|
73
|
-
else if (type === 'upload') {
|
|
74
|
-
const { name, contentType, id } = message
|
|
75
|
-
sendDown(await Agent.upload(name, contentType, false, id))
|
|
76
|
-
}
|
|
77
|
-
else if (type === 'download') {
|
|
78
|
-
sendDown(await Agent.download(message.id).url())
|
|
79
|
-
}
|
|
80
|
-
else if (type === 'login') {
|
|
81
|
-
const { provider, username, password } = message
|
|
82
|
-
sendDown(await Agent.login(provider, username, password))
|
|
83
|
-
}
|
|
84
|
-
else if (type === 'logout') Agent.logout()
|
|
85
|
-
else if (type === 'disconnect') sendDown(await Agent.disconnect())
|
|
86
|
-
else if (type === 'reconnect') sendDown(await Agent.reconnect())
|
|
87
|
-
else {
|
|
88
|
-
console.log('Unknown message type passed up...', message)
|
|
89
|
-
sendDown({})
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
window.addEventListener('message', ({ data }) => {
|
|
94
|
-
if (data.session === session) {
|
|
95
|
-
embeddedAgentInitialized = true
|
|
96
|
-
// TODO: ensure message index ordering!!!!!!!!!!!!!!!!!!!! (no order guarantee given in postMessage protocol, so we need to make a little buffer here)
|
|
97
|
-
handleMessage(data)
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// write in a temporary loading notification while frame loads
|
|
102
|
-
const cw = iframe.contentWindow
|
|
103
|
-
if (cw) cw.document.body.innerHTML = 'Loading...'
|
|
104
|
-
|
|
105
|
-
// TODO: make sure content security policy headers for embedded domain always restrict iframe
|
|
106
|
-
// src to only self for embedded domain
|
|
107
|
-
iframe.onload = () => {
|
|
108
|
-
frameLoaded = true
|
|
109
|
-
processPostMessageQueue()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
setUpEmbeddedFrame()
|
|
113
|
-
|
|
114
|
-
async function setUpEmbeddedFrame() {
|
|
115
|
-
const { protocol } = window.location
|
|
116
|
-
const { id } = environment
|
|
117
|
-
if (validateUUID(id)) {
|
|
118
|
-
const { domain } = await Agent.metadata(id)
|
|
119
|
-
iframe.src = `${protocol}//${domain}/${id}`
|
|
120
|
-
}
|
|
121
|
-
else iframe.src = id // TODO: ensure is url
|
|
122
|
-
|
|
123
|
-
while(!embeddedAgentInitialized) {
|
|
124
|
-
postMessage({ type: 'setup', session })
|
|
125
|
-
await new Promise(r => setTimeout(r, 100))
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function remove () {
|
|
130
|
-
if (iframe.parentNode) iframe.parentNode.removeChild(iframe)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function on(event, fn) {
|
|
134
|
-
listeners[event] = fn
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function auth(token, state) {
|
|
138
|
-
postMessage({ type: 'auth', token, state })
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
auth,
|
|
143
|
-
remove,
|
|
144
|
-
on
|
|
145
|
-
}
|
|
146
|
-
}
|