@knowlearning/agents 0.9.26 → 0.9.28

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.
@@ -7,6 +7,7 @@ export default function EmbeddedAgent() {
7
7
  const session = new Promise(r => resolveSession = r)
8
8
  const responses = {}
9
9
  const watchers = {}
10
+ const sentUpdates = {}
10
11
 
11
12
  function removeWatcher(key, fn) {
12
13
  const watcherIndex = watchers[key].findIndex(x => x === fn)
@@ -37,18 +38,33 @@ export default function EmbeddedAgent() {
37
38
  }
38
39
  }
39
40
 
41
+ let sessionResolved = false
40
42
  addEventListener('message', async ({ data }) => {
41
- if (data.type === 'setup') resolveSession(data.session)
43
+ if (data.type === 'setup' && !sessionResolved) {
44
+ sessionResolved = true
45
+ resolveSession(data.session)
46
+ }
47
+ else if (!sessionResolved || data.session !== await session) return
42
48
  else if (responses[data.requestId]) {
43
49
  const { resolve, reject } = responses[data.requestId]
44
50
  if (data.error) reject(data.error)
45
51
  else resolve(data.response)
46
52
  }
47
53
  else if (data.ii !== undefined) {
48
- const { scope, user } = data
49
- const { auth } = await environment()
50
- const key = isUUID(scope) ? scope : `${!user || auth.user === user ? '' : user}/${scope}`
51
- if (watchers[key]) watchers[key].forEach(fn => fn(data))
54
+ const { scope, user, domain } = data
55
+ const { auth, domain:rootDomain } = await environment()
56
+ const d = !domain || domain === rootDomain ? '' : domain
57
+ const u = !user || auth.user === user ? '' : user
58
+ const key = isUUID(scope) ? scope : `${d}/${u}/${scope}`
59
+ if (watchers[key]) {
60
+ if (sentUpdates[key] + 1 === data.ii) {
61
+ sentUpdates[key] = data.ii
62
+ watchers[key].forEach(fn => fn(data))
63
+ }
64
+ else if (data.ii !== sentUpdates[key]) {
65
+ console.warn('Out of order or repeated update for', key, data.ii, sentUpdates[key])
66
+ }
67
+ }
52
68
  }
53
69
  })
54
70
 
@@ -57,9 +73,11 @@ export default function EmbeddedAgent() {
57
73
  }
58
74
 
59
75
  function create({ id=uuid(), active_type, active }) {
60
- // TODO: collapse into 1 patch and 1 interact call
61
- interact(id, [{ op: 'add', path: ['active_type'], value: active_type }])
62
- interact(id, [{ op: 'add', path: ['active'], value: active }])
76
+ if (!active_type) active_type = 'application/json'
77
+ interact(id, [
78
+ { op: 'add', path: ['active_type'], value: active_type },
79
+ { op: 'add', path: ['active'], value: active }
80
+ ])
63
81
  return id
64
82
  }
65
83
 
@@ -78,13 +96,26 @@ export default function EmbeddedAgent() {
78
96
  await tag(tag_type, target)
79
97
  }
80
98
 
81
- function watch(id, fn, user) {
82
- tagIfNotYetTaggedInSession('subscribed', id)
83
- const key = isUUID(id) ? id : `${ user || ''}/${id}`
84
- if (!watchers[key]) watchers[key] = []
85
- watchers[key].push(fn)
86
- send({ type: 'state', scope: id, user })
87
- return () => removeWatcher(key, fn)
99
+ function watch(scope, fn, user, domain) {
100
+ tagIfNotYetTaggedInSession('subscribed', scope)
101
+
102
+ const key = (
103
+ environment()
104
+ .then(async ({ auth, domain:rootDomain }) => {
105
+ const d = !domain || domain === rootDomain ? '' : domain
106
+ const u = !user || auth.user === user ? '' : user
107
+ const key = isUUID(scope) ? scope : `${d}/${u}/${scope}`
108
+
109
+ const state = await send({ type: 'state', scope, user, domain })
110
+ const metadata = await send({ type: 'metadata', scope, user, domain })
111
+ fn({ state, patch: null, ii: metadata.ii })
112
+ sentUpdates[key] = metadata.ii
113
+ if (!watchers[key]) watchers[key] = []
114
+ watchers[key].push(fn)
115
+ return key
116
+ })
117
+ )
118
+ return async () => removeWatcher(await key, fn)
88
119
  }
89
120
 
90
121
  async function patch(root, scopes) {
@@ -92,14 +123,15 @@ export default function EmbeddedAgent() {
92
123
  return send({ type: 'patch', root, scopes })
93
124
  }
94
125
 
95
- async function state(scope) {
126
+ async function state(scope, user, domain) {
96
127
  if (scope === undefined) {
97
128
  const { context } = await environment()
98
129
  scope = JSON.stringify(context)
99
130
  }
100
131
  tagIfNotYetTaggedInSession('subscribed', scope)
101
- const startState = await send({ type: 'state', scope })
132
+ const startState = await send({ type: 'state', scope, user, domain })
102
133
  return new MutableProxy(startState, patch => {
134
+ // TODO: reject updates if user is not owner
103
135
  const activePatch = structuredClone(patch)
104
136
  activePatch.forEach(entry => entry.path.unshift('active'))
105
137
  interact(scope, activePatch)
@@ -4,18 +4,20 @@ import { v1 as uuid, validate as validateUUID } from 'uuid'
4
4
 
5
5
  let Agent
6
6
 
7
- export default function browserAgent() {
8
- if (Agent) return Agent
7
+ export default function browserAgent(options={}) {
8
+ if (Agent && !options.unique) 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
- Agent = embedded ? EmbeddedAgent() : RootAgent()
16
- Agent.embed = embed
15
+ const newAgent = embedded && !options.root ? EmbeddedAgent() : RootAgent(options)
16
+ newAgent.embed = embed
17
17
 
18
- return Agent
18
+ if (!Agent) Agent = newAgent
19
+
20
+ return newAgent
19
21
  }
20
22
 
21
23
  const copy = x => JSON.parse(JSON.stringify(x))
@@ -57,10 +59,9 @@ function embed(environment, iframe) {
57
59
  if (listeners.close) listeners.close(message.info)
58
60
  }
59
61
  else if (type === 'environment') {
62
+ const { context } = message
60
63
  const env = await Agent.environment()
61
- const context = [...env.context, environment.id]
62
- Object.assign(env, { ...env, context })
63
- sendDown(env)
64
+ sendDown({ ...env, context: [...env.context, environment.id] })
64
65
  }
65
66
  else if (type === 'interact') {
66
67
  const { scope, patch } = message
@@ -80,13 +81,13 @@ function embed(environment, iframe) {
80
81
  sendDown(await Agent.tag(tag_type, target, prependedContext))
81
82
  }
82
83
  else if (type === 'state') {
83
- const { scope, user } = message
84
+ const { scope, user, domain } = message
84
85
 
85
- const statePromise = Agent.state(scope, user)
86
+ const statePromise = Agent.state(scope, user, domain)
86
87
 
87
- const key = `${user || ''}/${scope}`
88
+ const key = `${ domain || ''}/${user || ''}/${scope}`
88
89
  if (!watchers[key]) {
89
- watchers[key] = Agent.watch(scope, postMessage, user)
90
+ watchers[key] = Agent.watch(scope, postMessage, user, domain)
90
91
  }
91
92
 
92
93
  if (listeners.state) listeners.state({ scope })
@@ -154,6 +155,7 @@ function embed(environment, iframe) {
154
155
  else iframe.src = id // TODO: ensure is url
155
156
 
156
157
  while(!embeddedAgentInitialized) {
158
+ // TODO: wait for any other agents that are initializing from other potential root agents
157
159
  postMessage({ type: 'setup', session })
158
160
  await new Promise(r => setTimeout(r, 100))
159
161
  }
@@ -8,13 +8,13 @@ const REMOTE_HOST = 'api.knowlearning.systems'
8
8
 
9
9
  function isLocal() { return localStorage.getItem('api') === 'local' }
10
10
 
11
- export default () => {
11
+ export default options => {
12
12
  const { host, protocol } = window.location
13
13
 
14
14
  const agent = GenericAgent({
15
15
  host: isLocal() ? DEVELOPMENT_HOST : REMOTE_HOST,
16
16
  protocol: protocol === 'https:' ? 'wss' : 'ws',
17
- token: getToken,
17
+ token: options.getToken || getToken,
18
18
  WebSocket,
19
19
  uuid,
20
20
  fetch,
@@ -44,6 +44,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
44
44
  tagIfNotYetTaggedInSession,
45
45
  interact,
46
46
  fetch,
47
+ synced,
47
48
  metadata
48
49
  }
49
50
 
@@ -51,7 +52,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
51
52
 
52
53
  async function environment() { return { ...(await environmentPromise), context: [] } }
53
54
 
54
- function state(scope, user) { return stateImplementation(scope, user, internalReferences) }
55
+ function state(scope, user, domain) { return stateImplementation(scope, user, domain, internalReferences) }
55
56
 
56
57
  function download(id) { return downloadImplementation(id, internalReferences) }
57
58
 
@@ -60,6 +61,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
60
61
  function log() { if (mode === 'debug') console.log(...arguments) }
61
62
 
62
63
  function create({ id=uuid(), active_type, active, name }) {
64
+ if (!active_type) active_type = 'application/json'
63
65
  const patch = [
64
66
  { op: 'add', path: ['active_type'], value: active_type },
65
67
  { op: 'add', path: ['active'], value: active }
@@ -110,7 +112,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
110
112
  const response = queueMessage({scope, patch})
111
113
 
112
114
  // if we are watching this scope, we want to keep track of last interaction we fired
113
- const qualifiedScope = isUUID(scope) ? scope : `/${scope}`
115
+ const qualifiedScope = isUUID(scope) ? scope : `//${scope}`
114
116
  if (states[qualifiedScope] !== undefined) {
115
117
  let resolve
116
118
  lastInteractionResponse[qualifiedScope] = new Promise(r => resolve = r)
@@ -18,7 +18,6 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
18
18
  let ws
19
19
  let user
20
20
  let authed = false
21
- let isSynced = false
22
21
  let session
23
22
  let server
24
23
  let si = -1
@@ -30,7 +29,7 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
30
29
  let lastSynchronousScopePatchPromise = null
31
30
  let restarting = false
32
31
  let disconnected = false
33
- const syncedPromiseResolutions = []
32
+ const outstandingSyncPromises = []
34
33
  const responses = {}
35
34
 
36
35
 
@@ -41,7 +40,6 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
41
40
  }
42
41
 
43
42
  function queueMessage({ scope, patch }) {
44
- isSynced = false
45
43
  if (lastSynchronousScopePatched === scope) {
46
44
  const i = messageQueue.length - 1
47
45
  messageQueue[i].patch = [...messageQueue[i].patch, ...patch]
@@ -74,8 +72,10 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
74
72
  }
75
73
 
76
74
  function resolveSyncPromises() {
77
- while (syncedPromiseResolutions.length) syncedPromiseResolutions.shift()()
78
- isSynced = true
75
+ const lowestOutstandingResponseIndex = Object.keys(responses).map(parseInt).sort()[0] || Infinity
76
+ while (outstandingSyncPromises[0] && outstandingSyncPromises[0].si < lowestOutstandingResponseIndex) {
77
+ outstandingSyncPromises.shift().resolve()
78
+ }
79
79
  }
80
80
 
81
81
  function checkHeartbeat() {
@@ -162,7 +162,7 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
162
162
 
163
163
  delete responses[message.si]
164
164
  ws.send(JSON.stringify({ack: message.si})) // acknowledgement that we have received the response for this message
165
- if (Object.keys(responses).length === 0) resolveSyncPromises()
165
+ resolveSyncPromises()
166
166
  }
167
167
  else {
168
168
  // TODO: consider what to do here... probably want to throw error if in dev env
@@ -170,7 +170,10 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
170
170
  }
171
171
  }
172
172
  else {
173
- const qualifiedScope = isUUID(message.scope) ? message.scope : `${ message.user === user ? '' : message.user}/${message.scope}`
173
+ const d = message.domain === window.location.host ? '' : message.domain
174
+ const u = message.user === user ? '' : message.user
175
+ const s = message.scope
176
+ const qualifiedScope = isUUID(s) ? s : `${d}/${u}/${s}`
174
177
  if (watchers[qualifiedScope]) {
175
178
  states[qualifiedScope] = await states[qualifiedScope]
176
179
 
@@ -205,7 +208,11 @@ export default function messageQueue(setEnvironment, { token, protocol, host, We
205
208
  checkHeartbeat()
206
209
  }
207
210
 
208
- async function synced() { return isSynced ? null : new Promise(res => syncedPromiseResolutions.push(res)) }
211
+ async function synced() {
212
+ const syncPromise = new Promise(resolve => outstandingSyncPromises.push({ si: lastSentSI, resolve }))
213
+ resolveSyncPromises()
214
+ return syncPromise
215
+ }
209
216
 
210
217
  function lastMessageResponse() { return new Promise((res, rej) => responses[si].push([res, rej])) }
211
218
 
@@ -3,12 +3,12 @@ import MutableProxy from '../../persistence/json.js'
3
3
 
4
4
  const SUBSCRIPTION_TYPE = 'application/json;type=subscription'
5
5
 
6
- export default function(scope='[]', user, { keyToSubscriptionId, watchers, states, create, environment, lastMessageResponse, lastInteractionResponse, tagIfNotYetTaggedInSession, interact }) {
6
+ export default function(scope='[]', user, domain, { keyToSubscriptionId, watchers, states, create, environment, lastMessageResponse, lastInteractionResponse, tagIfNotYetTaggedInSession, interact }) {
7
7
  let resolveMetadataPromise
8
8
  let metadataPromise = new Promise(resolve => resolveMetadataPromise = resolve)
9
9
 
10
10
  const statePromise = new Promise(async (resolveState, rejectState) => {
11
- const qualifiedScope = isUUID(scope) ? scope : `${user || ''}/${scope}`
11
+ const qualifiedScope = isUUID(scope) ? scope : `${domain || ''}/${user || ''}/${scope}`
12
12
  if (!keyToSubscriptionId[qualifiedScope]) {
13
13
  const id = uuid()
14
14
 
@@ -19,7 +19,7 @@ export default function(scope='[]', user, { keyToSubscriptionId, watchers, state
19
19
  create({
20
20
  id,
21
21
  active_type: SUBSCRIPTION_TYPE,
22
- active: { session, scope, user, ii: null, initialized: Date.now() },
22
+ active: { session, scope, user, domain, ii: null, initialized: Date.now() },
23
23
  })
24
24
 
25
25
  try {
@@ -1,39 +1,31 @@
1
1
  import { validate as isUUID } from 'uuid'
2
2
 
3
- export default function({ metadata, state, watchers }) {
3
+ export default function({ metadata, state, watchers, synced }) {
4
4
 
5
- function watch(scope=DEFAULT_SCOPE_NAME, fn, user) {
6
- if (Array.isArray(scope)) return watchResolution(scope, fn, user)
5
+ function watch(scope=DEFAULT_SCOPE_NAME, fn, user, domain) {
6
+ if (Array.isArray(scope)) return watchResolution(scope, fn, user, domain)
7
7
 
8
- let initialSent = false
9
- const queue = []
10
- function cb(update) {
11
- if (initialSent) fn(update)
12
- else queue.push(update)
13
- }
14
-
15
- const statePromise = state(scope, user)
16
- const qualifiedScope = isUUID(scope) ? scope : `${user || ''}/${scope}`
17
-
18
- if (!watchers[qualifiedScope]) watchers[qualifiedScope] = []
19
- watchers[qualifiedScope].push(cb)
8
+ const statePromise = state(scope, user, domain)
9
+ const qualifiedScope = isUUID(scope) ? scope : `${domain || ''}/${user || ''}/${scope}`
20
10
 
21
- metadata(scope, user)
11
+ metadata(scope, user, domain)
22
12
  .then(async ({ ii }) => {
23
13
  fn({
24
14
  scope,
15
+ user,
16
+ domain,
25
17
  state: await statePromise,
26
18
  patch: null,
27
19
  ii
28
20
  })
29
- initialSent = true
30
- queue.forEach(fn)
21
+ if (!watchers[qualifiedScope]) watchers[qualifiedScope] = []
22
+ watchers[qualifiedScope].push(fn)
31
23
  })
32
24
 
33
- return () => removeWatcher(qualifiedScope, cb)
25
+ return () => removeWatcher(qualifiedScope, fn)
34
26
  }
35
27
 
36
- function watchResolution(path, callback, user) {
28
+ function watchResolution(path, callback, user, domain) {
37
29
  const id = path[0]
38
30
  const references = path.slice(1)
39
31
  let unwatchDeeper = () => {}
@@ -57,13 +49,13 @@ export default function({ metadata, state, watchers }) {
57
49
  index === references.length - 1
58
50
  ) callback(value)
59
51
  else if (isUUID(value)) {
60
- unwatchDeeper = watchResolution([value, ...references.slice(index + 1)], callback, user)
52
+ unwatchDeeper = watchResolution([value, ...references.slice(index + 1)], callback, user, domain)
61
53
  return
62
54
  }
63
55
  }
64
56
  }
65
57
 
66
- const unwatch = watch(id, watchCallback, user)
58
+ const unwatch = watch(id, watchCallback, user, domain)
67
59
 
68
60
  return () => {
69
61
  unwatch()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowlearning/agents",
3
- "version": "0.9.26",
3
+ "version": "0.9.28",
4
4
  "description": "API for embedding applications in KnowLearning systems.",
5
5
  "main": "node.js",
6
6
  "browser": "browser.js",