@knowlearning/agents 0.7.7 → 0.8.1
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 +69 -51
- package/agents/browser/initialize.js +158 -7
- package/agents/browser/root.js +3 -3
- package/agents/generic.js +50 -49
- package/browser.js +2 -1
- package/package.json +1 -1
- package/vue/3/content.vue +1 -0
- package/vue/3/name.vue +45 -0
- 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('subscribed', 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,27 @@ export default function EmbeddedAgent() {
|
|
|
105
95
|
return send({ type: 'patch', root, scopes })
|
|
106
96
|
}
|
|
107
97
|
|
|
108
|
-
async function
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (initialize) {
|
|
114
|
-
// TODO: consider setting up watcher and updating state
|
|
115
|
-
return new MutableProxy(await state(scope) || {}, handlePatch)
|
|
98
|
+
async function state(scope) {
|
|
99
|
+
if (scope === undefined) {
|
|
100
|
+
const { context } = await environment()
|
|
101
|
+
scope = JSON.stringify(context)
|
|
116
102
|
}
|
|
117
|
-
|
|
103
|
+
tagIfNotYetTaggedInSession('subscribed', scope)
|
|
104
|
+
const startState = await send({ type: 'state', scope })
|
|
105
|
+
return new MutableProxy(startState, patch => {
|
|
106
|
+
const activePatch = structuredClone(patch)
|
|
107
|
+
activePatch.forEach(entry => entry.path.unshift('active'))
|
|
108
|
+
interact(scope, activePatch)
|
|
109
|
+
})
|
|
110
|
+
|
|
118
111
|
}
|
|
119
112
|
|
|
120
113
|
function reset(scope) {
|
|
121
|
-
return interact(scope, [{ op: 'add', path:[], value: null }])
|
|
114
|
+
return interact(scope, [{ op: 'add', path:['active'], value: null }])
|
|
122
115
|
}
|
|
123
116
|
|
|
124
117
|
function interact(scope, patch) {
|
|
118
|
+
tagIfNotYetTaggedInSession('mutated', scope)
|
|
125
119
|
return send({ type: 'interact', scope, patch })
|
|
126
120
|
}
|
|
127
121
|
|
|
@@ -181,8 +175,27 @@ export default function EmbeddedAgent() {
|
|
|
181
175
|
return promise
|
|
182
176
|
}
|
|
183
177
|
|
|
184
|
-
function
|
|
185
|
-
return
|
|
178
|
+
function isValidMetadataMutation({ path, op, value }) {
|
|
179
|
+
return (
|
|
180
|
+
['active_type', 'name'].includes(path[0])
|
|
181
|
+
&& path.length === 1
|
|
182
|
+
&& typeof value === 'string' || op === 'remove'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function metadata(scope) {
|
|
187
|
+
const md = await send({ type: 'metadata', scope })
|
|
188
|
+
return new MutableProxy(md, patch => {
|
|
189
|
+
const activePatch = structuredClone(patch)
|
|
190
|
+
activePatch.forEach(entry => {
|
|
191
|
+
if (!isValidMetadataMutation(entry)) throw new Error('You may only modify the type or name for a scope\'s metadata')
|
|
192
|
+
})
|
|
193
|
+
interact(scope, activePatch)
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function tag(tag_type, target, context=[]) {
|
|
198
|
+
return send({ type: 'tag', tag_type, target, context })
|
|
186
199
|
}
|
|
187
200
|
|
|
188
201
|
function login(provider, username, password) {
|
|
@@ -190,25 +203,30 @@ export default function EmbeddedAgent() {
|
|
|
190
203
|
}
|
|
191
204
|
|
|
192
205
|
function logout() { return send({ type: 'logout' }) }
|
|
193
|
-
|
|
194
206
|
function disconnect() { return send({ type: 'disconnect' }) }
|
|
195
|
-
|
|
196
207
|
function reconnect() { return send({ type: 'reconnect' }) }
|
|
208
|
+
function synced() { return send({ type: 'synced' }) }
|
|
209
|
+
function close() { return send({ type: 'close' }) }
|
|
197
210
|
|
|
198
211
|
return {
|
|
212
|
+
embedded: true,
|
|
213
|
+
uuid,
|
|
199
214
|
environment,
|
|
200
215
|
login,
|
|
201
216
|
logout,
|
|
202
217
|
create,
|
|
203
218
|
state,
|
|
219
|
+
watch,
|
|
204
220
|
upload,
|
|
205
221
|
download,
|
|
206
222
|
interact,
|
|
207
223
|
patch,
|
|
208
|
-
mutate,
|
|
209
224
|
reset,
|
|
210
225
|
metadata,
|
|
211
226
|
disconnect,
|
|
212
|
-
reconnect
|
|
227
|
+
reconnect,
|
|
228
|
+
synced,
|
|
229
|
+
close,
|
|
230
|
+
tag
|
|
213
231
|
}
|
|
214
232
|
}
|
|
@@ -1,19 +1,170 @@
|
|
|
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
|
+
console.log('closing?', listeners)
|
|
58
|
+
if (listeners.close) listeners.close()
|
|
59
|
+
}
|
|
60
|
+
else if (type === 'environment') {
|
|
61
|
+
const env = await Agent.environment()
|
|
62
|
+
const context = [...env.context, environment.id]
|
|
63
|
+
Object.assign(env, { ...env, context })
|
|
64
|
+
sendDown(env)
|
|
65
|
+
}
|
|
66
|
+
else if (type === 'interact') {
|
|
67
|
+
const { scope, patch } = message
|
|
68
|
+
await Agent.interact(scope, patch)
|
|
69
|
+
if (listeners.mutate) listeners.mutate({ scope })
|
|
70
|
+
sendDown({}) // TODO: might want to send down the interaction index
|
|
71
|
+
}
|
|
72
|
+
else if (type === 'metadata') {
|
|
73
|
+
sendDown(await Agent.metadata(message.scope))
|
|
74
|
+
}
|
|
75
|
+
else if (type === 'tag') {
|
|
76
|
+
const { tag_type, target, context } = message
|
|
77
|
+
const prependedContext = [environment.id, ...context]
|
|
78
|
+
sendDown(await Agent.tag(tag_type, target, prependedContext))
|
|
79
|
+
}
|
|
80
|
+
else if (type === 'state') {
|
|
81
|
+
const { scope } = message
|
|
82
|
+
|
|
83
|
+
const statePromise = Agent.state(scope)
|
|
84
|
+
|
|
85
|
+
if (!watchers[scope]) watchers[scope] = Agent.watch(scope, postMessage)
|
|
86
|
+
|
|
87
|
+
if (listeners.state) listeners.state({ scope })
|
|
88
|
+
sendDown(await statePromise)
|
|
89
|
+
}
|
|
90
|
+
else if (type === 'patch') {
|
|
91
|
+
const { root, scopes } = message
|
|
92
|
+
sendDown(await Agent.patch(root, scopes))
|
|
93
|
+
}
|
|
94
|
+
else if (type === 'upload') {
|
|
95
|
+
const { name, contentType, id } = message
|
|
96
|
+
sendDown(await Agent.upload(name, contentType, undefined, id))
|
|
97
|
+
}
|
|
98
|
+
else if (type === 'download') {
|
|
99
|
+
sendDown(await Agent.download(message.id).url())
|
|
100
|
+
}
|
|
101
|
+
else if (type === 'login') {
|
|
102
|
+
const { provider, username, password } = message
|
|
103
|
+
sendDown(await Agent.login(provider, username, password))
|
|
104
|
+
}
|
|
105
|
+
else if (type === 'logout') Agent.logout()
|
|
106
|
+
else if (type === 'disconnect') sendDown(await Agent.disconnect())
|
|
107
|
+
else if (type === 'reconnect') sendDown(await Agent.reconnect())
|
|
108
|
+
else if (type === 'synced') sendDown(await Agent.synced())
|
|
109
|
+
else {
|
|
110
|
+
console.log('Unknown message type passed up...', message)
|
|
111
|
+
sendDown({})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
window.addEventListener('message', ({ data }) => {
|
|
116
|
+
if (data.session === session) {
|
|
117
|
+
embeddedAgentInitialized = true
|
|
118
|
+
// TODO: ensure message index ordering!!!!!!!!!!!!!!!!!!!! (no order guarantee given in postMessage protocol, so we need to make a little buffer here)
|
|
119
|
+
handleMessage(data)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// write in a temporary loading notification while frame loads
|
|
124
|
+
const cw = iframe.contentWindow
|
|
125
|
+
if (cw) cw.document.body.innerHTML = 'Loading...'
|
|
126
|
+
|
|
127
|
+
// TODO: make sure content security policy headers for embedded domain always restrict iframe
|
|
128
|
+
// src to only self for embedded domain
|
|
129
|
+
iframe.onload = () => {
|
|
130
|
+
frameLoaded = true
|
|
131
|
+
processPostMessageQueue()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setUpEmbeddedFrame()
|
|
135
|
+
|
|
136
|
+
async function setUpEmbeddedFrame() {
|
|
137
|
+
browserAgent()
|
|
138
|
+
|
|
139
|
+
const { protocol } = window.location
|
|
140
|
+
const { id } = environment
|
|
141
|
+
if (validateUUID(id)) {
|
|
142
|
+
const { domain } = await Agent.metadata(id)
|
|
143
|
+
iframe.src = `${protocol}//${domain}/${id}`
|
|
144
|
+
}
|
|
145
|
+
else iframe.src = id // TODO: ensure is url
|
|
146
|
+
|
|
147
|
+
while(!embeddedAgentInitialized) {
|
|
148
|
+
postMessage({ type: 'setup', session })
|
|
149
|
+
await new Promise(r => setTimeout(r, 100))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function remove () {
|
|
154
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function on(event, fn) {
|
|
158
|
+
listeners[event] = fn
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function auth(token, state) {
|
|
162
|
+
postMessage({ type: 'auth', token, state })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
auth,
|
|
167
|
+
remove,
|
|
168
|
+
on
|
|
169
|
+
}
|
|
19
170
|
}
|
package/agents/browser/root.js
CHANGED
|
@@ -24,9 +24,6 @@ export default () => {
|
|
|
24
24
|
reboot: () => window.location.reload()
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
const { state } = agent
|
|
28
|
-
// TODO: remove agent.state proxy here as part of "get rid of default scope" work
|
|
29
|
-
agent.state = (scope=host,user,domain) => state(scope, user, domain)
|
|
30
27
|
agent.local = () => {
|
|
31
28
|
if (isLocal()) return
|
|
32
29
|
|
|
@@ -39,6 +36,9 @@ export default () => {
|
|
|
39
36
|
localStorage.removeItem('api')
|
|
40
37
|
location.reload()
|
|
41
38
|
}
|
|
39
|
+
agent.close = () => {
|
|
40
|
+
window.close()
|
|
41
|
+
}
|
|
42
42
|
|
|
43
43
|
return agent
|
|
44
44
|
}
|
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
|
}
|
|
@@ -212,27 +210,41 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
212
210
|
checkHeartbeat()
|
|
213
211
|
}
|
|
214
212
|
|
|
215
|
-
function create({ id=uuid(), active_type, active }) {
|
|
213
|
+
function create({ id=uuid(), active_type, active, name }) {
|
|
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
|
-
function state(scope) {
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
function state(scope='[]') {
|
|
238
|
+
tagIfNotYetTaggedInSession('subscribed', 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/browser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { default as GenericAgent } from './agents/generic.js'
|
|
2
2
|
export { default as browserAgent } from './agents/browser/initialize.js'
|
|
3
3
|
export { default as vuePersistentStore } from './persistence/vuex.js'
|
|
4
|
-
export { default as
|
|
4
|
+
export { default as vueEmbedComponent } from './vue/3/content.vue'
|
|
5
|
+
export { default as vueNameComponent } from './vue/3/name.vue'
|
|
5
6
|
export { default as vuePersistentComponent } from './vue/3/component.js'
|
package/package.json
CHANGED
package/vue/3/content.vue
CHANGED
|
@@ -26,6 +26,7 @@ export default {
|
|
|
26
26
|
this.embedding = Agent.embed({ id }, iframe)
|
|
27
27
|
this.embedding.on('state', e => this.$emit('state', e))
|
|
28
28
|
this.embedding.on('mutate', e => this.$emit('mutate', e))
|
|
29
|
+
this.embedding.on('close', e => this.$emit('close', e))
|
|
29
30
|
|
|
30
31
|
/*
|
|
31
32
|
const { handle } = this.embedding
|
package/vue/3/name.vue
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
draggable="true"
|
|
4
|
+
@dragstart="handleDragStart"
|
|
5
|
+
>
|
|
6
|
+
{{ loading ? '...' : name }}
|
|
7
|
+
</span>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
export default {
|
|
12
|
+
props: {
|
|
13
|
+
id: String
|
|
14
|
+
},
|
|
15
|
+
data() {
|
|
16
|
+
return {
|
|
17
|
+
loading: true,
|
|
18
|
+
metadata: null
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async created() {
|
|
22
|
+
this.metadata = await Agent.metadata(this.id)
|
|
23
|
+
this.loading = false
|
|
24
|
+
},
|
|
25
|
+
computed: {
|
|
26
|
+
name() {
|
|
27
|
+
if (this.loading) return
|
|
28
|
+
else return this.metadata && this.metadata.name ? this.metadata.name : 'unnamed'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
methods: {
|
|
32
|
+
handleDragStart(e) {
|
|
33
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
34
|
+
e.dataTransfer.setData('text/plain', this.id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
span
|
|
42
|
+
{
|
|
43
|
+
cursor: grab;
|
|
44
|
+
}
|
|
45
|
+
</style>
|
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
|
-
}
|