@knowlearning/agents 0.9.176 → 0.9.178

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,230 +1,10 @@
1
- import { validate as isUUID, v1 as uuid } from 'uuid'
2
- import PatchProxy from '@knowlearning/patch-proxy'
3
- import watchImplementation from '../watch.js'
1
+ import EmbeddedAgent from '../embedded.js'
4
2
 
5
- export default function EmbeddedAgent() {
6
- let messageIndex = 0
7
- let resolveSession
8
- const session = new Promise(r => resolveSession = r)
9
- const responses = {}
10
- const watchers = {}
11
- const sentUpdates = {}
3
+ export default () => {
4
+ // default to window opener if present
5
+ const parent = window.opener ? window.opener : window.parent
6
+ const agent = EmbeddedAgent(parent)
12
7
 
13
- const [ watch, removeWatcher ] = watchImplementation({ metadata, state, watchers, synced, sentUpdates, environment })
14
-
15
- async function send(message) {
16
- const requestId = message.requestId || uuid()
17
-
18
- messageIndex += 1
19
- // default to window opener if present
20
- const upstream = window.opener ? window.opener : window.parent
21
- try {
22
- upstream
23
- .postMessage({
24
- ...message,
25
- session: await session,
26
- requestId,
27
- index: messageIndex
28
- }, '*')
29
- return new Promise((resolve, reject) => {
30
- responses[requestId] = { resolve, reject }
31
- })
32
- }
33
- catch (error) {
34
- console.log('ERROR POSTING MESSAGE UP', message, error)
35
- }
36
- }
37
-
38
- let sessionResolved = false
39
- addEventListener('message', async ({ data }) => {
40
- if (data.type === 'setup' && !sessionResolved) {
41
- sessionResolved = true
42
- resolveSession(data.session)
43
- }
44
- else if (!sessionResolved || data.session !== await session) return
45
- else if (responses[data.requestId]) {
46
- const { resolve, reject } = responses[data.requestId]
47
- if (data.error) reject(data.error)
48
- else resolve(data.response)
49
- }
50
- else if (data.ii !== undefined) {
51
- const { scope, user, domain } = data
52
- const { auth, domain:rootDomain } = await environment()
53
- const d = !domain || domain === rootDomain ? '' : domain
54
- const u = !user || auth.user === user ? '' : user
55
- const key = isUUID(scope) ? scope : `${d}/${u}/${scope}`
56
-
57
- const sendUpdate = () => {
58
- sentUpdates[key] = data.ii
59
- watchers[key].forEach(fn => fn(data))
60
- }
61
- if (watchers[key]) {
62
- if (sentUpdates[key] === undefined || sentUpdates[key] + 1 === data.ii) {
63
- sendUpdate()
64
- } else if (data.ii === sentUpdates[key]) {
65
- //console.warn('Repeated update for', key, data, sentUpdates[key])
66
- } else if (data.ii < sentUpdates[key]) {
67
- //console.warn('Out of order update, from past', key, JSON.stringify(data, null, 4), sentUpdates[key])
68
- } else {
69
- //console.warn('Out of order update, fast forward', key, JSON.stringify(data, null, 4), sentUpdates[key])
70
- sendUpdate()
71
- }
72
- }
73
- }
74
- })
75
-
76
- let variables
77
-
78
- async function environment(user) {
79
- const response = await send({ type: 'environment', user })
80
- // keep copy on initialize symantics for environment variables
81
- if (!variables && !user) variables = response.variables
82
- return { ...response, variables }
83
- }
84
-
85
- function create({ id=uuid(), active_type, active }) {
86
- if (!active_type) active_type = 'application/json'
87
- interact(id, [
88
- { op: 'add', path: ['active_type'], value: active_type },
89
- { op: 'add', path: ['active'], value: active }
90
- ])
91
- return id
92
- }
93
-
94
- async function patch(root, scopes) {
95
- // TODO: consider watch function added to return to receive progress
96
- return send({ type: 'patch', root, scopes })
97
- }
98
-
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
- })
111
- }
112
-
113
- function reset(scope) {
114
- return interact(scope, [{ op: 'add', path:['active'], value: null }])
115
- }
116
-
117
- function interact(scope, patch, _, context) {
118
- return send({ type: 'interact', scope, patch, context })
119
- }
120
-
121
- async function upload(info) {
122
- let { name, type, data, id=uuid() } = info || {}
123
-
124
- const url = await send({ type: 'upload', info: { name, type, id } })
125
-
126
- if (data === undefined) return url
127
- else {
128
- const headers = { 'Content-Type': type }
129
- const response = await fetch(url, {method: 'PUT', headers, body: data})
130
- const { ok, statusText } = response
131
-
132
- if (ok) return id
133
- else throw new Error(statusText)
134
- }
135
- }
136
-
137
- function download(id) {
138
- let mode = 'fetch'
139
- const promise = new Promise(async (resolve, reject) => {
140
- const url = await send({ type: 'download', id })
141
-
142
- await new Promise(r => setTimeout(r))
143
- if (mode === 'url') resolve(url)
144
- else if (mode === 'fetch') {
145
- const response = await fetch(url)
146
- const { ok, statusText } = response
147
-
148
- if (ok) resolve(response)
149
- else reject(statusText)
150
- }
151
- else if (mode === 'direct') {
152
- // TODO: use browser progress UX instead of downloading all into memory first
153
- const res = await download(id)
154
- const { name } = await metadata(id)
155
- const type = res.headers.get('Content-Type')
156
- const blob = new Blob([ await res.blob() ], { type })
157
- const url = window.URL.createObjectURL(blob)
158
- const a = document.createElement('a')
159
- a.style.display = 'none'
160
- a.href = url
161
- a.download = name
162
- document.body.appendChild(a)
163
- a.click()
164
- window.URL.revokeObjectURL(url)
165
- resolve()
166
- }
167
- })
168
- promise.direct = () => {
169
- mode = 'direct'
170
- return promise
171
- }
172
- promise.url = () => {
173
- mode = 'url'
174
- return promise
175
- }
176
- return promise
177
- }
178
-
179
- function isValidMetadataMutation({ path, op, value }) {
180
- return (
181
- ['active_type', 'name'].includes(path[0])
182
- && path.length === 1
183
- && typeof value === 'string' || op === 'remove'
184
- )
185
- }
186
-
187
- async function metadata(scope, user, domain) {
188
- const md = await send({ type: 'metadata', scope, user, domain })
189
- return new PatchProxy(md, patch => {
190
- const activePatch = structuredClone(patch)
191
- activePatch.forEach(entry => {
192
- if (!isValidMetadataMutation(entry)) throw new Error('You may only modify the type or name for a scope\'s metadata')
193
- })
194
- interact(scope, activePatch)
195
- })
196
- }
197
-
198
- function login(provider, username, password) {
199
- return send({ type: 'login', provider, username, password })
200
- }
201
-
202
- function query(query, params, domain, context=[]) { return send({ type: 'query', query, params, domain, context }) }
203
- function logout() { return send({ type: 'logout' }) }
204
- function disconnect() { return send({ type: 'disconnect' }) }
205
- function reconnect() { return send({ type: 'reconnect' }) }
206
- function synced() { return send({ type: 'synced' }) }
207
- function close(info) { return send({ type: 'close', info }) }
208
-
209
- return {
210
- embedded: true,
211
- uuid,
212
- environment,
213
- login,
214
- logout,
215
- create,
216
- state,
217
- watch,
218
- upload,
219
- download,
220
- interact,
221
- patch,
222
- reset,
223
- metadata,
224
- disconnect,
225
- reconnect,
226
- synced,
227
- close,
228
- query
229
- }
8
+ return agent
230
9
  }
10
+
@@ -0,0 +1,228 @@
1
+ import { validate as isUUID, v1 as uuid } from 'uuid'
2
+ import PatchProxy from '@knowlearning/patch-proxy'
3
+ import watchImplementation from './watch.js'
4
+
5
+ export default function EmbeddedAgent(parent) {
6
+ let messageIndex = 0
7
+ let resolveSession
8
+ const session = new Promise(r => resolveSession = r)
9
+ const responses = {}
10
+ const watchers = {}
11
+ const sentUpdates = {}
12
+
13
+ const [ watch, removeWatcher ] = watchImplementation({ metadata, state, watchers, synced, sentUpdates, environment })
14
+
15
+ async function send(message) {
16
+ const requestId = message.requestId || uuid()
17
+
18
+ messageIndex += 1
19
+ try {
20
+ parent
21
+ .postMessage({
22
+ ...message,
23
+ session: await session,
24
+ requestId,
25
+ index: messageIndex
26
+ }, '*')
27
+ return new Promise((resolve, reject) => {
28
+ responses[requestId] = { resolve, reject }
29
+ })
30
+ }
31
+ catch (error) {
32
+ console.log('ERROR POSTING MESSAGE UP', message, error)
33
+ }
34
+ }
35
+
36
+ let sessionResolved = false
37
+ addEventListener('message', async ({ data }) => {
38
+ if (data.type === 'setup' && !sessionResolved) {
39
+ sessionResolved = true
40
+ resolveSession(data.session)
41
+ }
42
+ else if (!sessionResolved || data.session !== await session) return
43
+ else if (responses[data.requestId]) {
44
+ const { resolve, reject } = responses[data.requestId]
45
+ if (data.error) reject(data.error)
46
+ else resolve(data.response)
47
+ }
48
+ else if (data.ii !== undefined) {
49
+ const { scope, user, domain } = data
50
+ const { auth, domain:rootDomain } = await environment()
51
+ const d = !domain || domain === rootDomain ? '' : domain
52
+ const u = !user || auth.user === user ? '' : user
53
+ const key = isUUID(scope) ? scope : `${d}/${u}/${scope}`
54
+
55
+ const sendUpdate = () => {
56
+ sentUpdates[key] = data.ii
57
+ watchers[key].forEach(fn => fn(data))
58
+ }
59
+ if (watchers[key]) {
60
+ if (sentUpdates[key] === undefined || sentUpdates[key] + 1 === data.ii) {
61
+ sendUpdate()
62
+ } else if (data.ii === sentUpdates[key]) {
63
+ //console.warn('Repeated update for', key, data, sentUpdates[key])
64
+ } else if (data.ii < sentUpdates[key]) {
65
+ //console.warn('Out of order update, from past', key, JSON.stringify(data, null, 4), sentUpdates[key])
66
+ } else {
67
+ //console.warn('Out of order update, fast forward', key, JSON.stringify(data, null, 4), sentUpdates[key])
68
+ sendUpdate()
69
+ }
70
+ }
71
+ }
72
+ })
73
+
74
+ let variables
75
+
76
+ async function environment(user) {
77
+ const response = await send({ type: 'environment', user })
78
+ // keep copy on initialize symantics for environment variables
79
+ if (!variables && !user) variables = response.variables
80
+ return { ...response, variables }
81
+ }
82
+
83
+ function create({ id=uuid(), active_type, active }) {
84
+ if (!active_type) active_type = 'application/json'
85
+ interact(id, [
86
+ { op: 'add', path: ['active_type'], value: active_type },
87
+ { op: 'add', path: ['active'], value: active }
88
+ ])
89
+ return id
90
+ }
91
+
92
+ async function patch(root, scopes) {
93
+ // TODO: consider watch function added to return to receive progress
94
+ return send({ type: 'patch', root, scopes })
95
+ }
96
+
97
+ async function state(scope, user, domain) {
98
+ if (scope === undefined) {
99
+ const { context } = await environment()
100
+ scope = JSON.stringify(context)
101
+ }
102
+ const startState = await send({ type: 'state', scope, user, domain })
103
+ return new PatchProxy(startState, patch => {
104
+ // TODO: reject updates if user is not owner
105
+ const activePatch = structuredClone(patch)
106
+ activePatch.forEach(entry => entry.path.unshift('active'))
107
+ interact(scope, activePatch)
108
+ })
109
+ }
110
+
111
+ function reset(scope) {
112
+ return interact(scope, [{ op: 'add', path:['active'], value: null }])
113
+ }
114
+
115
+ function interact(scope, patch, _, context) {
116
+ return send({ type: 'interact', scope, patch, context })
117
+ }
118
+
119
+ async function upload(info) {
120
+ let { name, type, data, id=uuid() } = info || {}
121
+
122
+ const url = await send({ type: 'upload', info: { name, type, id } })
123
+
124
+ if (data === undefined) return url
125
+ else {
126
+ const headers = { 'Content-Type': type }
127
+ const response = await fetch(url, {method: 'PUT', headers, body: data})
128
+ const { ok, statusText } = response
129
+
130
+ if (ok) return id
131
+ else throw new Error(statusText)
132
+ }
133
+ }
134
+
135
+ function download(id) {
136
+ let mode = 'fetch'
137
+ const promise = new Promise(async (resolve, reject) => {
138
+ const url = await send({ type: 'download', id })
139
+
140
+ await new Promise(r => setTimeout(r))
141
+ if (mode === 'url') resolve(url)
142
+ else if (mode === 'fetch') {
143
+ const response = await fetch(url)
144
+ const { ok, statusText } = response
145
+
146
+ if (ok) resolve(response)
147
+ else reject(statusText)
148
+ }
149
+ else if (mode === 'direct') {
150
+ // TODO: use browser progress UX instead of downloading all into memory first
151
+ const res = await download(id)
152
+ const { name } = await metadata(id)
153
+ const type = res.headers.get('Content-Type')
154
+ const blob = new Blob([ await res.blob() ], { type })
155
+ const url = window.URL.createObjectURL(blob)
156
+ const a = document.createElement('a')
157
+ a.style.display = 'none'
158
+ a.href = url
159
+ a.download = name
160
+ document.body.appendChild(a)
161
+ a.click()
162
+ window.URL.revokeObjectURL(url)
163
+ resolve()
164
+ }
165
+ })
166
+ promise.direct = () => {
167
+ mode = 'direct'
168
+ return promise
169
+ }
170
+ promise.url = () => {
171
+ mode = 'url'
172
+ return promise
173
+ }
174
+ return promise
175
+ }
176
+
177
+ function isValidMetadataMutation({ path, op, value }) {
178
+ return (
179
+ ['active_type', 'name'].includes(path[0])
180
+ && path.length === 1
181
+ && typeof value === 'string' || op === 'remove'
182
+ )
183
+ }
184
+
185
+ async function metadata(scope, user, domain) {
186
+ const md = await send({ type: 'metadata', scope, user, domain })
187
+ return new PatchProxy(md, patch => {
188
+ const activePatch = structuredClone(patch)
189
+ activePatch.forEach(entry => {
190
+ if (!isValidMetadataMutation(entry)) throw new Error('You may only modify the type or name for a scope\'s metadata')
191
+ })
192
+ interact(scope, activePatch)
193
+ })
194
+ }
195
+
196
+ function login(provider, username, password) {
197
+ return send({ type: 'login', provider, username, password })
198
+ }
199
+
200
+ function query(query, params, domain, context=[]) { return send({ type: 'query', query, params, domain, context }) }
201
+ function logout() { return send({ type: 'logout' }) }
202
+ function disconnect() { return send({ type: 'disconnect' }) }
203
+ function reconnect() { return send({ type: 'reconnect' }) }
204
+ function synced() { return send({ type: 'synced' }) }
205
+ function close(info) { return send({ type: 'close', info }) }
206
+
207
+ return {
208
+ embedded: true,
209
+ uuid,
210
+ environment,
211
+ login,
212
+ logout,
213
+ create,
214
+ state,
215
+ watch,
216
+ upload,
217
+ download,
218
+ interact,
219
+ patch,
220
+ reset,
221
+ metadata,
222
+ disconnect,
223
+ reconnect,
224
+ synced,
225
+ close,
226
+ query
227
+ }
228
+ }
@@ -127,7 +127,7 @@ export default function messageQueue({ token, sid, domain, Connection, watchers,
127
127
 
128
128
  if (!authed) {
129
129
  // TODO: credential refresh flow instead of forcing login
130
- if (message.error) return login()
130
+ if (message.error) alert('Authentication Error. Please try again.')
131
131
 
132
132
  authed = true
133
133
  if (!user) { // this is the first authed websocket connection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knowlearning/agents",
3
- "version": "0.9.176",
3
+ "version": "0.9.178",
4
4
  "description": "API for embedding applications in KnowLearning systems.",
5
5
  "main": "node.js",
6
6
  "browser": "browser.js",