@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.
@@ -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 { domain, user, scope } = data
50
- const key = `${domain}/${user}/${scope}`
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 state(scope, user, domain) {
60
- let watchFn
61
- let resolveKey
62
- const key = new Promise(r => resolveKey = r)
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
- const promise = new Promise(async resolve => {
65
- const { domain: d, auth: { user: u }, scope: s } = await environment()
66
- domain = domain || d
67
- user = user || u
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
- promise.watch = fn => {
74
- key
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
- promise.unwatch = () => {
85
- key
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
- return promise
92
- }
82
+ await tag(tag_type, target)
83
+ }
93
84
 
94
- return promise
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 mutate (scope, initialize=true) {
103
- const handlePatch = patch => interact(scope, patch)
104
-
105
- if (!scope) scope = (await environment()).scope
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 metadata(id) {
179
- return state(id, 'metadata', 'core')
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 embed from './embed.js'
3
+ import { v1 as uuid, validate as validateUUID } from 'uuid'
4
4
 
5
- let agentInstance
5
+ let Agent
6
6
 
7
- export default function() {
8
- if (agentInstance) return agentInstance
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
- agentInstance = embedded ? EmbeddedAgent() : RootAgent()
16
- agentInstance.embed = embed
15
+ Agent = embedded ? EmbeddedAgent() : RootAgent()
16
+ Agent.embed = embed
17
17
 
18
- return agentInstance
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
  }
@@ -1,11 +1,11 @@
1
- export default function download (id, { uuid, initialize, lastMessageResponse, fetch, metadata }) {
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
- const downloadId = uuid()
4
- initialize(
5
- downloadId,
6
- 'application/json;type=download',
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, { uuid, initialize, lastMessageResponse, fetch, metadata })
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
- const session = uuid()
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
- syncedPromiseResolutions.shift()()
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 fromt queue and do accounting with si
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 initialize(id, type, value) {
213
+ function create({ id=uuid(), active_type, active }) {
213
214
  // TODO: collapse into 1 patch and 1 interact call
214
- interact(id, [{
215
- op: 'add',
216
- path: ['active_type'],
217
- value: type
218
- }])
219
- interact(id, [{
220
- op: 'add',
221
- path: ['active'],
222
- value
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() { return environmentPromise }
233
+ async function environment() {
234
+ return { ...(await environmentPromise), context: [] }
235
+ }
227
236
 
228
237
  function state(scope) {
229
- let watchFn
230
- const promise = new Promise(async (resolveState, rejectState) => {
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
- const subscriptionId = uuid()
239
- initialize(
240
- subscriptionId,
241
- 'application/json;type=subscription',
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(subscriptionId, [{
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
- const uploadId = uuid()
338
- initialize(
339
- uploadId,
340
- 'application/json;type=upload',
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
- async function interact(scope, patch) {
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
- initialize(
443
- uuid(),
444
- 'application/json;type=postgres-query',
445
- { query, params, domain }
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, { uuid, initialize, lastMessageResponse, fetch, download, metadata }),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowlearning/agents",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "API for embedding applications in KnowLearning systems.",
5
5
  "main": "node.js",
6
6
  "browser": "browser.js",
@@ -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) {
@@ -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
- }