@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.
@@ -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
 
@@ -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
- function state(scope, user, domain) {
66
- let watchFn
67
- let resolveKey
68
- const key = new Promise(r => resolveKey = r)
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
- const promise = new Promise(async resolve => {
71
- const { domain: d, auth: { user: u }, scope: s } = await environment()
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
- promise.watch = fn => {
80
- key
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
- promise.unwatch = () => {
91
- key
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
- return promise
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 mutate (scope, initialize=true) {
109
- const handlePatch = patch => interact(scope, patch)
110
-
111
- if (!scope) scope = (await environment()).scope
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
- else return new MutableProxy({}, handlePatch)
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 metadata(id) {
185
- return state(id, 'metadata', 'core')
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 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
+ 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
  }
@@ -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
- const session = uuid()
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
- syncedPromiseResolutions.shift()()
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 fromt queue and do accounting with si
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() { return environmentPromise }
233
+ async function environment() {
234
+ return { ...(await environmentPromise), context: [] }
235
+ }
222
236
 
223
- function state(scope) {
224
- let watchFn
225
- const promise = new Promise(async (resolveState, rejectState) => {
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: subscriptionId,
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(subscriptionId, [{
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
- 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)
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 vueContentComponent } from './vue/3/content.vue'
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowlearning/agents",
3
- "version": "0.7.7",
3
+ "version": "0.8.1",
4
4
  "description": "API for embedding applications in KnowLearning systems.",
5
5
  "main": "node.js",
6
6
  "browser": "browser.js",
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>
@@ -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
- }