@knowlearning/agents 0.9.188 → 0.9.190

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.
@@ -0,0 +1,83 @@
1
+ import { applyPatch } from 'fast-json-patch'
2
+ import { standardJSONPatch } from '@knowlearning/patch-proxy'
3
+
4
+ // Wraps a state promise with a .synced(callback?) method that applies external
5
+ // patches to the live proxy in-place.
6
+ //
7
+ // Usage:
8
+ // const promise = attachSynced(watchers, (ctx, resolve) => {
9
+ // const p = new Promise(async (...) => {
10
+ // const proxy = new PatchProxy(data, patch => {
11
+ // if (ctx.applyingExternalUpdate) return
12
+ // // ... interact
13
+ // })
14
+ // resolve(proxy, qualifiedKey)
15
+ // resolvePromise(proxy)
16
+ // })
17
+ // return p
18
+ // })
19
+ export default function attachSynced(watchers, init) {
20
+ const ctx = { applyingExternalUpdate: false }
21
+ let shouldSync = false
22
+ let syncCallbacks = []
23
+ let syncRegistered = false
24
+ let resolvedProxy = null
25
+ let resolvedKey = null
26
+
27
+ function registerSyncWatcher() {
28
+ if (syncRegistered) return
29
+ syncRegistered = true
30
+ if (!watchers[resolvedKey]) watchers[resolvedKey] = []
31
+ watchers[resolvedKey].push(({ patch, state, ii }) => {
32
+ if (!patch) return
33
+ const pending = ctx.pendingInteractions?.size
34
+ ? Promise.all(ctx.pendingInteractions)
35
+ : Promise.resolve()
36
+ pending.then(() => {
37
+ const isOwn = ctx.ownInteractions?.has(ii)
38
+ const echoBatch = isOwn ? ctx.ownInteractionBatches?.get(ii) : null
39
+ if (isOwn) {
40
+ ctx.ownInteractions.delete(ii)
41
+ if (echoBatch) ctx.ownInteractionBatches.delete(ii)
42
+ } else {
43
+ ctx.applyingExternalUpdate = true
44
+ applyPatch(resolvedProxy, standardJSONPatch(patch))
45
+ ctx.applyingExternalUpdate = false
46
+ }
47
+ if (echoBatch) {
48
+ echoBatch.state = state
49
+ if (echoBatch.remaining !== undefined) echoBatch.remaining -= 1
50
+ if ((echoBatch.remaining === undefined || echoBatch.remaining === 0) && !echoBatch.resolved) {
51
+ echoBatch.resolved = true
52
+ syncCallbacks.forEach(cb => cb(echoBatch.state, patch))
53
+ echoBatch.resolve()
54
+ }
55
+ return
56
+ }
57
+ syncCallbacks.forEach(cb => cb(state, patch))
58
+ if (isOwn && ctx.echoResolvers?.length) {
59
+ ctx.echoResolvers.shift()()
60
+ }
61
+ })
62
+ })
63
+ }
64
+
65
+ function resolve(proxy, key) {
66
+ resolvedProxy = proxy
67
+ resolvedKey = key
68
+ if (shouldSync) registerSyncWatcher()
69
+ }
70
+
71
+ const promise = init(ctx, resolve)
72
+
73
+ promise.synced = function(callback) {
74
+ shouldSync = true
75
+ ctx.syncActive = true
76
+ if (!ctx.echoResolvers) ctx.echoResolvers = []
77
+ if (callback) syncCallbacks.push(callback)
78
+ if (resolvedProxy) registerSyncWatcher()
79
+ return promise
80
+ }
81
+
82
+ return promise
83
+ }
@@ -1,5 +1,6 @@
1
1
  import { validate as isUUID, v1 as uuid } from 'uuid'
2
2
  import PatchProxy from '@knowlearning/patch-proxy'
3
+ import attachSynced from './attach-synced.js'
3
4
  import watchImplementation from './watch.js'
4
5
  import sync from './sync.js'
5
6
 
@@ -10,6 +11,7 @@ export default function EmbeddedAgent(postMessage) {
10
11
  const responses = {}
11
12
  const watchers = {}
12
13
  const sentUpdates = {}
14
+ const pendingEchoCallbacks = new Set()
13
15
 
14
16
  const [ watch, removeWatcher ] = watchImplementation({ metadata, state, watchers, synced, sentUpdates, environment })
15
17
 
@@ -96,18 +98,63 @@ export default function EmbeddedAgent(postMessage) {
96
98
  return send({ type: 'patch', root, scopes })
97
99
  }
98
100
 
99
- async function state(scope, user, domain) {
100
- if (scope === undefined) {
101
- const { context } = await environment()
102
- scope = JSON.stringify(context)
103
- }
104
- const startState = await send({ type: 'state', scope, user, domain })
105
- return new PatchProxy(startState, patch => {
106
- // TODO: reject updates if user is not owner
107
- const activePatch = structuredClone(patch)
108
- activePatch.forEach(entry => entry.path.unshift('active'))
109
- interact(scope, activePatch)
110
- })
101
+ function state(scope, user, domain) {
102
+ return attachSynced(watchers, (ctx, resolveSync) => (async () => {
103
+ if (scope === undefined) {
104
+ const { context } = await environment()
105
+ scope = JSON.stringify(context)
106
+ }
107
+ const startState = await send({ type: 'state', scope, user, domain })
108
+ const { auth, domain: rootDomain } = await environment()
109
+ const d = !domain || domain === rootDomain ? '' : domain
110
+ const u = !user || auth.user === user ? '' : user
111
+ const key = isUUID(scope) ? scope : `${d}/${u}/${scope}`
112
+ if (!ctx.ownInteractions) ctx.ownInteractions = new Set()
113
+ if (!ctx.ownInteractionBatches) ctx.ownInteractionBatches = new Map()
114
+ if (!ctx.pendingInteractions) ctx.pendingInteractions = new Set()
115
+ if (!ctx.echoResolvers) ctx.echoResolvers = []
116
+ let currentEchoBatch = null
117
+ const proxy = new PatchProxy(startState, patch => {
118
+ // TODO: reject updates if user is not owner
119
+ if (ctx.applyingExternalUpdate) return
120
+ const activePatch = structuredClone(patch)
121
+ activePatch.forEach(entry => entry.path.unshift('active'))
122
+ let echoBatch
123
+ if (ctx.syncActive) {
124
+ if (!currentEchoBatch) {
125
+ let resolveEcho
126
+ const echoPromise = new Promise(r => resolveEcho = r)
127
+ echoBatch = currentEchoBatch = { remaining: 0, resolve: resolveEcho, resolved: false }
128
+ pendingEchoCallbacks.add(echoPromise)
129
+ echoPromise.then(() => pendingEchoCallbacks.delete(echoPromise))
130
+ Promise.resolve().then(() => { currentEchoBatch = null })
131
+ }
132
+ else echoBatch = currentEchoBatch
133
+ echoBatch.remaining += 1
134
+ }
135
+ const interactPromise = interact(scope, activePatch)
136
+ const p = interactPromise.then(
137
+ r => {
138
+ ctx.ownInteractions.add(r.ii)
139
+ if (echoBatch) ctx.ownInteractionBatches.set(r.ii, echoBatch)
140
+ ctx.pendingInteractions.delete(p)
141
+ },
142
+ () => {
143
+ if (echoBatch) {
144
+ echoBatch.remaining -= 1
145
+ if (echoBatch.remaining === 0 && !echoBatch.resolved) {
146
+ echoBatch.resolved = true
147
+ echoBatch.resolve()
148
+ }
149
+ }
150
+ ctx.pendingInteractions.delete(p)
151
+ }
152
+ )
153
+ ctx.pendingInteractions.add(p)
154
+ })
155
+ resolveSync(proxy, key)
156
+ return proxy
157
+ })())
111
158
  }
112
159
 
113
160
  function reset(scope) {
@@ -203,7 +250,11 @@ export default function EmbeddedAgent(postMessage) {
203
250
  function logout() { return send({ type: 'logout' }) }
204
251
  function disconnect() { return send({ type: 'disconnect' }) }
205
252
  function reconnect() { return send({ type: 'reconnect' }) }
206
- function synced() { return send({ type: 'synced' }) }
253
+ async function synced() {
254
+ const echoSnapshot = [...pendingEchoCallbacks]
255
+ await send({ type: 'synced' })
256
+ if (echoSnapshot.length) await Promise.all(echoSnapshot)
257
+ }
207
258
  function close(info) { return send({ type: 'close', info }) }
208
259
  function guarantee(script, namespaces, context) { return send({ type: 'guarantee', script, namespaces, context }) }
209
260
  function response(id=lastRequestId) { return send({ type: 'response', id }) }
@@ -17,6 +17,7 @@ export default function Agent({ Connection, domain, token, sid, uuid, fetch, app
17
17
  const watchers = {}
18
18
  const keyToSubscriptionId = {}
19
19
  const lastInteractionResponse = {}
20
+ const pendingEchoCallbacks = new Set()
20
21
 
21
22
  log('INITIALIZING AGENT CONNECTION')
22
23
  const [
@@ -47,6 +48,7 @@ export default function Agent({ Connection, domain, token, sid, uuid, fetch, app
47
48
  interact,
48
49
  fetch,
49
50
  synced,
51
+ pendingEchoCallbacks,
50
52
  metadata,
51
53
  log
52
54
  }
@@ -204,6 +206,12 @@ export default function Agent({ Connection, domain, token, sid, uuid, fetch, app
204
206
  }
205
207
  }
206
208
 
209
+ async function syncedWithEchoCallbacks() {
210
+ const echoSnapshot = [...pendingEchoCallbacks]
211
+ await synced()
212
+ if (echoSnapshot.length) await Promise.all(echoSnapshot)
213
+ }
214
+
207
215
  function response() {
208
216
  return lastMessageResponse()
209
217
  }
@@ -224,7 +232,7 @@ export default function Agent({ Connection, domain, token, sid, uuid, fetch, app
224
232
  reset,
225
233
  metadata,
226
234
  query,
227
- synced,
235
+ synced: syncedWithEchoCallbacks,
228
236
  disconnect,
229
237
  reconnect,
230
238
  debug,
@@ -1,11 +1,12 @@
1
1
  import { v4 as uuid, validate as isUUID } from 'uuid'
2
2
  import PatchProxy from '@knowlearning/patch-proxy'
3
+ import attachSynced from '../attach-synced.js'
3
4
 
4
- export default function(scope='[]', user, domain, { keyToSubscriptionId, watchers, states, create, environment, lastMessageResponse, lastInteractionResponse, interact, log }) {
5
+ export default function(scope='[]', user, domain, { keyToSubscriptionId, watchers, states, create, environment, lastMessageResponse, lastInteractionResponse, interact, pendingEchoCallbacks, log }) {
5
6
  let resolveMetadataPromise
6
7
  let metadataPromise = new Promise(resolve => resolveMetadataPromise = resolve)
7
8
 
8
- const statePromise = new Promise(async (resolveState, rejectState) => {
9
+ const statePromise = attachSynced(watchers, (ctx, resolveSync) => new Promise(async (resolveState, rejectState) => {
9
10
  const { auth: { user: u }, domain: d, session } = await environment()
10
11
 
11
12
  const qualifiedScope = isUUID(scope) ? scope : `${!domain || domain === d ? '' : domain}/${!user || user === u ? '' : user}/${scope}`
@@ -35,17 +36,52 @@ export default function(scope='[]', user, domain, { keyToSubscriptionId, watcher
35
36
  const active = data.active
36
37
  delete data.active
37
38
  resolveMetadataPromise(data)
38
- resolveState(new PatchProxy(active || {}, patch => {
39
+ if (!ctx.ownInteractions) ctx.ownInteractions = new Set()
40
+ if (!ctx.ownInteractionBatches) ctx.ownInteractionBatches = new Map()
41
+ if (!ctx.pendingInteractions) ctx.pendingInteractions = new Set()
42
+ if (!ctx.echoResolvers) ctx.echoResolvers = []
43
+ let currentEchoBatch = null
44
+ const proxy = new PatchProxy(active || {}, patch => {
45
+ if (ctx.applyingExternalUpdate) return
39
46
  const activePatch = structuredClone(patch)
40
47
  activePatch.forEach(entry => entry.path.unshift('active'))
41
- interact(scope, activePatch)
42
- }))
48
+ let echoBatch
49
+ if (ctx.syncActive && pendingEchoCallbacks) {
50
+ if (!currentEchoBatch) {
51
+ let resolveEcho
52
+ const echoPromise = new Promise(r => resolveEcho = r)
53
+ echoBatch = currentEchoBatch = { resolve: resolveEcho, resolved: false }
54
+ pendingEchoCallbacks.add(echoPromise)
55
+ echoPromise.then(() => pendingEchoCallbacks.delete(echoPromise))
56
+ Promise.resolve().then(() => { currentEchoBatch = null })
57
+ }
58
+ else echoBatch = currentEchoBatch
59
+ }
60
+ const interactPromise = interact(scope, activePatch)
61
+ const p = interactPromise.then(
62
+ r => {
63
+ ctx.ownInteractions.add(r.ii)
64
+ if (echoBatch) ctx.ownInteractionBatches.set(r.ii, echoBatch)
65
+ ctx.pendingInteractions.delete(p)
66
+ },
67
+ () => {
68
+ if (echoBatch && !echoBatch.resolved) {
69
+ echoBatch.resolved = true
70
+ echoBatch.resolve()
71
+ }
72
+ ctx.pendingInteractions.delete(p)
73
+ }
74
+ )
75
+ ctx.pendingInteractions.add(p)
76
+ })
77
+ resolveSync(proxy, qualifiedScope)
78
+ resolveState(proxy)
43
79
  }
44
80
  catch (error) {
45
81
  rejectState(error)
46
82
  }
47
- })
83
+ }))
48
84
 
49
85
  statePromise.metadata = metadataPromise
50
86
  return statePromise
51
- }
87
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowlearning/agents",
3
- "version": "0.9.188",
3
+ "version": "0.9.190",
4
4
  "description": "API for embedding applications in KnowLearning systems.",
5
5
  "main": "node.js",
6
6
  "browser": "browser.js",
package/types.d.ts CHANGED
@@ -27,11 +27,16 @@ export interface AgentUploadInfo {
27
27
  accept?: string;
28
28
  }
29
29
 
30
+ export interface SyncedStatePromise extends Promise<object> {
31
+ synced(callback?: (state: object, patch: object[]) => void): SyncedStatePromise;
32
+ metadata: Promise<object>;
33
+ }
34
+
30
35
  export interface Agent {
31
36
  login(provider: string): void;
32
37
  logout(): void;
33
38
  uuid(): string;
34
- state(id: string, user?: string, domain?: string): Promise<object>;
39
+ state(id: string, user?: string, domain?: string): SyncedStatePromise;
35
40
  metadata(id: string, user?: string, domain?: string): Promise<object>;
36
41
  watch(id: string, callback: (update: { state: object }) => void, user?: string, domain?: string): void;
37
42
  upload(info?: AgentUploadInfo): Promise<string>;