@knowlearning/agents 0.7.6 → 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 +70 -51
- package/agents/browser/initialize.js +157 -7
- package/agents/download.js +8 -8
- package/agents/generic.js +66 -75
- package/package.json +1 -1
- package/vue/3/component.js +3 -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
|
|
|
@@ -56,42 +61,33 @@ export default function EmbeddedAgent() {
|
|
|
56
61
|
return send({ type: 'environment' })
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
function create({ id=uuid(), active_type, active }) {
|
|
65
|
+
// TODO: collapse into 1 patch and 1 interact call
|
|
66
|
+
interact(id, [{ op: 'add', path: ['active_type'], value: active_type }])
|
|
67
|
+
interact(id, [{ op: 'add', path: ['active'], value: active }])
|
|
68
|
+
return id
|
|
69
|
+
}
|
|
63
70
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
scope = scope || s
|
|
69
|
-
resolveKey(`${domain}/${user}/${scope}`)
|
|
70
|
-
resolve(send({ type: 'state', scope, user, domain }))
|
|
71
|
-
})
|
|
71
|
+
const tagTypeToTargetCache = {}
|
|
72
|
+
async function tagIfNotYetTaggedInSession(tag_type, target) {
|
|
73
|
+
const targetCache = tagTypeToTargetCache[tag_type]
|
|
74
|
+
if (targetCache && targetCache[target]) return
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.then( async k => {
|
|
76
|
-
if (watchFn) throw new Error('Only one watcher allowed')
|
|
77
|
-
if (!watchers[k]) watchers[k] = []
|
|
78
|
-
watchers[k].push(fn)
|
|
79
|
-
watchFn = fn
|
|
80
|
-
})
|
|
81
|
-
return promise
|
|
82
|
-
}
|
|
76
|
+
// always use absolute referene when tagging
|
|
77
|
+
if (!isUUID(target)) target = (await metadata(target)).id
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.then( k => {
|
|
87
|
-
watchers[k] = watchers[k].filter(x => x !== watchFn)
|
|
88
|
-
if (watchers[k].length === 0) delete watchers[k]
|
|
89
|
-
})
|
|
79
|
+
if (!tagTypeToTargetCache[tag_type]) tagTypeToTargetCache[tag_type] = {}
|
|
80
|
+
tagTypeToTargetCache[tag_type][target] = true
|
|
90
81
|
|
|
91
|
-
|
|
92
|
-
|
|
82
|
+
await tag(tag_type, target)
|
|
83
|
+
}
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
|
|
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)
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
async function patch(root, scopes) {
|
|
@@ -99,23 +95,23 @@ export default function EmbeddedAgent() {
|
|
|
99
95
|
return send({ type: 'patch', root, scopes })
|
|
100
96
|
}
|
|
101
97
|
|
|
102
|
-
async function
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
})
|
|
106
106
|
|
|
107
|
-
if (initialize) {
|
|
108
|
-
// TODO: consider setting up watcher and updating state
|
|
109
|
-
return new MutableProxy(await state(scope) || {}, handlePatch)
|
|
110
|
-
}
|
|
111
|
-
else return new MutableProxy({}, handlePatch)
|
|
112
107
|
}
|
|
113
108
|
|
|
114
109
|
function reset(scope) {
|
|
115
|
-
return interact(scope, [{ op: 'add', path:[], value: null }])
|
|
110
|
+
return interact(scope, [{ op: 'add', path:['active'], value: null }])
|
|
116
111
|
}
|
|
117
112
|
|
|
118
113
|
function interact(scope, patch) {
|
|
114
|
+
tagIfNotYetTaggedInSession('mutated', scope)
|
|
119
115
|
return send({ type: 'interact', scope, patch })
|
|
120
116
|
}
|
|
121
117
|
|
|
@@ -175,8 +171,27 @@ export default function EmbeddedAgent() {
|
|
|
175
171
|
return promise
|
|
176
172
|
}
|
|
177
173
|
|
|
178
|
-
function
|
|
179
|
-
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 })
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
function login(provider, username, password) {
|
|
@@ -184,24 +199,28 @@ export default function EmbeddedAgent() {
|
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
function logout() { return send({ type: 'logout' }) }
|
|
187
|
-
|
|
188
202
|
function disconnect() { return send({ type: 'disconnect' }) }
|
|
189
|
-
|
|
190
203
|
function reconnect() { return send({ type: 'reconnect' }) }
|
|
204
|
+
function synced() { return send({ type: 'synced' }) }
|
|
191
205
|
|
|
192
206
|
return {
|
|
207
|
+
embedded: true,
|
|
208
|
+
uuid,
|
|
193
209
|
environment,
|
|
194
210
|
login,
|
|
195
211
|
logout,
|
|
212
|
+
create,
|
|
196
213
|
state,
|
|
214
|
+
watch,
|
|
197
215
|
upload,
|
|
198
216
|
download,
|
|
199
217
|
interact,
|
|
200
218
|
patch,
|
|
201
|
-
mutate,
|
|
202
219
|
reset,
|
|
203
220
|
metadata,
|
|
204
221
|
disconnect,
|
|
205
|
-
reconnect
|
|
222
|
+
reconnect,
|
|
223
|
+
synced,
|
|
224
|
+
tag
|
|
206
225
|
}
|
|
207
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/download.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
const DOWNLOAD_TYPE = 'application/json;type=download'
|
|
2
|
+
|
|
3
|
+
export default function download (id, { create, lastMessageResponse, fetch, metadata }) {
|
|
2
4
|
// TODO: initialize size info
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
{ id }
|
|
8
|
-
)
|
|
5
|
+
create({
|
|
6
|
+
active_type: DOWNLOAD_TYPE,
|
|
7
|
+
active: { id }
|
|
8
|
+
})
|
|
9
9
|
|
|
10
10
|
let mode = 'fetch'
|
|
11
11
|
|
|
@@ -25,7 +25,7 @@ export default function download (id, { uuid, initialize, lastMessageResponse, f
|
|
|
25
25
|
// TODO: throw meaningful error if not in browser context
|
|
26
26
|
// (following block assumes browser context)
|
|
27
27
|
// TODO: use browser progress UX instead of downloading all into memory first
|
|
28
|
-
const res = await download(id, {
|
|
28
|
+
const res = await download(id, { create, lastMessageResponse, fetch, metadata })
|
|
29
29
|
const { name } = await metadata(id)
|
|
30
30
|
const type = res.headers.get('Content-Type')
|
|
31
31
|
const blob = new Blob([ await res.blob() ], { type })
|
package/agents/generic.js
CHANGED
|
@@ -2,6 +2,10 @@ import MutableProxy from '../persistence/json.js'
|
|
|
2
2
|
import download from './download.js'
|
|
3
3
|
|
|
4
4
|
const HEARTBEAT_TIMEOUT = 10000
|
|
5
|
+
const UPLOAD_TYPE = 'application/json;type=upload'
|
|
6
|
+
const SUBSCRIPTION_TYPE = 'application/json;type=subscription'
|
|
7
|
+
const POSTGRES_QUERY_TYPE = 'application/json;type=postgres-query'
|
|
8
|
+
const TAG_TYPE = 'application/json;type=tag'
|
|
5
9
|
|
|
6
10
|
// transform our custom path implementation to the standard JSONPatch path
|
|
7
11
|
function standardJSONPatch(patch) {
|
|
@@ -10,6 +14,10 @@ function standardJSONPatch(patch) {
|
|
|
10
14
|
})
|
|
11
15
|
}
|
|
12
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
|
+
|
|
13
21
|
function sanitizeJSONPatchPathSegment(s) {
|
|
14
22
|
if (typeof s === "string") return s.replaceAll('~', '~0').replaceAll('/', '~1')
|
|
15
23
|
else return s
|
|
@@ -19,8 +27,9 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
19
27
|
let ws
|
|
20
28
|
let user
|
|
21
29
|
let domain
|
|
22
|
-
|
|
30
|
+
let session
|
|
23
31
|
let server
|
|
32
|
+
let isSynced = false
|
|
24
33
|
let si = -1
|
|
25
34
|
let authed = false
|
|
26
35
|
const states = {}
|
|
@@ -28,6 +37,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
28
37
|
const watchers = {}
|
|
29
38
|
const keyToSubscriptionId = {}
|
|
30
39
|
const lastInteractionResponse = {}
|
|
40
|
+
const tagTypeToTargetCache = {}
|
|
31
41
|
const messageQueue = []
|
|
32
42
|
let resolveEnvironment
|
|
33
43
|
let disconnected = false
|
|
@@ -38,7 +48,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
38
48
|
let lastHeartbeat
|
|
39
49
|
const syncedPromiseResolutions = []
|
|
40
50
|
|
|
41
|
-
const subscriptions = {}
|
|
42
51
|
const patches = state('patches')
|
|
43
52
|
|
|
44
53
|
log('INITIALIZING AGENT CONNECTION')
|
|
@@ -49,9 +58,8 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
function resolveSyncPromises() {
|
|
52
|
-
while (syncedPromiseResolutions.length)
|
|
53
|
-
|
|
54
|
-
}
|
|
61
|
+
while (syncedPromiseResolutions.length) syncedPromiseResolutions.shift()()
|
|
62
|
+
isSynced = true
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
function removeWatcher(key, fn) {
|
|
@@ -73,6 +81,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
function queueMessage(message) {
|
|
84
|
+
isSynced = false
|
|
76
85
|
si += 1
|
|
77
86
|
const promise = new Promise((resolve, reject) => responses[si] = [[resolve, reject]])
|
|
78
87
|
messageQueue.push({...message, si, ts: Date.now()})
|
|
@@ -133,6 +142,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
133
142
|
authed = true
|
|
134
143
|
if (!user) { // this is the first authed websocket connection
|
|
135
144
|
user = message.auth.user
|
|
145
|
+
session = message.session
|
|
136
146
|
domain = message.domain
|
|
137
147
|
server = message.server
|
|
138
148
|
resolveEnvironment(message)
|
|
@@ -149,14 +159,13 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
149
159
|
else {
|
|
150
160
|
if (message.si !== undefined) {
|
|
151
161
|
if (responses[message.si]) {
|
|
152
|
-
// TODO: remove "acknowledged" messages
|
|
162
|
+
// TODO: remove "acknowledged" messages from queue and do accounting with si
|
|
153
163
|
responses[message.si]
|
|
154
164
|
.forEach(([resolve, reject]) => {
|
|
155
165
|
message.error ? reject(message) : resolve(message)
|
|
156
166
|
})
|
|
157
167
|
delete responses[message.si]
|
|
158
168
|
ws.send(JSON.stringify({ack: message.si})) // acknowledgement that we have received the response for this message
|
|
159
|
-
|
|
160
169
|
if (Object.keys(responses).length === 0) {
|
|
161
170
|
resolveSyncPromises()
|
|
162
171
|
}
|
|
@@ -167,14 +176,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
167
176
|
}
|
|
168
177
|
}
|
|
169
178
|
else {
|
|
170
|
-
const sub = subscriptions[keyToSubscriptionId[message.scope]]
|
|
171
179
|
if (watchers[message.scope]) {
|
|
172
|
-
// TODO: debug case where sub is not defined
|
|
173
|
-
if (sub && sub.ii + 1 !== message.ii) {
|
|
174
|
-
// TODO: this seems to be an error that happens with decent regularity (an answer with a given si was skipped/failed)
|
|
175
|
-
// we should be wary of out-of-order ii being passed down (maybe need to wait for older ones???)
|
|
176
|
-
console.warn('UNEXPECTED UPDATE INTERACTION INDEX!!!!!!!!!!! last index in session', sub, ' passed index ', message.ii)
|
|
177
|
-
}
|
|
178
180
|
states[message.scope] = await states[message.scope]
|
|
179
181
|
|
|
180
182
|
const lastResetPatchIndex = message.patch.findLastIndex(p => p.path.length === 0)
|
|
@@ -187,7 +189,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
187
189
|
const state = structuredClone(states[message.scope].active)
|
|
188
190
|
fn({ ...message, state })
|
|
189
191
|
})
|
|
190
|
-
if (sub) sub.ii = message.ii
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
}
|
|
@@ -209,43 +210,49 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
209
210
|
checkHeartbeat()
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
function
|
|
213
|
+
function create({ id=uuid(), active_type, active }) {
|
|
213
214
|
// TODO: collapse into 1 patch and 1 interact call
|
|
214
|
-
interact(id, [{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
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)
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
function environment() {
|
|
233
|
+
async function environment() {
|
|
234
|
+
return { ...(await environmentPromise), context: [] }
|
|
235
|
+
}
|
|
227
236
|
|
|
228
237
|
function state(scope) {
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
tagIfNotYetTaggedInSession('connected', scope)
|
|
239
|
+
return new Promise(async (resolveState, rejectState) => {
|
|
231
240
|
if (!keyToSubscriptionId[scope]) {
|
|
232
241
|
const id = uuid()
|
|
233
242
|
|
|
234
243
|
keyToSubscriptionId[scope] = id
|
|
235
244
|
watchers[scope] = []
|
|
236
245
|
states[scope] = new Promise(async (resolve, reject) => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{ session, scope, ii: null }
|
|
243
|
-
)
|
|
246
|
+
create({
|
|
247
|
+
id,
|
|
248
|
+
active_type: SUBSCRIPTION_TYPE,
|
|
249
|
+
active: { session, scope, ii: null }
|
|
250
|
+
})
|
|
244
251
|
|
|
245
252
|
try {
|
|
246
253
|
const state = await lastMessageResponse()
|
|
247
254
|
// TODO: replace with editing scope of type
|
|
248
|
-
interact(
|
|
255
|
+
interact(id, [{
|
|
249
256
|
op: 'add',
|
|
250
257
|
path: ['active', 'ii'],
|
|
251
258
|
value: 1 // TODO: use state.ii when is coming down properly...
|
|
@@ -258,7 +265,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
258
265
|
// ii does not apply (we clear out the subscription to not cache value)
|
|
259
266
|
delete states[scope]
|
|
260
267
|
delete keyToSubscriptionId[scope]
|
|
261
|
-
delete subscriptions[id]
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
270
|
catch (error) {
|
|
@@ -282,26 +288,6 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
282
288
|
rejectState(error)
|
|
283
289
|
}
|
|
284
290
|
})
|
|
285
|
-
|
|
286
|
-
promise.watch = fn => {
|
|
287
|
-
if (watchFn) throw new Error('Only one watcher allowed')
|
|
288
|
-
if (!watchers[scope]) watchers[scope] = []
|
|
289
|
-
watchers[scope].push(fn)
|
|
290
|
-
watchFn = fn
|
|
291
|
-
|
|
292
|
-
return promise
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
promise.unwatch = async () => {
|
|
296
|
-
removeWatcher(scope, watchFn)
|
|
297
|
-
if (watchers[scope].length === 0) {
|
|
298
|
-
delete subscriptions[keyToSubscriptionId[scope]]
|
|
299
|
-
delete keyToSubscriptionId[scope]
|
|
300
|
-
delete watchers[scope]
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return promise
|
|
305
291
|
}
|
|
306
292
|
|
|
307
293
|
function watch(id, fn) {
|
|
@@ -334,15 +320,10 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
334
320
|
// TODO: if no data, set up streaming upload
|
|
335
321
|
async function upload(name, type, data, id=uuid()) {
|
|
336
322
|
// TODO: include data size info...
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
{
|
|
342
|
-
id,
|
|
343
|
-
type
|
|
344
|
-
}
|
|
345
|
-
)
|
|
323
|
+
create({
|
|
324
|
+
active_type: UPLOAD_TYPE,
|
|
325
|
+
active: { id, type }
|
|
326
|
+
})
|
|
346
327
|
const { url } = await lastMessageResponse()
|
|
347
328
|
|
|
348
329
|
if (data === undefined) return url
|
|
@@ -362,7 +343,9 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
362
343
|
return { swaps }
|
|
363
344
|
}
|
|
364
345
|
|
|
365
|
-
|
|
346
|
+
// TODO: addTag option should probably not be exposed
|
|
347
|
+
async function interact(scope, patch, addTag=true) {
|
|
348
|
+
if (addTag) tagIfNotYetTaggedInSession('mutated', scope)
|
|
366
349
|
// TODO: ensure user is owner of scope
|
|
367
350
|
const response = queueMessage({scope, patch})
|
|
368
351
|
|
|
@@ -419,7 +402,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
419
402
|
}
|
|
420
403
|
|
|
421
404
|
async function synced() {
|
|
422
|
-
return new Promise(resolve => syncedPromiseResolutions.push(resolve))
|
|
405
|
+
return isSynced ? null : new Promise(resolve => syncedPromiseResolutions.push(resolve))
|
|
423
406
|
}
|
|
424
407
|
|
|
425
408
|
function disconnect() {
|
|
@@ -439,23 +422,30 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
439
422
|
}
|
|
440
423
|
|
|
441
424
|
function query(query, params, domain) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
)
|
|
425
|
+
create({
|
|
426
|
+
active_type: POSTGRES_QUERY_TYPE,
|
|
427
|
+
active: { query, params, domain }
|
|
428
|
+
})
|
|
447
429
|
return lastMessageResponse()
|
|
448
430
|
}
|
|
449
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
|
+
|
|
450
439
|
return {
|
|
451
440
|
uuid,
|
|
452
441
|
environment,
|
|
453
442
|
login,
|
|
454
443
|
logout,
|
|
444
|
+
create,
|
|
455
445
|
state,
|
|
456
446
|
watch,
|
|
457
447
|
upload,
|
|
458
|
-
download: id => download(id, {
|
|
448
|
+
download: id => download(id, { create, lastMessageResponse, fetch, metadata }),
|
|
459
449
|
interact,
|
|
460
450
|
patch,
|
|
461
451
|
claim,
|
|
@@ -465,6 +455,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
|
|
|
465
455
|
synced,
|
|
466
456
|
disconnect,
|
|
467
457
|
reconnect,
|
|
458
|
+
tag,
|
|
468
459
|
debug
|
|
469
460
|
}
|
|
470
461
|
}
|
package/package.json
CHANGED
package/vue/3/component.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { watchEffect, defineAsyncComponent } from 'vue'
|
|
2
|
+
import { browserAgent } from '../../browser.js'
|
|
3
|
+
|
|
4
|
+
const Agent = browserAgent()
|
|
2
5
|
|
|
3
6
|
// TODO: probably want to make this a util, and better fleshed out (with white instead of blacklist)
|
|
4
7
|
function isScopeSerializable(v) {
|
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
|
-
}
|