@knowlearning/agents 0.4.13 → 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,76 +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 subscriptions[id]
256
+ catch (error) {
257
+ reject(error)
234
258
  }
235
259
  })
236
260
  }
237
261
 
238
- // TODO: make sure to get the state that represents the last interaction we sent
239
- // when this "state" method was called!!!!!!!!!!!!!!!!!!!!!!!
240
- // Right now this is simply returning whatever we have at the time we get here
241
- // when some interaction responses may not have been applied yet...
242
- // POSSIBLE: wait for most recent interaction update response for key k that we sent...
243
- await lastInteractionUpdateForWatchedScope[k]
244
- // TODO: probably something like await updateMessageReceivedForLastInteractionWeSentForKey[k]
245
- 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
+ }
246
276
  })
247
277
 
248
278
  promise.watch = fn => {
249
- key
250
- .then( k => {
251
- if (watchFn) throw new Error('Only one watcher allowed')
252
- if (!watchers[k]) watchers[k] = []
253
- watchers[k].push(fn)
254
- watchFn = fn
255
- })
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
+
256
284
  return promise
257
285
  }
258
286
 
259
287
  promise.unwatch = async () => {
260
- const k = await key
261
- removeWatcher(k, watchFn)
262
- if (watchers[k].length === 0) {
263
- delete subscriptions[keyToSubscriptionId[k]]
264
- 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]
265
293
  }
266
294
  }
267
295
 
@@ -271,9 +299,16 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
271
299
  // TODO: if no data, set up streaming upload
272
300
  async function upload(name, type, data, id=uuid()) {
273
301
  // TODO: include data size info...
274
- 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
+ )
275
311
  const { url } = await lastMessageResponse()
276
- uploads[id].url = url
277
312
 
278
313
  if (data === undefined) return url
279
314
  else {
@@ -287,9 +322,14 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
287
322
  }
288
323
 
289
324
  async function download(id, passthrough=false) {
290
- 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
+ )
291
332
  const { url } = await lastMessageResponse()
292
- downloads[id].url = url
293
333
 
294
334
  if (passthrough) return url
295
335
  else {
@@ -308,30 +348,25 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
308
348
  }
309
349
 
310
350
  async function interact(scope, patch) {
311
- queueMessage({scope, patch})
312
- const response = lastMessageResponse()
313
-
314
- await environmentPromise
315
-
316
- const key = `${domain}/${user}/${scope}`
351
+ // TODO: ensure user is owner of scope
352
+ const response = queueMessage({scope, patch})
317
353
 
318
- // if we are watching this key, we want to keep track of last interaction we fired
319
- 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) {
320
356
  let resolve
321
- lastInteractionUpdateForWatchedScope[key] = new Promise(r => resolve = r)
357
+ lastInteractionResponse[scope] = new Promise(r => resolve = r)
322
358
 
323
- const { ii } = await response
324
-
325
- const resolveAndUnwatch = (update) => {
359
+ const resolveAndUnwatch = async (update) => {
360
+ const { ii } = await response
326
361
  if (update.ii === ii) {
327
362
  resolve(ii)
328
- removeWatcher(key, resolveAndUnwatch)
363
+ removeWatcher(scope, resolveAndUnwatch)
329
364
  }
330
365
  }
331
366
 
332
- watchers[key].push(resolveAndUnwatch)
367
+ watchers[scope].push(resolveAndUnwatch)
333
368
 
334
- return { ii }
369
+ return response
335
370
  }
336
371
  else {
337
372
  const { ii } = await response
@@ -340,20 +375,36 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
340
375
  }
341
376
 
342
377
  async function claim(domain) {
343
- return interact('claims', [{ op: 'add', path: [domain], value: null }])
378
+ return interact('claims', [{ op: 'add', path: ['active', domain], value: null }])
344
379
  }
345
380
 
346
- function mutate(scope, initialize=true) {
347
- const mp = s => new MutableProxy(s || {}, patch => interact(scope, patch))
348
- return initialize ? state(scope).then(mp) : mp({})
381
+ function reset(scope) {
382
+ return interact(scope, [{ op: 'remove', path:['active'] }])
349
383
  }
350
384
 
351
- function reset(scope) {
352
- 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
+ })
353
404
  }
354
405
 
355
- function metadata(id) {
356
- return state(id, 'metadata', 'core')
406
+ async function synced() {
407
+ return new Promise(resolve => syncedPromiseResolutions.push(resolve))
357
408
  }
358
409
 
359
410
  function disconnect() {
@@ -382,9 +433,9 @@ export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fet
382
433
  interact,
383
434
  patch,
384
435
  claim,
385
- mutate,
386
436
  reset,
387
437
  metadata,
438
+ synced,
388
439
  disconnect,
389
440
  reconnect,
390
441
  debug
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
1
  {
2
- "name": "@knowlearning/agents",
3
- "version": "0.4.13",
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