@knowlearning/agents 0.0.1
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.
- package/agents/browser/embedded.js +164 -0
- package/agents/browser/experience.js +126 -0
- package/agents/browser/firebase-auth.js +60 -0
- package/agents/browser/root.js +21 -0
- package/agents/deno.js +14 -0
- package/agents/generic.js +305 -0
- package/agents/node.js +17 -0
- package/create-folder-content.js +67 -0
- package/metadata-utils.js +20 -0
- package/package.js +6 -0
- package/package.json +29 -0
- package/persistence/json.js +92 -0
- package/persistence/vuex.js +84 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { v1 as uuid } from 'npm/unscoped/uuid/9.0.0'
|
|
2
|
+
import MutableProxy from '../../persistence/json.js'
|
|
3
|
+
import Experience from './experience.js'
|
|
4
|
+
|
|
5
|
+
export default EmbeddedAgent()
|
|
6
|
+
|
|
7
|
+
function EmbeddedAgent() {
|
|
8
|
+
let messageIndex = 0
|
|
9
|
+
let resolveSession
|
|
10
|
+
const session = new Promise(r => resolveSession = r)
|
|
11
|
+
const responses = {}
|
|
12
|
+
const watchers = {}
|
|
13
|
+
|
|
14
|
+
async function send(message) {
|
|
15
|
+
const requestId = message.requestId || uuid()
|
|
16
|
+
|
|
17
|
+
messageIndex += 1
|
|
18
|
+
// default to window opener if present
|
|
19
|
+
const upstream = window.opener ? window.opener : window.parent
|
|
20
|
+
try {
|
|
21
|
+
upstream
|
|
22
|
+
.postMessage({
|
|
23
|
+
...message,
|
|
24
|
+
session: await session,
|
|
25
|
+
requestId,
|
|
26
|
+
index: messageIndex
|
|
27
|
+
}, '*')
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
responses[requestId] = { resolve, reject }
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.log('ERROR POSTING MESSAGE UP', message, error)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
addEventListener('message', async ({ data }) => {
|
|
38
|
+
if (data.type === 'setup') resolveSession(data.session)
|
|
39
|
+
else if (responses[data.requestId]) {
|
|
40
|
+
const { resolve, reject } = responses[data.requestId]
|
|
41
|
+
if (data.error) reject(data.error)
|
|
42
|
+
else resolve(data.response)
|
|
43
|
+
}
|
|
44
|
+
else if (data.ii !== undefined) {
|
|
45
|
+
const { domain, user, scope } = data
|
|
46
|
+
const key = `${domain}/${user}/${scope}`
|
|
47
|
+
if (watchers[key]) watchers[key].forEach(fn => fn(data))
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function environment() {
|
|
52
|
+
return send({ type: 'environment' })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function state(scope, user, domain) {
|
|
56
|
+
let watchFn
|
|
57
|
+
let resolveKey
|
|
58
|
+
const key = new Promise(r => resolveKey = r)
|
|
59
|
+
|
|
60
|
+
const promise = new Promise(async resolve => {
|
|
61
|
+
const { domain: d, auth: { user: u }, scope: s } = await environment()
|
|
62
|
+
domain = domain || d
|
|
63
|
+
user = user || u
|
|
64
|
+
scope = scope || s
|
|
65
|
+
resolveKey(`${domain}/${user}/${scope}`)
|
|
66
|
+
resolve(send({ type: 'state', scope, user, domain }))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
promise.watch = fn => {
|
|
70
|
+
key
|
|
71
|
+
.then( async k => {
|
|
72
|
+
if (!watchers[k]) watchers[k] = []
|
|
73
|
+
watchers[k].push(fn)
|
|
74
|
+
watchFn = fn
|
|
75
|
+
})
|
|
76
|
+
return promise
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
promise.unwatch = () => {
|
|
80
|
+
key
|
|
81
|
+
.then( k => {
|
|
82
|
+
watchers[k] = watchers[k].filter(x => x !== watchFn)
|
|
83
|
+
if (watchers[k].length === 0) delete watchers[k]
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
return promise
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return promise
|
|
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 mutate (scope, initialize=true) {
|
|
98
|
+
const handlePatch = patch => interact(scope, patch)
|
|
99
|
+
|
|
100
|
+
if (!scope) scope = (await environment()).scope
|
|
101
|
+
|
|
102
|
+
if (initialize) {
|
|
103
|
+
// TODO: consider setting up watcher and updating state
|
|
104
|
+
return new MutableProxy(await state(scope) || {}, handlePatch)
|
|
105
|
+
}
|
|
106
|
+
else return new MutableProxy({}, handlePatch)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function interact(scope, patch) {
|
|
110
|
+
return send({ type: 'interact', scope, patch })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function upload(name, type, data, id=uuid()) {
|
|
114
|
+
const url = await send({ type: 'upload', name, contentType: type, id })
|
|
115
|
+
|
|
116
|
+
if (!data) return url
|
|
117
|
+
else {
|
|
118
|
+
console.log('UPLOADING TO URL!!!!!!!!!!!!!!', id, url)
|
|
119
|
+
const headers = { 'Content-Type': type }
|
|
120
|
+
const response = await fetch(url, {method: 'PUT', headers, body: data})
|
|
121
|
+
const { ok, statusText } = response
|
|
122
|
+
|
|
123
|
+
console.log(ok, statusText)
|
|
124
|
+
|
|
125
|
+
if (ok) return id
|
|
126
|
+
else throw new Error(statusText)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function download(id, passthrough=false) {
|
|
131
|
+
const url = await send({ type: 'download', id })
|
|
132
|
+
return passthrough ? url : fetch(url)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function metadata(id) {
|
|
136
|
+
return state(id, 'metadata', 'core')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function login(provider, username, password) {
|
|
140
|
+
return send({ type: 'login', provider, username, password })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function logout() { return send({ type: 'logout' }) }
|
|
144
|
+
|
|
145
|
+
function disconnect() { return send({ type: 'disconnect' }) }
|
|
146
|
+
|
|
147
|
+
function reconnect() { return send({ type: 'reconnect' }) }
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
login,
|
|
151
|
+
logout,
|
|
152
|
+
environment,
|
|
153
|
+
state,
|
|
154
|
+
mutate,
|
|
155
|
+
patch,
|
|
156
|
+
interact,
|
|
157
|
+
metadata,
|
|
158
|
+
upload,
|
|
159
|
+
download,
|
|
160
|
+
disconnect,
|
|
161
|
+
reconnect,
|
|
162
|
+
Experience
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { v1 as uuid, validate as validateUUID } from 'npm/unscoped/uuid/9.0.0'
|
|
2
|
+
|
|
3
|
+
const copy = x => JSON.parse(JSON.stringify(x))
|
|
4
|
+
|
|
5
|
+
const watchers = {}
|
|
6
|
+
|
|
7
|
+
export default function Experience(environment, iframe) {
|
|
8
|
+
const postMessageQueue = []
|
|
9
|
+
let frameLoaded = false
|
|
10
|
+
let embeddedAgentInitialized = false
|
|
11
|
+
const session = uuid()
|
|
12
|
+
|
|
13
|
+
const postMessage = m => new Promise((resolve, reject) => {
|
|
14
|
+
const message = { ...copy(m), session }
|
|
15
|
+
postMessageQueue.push({ message, sent: resolve })
|
|
16
|
+
if (frameLoaded) processPostMessageQueue()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const processPostMessageQueue = () => {
|
|
20
|
+
while (iframe.parentNode && postMessageQueue.length) {
|
|
21
|
+
const { message, sent } = postMessageQueue.shift()
|
|
22
|
+
iframe.contentWindow.postMessage(message, '*')
|
|
23
|
+
sent()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.remove = () => {
|
|
28
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const handleMessage = async message => {
|
|
32
|
+
const { requestId, type } = message
|
|
33
|
+
|
|
34
|
+
const sendDown = response => {
|
|
35
|
+
if (message.type === 'environment') Object.assign(response, environment)
|
|
36
|
+
return postMessage({ requestId, response })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (type === 'error') {
|
|
40
|
+
console.error(message)
|
|
41
|
+
sendDown({})
|
|
42
|
+
}
|
|
43
|
+
else if (type === 'environment') {
|
|
44
|
+
sendDown(await Agent.environment())
|
|
45
|
+
}
|
|
46
|
+
else if (type === 'interact') {
|
|
47
|
+
await Agent.interact(message.scope, message.patch)
|
|
48
|
+
sendDown({}) // TODO: might want to send down the interaction index
|
|
49
|
+
}
|
|
50
|
+
else if (type === 'state') {
|
|
51
|
+
const { scope, user, domain } = message
|
|
52
|
+
const statePromise = Agent.state(scope, user, domain)
|
|
53
|
+
|
|
54
|
+
const key = `${session}/${domain}/${user}/${scope}`
|
|
55
|
+
|
|
56
|
+
// only watch once per key per experience session
|
|
57
|
+
if (!watchers[key]) {
|
|
58
|
+
watchers[key] = statePromise.unwatch // TODO: consider how to use this...
|
|
59
|
+
statePromise.watch(postMessage)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
sendDown(await statePromise)
|
|
63
|
+
}
|
|
64
|
+
else if (type === 'patch') {
|
|
65
|
+
const { root, scopes } = message
|
|
66
|
+
sendDown(await Agent.patch(root, scopes))
|
|
67
|
+
}
|
|
68
|
+
else if (type === 'upload') {
|
|
69
|
+
const { name, contentType, id } = message
|
|
70
|
+
sendDown(await Agent.upload(name, contentType, false, id))
|
|
71
|
+
}
|
|
72
|
+
else if (type === 'download') {
|
|
73
|
+
sendDown(await Agent.download(message.id, true))
|
|
74
|
+
}
|
|
75
|
+
else if (type === 'login') {
|
|
76
|
+
const { provider, username, password } = message
|
|
77
|
+
sendDown(await Agent.login(provider, username, password))
|
|
78
|
+
}
|
|
79
|
+
else if (type === 'logout') Agent.logout()
|
|
80
|
+
else if (type === 'disconnect') sendDown(await Agent.disconnect())
|
|
81
|
+
else if (type === 'reconnect') sendDown(await Agent.reconnect())
|
|
82
|
+
else {
|
|
83
|
+
console.log('Unknown message type passed up...', message)
|
|
84
|
+
sendDown({})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
window.addEventListener('message', ({ data }) => {
|
|
89
|
+
if (data.session === session) {
|
|
90
|
+
embeddedAgentInitialized = true
|
|
91
|
+
// TODO: ensure message index ordering!!!!!!!!!!!!!!!!!!!! (no order guarantee given in postMessage protocol, so we need to make a little buffer here)
|
|
92
|
+
handleMessage(data)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// write in a temporary loading notification while frame loads
|
|
97
|
+
const cw = iframe.contentWindow
|
|
98
|
+
if (cw) cw.document.body.innerHTML = 'Loading...'
|
|
99
|
+
|
|
100
|
+
// TODO: make sure content security policy headers for embedded domain always restrict iframe
|
|
101
|
+
// src to only self for embedded domain
|
|
102
|
+
Agent
|
|
103
|
+
.environment()
|
|
104
|
+
.then(async ({ embed }) => {
|
|
105
|
+
iframe.onload = () => {
|
|
106
|
+
frameLoaded = true
|
|
107
|
+
processPostMessageQueue()
|
|
108
|
+
}
|
|
109
|
+
const { protocol, pathname } = window.location
|
|
110
|
+
if (validateUUID(environment.content)) {
|
|
111
|
+
// TODO: fix development port workararounds for embed
|
|
112
|
+
if (protocol === 'http:') embed = embed.replace(':32002', ':32020')
|
|
113
|
+
iframe.src = `${protocol}//${embed}${pathname}?s=${session}`
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
iframe.src = environment.content
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
while(!embeddedAgentInitialized) {
|
|
120
|
+
postMessage({ type: 'setup', session })
|
|
121
|
+
await new Promise(r => setTimeout(r, 100))
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
return this
|
|
126
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { initializeApp } from 'npm/unscoped/firebase/9.12.1/app'
|
|
2
|
+
import {
|
|
3
|
+
getAuth,
|
|
4
|
+
signOut as firebaseSignOut,
|
|
5
|
+
GoogleAuthProvider,
|
|
6
|
+
OAuthProvider,
|
|
7
|
+
onAuthStateChanged,
|
|
8
|
+
signInWithRedirect,
|
|
9
|
+
signInWithEmailAndPassword,
|
|
10
|
+
signInAnonymously
|
|
11
|
+
} from 'npm/unscoped/firebase/9.12.1/auth'
|
|
12
|
+
|
|
13
|
+
initializeApp({
|
|
14
|
+
"apiKey": "AIzaSyAxjYuF-2JmXxlXnGgNu2CO4Q41EAtUgrY",
|
|
15
|
+
"authDomain": "opensourcelearningplatform.firebaseapp.com",
|
|
16
|
+
"databaseURL": "https://opensourcelearningplatform.firebaseio.com",
|
|
17
|
+
"projectId": "opensourcelearningplatform",
|
|
18
|
+
"storageBucket": "opensourcelearningplatform.appspot.com",
|
|
19
|
+
"messagingSenderId": "831020253582",
|
|
20
|
+
"appId: 1:831020253582": "web:6e1dfc6e89f5c2107164df",
|
|
21
|
+
"measurementId": "G-8QD6034RJ0"
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
let currentUser
|
|
25
|
+
const authClient = getAuth()
|
|
26
|
+
const onAuth = fn => onAuthStateChanged(authClient, user => {
|
|
27
|
+
if (!user) signInAnonymously(authClient)
|
|
28
|
+
else if (user.uid !== currentUser) {
|
|
29
|
+
const { uid } = user
|
|
30
|
+
currentUser = uid
|
|
31
|
+
const provider = user.isAnonymous ? 'anonymous' : user.providerData[0].providerId
|
|
32
|
+
fn({ user: uid, provider, getIdToken: () => user.getIdToken() })
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const authProviders = {
|
|
37
|
+
google: () => new GoogleAuthProvider(),
|
|
38
|
+
microsoft: () => new OAuthProvider('microsoft.com')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const login = async (provider, username, password) => {
|
|
42
|
+
if (authProviders[provider]) {
|
|
43
|
+
signInWithRedirect(authClient, authProviders[provider]())
|
|
44
|
+
return { success: true }
|
|
45
|
+
}
|
|
46
|
+
else if (provider === 'email') {
|
|
47
|
+
try {
|
|
48
|
+
await signInWithEmailAndPassword(authClient, username, password)
|
|
49
|
+
location.reload()
|
|
50
|
+
}
|
|
51
|
+
catch (error) { return { success: false } }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const logout = async () => {
|
|
56
|
+
await firebaseSignOut(authClient)
|
|
57
|
+
location.reload()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { onAuth, login, logout }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { v1 as uuid } from 'npm/unscoped/uuid/9.0.0'
|
|
2
|
+
import { applyPatch } from 'npm/unscoped/fast-json-patch/3.1.1'
|
|
3
|
+
import { onAuth, login, logout } from './firebase-auth.js'
|
|
4
|
+
import GenericAgent from '../generic.js'
|
|
5
|
+
import Experience from './experience.js'
|
|
6
|
+
|
|
7
|
+
const Agent = new GenericAgent({
|
|
8
|
+
host: window.location.host,
|
|
9
|
+
protocol: window.location.protocol === 'https:' ? 'wss' : 'ws',
|
|
10
|
+
token: new Promise(r => onAuth(({ getIdToken: g }) => r(g()))),
|
|
11
|
+
WebSocket,
|
|
12
|
+
uuid,
|
|
13
|
+
fetch,
|
|
14
|
+
applyPatch,
|
|
15
|
+
login,
|
|
16
|
+
logout
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
Agent.Experience = Experience
|
|
20
|
+
|
|
21
|
+
export default Agent
|
package/agents/deno.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { applyPatch } from 'https://esm.sh/fast-json-patch@3.1.1'
|
|
2
|
+
import Agent from './generic.js'
|
|
3
|
+
|
|
4
|
+
const SERVE_HOST = Deno.env.get("SERVE_HOST")
|
|
5
|
+
const SERVICE_ACCOUNT_TOKEN = Deno.env.get("SERVICE_ACCOUNT_TOKEN")
|
|
6
|
+
|
|
7
|
+
export default new Agent({
|
|
8
|
+
host: SERVE_HOST,
|
|
9
|
+
token: Deno.readTextFile(SERVICE_ACCOUNT_TOKEN),
|
|
10
|
+
WebSocket,
|
|
11
|
+
uuid: () => crypto.randomUUID(),
|
|
12
|
+
fetch,
|
|
13
|
+
applyPatch
|
|
14
|
+
})
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import MutableProxy from '../persistence/json.js'
|
|
2
|
+
|
|
3
|
+
// transform our custom path implementation to the standard JSONPatch path
|
|
4
|
+
function standardJSONPatch(patch) {
|
|
5
|
+
return patch.map(p => {
|
|
6
|
+
return {...p, path: '/' + p.path.map(sanitizeJSONPatchPathSegment).join('/')}
|
|
7
|
+
})
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sanitizeJSONPatchPathSegment(s) {
|
|
11
|
+
if (typeof s === "string") return s.replaceAll('~', '~0').replaceAll('/', '~1')
|
|
12
|
+
else return s
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function Agent({ host, token, WebSocket, protocol='ws', uuid, fetch, applyPatch, login, logout }) {
|
|
16
|
+
let ws
|
|
17
|
+
let user
|
|
18
|
+
let domain
|
|
19
|
+
const session = uuid()
|
|
20
|
+
let server
|
|
21
|
+
let si = -1
|
|
22
|
+
let authed = false
|
|
23
|
+
const states = {}
|
|
24
|
+
const responses = {}
|
|
25
|
+
const watchers = {}
|
|
26
|
+
const lastInteractionUpdateForWatchedScope = {}
|
|
27
|
+
const messageQueue = []
|
|
28
|
+
let resolveEnvironment
|
|
29
|
+
let disconnected = false
|
|
30
|
+
const environment = new Promise(r => resolveEnvironment = r)
|
|
31
|
+
|
|
32
|
+
const sessionData = new MutableProxy({}, patch => queueMessage({scope: 'sessions', patch}))
|
|
33
|
+
|
|
34
|
+
sessionData[session] = {
|
|
35
|
+
subscriptions: {},
|
|
36
|
+
uploads: {},
|
|
37
|
+
downloads: {},
|
|
38
|
+
patches: {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function removeWatcher(key, fn) {
|
|
42
|
+
const watcherIndex = watchers[key].findIndex(x => x === fn)
|
|
43
|
+
if (watcherIndex > -1) watchers[key].splice(watcherIndex, 1)
|
|
44
|
+
else console.warn('TRIED TO REMOVE WATCHER THAT DOES NOT EXIST')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.environment = function () { return environment }
|
|
48
|
+
|
|
49
|
+
this.state = state
|
|
50
|
+
this.upload = upload
|
|
51
|
+
this.download = download
|
|
52
|
+
this.interact = interact
|
|
53
|
+
this.patch = patch
|
|
54
|
+
this.mutate = mutate
|
|
55
|
+
this.metadata = metadata
|
|
56
|
+
|
|
57
|
+
this.login = login
|
|
58
|
+
this.logout = logout
|
|
59
|
+
this.disconnect = disconnect
|
|
60
|
+
this.reconnect = reconnect
|
|
61
|
+
|
|
62
|
+
initWS()
|
|
63
|
+
|
|
64
|
+
let lastSentSI = -1
|
|
65
|
+
function flushMessageQueue() {
|
|
66
|
+
// TODO: probably want to make this loop async so we don't try and push more to
|
|
67
|
+
// a closed connection
|
|
68
|
+
while (authed && ws.readyState === WebSocket.OPEN && lastSentSI+1 < messageQueue.length) {
|
|
69
|
+
lastSentSI += 1
|
|
70
|
+
ws.send(JSON.stringify(messageQueue[lastSentSI]))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function lastMessageResponse() { // TODO: handle error responses
|
|
75
|
+
return new Promise(r => responses[si].push(r))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function queueMessage(message) {
|
|
79
|
+
si += 1
|
|
80
|
+
responses[si] = []
|
|
81
|
+
messageQueue.push({...message, si, ts: Date.now()})
|
|
82
|
+
flushMessageQueue()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function initWS() {
|
|
86
|
+
ws = new WebSocket(`${protocol}://${host}`)
|
|
87
|
+
|
|
88
|
+
ws.onopen = async () => ws.send(JSON.stringify({ token: await token, session }))
|
|
89
|
+
|
|
90
|
+
ws.onmessage = async ({ data }) => {
|
|
91
|
+
try {
|
|
92
|
+
const message = JSON.parse(data)
|
|
93
|
+
if (message.error) console.warn('ERROR RESPONSE', message)
|
|
94
|
+
|
|
95
|
+
if (!authed) {
|
|
96
|
+
authed = true
|
|
97
|
+
if (!user) { // this is the first authed websocket connection
|
|
98
|
+
user = message.auth.user
|
|
99
|
+
domain = message.domain
|
|
100
|
+
server = message.server
|
|
101
|
+
resolveEnvironment(message)
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lastSentSI = message.ack
|
|
105
|
+
}
|
|
106
|
+
flushMessageQueue()
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (message.si !== undefined) {
|
|
110
|
+
if (responses[message.si]) {
|
|
111
|
+
// TODO: remove "acknowledged" messages fromt queue and do accounting with si
|
|
112
|
+
responses[message.si].forEach(fn => fn(message))
|
|
113
|
+
delete responses[message.si]
|
|
114
|
+
ws.send(JSON.stringify({ack: message.si})) // acknowledgement that we have received the response for this message
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// TODO: consider what to do here... probably want to throw error if in dev env
|
|
118
|
+
console.warn('received MULTIPLE responses for message with si', message.si, message)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const key = `${message.domain}/${message.user}/${message.scope}`
|
|
123
|
+
if (watchers[key]) {
|
|
124
|
+
if (sessionData[session].subscriptions[key] + 1 !== message.ii) {
|
|
125
|
+
// TODO: this seems to be an error that happens with decent regularity (an answer with a given si was skipped/failed)
|
|
126
|
+
// we should be wary of out-of-order ii being passed down (maybe need to wait for older ones???)
|
|
127
|
+
console.warn('UNEXPECTED UPDATE INTERACTION INDEX!!!!!!!!!!! last index in session', sessionData[session].subscriptions[key], ' passed index ', message.ii)
|
|
128
|
+
}
|
|
129
|
+
states[key] = await states[key] || {}
|
|
130
|
+
|
|
131
|
+
const lastResetPatchIndex = message.patch.findLastIndex(p => p.path.length === 0)
|
|
132
|
+
if (lastResetPatchIndex > -1) states[key] = message.patch[lastResetPatchIndex].value
|
|
133
|
+
|
|
134
|
+
applyPatch(states[key], standardJSONPatch(message.patch.slice(lastResetPatchIndex + 1)))
|
|
135
|
+
watchers[key].forEach(fn => fn({ ...message, state: states[key] }))
|
|
136
|
+
sessionData[session].subscriptions[key] = message.ii
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error('ERROR HANDLING WS MESSAGE', error)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
ws.onerror = async error => {
|
|
147
|
+
//console.log('WS CONNECTION ERROR', error.message)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
ws.onclose = async () => {
|
|
151
|
+
authed = false
|
|
152
|
+
console.log(`CLOSED DOMAIN ${domain} USER ${user} SESSION ${session} CONNECTION TO SERVER ${server}`)
|
|
153
|
+
if (!disconnected) initWS() // TODO: don't do this if we are purposefully unloading...
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function state(scope, u, d) {
|
|
158
|
+
let watchFn
|
|
159
|
+
let resolveKey
|
|
160
|
+
const key = new Promise(r => resolveKey = r)
|
|
161
|
+
|
|
162
|
+
const promise = new Promise(async resolveState => {
|
|
163
|
+
await environment // environment not set until first connection
|
|
164
|
+
|
|
165
|
+
if (!u) u = user
|
|
166
|
+
if (!d) d = domain
|
|
167
|
+
|
|
168
|
+
const k = `${d || domain}/${u || user}/${scope}`
|
|
169
|
+
resolveKey(k)
|
|
170
|
+
|
|
171
|
+
if (states[k] === undefined) {
|
|
172
|
+
watchers[k] = []
|
|
173
|
+
states[k] = new Promise(async resolve => {
|
|
174
|
+
sessionData[session].subscriptions[k] = null
|
|
175
|
+
const { ii, state } = await lastMessageResponse()
|
|
176
|
+
sessionData[session].subscriptions[k] = ii
|
|
177
|
+
resolve(state)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// TODO: make sure to get the state that represents the last interaction we sent
|
|
182
|
+
// when this "state" method was called!!!!!!!!!!!!!!!!!!!!!!!
|
|
183
|
+
// Right now this is simply returning whatever we have at the time we get here
|
|
184
|
+
// when some interaction responses may not have been applied yet...
|
|
185
|
+
// POSSIBLE: wait for most recent interaction update response for key k that we sent...
|
|
186
|
+
await lastInteractionUpdateForWatchedScope[k]
|
|
187
|
+
// TODO: probably something like await updateMessageReceivedForLastInteractionWeSentForKey[k]
|
|
188
|
+
resolveState(states[k])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
promise.watch = fn => {
|
|
192
|
+
key
|
|
193
|
+
.then( k => {
|
|
194
|
+
if (watchFn) throw new Error('Only one watcher allowed') // TODO: consider allowing more watchers per state call
|
|
195
|
+
watchers[k].push(fn)
|
|
196
|
+
watchFn = fn
|
|
197
|
+
})
|
|
198
|
+
return promise
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
promise.unwatch = async () => {
|
|
202
|
+
const k = await key
|
|
203
|
+
removeWatcher(k, watchFn)
|
|
204
|
+
if (watchers[k].length === 0) {
|
|
205
|
+
delete sessionData[session].subscriptions[k]
|
|
206
|
+
delete watchers[k]
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return promise
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// TODO: if no data, set up streaming upload
|
|
214
|
+
async function upload(name, type, data, id=uuid()) {
|
|
215
|
+
// TODO: include data size info...
|
|
216
|
+
sessionData[session].uploads[id] = { url: null, sent: 0, name, type }
|
|
217
|
+
const { url } = await lastMessageResponse()
|
|
218
|
+
sessionData[session].uploads[id].url = url
|
|
219
|
+
|
|
220
|
+
if (!data) return url
|
|
221
|
+
else {
|
|
222
|
+
const headers = { 'Content-Type': type }
|
|
223
|
+
const response = await fetch(url, {method: 'PUT', headers, body: data})
|
|
224
|
+
const { ok, statusText } = response
|
|
225
|
+
|
|
226
|
+
if (ok) return id
|
|
227
|
+
else throw new Error(statusText)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function download(id, passthrough=false) {
|
|
232
|
+
sessionData[session].downloads[id] = { url: null, size: null, sent: 0 }
|
|
233
|
+
const { url } = await lastMessageResponse()
|
|
234
|
+
sessionData[session].downloads[id].url = url
|
|
235
|
+
|
|
236
|
+
if (passthrough) return url
|
|
237
|
+
else {
|
|
238
|
+
const response = await fetch(url)
|
|
239
|
+
const { ok, statusText } = response
|
|
240
|
+
|
|
241
|
+
if (ok) return response
|
|
242
|
+
else throw new Error(statusText)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function patch(root, scopes) {
|
|
247
|
+
sessionData[session].patches[uuid()] = { root, scopes }
|
|
248
|
+
const { swaps } = await lastMessageResponse()
|
|
249
|
+
return { swaps }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function interact(scope, patch) {
|
|
253
|
+
queueMessage({scope, patch})
|
|
254
|
+
const response = lastMessageResponse()
|
|
255
|
+
|
|
256
|
+
await environment
|
|
257
|
+
|
|
258
|
+
const key = `${domain}/${user}/${scope}`
|
|
259
|
+
|
|
260
|
+
// if we are watching this key, we want to keep track of last interaction we fired
|
|
261
|
+
if (states[key] !== undefined) {
|
|
262
|
+
let resolve
|
|
263
|
+
lastInteractionUpdateForWatchedScope[key] = new Promise(r => resolve = r)
|
|
264
|
+
|
|
265
|
+
const { ii } = await response
|
|
266
|
+
|
|
267
|
+
const resolveAndUnwatch = (update) => {
|
|
268
|
+
if (update.ii === ii) {
|
|
269
|
+
resolve(ii)
|
|
270
|
+
removeWatcher(key, resolveAndUnwatch)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
watchers[key].push(resolveAndUnwatch)
|
|
275
|
+
|
|
276
|
+
return { ii }
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const { ii } = await response
|
|
280
|
+
return { ii }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function mutate(scope, initialize=true) {
|
|
285
|
+
const initial = initialize ? await this.state(scope) || {} : {}
|
|
286
|
+
return new MutableProxy(initial, patch => interact(scope, patch))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function metadata(id) {
|
|
290
|
+
return state(id, 'metadata', 'core')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function disconnect() {
|
|
294
|
+
console.log('DISCONNECTED AGENT!!!!!!!!!!!!!!!')
|
|
295
|
+
disconnected = true
|
|
296
|
+
ws.close()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function reconnect() {
|
|
300
|
+
console.log('RECONNECTED AGENT!!!!!!!!!!!!!!!')
|
|
301
|
+
initWS()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return this
|
|
305
|
+
}
|
package/agents/node.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import WebSocket from 'ws'
|
|
3
|
+
import { v1 as uuid } from 'uuid'
|
|
4
|
+
import fastJSONPatch from 'fast-json-patch'
|
|
5
|
+
import fetch from 'node-fetch'
|
|
6
|
+
import Agent from './generic.js'
|
|
7
|
+
|
|
8
|
+
const { SERVE_HOST, SERVICE_ACCOUNT_TOKEN } = process.env
|
|
9
|
+
|
|
10
|
+
export default new Agent({
|
|
11
|
+
host: SERVE_HOST,
|
|
12
|
+
token: fs.promises.readFile(SERVICE_ACCOUNT_TOKEN).then(f => f.toString()),
|
|
13
|
+
WebSocket,
|
|
14
|
+
uuid,
|
|
15
|
+
fetch,
|
|
16
|
+
applyPatch: fastJSONPatch.applyPatch
|
|
17
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { extensionToType } from './metadata-utils.js'
|
|
2
|
+
|
|
3
|
+
// these utility functions work in a web environment
|
|
4
|
+
const fileFromEntry = entry => new Promise((s,e) => entry.file(s,e))
|
|
5
|
+
const getTypeFromEntry = entry => fileFromEntry(entry).then(file => file.type)
|
|
6
|
+
const getDataFromEntry = entry => fileFromEntry(entry).then(file => file.arrayBuffer())
|
|
7
|
+
|
|
8
|
+
// entries is of form: [{ fullPath, name }, ....]
|
|
9
|
+
export default async function createFolderContent(entries, upload, uuid, getType=getTypeFromEntry, getData=getDataFromEntry) {
|
|
10
|
+
// TODO: swap out internal references in files
|
|
11
|
+
const swaps = {}
|
|
12
|
+
const roots = {}
|
|
13
|
+
const filenames = []
|
|
14
|
+
entries.forEach(e => {
|
|
15
|
+
const id = uuid(e.fullPath)
|
|
16
|
+
swaps[e.fullPath] = id
|
|
17
|
+
roots[id] = true
|
|
18
|
+
filenames.push(e.name)
|
|
19
|
+
})
|
|
20
|
+
const metadata = []
|
|
21
|
+
const body = []
|
|
22
|
+
await Promise.all(
|
|
23
|
+
entries.map(async entry => {
|
|
24
|
+
const id = swaps[entry.fullPath]
|
|
25
|
+
const type = await getType(entry)
|
|
26
|
+
let data = await getData(entry)
|
|
27
|
+
if (!type.startsWith('image/')) {
|
|
28
|
+
// TODO: this is probably the most fragile part and we probably want unit tests
|
|
29
|
+
// for reasonable return values for getData
|
|
30
|
+
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
31
|
+
data = new TextDecoder("utf-8").decode(data)
|
|
32
|
+
}
|
|
33
|
+
Object
|
|
34
|
+
.entries(swaps)
|
|
35
|
+
.forEach(([from, to]) => {
|
|
36
|
+
let reference = ''
|
|
37
|
+
if (from === entry.fullPath) reference = `./${entry.name}`
|
|
38
|
+
else {
|
|
39
|
+
const e = entry.fullPath.split('/')
|
|
40
|
+
const f = from.split('/')
|
|
41
|
+
while (e[0] !== undefined && f[0] !== undefined && e[0] === f[0]) {
|
|
42
|
+
e.shift()
|
|
43
|
+
f.shift()
|
|
44
|
+
}
|
|
45
|
+
// for every entry dir past the common prefix, add a ../ for the reference
|
|
46
|
+
if (e.length > 1) e.slice(0,-1).forEach(() => reference += '../')
|
|
47
|
+
else reference = './'
|
|
48
|
+
|
|
49
|
+
reference += f.join('/')
|
|
50
|
+
}
|
|
51
|
+
if (data.includes(reference)) {
|
|
52
|
+
// if found a reference to this swap, then
|
|
53
|
+
// the from reference cannot be a root
|
|
54
|
+
// TODO: fix the bug where this will delete root content that is part of a cycle
|
|
55
|
+
delete roots[to]
|
|
56
|
+
data = data.replaceAll(reference, to)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
const segments = entry.name.split('.')
|
|
61
|
+
const contentType = extensionToType(segments[segments.length - 1])
|
|
62
|
+
|
|
63
|
+
await upload(entry.fullPath, contentType, data, id)
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
return Object.keys(roots)
|
|
67
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const tePairs = [
|
|
2
|
+
["application/javascript;syntax=vue-template", "vue"],
|
|
3
|
+
["application/javascript", "js"],
|
|
4
|
+
["application/json", "json"],
|
|
5
|
+
["text/html", "html"],
|
|
6
|
+
["text/css", "css"],
|
|
7
|
+
["text/plain", "txt"],
|
|
8
|
+
["text/yaml", "yaml"],
|
|
9
|
+
["image/png", "png"],
|
|
10
|
+
["text/plain;syntax=Dockerfile", "Dockerfile"]
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const typeToExtensionMap = tePairs.reduce((o,[t,e]) => (o[t]=e, o), {})
|
|
14
|
+
const extensionToTypeMap = tePairs.reduce((o,[t,e]) => (o[e]=t, o), {})
|
|
15
|
+
|
|
16
|
+
// TODO: do better fallback matching (do a smarter look at params/subtypes)
|
|
17
|
+
const typeToExtension = t => typeToExtensionMap[t] || typeToExtensionMap[t.split(';')[0]]
|
|
18
|
+
const extensionToType = e => extensionToTypeMap[e] || "text/plain"
|
|
19
|
+
|
|
20
|
+
export { typeToExtension, extensionToType }
|
package/package.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// TODO: export browser, node, deno, and generic agents
|
|
2
|
+
export { default as BrowserAgent } from './lib/agents/browser/embedded.js'
|
|
3
|
+
export { default as NodeAgent } from './lib/agents/node.js'
|
|
4
|
+
export { default as DenoAgent } from './lib/agents/deno.js'
|
|
5
|
+
export { default as GenericAgent } from './lib/agents/generic.js'
|
|
6
|
+
export { default as createPersistentVuexStore } from './persistence/vuex.js'
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@knowlearning/agents",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "API for embedding applications in KnowLearning systems.",
|
|
5
|
+
"main": "package.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"directories": {
|
|
8
|
+
"example": "examples",
|
|
9
|
+
"test": "test"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/knowlearning/core.git"
|
|
17
|
+
},
|
|
18
|
+
"author": "KnowLearning",
|
|
19
|
+
"license": "MPL-2.0",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/knowlearning/core/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/knowlearning/core#readme",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"fast-json-patch": "^3.1.1",
|
|
26
|
+
"uuid": "^8.3.2"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const proxies = new Set()
|
|
2
|
+
|
|
3
|
+
// TODO make serialization more reliable... basically: handle if prop includes "
|
|
4
|
+
function serializePath(path) {
|
|
5
|
+
return `/${path.join('/')}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// TODO: add extra argument for "ignorePrefixes" to ignore interactions at certain paths
|
|
9
|
+
export default function MutableProxy(state, interact, ephemeralPaths={}, parentPath=[], rootState) {
|
|
10
|
+
if (ephemeralPaths[serializePath(parentPath)]) return state
|
|
11
|
+
if (!rootState) rootState = state
|
|
12
|
+
if (proxies.has(state)) {
|
|
13
|
+
// TODO: simply return with no effect if redundantly setting state like a[x] = a[x]
|
|
14
|
+
throw new Error(`Cannot add mutable state to multiple mutable parents. Attempted Path: ${serializePath(parentPath)}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isArray = Array.isArray(state)
|
|
18
|
+
|
|
19
|
+
const childMutableProxy = (prop, value) => MutableProxy(
|
|
20
|
+
value,
|
|
21
|
+
interact,
|
|
22
|
+
ephemeralPaths,
|
|
23
|
+
[...parentPath, prop],
|
|
24
|
+
rootState
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// recursively ensure child objects are converted to proxies (unless they are in an ephemeral path)
|
|
28
|
+
Object
|
|
29
|
+
.entries(state)
|
|
30
|
+
.filter(([, value]) => value instanceof Object)
|
|
31
|
+
.forEach(([key, value]) => state[key] = childMutableProxy(key, value))
|
|
32
|
+
|
|
33
|
+
const traps = {
|
|
34
|
+
set(target, prop, value) {
|
|
35
|
+
const path = [...parentPath, prop]
|
|
36
|
+
const serializedPath = serializePath(path)
|
|
37
|
+
|
|
38
|
+
if (ephemeralPaths[serializedPath]) {
|
|
39
|
+
target[prop] = value
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isArray) {
|
|
44
|
+
if (!/^\d+$/.test(prop)) {
|
|
45
|
+
target[prop] = value
|
|
46
|
+
return true
|
|
47
|
+
}
|
|
48
|
+
else prop = parseInt(prop)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (value === undefined) {
|
|
52
|
+
console.log('EXTRA ERROR INFO. target, prop, value', target, prop, value)
|
|
53
|
+
throw new Error(`Setting properties to undefined is not supported. Please use a delete statement. Attempted to set ${serializedPath}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO: probably want to batch interactions that represent array mutations... and/or do
|
|
57
|
+
// something smart around insertions/deletions
|
|
58
|
+
// TODO: if is array and prop is last element, consider passing -1 as prop
|
|
59
|
+
interact([{
|
|
60
|
+
op: target[prop] ? 'replace' : 'add',
|
|
61
|
+
value: JSON.parse(JSON.stringify(value)), // TODO: more efficient sanitization
|
|
62
|
+
path
|
|
63
|
+
}])
|
|
64
|
+
|
|
65
|
+
if (value instanceof Object) target[prop] = childMutableProxy(prop, value)
|
|
66
|
+
else target[prop] = value
|
|
67
|
+
|
|
68
|
+
return true
|
|
69
|
+
},
|
|
70
|
+
deleteProperty(target, prop) {
|
|
71
|
+
if (prop in target) {
|
|
72
|
+
proxies.delete(proxy)
|
|
73
|
+
delete target[prop]
|
|
74
|
+
|
|
75
|
+
if (isArray) {
|
|
76
|
+
if (!/^\d+$/.test(prop)) return true
|
|
77
|
+
else prop = parseInt(prop)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// TODO: if is array and prop is last element, consider passing -1 as prop
|
|
81
|
+
const path = [...parentPath, prop]
|
|
82
|
+
if (!ephemeralPaths[serializePath(path)]) interact([{ op: 'remove', path }])
|
|
83
|
+
}
|
|
84
|
+
return true
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const proxy = new Proxy(state, traps)
|
|
89
|
+
proxies.add(proxy)
|
|
90
|
+
|
|
91
|
+
return proxy
|
|
92
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createStore } from 'vuex'
|
|
2
|
+
import MutableProxy from './json.js'
|
|
3
|
+
|
|
4
|
+
// TODO: consider path serialization approach. Also consider just using a mutable proxy from agent.state...
|
|
5
|
+
|
|
6
|
+
const copy = x => JSON.parse(JSON.stringify(x))
|
|
7
|
+
|
|
8
|
+
export default async function (storeDefinition) {
|
|
9
|
+
// Set up persistance
|
|
10
|
+
const scope ='whatevah' // TODO: resolve what scope should be...
|
|
11
|
+
// TODO: assess the function of this from the old implementation
|
|
12
|
+
const savedState = await Agent.state(scope)
|
|
13
|
+
|
|
14
|
+
const scopedPaths = getScopedPaths(storeDefinition)
|
|
15
|
+
const stateAttachedStore = await attachModuleState(savedState, storeDefinition, scopedPaths)
|
|
16
|
+
const s = stateAttachedStore.state
|
|
17
|
+
const originalState = s instanceof Function ? s() : s
|
|
18
|
+
const handlePatch = patch => {
|
|
19
|
+
console.log('PATCHING STATE', patch)
|
|
20
|
+
Agent.interact(scope, patch)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (savedState === null) handlePatch([{ op: 'add', path: [], value: copy(originalState) }])
|
|
24
|
+
|
|
25
|
+
// ephemeral paths for the root mutable proxy are any paths with a scope specified (including null scopes)
|
|
26
|
+
const ephemeralPaths = {}
|
|
27
|
+
Object.keys(scopedPaths).forEach(key => ephemeralPaths[key] = true)
|
|
28
|
+
|
|
29
|
+
stateAttachedStore.state = () => MutableProxy(savedState || originalState, handlePatch, ephemeralPaths)
|
|
30
|
+
return createStore(stateAttachedStore)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// TODO: sanitize module names for path serialization approach
|
|
34
|
+
function descendantPaths(path, pathSet) {
|
|
35
|
+
return (
|
|
36
|
+
Object
|
|
37
|
+
.keys(pathSet)
|
|
38
|
+
.filter(p => p.startsWith(path + '/')) // filter for descendant paths
|
|
39
|
+
.reduce((acc, p) => {
|
|
40
|
+
const descendantPart = p.slice(path.length)
|
|
41
|
+
acc[descendantPart] = pathSet[p]
|
|
42
|
+
return acc
|
|
43
|
+
}, {})
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function attachModuleState(state, module, scopedPaths, path='') {
|
|
48
|
+
await Promise.all(
|
|
49
|
+
Object
|
|
50
|
+
.entries(module.modules || {})
|
|
51
|
+
.map(async ([subModuleName, subModule]) => {
|
|
52
|
+
const subModuleStartState = state ? state[subModuleName] : null
|
|
53
|
+
|
|
54
|
+
// vuex expects to initialize state from submodules
|
|
55
|
+
if (subModuleStartState) delete state[subModuleName]
|
|
56
|
+
|
|
57
|
+
module.modules[subModuleName] = await attachModuleState(subModuleStartState, subModule, scopedPaths, `${path}/${subModuleName}`)
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// if our path is in scoped paths, return new Mutable proxy attached to scope
|
|
62
|
+
const scope = scopedPaths[path]
|
|
63
|
+
if (scope) {
|
|
64
|
+
const handlePatch = patch => Agent.interact(scope, patch)
|
|
65
|
+
const initState = await Agent.state(scope)
|
|
66
|
+
const ephemeralPaths = descendantPaths(path, scopedPaths)
|
|
67
|
+
state = MutableProxy(initState || {}, handlePatch, ephemeralPaths)
|
|
68
|
+
if (!initState) Object.assign(state, module.state())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return state ? { ...module, state: () => state } : module
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getScopedPaths(module, path="", paths={}) {
|
|
75
|
+
if (module.scope !== undefined) paths[path] = module.scope
|
|
76
|
+
|
|
77
|
+
Object
|
|
78
|
+
.entries(module.modules || {})
|
|
79
|
+
.forEach(([subModuleName, subModule]) => {
|
|
80
|
+
getScopedPaths(subModule, `${path}/${subModuleName}`, paths)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
return paths
|
|
84
|
+
}
|