@muze-nl/simplystore 0.6.11 → 0.6.14
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/README.md +8 -12
- package/package.json +2 -2
- package/src/command-worker-module.mjs +44 -29
- package/src/load-worker.mjs +15 -14
- package/src/query-worker-module.mjs +59 -11
- package/src/server.mjs +16 -13
- package/src/workerPool.mjs +19 -4
package/README.md
CHANGED
|
@@ -152,18 +152,14 @@ In addition, SimplyStore is meant to be a real-world testcase for JSONTag.
|
|
|
152
152
|
<a name="roadmap"></a>
|
|
153
153
|
## Roadmap
|
|
154
154
|
|
|
155
|
-
- allow changes to dataset by creating a new root
|
|
156
|
-
- command handling with crud commands and command log
|
|
157
|
-
- backup current dataset to JSONTag file
|
|
158
|
-
- on startup check if any commands in the log haven't been resolved, if so run them
|
|
159
|
-
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
-
|
|
163
|
-
- allow custom templates, instead of the default index.html
|
|
164
|
-
- add support for access control, based on webid / openid connect
|
|
165
|
-
- switch from VM2 to V8-isolate, which is more secure
|
|
166
|
-
- switch the server runtime to Rust, so SimplyStore can share immutable data between threads
|
|
155
|
+
- [x] allow changes to dataset by creating a new root
|
|
156
|
+
- [x] command handling with crud commands and command log
|
|
157
|
+
- [x] backup current dataset to JSONTag file
|
|
158
|
+
- [x] on startup check if any commands in the log haven't been resolved, if so run them
|
|
159
|
+
- [ ] improved web client with type-specific views and form elements
|
|
160
|
+
- [ ] allow custom templates, instead of the default index.html
|
|
161
|
+
- [ ] add support for access control, based on webid / openid connect
|
|
162
|
+
- [ ] switch from VM2 to V8-isolate or QuickJS, which is more secure
|
|
167
163
|
|
|
168
164
|
<a name="license"></a>
|
|
169
165
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muze-nl/simplystore",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14",
|
|
4
4
|
"main": "src/server.mjs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"homepage": "https://github.com/simplyedit/simplystore#readme",
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@muze-nl/jsontag": "^0.9.6",
|
|
20
|
-
"@muze-nl/od-jsontag": "^0.
|
|
20
|
+
"@muze-nl/od-jsontag": "^0.2.6",
|
|
21
21
|
"codemirror": "^6.0.1",
|
|
22
22
|
"express": "^4.18.1",
|
|
23
23
|
"jaqt": "^0.6.2",
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import JSONTag from '@muze-nl/jsontag'
|
|
2
|
-
import {
|
|
2
|
+
import {getIndex, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
|
|
3
3
|
import parse from '@muze-nl/od-jsontag/src/parse.mjs'
|
|
4
|
-
import serialize
|
|
5
|
-
import * as FastJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
|
|
4
|
+
import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
|
|
6
5
|
import writeFileAtomic from 'write-file-atomic'
|
|
7
|
-
import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min} from 'jaqt'
|
|
8
6
|
|
|
9
7
|
let commands = {}
|
|
10
8
|
let resultArr = []
|
|
@@ -53,48 +51,65 @@ export const metaIdProxy = {
|
|
|
53
51
|
}
|
|
54
52
|
}
|
|
55
53
|
|
|
54
|
+
const metaReadProxy = {
|
|
55
|
+
foreach: metaProxy.forEach,
|
|
56
|
+
get: metaProxy.get,
|
|
57
|
+
has: metaProxy.has,
|
|
58
|
+
set: meta.set
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
export async function initialize(task) {
|
|
57
|
-
|
|
62
|
+
for(let jsontag of task.data) {
|
|
63
|
+
dataspace = parse(jsontag, task.meta, false) // false means mutable
|
|
64
|
+
}
|
|
58
65
|
resultArr = dataspace[resultSet]
|
|
59
66
|
meta = task.meta
|
|
60
67
|
metaProxy.index.id = metaIdProxy
|
|
61
68
|
datafile = task.datafile
|
|
62
69
|
commands = await import(task.commandsFile).then(mod => {
|
|
63
|
-
console.log('commands loaded:',Object.keys(mod.default))
|
|
64
70
|
return mod.default
|
|
65
71
|
})
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
export default async function runCommand(commandStr, request) {
|
|
69
|
-
let task = JSONTag.parse(commandStr, null, metaProxy)
|
|
70
|
-
if (!task.id) { throw new Error('missing command id')}
|
|
71
|
-
if (!task.name) { throw new Error('missing command name parameter')}
|
|
72
75
|
let response = {
|
|
73
76
|
jsontag: true
|
|
74
77
|
}
|
|
75
|
-
|
|
76
|
-
let
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
try {
|
|
79
|
+
let task = JSONTag.parse(commandStr, null, metaReadProxy)
|
|
80
|
+
if (!task.id) { throw new Error('missing command id')}
|
|
81
|
+
if (!task.name) { throw new Error('missing command name parameter')}
|
|
82
|
+
if (commands[task.name]) {
|
|
83
|
+
let time = Date.now()
|
|
84
|
+
commands[task.name](dataspace, task, request, metaProxy)
|
|
85
|
+
//TODO: if command/task makes no changes, skip updating data.jsontag and writing it, skip response.data
|
|
86
|
+
JSONTag.setAttribute(dataspace, 'command', task.id)
|
|
87
|
+
|
|
88
|
+
const uint8sab = serialize(dataspace, {meta, changes: true}) // serialize only changes
|
|
89
|
+
response.data = uint8sab
|
|
90
|
+
response.meta = {
|
|
91
|
+
index: {
|
|
92
|
+
id: meta.index.id
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//TODO: write data every x commands or x minutes, in seperate thread
|
|
80
96
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
let newfilename = datafile + (meta.parts ? '.'+meta.parts : '')
|
|
98
|
+
await writeFileAtomic(newfilename, uint8sab)
|
|
99
|
+
meta.parts++
|
|
100
|
+
response.meta.parts = meta.parts
|
|
101
|
+
let end = Date.now()
|
|
102
|
+
console.log('task time',end-time)
|
|
103
|
+
} else {
|
|
104
|
+
console.error('Command not found', task.name)
|
|
105
|
+
throw {
|
|
106
|
+
code: 404,
|
|
107
|
+
message: "Command "+task.name+" not found"
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
console.log('task time',end-time)
|
|
92
|
-
} else {
|
|
93
|
-
console.error('Command not found', task.name)
|
|
94
|
-
throw {
|
|
95
|
-
code: 404,
|
|
96
|
-
message: "Command "+task.name+" not found"
|
|
97
|
-
}
|
|
110
|
+
} catch(err) {
|
|
111
|
+
console.error('task error', err)
|
|
112
|
+
throw err
|
|
98
113
|
}
|
|
99
114
|
return response
|
|
100
115
|
}
|
package/src/load-worker.mjs
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { parentPort } from 'node:worker_threads'
|
|
2
|
-
import JSONTag from '@muze-nl/jsontag'
|
|
3
2
|
import parse from '@muze-nl/od-jsontag/src/parse.mjs'
|
|
4
3
|
import fs from 'fs'
|
|
5
4
|
import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
|
|
6
|
-
import {source, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
|
|
7
5
|
|
|
8
6
|
parentPort.on('message', datafile => {
|
|
9
|
-
const jsontag = fs.readFileSync(datafile)
|
|
10
7
|
let meta = {
|
|
11
8
|
index: {
|
|
12
9
|
id: new Map()
|
|
13
10
|
}
|
|
14
11
|
}
|
|
15
|
-
const data = parse(jsontag)
|
|
16
|
-
const resultArr = data[resultSet]
|
|
17
12
|
|
|
18
|
-
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
meta.index.id.set(id,i)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
13
|
+
let count = 0
|
|
14
|
+
let basefile = datafile
|
|
15
|
+
let data
|
|
16
|
+
let jsontag
|
|
17
|
+
let tempMeta = {}
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
do {
|
|
20
|
+
jsontag = fs.readFileSync(datafile)
|
|
21
|
+
data = parse(jsontag, tempMeta) // tempMeta is needed to combine the resultArray, using meta conflicts with meta.index.id
|
|
22
|
+
count++
|
|
23
|
+
datafile = basefile + '.' + count
|
|
24
|
+
} while(fs.existsSync(datafile))
|
|
25
|
+
meta.parts = count
|
|
26
|
+
|
|
27
|
+
const sab = serialize(data, {meta})
|
|
28
28
|
|
|
29
29
|
parentPort.postMessage({
|
|
30
30
|
data: sab,
|
|
31
31
|
meta
|
|
32
32
|
})
|
|
33
|
+
|
|
33
34
|
})
|
|
@@ -2,12 +2,11 @@ import pointer from 'json-pointer'
|
|
|
2
2
|
import {VM} from 'vm2'
|
|
3
3
|
import { memoryUsage } from 'node:process'
|
|
4
4
|
import JSONTag from '@muze-nl/jsontag'
|
|
5
|
+
import * as odJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
|
|
5
6
|
import {source, isProxy, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
|
|
6
7
|
import parse from '@muze-nl/od-jsontag/src/parse.mjs'
|
|
7
|
-
import * as FastJSONTag from '@muze-nl/od-jsontag/src/jsontag.mjs'
|
|
8
8
|
import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min} from 'jaqt'
|
|
9
9
|
|
|
10
|
-
let resultArr = []
|
|
11
10
|
let dataspace
|
|
12
11
|
let meta = {}
|
|
13
12
|
let metaProxy = {
|
|
@@ -15,11 +14,49 @@ let metaProxy = {
|
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
function protect(target) {
|
|
18
|
+
if (target[source]) {
|
|
19
|
+
throw new Error('Data is immutable')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const myJSONTag = {
|
|
24
|
+
getAttribute: odJSONTag.getAttribute,
|
|
25
|
+
getAttributes: odJSONTag.getAttributes,
|
|
26
|
+
getType: odJSONTag.getType,
|
|
27
|
+
getTypeString: odJSONTag.getTypeString,
|
|
28
|
+
setAttribute: (target, name, value) => {
|
|
29
|
+
protect(target)
|
|
30
|
+
return odJSONTag.setAttribute(target, name, value)
|
|
31
|
+
},
|
|
32
|
+
setType: (target, type) => {
|
|
33
|
+
protect(target)
|
|
34
|
+
return odJSONTag.setType(target, type)
|
|
35
|
+
},
|
|
36
|
+
setAttributes: (target, attributes) => {
|
|
37
|
+
protect(target)
|
|
38
|
+
return odJSONTag.setAttributes(target, attributes)
|
|
39
|
+
},
|
|
40
|
+
addAttribute: (target, name, value) => {
|
|
41
|
+
protect(target)
|
|
42
|
+
return odJSONTag.addAttribute(target, name, value)
|
|
43
|
+
},
|
|
44
|
+
removeAttribute: (target, name) => {
|
|
45
|
+
protect(target)
|
|
46
|
+
return odJSONTag.removeAttribute(target, name)
|
|
47
|
+
},
|
|
48
|
+
getAttributesString: odJSONTag.getAttributesString,
|
|
49
|
+
isNull: odJSONTag.isNull,
|
|
50
|
+
clone: JSONTag.clone,
|
|
51
|
+
Link: JSONTag.Link,
|
|
52
|
+
Null: JSONTag.Null
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
const metaIdProxy = {
|
|
19
56
|
get: (id) => {
|
|
20
57
|
let index = meta.index.id.get(id)
|
|
21
58
|
if (index || index===0) {
|
|
22
|
-
return resultArr[index]
|
|
59
|
+
return meta.resultArr[index]
|
|
23
60
|
}
|
|
24
61
|
},
|
|
25
62
|
has: (id) => {
|
|
@@ -32,14 +69,25 @@ const tasks = {
|
|
|
32
69
|
if (task.req.access) {
|
|
33
70
|
task.req.access = await import(task.req.access)
|
|
34
71
|
task.req.access = task.req.access.default
|
|
72
|
+
meta.access = task.req.access
|
|
73
|
+
}
|
|
74
|
+
if (task.req.meta.index) {
|
|
75
|
+
meta.index = task.req.meta.index
|
|
76
|
+
}
|
|
77
|
+
for (let sab of task.req.body) { //body contains an array of sharedArrayBuffers with initial data and changes
|
|
78
|
+
dataspace = parse(sab, meta)
|
|
35
79
|
}
|
|
36
|
-
dataspace = parse(task.req.body, { access: task.req.access })
|
|
37
|
-
resultArr = dataspace[resultSet]
|
|
38
|
-
meta = task.req.meta
|
|
39
80
|
metaProxy.index.id = metaIdProxy
|
|
40
81
|
//@TODO: add meta.index.references? and baseURL
|
|
41
82
|
return true
|
|
42
83
|
},
|
|
84
|
+
update: async (task) => {
|
|
85
|
+
if (task.req.meta.index) {
|
|
86
|
+
meta.index = task.req.meta.index
|
|
87
|
+
}
|
|
88
|
+
dataspace = parse(task.req.body, meta) //update only has a single changeset
|
|
89
|
+
return true
|
|
90
|
+
},
|
|
43
91
|
query: async (task) => {
|
|
44
92
|
return runQuery(task.req.path, task.req, task.req.body)
|
|
45
93
|
},
|
|
@@ -83,7 +131,7 @@ export function runQuery(pointer, request, query) {
|
|
|
83
131
|
max,
|
|
84
132
|
min,
|
|
85
133
|
// console: connectConsole(res),
|
|
86
|
-
JSONTag:
|
|
134
|
+
JSONTag: myJSONTag,
|
|
87
135
|
request
|
|
88
136
|
},
|
|
89
137
|
wasm: false
|
|
@@ -145,8 +193,8 @@ export function getDataSpace(path, dataspace) {
|
|
|
145
193
|
}
|
|
146
194
|
|
|
147
195
|
export function linkReplacer(data, baseURL) {
|
|
148
|
-
let type =
|
|
149
|
-
let attributes =
|
|
196
|
+
let type = JSONTag.getType(data)
|
|
197
|
+
let attributes = JSONTag.getAttributes(data)
|
|
150
198
|
if (Array.isArray(data)) {
|
|
151
199
|
data = data.map((entry,index) => {
|
|
152
200
|
return linkReplacer(data[index], baseURL+index+'/')
|
|
@@ -162,8 +210,8 @@ export function linkReplacer(data, baseURL) {
|
|
|
162
210
|
if (Array.isArray(data[key])) {
|
|
163
211
|
data[key] = new JSONTag.Link(baseURL+key+'/')
|
|
164
212
|
} else if (data[key] && typeof data[key] === 'object') {
|
|
165
|
-
if (
|
|
166
|
-
let id=
|
|
213
|
+
if (JSONTag.getType(data[key])!=='link') {
|
|
214
|
+
let id=JSONTag.getAttribute(data[key], 'id')
|
|
167
215
|
if (!id) {
|
|
168
216
|
id = baseURL+key+'/'
|
|
169
217
|
}
|
package/src/server.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import writeFileAtomic from 'write-file-atomic'
|
|
|
11
11
|
|
|
12
12
|
const server = express()
|
|
13
13
|
const __dirname = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
14
|
-
let
|
|
14
|
+
let jsontagBuffers = null
|
|
15
15
|
let meta = {}
|
|
16
16
|
|
|
17
17
|
async function main(options) {
|
|
@@ -34,7 +34,8 @@ async function main(options) {
|
|
|
34
34
|
|
|
35
35
|
// allow access to raw body, used to parse a query send as post body
|
|
36
36
|
server.use(express.raw({
|
|
37
|
-
type: (req) => true // parse body on all requests
|
|
37
|
+
type: (req) => true, // parse body on all requests
|
|
38
|
+
limit: '50MB'
|
|
38
39
|
}))
|
|
39
40
|
|
|
40
41
|
function loadData() {
|
|
@@ -53,7 +54,7 @@ async function main(options) {
|
|
|
53
54
|
}
|
|
54
55
|
try {
|
|
55
56
|
let data = await loadData()
|
|
56
|
-
|
|
57
|
+
jsontagBuffers = [data.data]
|
|
57
58
|
meta = data.meta
|
|
58
59
|
} catch(err) {
|
|
59
60
|
console.error('ERROR: SimplyStore cannot load '+datafile, err)
|
|
@@ -64,13 +65,14 @@ async function main(options) {
|
|
|
64
65
|
return {
|
|
65
66
|
name: 'init',
|
|
66
67
|
req: {
|
|
67
|
-
body:
|
|
68
|
+
body: jsontagBuffers,
|
|
68
69
|
meta,
|
|
69
70
|
access
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
|
|
74
76
|
let queryWorkerPool = new WorkerPool(maxWorkers, queryWorker, queryWorkerInitTask())
|
|
75
77
|
|
|
76
78
|
server.get('/query/*', async (req, res, next) =>
|
|
@@ -227,22 +229,23 @@ async function main(options) {
|
|
|
227
229
|
s = {code: 200, status: "done"}
|
|
228
230
|
status.set(command.id, s)
|
|
229
231
|
if (data.data) { // data has changed, commands may do other things instead of changing data
|
|
230
|
-
|
|
232
|
+
jsontagBuffers.push(data.data) // push changeset to jsontagBuffers so that new query workers get all changes from scratch
|
|
231
233
|
meta = data.meta
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
queryWorkerPool.update({
|
|
235
|
+
name: 'update',
|
|
236
|
+
req: {
|
|
237
|
+
body: jsontagBuffers[jsontagBuffers.length-1], // only add the last change, update tasks for earlier changes have already been sent
|
|
238
|
+
meta
|
|
239
|
+
}
|
|
240
|
+
})
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
let l = Object.assign({command:command.id}, s)
|
|
241
|
-
console.log('appending status ',l,s)
|
|
242
244
|
appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
|
|
243
245
|
},
|
|
244
246
|
//reject()
|
|
245
247
|
(error) => {
|
|
248
|
+
console.error(error)
|
|
246
249
|
let s = {status: "failed", code: error.code, message: error.message, details: error.details}
|
|
247
250
|
status.set(command.id, s)
|
|
248
251
|
appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
|
|
@@ -276,7 +279,7 @@ async function main(options) {
|
|
|
276
279
|
command:commandStr,
|
|
277
280
|
request,
|
|
278
281
|
meta,
|
|
279
|
-
data:
|
|
282
|
+
data:jsontagBuffers,
|
|
280
283
|
commandsFile,
|
|
281
284
|
datafile
|
|
282
285
|
})
|
package/src/workerPool.mjs
CHANGED
|
@@ -19,7 +19,6 @@ class WorkerPoolTaskInfo extends AsyncResource {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
//@TODO: only create new workers when needed, not immediately
|
|
22
|
-
//@TODO: allow initialization of newly created workers
|
|
23
22
|
|
|
24
23
|
export default class WorkerPool extends EventEmitter {
|
|
25
24
|
constructor(numThreads, workerFile, initTask) {
|
|
@@ -29,13 +28,14 @@ export default class WorkerPool extends EventEmitter {
|
|
|
29
28
|
this.initTask = initTask
|
|
30
29
|
this.workers = []
|
|
31
30
|
this.freeWorkers = []
|
|
32
|
-
|
|
31
|
+
this.priority = new Map()
|
|
33
32
|
for (let i = 0; i < numThreads; i++)
|
|
34
33
|
this.addNewWorker();
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
addNewWorker() {
|
|
38
37
|
const worker = new Worker(path.resolve(this.workerFile));
|
|
38
|
+
this.priority[worker] = []
|
|
39
39
|
worker.on('message', (result) => {
|
|
40
40
|
// In case of success: Call the callback that was passed to `runTask`,
|
|
41
41
|
// remove the `TaskInfo` associated with the Worker, and mark it as free
|
|
@@ -44,8 +44,13 @@ export default class WorkerPool extends EventEmitter {
|
|
|
44
44
|
worker[kTaskInfo].done(null, result);
|
|
45
45
|
worker[kTaskInfo] = null;
|
|
46
46
|
}
|
|
47
|
-
this.
|
|
48
|
-
|
|
47
|
+
if (this.priority[worker].length) {
|
|
48
|
+
let task = this.priority[worker].shift()
|
|
49
|
+
worker.postMessage(task)
|
|
50
|
+
} else {
|
|
51
|
+
this.freeWorkers.push(worker);
|
|
52
|
+
this.emit(kWorkerFreedEvent);
|
|
53
|
+
}
|
|
49
54
|
});
|
|
50
55
|
worker.on('error', (err) => {
|
|
51
56
|
// In case of an uncaught exception: Call the callback that was passed to
|
|
@@ -86,6 +91,16 @@ export default class WorkerPool extends EventEmitter {
|
|
|
86
91
|
worker.postMessage(task);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
update(task) {
|
|
95
|
+
for (let worker of this.workers) {
|
|
96
|
+
if (worker[kTaskInfo]) { // worker is busy
|
|
97
|
+
this.priority[worker].push(task) // push task on priority list for this specific worker
|
|
98
|
+
} else { // worker is free
|
|
99
|
+
worker.postMessage(task) // run task immediately
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
89
104
|
memoryUsage() {
|
|
90
105
|
for (let worker of this.freeWorkers) {
|
|
91
106
|
worker[kTaskInfo] = new WorkerPoolTaskInfo((result) => {})
|