@knowlearning/agents 0.4.14 → 0.5.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.
@@ -117,13 +117,10 @@ export default function EmbeddedAgent() {
117
117
 
118
118
  if (data === undefined) return url
119
119
  else {
120
- console.log('UPLOADING TO URL!!!!!!!!!!!!!!', id, url)
121
120
  const headers = { 'Content-Type': type }
122
121
  const response = await fetch(url, {method: 'PUT', headers, body: data})
123
122
  const { ok, statusText } = response
124
123
 
125
- console.log(ok, statusText)
126
-
127
124
  if (ok) return id
128
125
  else throw new Error(statusText)
129
126
  }
@@ -1,4 +1,4 @@
1
- import { initializeApp } from 'firebase/app'
1
+ import { initializeApp } from '@firebase/app'
2
2
  import {
3
3
  getAuth,
4
4
  signOut as firebaseSignOut,
@@ -8,7 +8,7 @@ import {
8
8
  signInWithRedirect,
9
9
  signInWithEmailAndPassword,
10
10
  signInAnonymously
11
- } from 'firebase/auth'
11
+ } from '@firebase/auth'
12
12
 
13
13
  initializeApp({
14
14
  "apiKey": "AIzaSyAxjYuF-2JmXxlXnGgNu2CO4Q41EAtUgrY",
package/agents/generic.js CHANGED
@@ -26,7 +26,7 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
26
26
  const responses = {}
27
27
  const watchers = {}
28
28
  const keyToSubscriptionId = {}
29
- const lastInteractionUpdateForWatchedScope = {}
29
+ const lastInteractionResponse = {}
30
30
  const messageQueue = []
31
31
  let resolveEnvironment
32
32
  let disconnected = false
@@ -35,23 +35,24 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
35
35
  const environmentPromise = new Promise(r => resolveEnvironment = r)
36
36
  let lastSentSI = -1
37
37
  let lastHeartbeat
38
+ const syncedPromiseResolutions = []
38
39
 
39
- const subscriptions = mutate('subscriptions', false)
40
- const uploads = mutate('uploads', false)
41
- const downloads = mutate('downloads', false)
42
- const patches = mutate('patches', false)
40
+ const subscriptions = {}
41
+ const patches = state('patches')
43
42
 
44
43
  log('INITIALIZING AGENT CONNECTION')
45
44
  initWS()
46
45
 
47
- function subscription(key) {
48
- return subscriptions[keyToSubscriptionId[key]]
49
- }
50
-
51
46
  function log() {
52
47
  if (mode === 'debug') console.log(...arguments)
53
48
  }
54
49
 
50
+ function resolveSyncPromises() {
51
+ while (syncedPromiseResolutions.length) {
52
+ syncedPromiseResolutions.shift()()
53
+ }
54
+ }
55
+
55
56
  function removeWatcher(key, fn) {
56
57
  const watcherIndex = watchers[key].findIndex(x => x === fn)
57
58
  if (watcherIndex > -1) watchers[key].splice(watcherIndex, 1)
@@ -67,14 +68,15 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
67
68
  }
68
69
 
69
70
  function lastMessageResponse() { // TODO: handle error responses
70
- return new Promise(r => responses[si].push(r))
71
+ return new Promise((resolve, reject) => responses[si].push([resolve, reject]))
71
72
  }
72
73
 
73
74
  function queueMessage(message) {
74
75
  si += 1
75
- responses[si] = []
76
+ const promise = new Promise((resolve, reject) => responses[si] = [[resolve, reject]])
76
77
  messageQueue.push({...message, si, ts: Date.now()})
77
78
  flushMessageQueue()
79
+ return promise
78
80
  }
79
81
 
80
82
  function checkHeartbeat() {
@@ -144,9 +146,16 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
144
146
  if (message.si !== undefined) {
145
147
  if (responses[message.si]) {
146
148
  // TODO: remove "acknowledged" messages fromt queue and do accounting with si
147
- responses[message.si].forEach(fn => fn(message))
149
+ responses[message.si]
150
+ .forEach(([resolve, reject]) => {
151
+ message.error ? reject(message) : resolve(message)
152
+ })
148
153
  delete responses[message.si]
149
154
  ws.send(JSON.stringify({ack: message.si})) // acknowledgement that we have received the response for this message
155
+
156
+ if (Object.keys(responses).length === 0) {
157
+ resolveSyncPromises()
158
+ }
150
159
  }
151
160
  else {
152
161
  // TODO: consider what to do here... probably want to throw error if in dev env
@@ -154,22 +163,22 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
154
163
  }
155
164
  }
156
165
  else {
157
- const key = `${message.domain}/${message.user}/${message.scope}`
158
- const sub = subscription(key)
159
- if (watchers[key]) {
166
+ const sub = subscriptions[keyToSubscriptionId[message.scope]]
167
+ if (watchers[message.scope]) {
160
168
  // TODO: debug case where sub is not defined
161
169
  if (sub && sub.ii + 1 !== message.ii) {
162
170
  // TODO: this seems to be an error that happens with decent regularity (an answer with a given si was skipped/failed)
163
171
  // we should be wary of out-of-order ii being passed down (maybe need to wait for older ones???)
164
172
  console.warn('UNEXPECTED UPDATE INTERACTION INDEX!!!!!!!!!!! last index in session', sub, ' passed index ', message.ii)
165
173
  }
166
- states[key] = await states[key] || {}
174
+ states[message.scope] = await states[message.scope]
167
175
 
168
176
  const lastResetPatchIndex = message.patch.findLastIndex(p => p.path.length === 0)
169
- if (lastResetPatchIndex > -1) states[key] = message.patch[lastResetPatchIndex].value
177
+ if (lastResetPatchIndex > -1) states[message.scope] = message.patch[lastResetPatchIndex].value
170
178
 
171
- applyPatch(states[key], standardJSONPatch(message.patch.slice(lastResetPatchIndex + 1)))
172
- watchers[key].forEach(fn => fn({ ...message, state: states[key] }))
179
+ if (states[message.scope].active === undefined) states[message.scope].active = {}
180
+ applyPatch(states[message.scope], standardJSONPatch(message.patch.slice(lastResetPatchIndex + 1)))
181
+ watchers[message.scope].forEach(fn => fn({ ...message, state: states[message.scope] }))
173
182
  if (sub) sub.ii = message.ii
174
183
  }
175
184
  }
@@ -192,78 +201,95 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
192
201
  checkHeartbeat()
193
202
  }
194
203
 
204
+ function initialize(id, type, value) {
205
+ // TODO: collapse into 1 patch and 1 interact call
206
+ interact(id, [{
207
+ op: 'add',
208
+ path: ['active_type'],
209
+ value: type
210
+ }])
211
+ interact(id, [{
212
+ op: 'add',
213
+ path: ['active'],
214
+ value
215
+ }])
216
+ }
217
+
195
218
  function environment() { return environmentPromise }
196
219
 
197
- function state(scope, u, d) {
220
+ function state(scope) {
198
221
  let watchFn
199
- let resolveKey
200
- const key = new Promise(r => resolveKey = r)
201
-
202
- const promise = new Promise(async resolveState => {
203
- await environmentPromise // environment not set until first connection
204
-
205
- if (!u) u = user
206
- if (!d) d = domain
207
-
208
- const k = `${d || domain}/${u || user}/${scope}`
209
- resolveKey(k)
210
-
211
- if (!keyToSubscriptionId[k]) {
222
+ const promise = new Promise(async (resolveState, rejectState) => {
223
+ if (!keyToSubscriptionId[scope]) {
212
224
  const id = uuid()
213
225
 
214
- keyToSubscriptionId[k] = id
215
- watchers[k] = []
216
- states[k] = new Promise(async resolve => {
217
-
218
- subscriptions[id] = {
219
- session,
220
- domain: d,
221
- user: u,
222
- scope,
223
- ii: null
226
+ keyToSubscriptionId[scope] = id
227
+ watchers[scope] = []
228
+ states[scope] = new Promise(async (resolve, reject) => {
229
+
230
+ const subscriptionId = uuid()
231
+ initialize(
232
+ subscriptionId,
233
+ 'application/json;type=subscription',
234
+ { session, scope, ii: null }
235
+ )
236
+
237
+ try {
238
+ const state = await lastMessageResponse()
239
+ // TODO: replace with editing scope of type
240
+ interact(subscriptionId, [{
241
+ op: 'add',
242
+ path: ['active', 'ii'],
243
+ value: 1 // TODO: use state.ii when is coming down properly...
244
+ }])
245
+
246
+ resolve(state)
247
+
248
+ if (state.ii === -1) {
249
+ // -1 indicates the result is a computed scope, so
250
+ // ii does not apply (we clear out the subscription to not cache value)
251
+ delete states[scope]
252
+ delete keyToSubscriptionId[scope]
253
+ delete subscriptions[id]
254
+ }
224
255
  }
225
-
226
- const { ii, state } = await lastMessageResponse()
227
- subscriptions[id].ii = ii
228
- resolve(state)
229
- if (ii === -1) {
230
- // -1 indicates the result is a computed scope, so
231
- // ii does not apply (we clear out the subscription to not cache value)
232
- delete states[k]
233
- delete keyToSubscriptionId[k]
234
- delete subscriptions[id]
256
+ catch (error) {
257
+ reject(error)
235
258
  }
236
259
  })
237
260
  }
238
261
 
239
- // TODO: make sure to get the state that represents the last interaction we sent
240
- // when this "state" method was called!!!!!!!!!!!!!!!!!!!!!!!
241
- // Right now this is simply returning whatever we have at the time we get here
242
- // when some interaction responses may not have been applied yet...
243
- // POSSIBLE: wait for most recent interaction update response for key k that we sent...
244
- await lastInteractionUpdateForWatchedScope[k]
245
- // TODO: probably something like await updateMessageReceivedForLastInteractionWeSentForKey[k]
246
- resolveState(structuredClone(await states[k]))
262
+ await lastInteractionResponse[scope]
263
+
264
+ try {
265
+ const state = structuredClone(await states[scope])
266
+
267
+ resolveState(new MutableProxy(state.active || {}, patch => {
268
+ const activePatch = structuredClone(patch)
269
+ activePatch.forEach(entry => entry.path.unshift('active'))
270
+ interact(scope, activePatch)
271
+ }))
272
+ }
273
+ catch (error) {
274
+ rejectState(error)
275
+ }
247
276
  })
248
277
 
249
278
  promise.watch = fn => {
250
- key
251
- .then( k => {
252
- if (watchFn) throw new Error('Only one watcher allowed')
253
- if (!watchers[k]) watchers[k] = []
254
- watchers[k].push(fn)
255
- watchFn = fn
256
- })
279
+ if (watchFn) throw new Error('Only one watcher allowed')
280
+ if (!watchers[scope]) watchers[scope] = []
281
+ watchers[scope].push(fn)
282
+ watchFn = fn
283
+
257
284
  return promise
258
285
  }
259
286
 
260
287
  promise.unwatch = async () => {
261
- const k = await key
262
- removeWatcher(k, watchFn)
263
- if (watchers[k].length === 0) {
264
- delete subscriptions[keyToSubscriptionId[k]]
265
- delete keyToSubscriptionId[k]
266
- delete watchers[k]
288
+ removeWatcher(scope, watchFn)
289
+ if (watchers[scope].length === 0) {
290
+ delete subscriptions[keyToSubscriptionId[scope]]
291
+ delete keyToSubscriptionId[scope]
292
+ delete watchers[scope]
267
293
  }
268
294
  }
269
295
 
@@ -273,9 +299,16 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
273
299
  // TODO: if no data, set up streaming upload
274
300
  async function upload(name, type, data, id=uuid()) {
275
301
  // TODO: include data size info...
276
- uploads[id] = { url: null, sent: 0, name, type }
302
+ const uploadId = uuid()
303
+ initialize(
304
+ uploadId,
305
+ 'application/json;type=upload',
306
+ {
307
+ id,
308
+ type
309
+ }
310
+ )
277
311
  const { url } = await lastMessageResponse()
278
- uploads[id].url = url
279
312
 
280
313
  if (data === undefined) return url
281
314
  else {
@@ -289,9 +322,14 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
289
322
  }
290
323
 
291
324
  async function download(id, passthrough=false) {
292
- downloads[id] = { url: null, size: null, sent: 0 }
325
+ // TODO: initialize size info
326
+ const downloadId = uuid()
327
+ initialize(
328
+ downloadId,
329
+ 'application/json;type=download',
330
+ { id }
331
+ )
293
332
  const { url } = await lastMessageResponse()
294
- downloads[id].url = url
295
333
 
296
334
  if (passthrough) return url
297
335
  else {
@@ -310,30 +348,25 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
310
348
  }
311
349
 
312
350
  async function interact(scope, patch) {
313
- queueMessage({scope, patch})
314
- const response = lastMessageResponse()
315
-
316
- await environmentPromise
317
-
318
- const key = `${domain}/${user}/${scope}`
351
+ // TODO: ensure user is owner of scope
352
+ const response = queueMessage({scope, patch})
319
353
 
320
- // if we are watching this key, we want to keep track of last interaction we fired
321
- if (states[key] !== undefined) {
354
+ // if we are watching this scope, we want to keep track of last interaction we fired
355
+ if (states[scope] !== undefined) {
322
356
  let resolve
323
- lastInteractionUpdateForWatchedScope[key] = new Promise(r => resolve = r)
357
+ lastInteractionResponse[scope] = new Promise(r => resolve = r)
324
358
 
325
- const { ii } = await response
326
-
327
- const resolveAndUnwatch = (update) => {
359
+ const resolveAndUnwatch = async (update) => {
360
+ const { ii } = await response
328
361
  if (update.ii === ii) {
329
362
  resolve(ii)
330
- removeWatcher(key, resolveAndUnwatch)
363
+ removeWatcher(scope, resolveAndUnwatch)
331
364
  }
332
365
  }
333
366
 
334
- watchers[key].push(resolveAndUnwatch)
367
+ watchers[scope].push(resolveAndUnwatch)
335
368
 
336
- return { ii }
369
+ return response
337
370
  }
338
371
  else {
339
372
  const { ii } = await response
@@ -342,20 +375,36 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
342
375
  }
343
376
 
344
377
  async function claim(domain) {
345
- return interact('claims', [{ op: 'add', path: [domain], value: null }])
378
+ return interact('claims', [{ op: 'add', path: ['active', domain], value: null }])
346
379
  }
347
380
 
348
- function mutate(scope, initialize=true) {
349
- const mp = s => new MutableProxy(s || {}, patch => interact(scope, patch))
350
- return initialize ? state(scope).then(mp) : mp({})
381
+ function reset(scope) {
382
+ return interact(scope, [{ op: 'remove', path:['active'] }])
351
383
  }
352
384
 
353
- function reset(scope) {
354
- return interact(scope, [{ op: 'add', path:[], value: null }])
385
+ function isValidMetadataMutation({ path, op, value }) {
386
+ return (
387
+ ['active_type', 'name'].includes(path[0])
388
+ && path.length === 1
389
+ && typeof value === 'string' || op === 'remove'
390
+ )
391
+ }
392
+
393
+ async function metadata(id) {
394
+ await state(id)
395
+ const md = structuredClone(await states[id])
396
+ delete md.active
397
+ return new MutableProxy(md, patch => {
398
+ const activePatch = structuredClone(patch)
399
+ activePatch.forEach(entry => {
400
+ if (!isValidMetadataMutation(entry)) throw new Error('You may only modify the type or name for a scope\'s metadata')
401
+ })
402
+ interact(id, activePatch)
403
+ })
355
404
  }
356
405
 
357
- function metadata(id) {
358
- return state(id, 'metadata', 'core')
406
+ async function synced() {
407
+ return new Promise(resolve => syncedPromiseResolutions.push(resolve))
359
408
  }
360
409
 
361
410
  function disconnect() {
@@ -384,9 +433,9 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
384
433
  interact,
385
434
  patch,
386
435
  claim,
387
- mutate,
388
436
  reset,
389
437
  metadata,
438
+ synced,
390
439
  disconnect,
391
440
  reconnect,
392
441
  debug
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
1
  {
2
- "name": "@knowlearning/agents",
3
- "version": "0.4.14",
4
- "description": "API for embedding applications in KnowLearning systems.",
5
- "main": "node.js",
6
- "browser": "browser.js",
7
- "type": "module",
8
- "directories": {
9
- "example": "examples",
10
- "test": "test"
11
- },
12
- "scripts": {
13
- "test": "echo \"Error: no test specified\" && exit 1"
14
- },
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/knowlearning/core.git"
18
- },
19
- "author": "KnowLearning",
20
- "license": "MPL-2.0",
21
- "bugs": {
22
- "url": "https://github.com/knowlearning/core/issues"
23
- },
24
- "homepage": "https://github.com/knowlearning/core#readme",
25
- "dependencies": {
26
- "fast-json-patch": "^3.1.1",
27
- "node-fetch": "^3.3.1",
28
- "uuid": "^8.3.2",
29
- "firebase": "^9.12.1"
30
- }
2
+ "name": "@knowlearning/agents",
3
+ "version": "0.5.0",
4
+ "description": "API for embedding applications in KnowLearning systems.",
5
+ "main": "node.js",
6
+ "browser": "browser.js",
7
+ "type": "module",
8
+ "directories": {
9
+ "example": "examples",
10
+ "test": "test"
11
+ },
12
+ "scripts": {
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/knowlearning/core.git"
18
+ },
19
+ "author": "KnowLearning",
20
+ "license": "MPL-2.0",
21
+ "bugs": {
22
+ "url": "https://github.com/knowlearning/core/issues"
23
+ },
24
+ "homepage": "https://github.com/knowlearning/core#readme",
25
+ "dependencies": {
26
+ "@firebase/app": "^0.9.14",
27
+ "@firebase/auth": "^1.0.0",
28
+ "fast-json-patch": "^3.1.1",
29
+ "node-fetch": "^3.3.1",
30
+ "uuid": "^8.3.2"
31
31
  }
32
-
32
+ }
@@ -13,10 +13,7 @@ export default async function (storeDefinition) {
13
13
  const stateAttachedStore = await attachModuleState(savedState, storeDefinition, scopedPaths)
14
14
  const s = stateAttachedStore.state
15
15
  const originalState = s instanceof Function ? s() : s
16
- const handlePatch = patch => {
17
- console.log('PATCHING STATE', patch)
18
- Agent.interact(scope, patch)
19
- }
16
+ const handlePatch = patch => Agent.interact(scope, patch)
20
17
 
21
18
  if (savedState === null) handlePatch([{ op: 'add', path: [], value: copy(originalState) }])
22
19
 
@@ -12,7 +12,7 @@ export default function (module, scope) {
12
12
  const component = module.default ? { ...module.default } : { ...module } // copy component since we will mess with mounted and data functions
13
13
 
14
14
  if (!component.ephemeral) {
15
- const state = await Agent.mutate(scope)
15
+ const state = await Agent.state(scope)
16
16
 
17
17
  if (component.data) {
18
18
  const origDataFn = component.data