mikser-io 6.0.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.
package/src/journal.js ADDED
@@ -0,0 +1,108 @@
1
+ import runtime from './runtime.js'
2
+ import { onLoaded, onCancelled, onFinalized } from './lifecycle.js'
3
+ import { unlink } from 'fs/promises'
4
+ import knex from 'knex'
5
+ import path from 'path'
6
+ import { stopProgress, trackProgress, updateProgress } from './tracking.js'
7
+ import { AbortError } from './utils.js'
8
+
9
+ let journal
10
+
11
+ export async function addEntry({ entity, operation, context, options }) {
12
+ await journal('operations').insert([{ entity, operation, context, options }])
13
+ }
14
+
15
+ export async function addEntries(entries) {
16
+ await journal.batchInsert('operations', entries.map(({ entity, operation, context, options }) => ({
17
+ entity: JSON.stringify(entity),
18
+ operation,
19
+ context: JSON.stringify(context),
20
+ options: JSON.stringify(options)
21
+ })), 10)
22
+ }
23
+
24
+ export async function updateEntry({ id, entity, output }) {
25
+ const data = {}
26
+ if (entity) data.entity = JSON.stringify(entity)
27
+ if (output) data.output = JSON.stringify(output)
28
+ await journal('operations').where({ id }).update(data)
29
+ }
30
+
31
+ export async function* useJournal(name, operations, signal) {
32
+ let query = journal('operations')
33
+ if (operations?.length) {
34
+ query.whereIn('operation', operations)
35
+ }
36
+ let [total] = await query.clone().count()
37
+ total = total['count(*)']
38
+ if (!total) return
39
+
40
+ trackProgress(name, total)
41
+
42
+ let offset = 0
43
+ const limit = 1000
44
+ let count = 0
45
+ do {
46
+ count = 0
47
+ const entries = await query.clone().orderBy('id').select().offset(offset).limit(limit)
48
+ for (let { id, entity, operation, context, options, output } of entries) {
49
+ if (signal?.aborted) {
50
+ stopProgress()
51
+ throw new AbortError()
52
+ }
53
+ count++
54
+
55
+ updateProgress()
56
+ yield {
57
+ id,
58
+ entity: JSON.parse(entity),
59
+ operation,
60
+ context: JSON.parse(context),
61
+ options: JSON.parse(options),
62
+ output: JSON.parse(output)
63
+ }
64
+ }
65
+ offset += limit
66
+ } while (count == limit)
67
+ }
68
+
69
+ export async function clearJournal(aborted) {
70
+ await journal('operations').del()
71
+ if (!aborted) {
72
+ if (runtime.options.watch !== true) {
73
+ journal.destroy()
74
+ }
75
+ }
76
+ }
77
+
78
+ onLoaded(async () => {
79
+ const filename = path.join(runtime.options.runtimeFolder, `journal.db`)
80
+ try {
81
+ await unlink(filename)
82
+ } catch { }
83
+
84
+ journal = knex({
85
+ client: 'sqlite3',
86
+ connection: {
87
+ filename
88
+ },
89
+ useNullAsDefault: true
90
+ })
91
+
92
+ await journal.schema.createTable('operations', table => {
93
+ table.increments('id')
94
+ table.string('operation').index()
95
+ table.json('entity')
96
+ table.json('context')
97
+ table.json('options')
98
+ table.json('output')
99
+ })
100
+ })
101
+
102
+ onFinalized(async (signal) => {
103
+ await clearJournal(signal.aborted)
104
+ })
105
+
106
+ onCancelled(async () => {
107
+ await clearJournal(true)
108
+ })
@@ -0,0 +1,299 @@
1
+ import runtime from './runtime.js'
2
+ import { OPERATION } from './constants.js'
3
+ import { useLogger } from './engine.js'
4
+ import { addEntry, addEntries } from './journal.js'
5
+
6
+ export async function createEntity(entity) {
7
+ const logger = useLogger()
8
+ entity.stamp = runtime.stamp
9
+ entity.time = Date.now()
10
+ const entry = { operation: OPERATION.CREATE, entity }
11
+ if (await runtime.validate(entry)) {
12
+ logger.debug('Create %s entity: %s', entity.collection, entity.id)
13
+ await addEntry(entry)
14
+ }
15
+ }
16
+
17
+ export async function deleteEntity({ id, collection, type }) {
18
+ const logger = useLogger()
19
+ const entry = { operation: OPERATION.DELETE, entity: { id, type, collection } }
20
+ if (await runtime.validate(entry)) {
21
+ logger.debug('Delete %s entity: %s %s', collection, type, id)
22
+ await addEntry(entry)
23
+ }
24
+ }
25
+
26
+ export async function updateEntity(entity) {
27
+ const logger = useLogger()
28
+ entity.stamp = runtime.stamp
29
+ entity.time = Date.now()
30
+ const entry = { operation: OPERATION.UPDATE, entity }
31
+ if (await runtime.validate(entry)) {
32
+ logger.debug('Update %s entity: %s', entity.collection, entity.id)
33
+ await addEntry(entry)
34
+ }
35
+ }
36
+
37
+ export async function postprocessEntity(entity, options = {}, context = {}) {
38
+ const logger = useLogger()
39
+ const entry = { operation: OPERATION.POSTPROCESS, entity, options, context }
40
+ logger.debug('Postprocess %s entity: [%s] %s → %s', entity.collection, options.postprocessor, entity.id, entity.destination)
41
+ await addEntry(entry)
42
+ }
43
+
44
+ export async function postprocessEntities(tasks) {
45
+ const logger = useLogger()
46
+ if (!tasks.length) return
47
+ const entries = []
48
+ for (let { entity, options = {}, context = {} } of tasks) {
49
+ const entry = { operation: OPERATION.POSTPROCESS, entity, options, context }
50
+ logger.debug('Postprocess %s entity: [%s] %s → %s', entity.collection, options.postprocessor, entity.id, entity.destination)
51
+ entries.push(entry)
52
+ }
53
+ await addEntries(entries)
54
+ }
55
+
56
+ export async function renderEntities(tasks) {
57
+ const logger = useLogger()
58
+ if (!tasks.length) return
59
+ const entries = []
60
+ for (let { entity, options = {}, context = {} } of tasks) {
61
+ const entry = { operation: OPERATION.RENDER, entity, options, context, }
62
+ if (options.ignore) {
63
+ logger.trace('Render %s entity: [%s] %s → %s %s', entity.collection, options.renderer, entity.id, entity.destination, !options.ignore)
64
+ } else {
65
+ logger.debug('Render %s entity: [%s] %s → %s %s', entity.collection, options.renderer, entity.id, entity.destination, !options.ignore)
66
+ }
67
+ entries.push(entry)
68
+ }
69
+ await addEntries(entries)
70
+ }
71
+
72
+ export async function renderEntity(entity, options = {}, context = {}) {
73
+ const logger = useLogger()
74
+ const entry = { operation: OPERATION.RENDER, entity, options, context, }
75
+ if (options.ignore) {
76
+ logger.trace('Render %s entity: [%s] %s → %s %s', entity.collection, options.renderer, entity.id, entity.destination, !options.ignore)
77
+ } else {
78
+ logger.debug('Render %s entity: [%s] %s → %s %s', entity.collection, options.renderer, entity.id, entity.destination, !options.ignore)
79
+ }
80
+ await addEntry(entry)
81
+ }
82
+
83
+ export async function onInitialize(callback) {
84
+ runtime.hooks.initialize.push(callback)
85
+ }
86
+
87
+ export async function onInitialized(callback) {
88
+ runtime.hooks.initialized.push(callback)
89
+ }
90
+
91
+ export async function onLoad(callback) {
92
+ runtime.hooks.load.push(callback)
93
+ }
94
+
95
+ export async function onLoaded(callback) {
96
+ runtime.hooks.loaded.push(callback)
97
+ }
98
+
99
+ export async function onImport(callback) {
100
+ runtime.hooks.import.push(callback)
101
+ }
102
+
103
+ export async function onImported(callback) {
104
+ runtime.hooks.imported.push(callback)
105
+ }
106
+
107
+ export async function onProcess(callback, once) {
108
+ if (!once) runtime.hooks.process.push(callback)
109
+ else {
110
+ let called = false
111
+ runtime.hooks.process.push((signal) => {
112
+ if (!called) {
113
+ called = true
114
+ callback(signal)
115
+ }
116
+ })
117
+ }
118
+ }
119
+
120
+ export async function onProcessed(callback, once) {
121
+ if (!once) runtime.hooks.processed.push(callback)
122
+ else {
123
+ let called = false
124
+ runtime.hooks.processed.push((signal) => {
125
+ if (!called) {
126
+ called = true
127
+ callback(signal)
128
+ }
129
+ })
130
+ }
131
+ }
132
+
133
+ export async function onPersist(callback, once) {
134
+ if (!once) runtime.hooks.persist.push(callback)
135
+ else {
136
+ let called = false
137
+ runtime.hooks.persist.push((signal) => {
138
+ if (!called) {
139
+ called = true
140
+ callback(signal)
141
+ }
142
+ })
143
+ }
144
+ }
145
+
146
+ export async function onPersisted(callback, once) {
147
+ if (!once) runtime.hooks.persisted.push(callback)
148
+ else {
149
+ let called = false
150
+ runtime.hooks.persisted.push((signal) => {
151
+ if (!called) {
152
+ called = true
153
+ callback(signal)
154
+ }
155
+ })
156
+ }
157
+ }
158
+
159
+ export async function onCancel(callback) {
160
+ runtime.hooks.cancel.push(callback)
161
+ }
162
+
163
+ export async function onCancelled(callback) {
164
+ runtime.hooks.cancelled.push(callback)
165
+ }
166
+
167
+ export async function onBeforeRender(callback, once) {
168
+ if (!once) runtime.hooks.beforeRender.push(callback)
169
+ else {
170
+ let called = false
171
+ runtime.hooks.beforeRender.push((signal) => {
172
+ if (!called) {
173
+ called = true
174
+ callback(signal)
175
+ }
176
+ })
177
+ }
178
+ }
179
+
180
+ export async function onRender(callback, once) {
181
+ if (!once) runtime.hooks.render.push(callback)
182
+ else {
183
+ let called = false
184
+ runtime.hooks.render.push((signal) => {
185
+ if (!called) {
186
+ called = true
187
+ callback(signal)
188
+ }
189
+ })
190
+ }
191
+ }
192
+
193
+ export async function onAfterRender(callback, once) {
194
+ if (!once) runtime.hooks.afterRender.push(callback)
195
+ else {
196
+ let called = false
197
+ runtime.hooks.afterRender.push((signal) => {
198
+ if (!called) {
199
+ called = true
200
+ callback(signal)
201
+ }
202
+ })
203
+ }
204
+ }
205
+
206
+ export async function onBeforePostprocess(callback, once) {
207
+ if (!once) runtime.hooks.beforePostprocess.push(callback)
208
+ else {
209
+ let called = false
210
+ runtime.hooks.beforePostprocess.push((signal) => {
211
+ if (!called) {
212
+ called = true
213
+ callback(signal)
214
+ }
215
+ })
216
+ }
217
+ }
218
+
219
+ export async function onPostprocess(callback, once) {
220
+ if (!once) runtime.hooks.postprocess.push(callback)
221
+ else {
222
+ let called = false
223
+ runtime.hooks.postprocess.push((signal) => {
224
+ if (!called) {
225
+ called = true
226
+ callback(signal)
227
+ }
228
+ })
229
+ }
230
+ }
231
+
232
+ export async function onAfterPostprocess(callback, once) {
233
+ if (!once) runtime.hooks.afterPostprocess.push(callback)
234
+ else {
235
+ let called = false
236
+ runtime.hooks.afterPostprocess.push((signal) => {
237
+ if (!called) {
238
+ called = true
239
+ callback(signal)
240
+ }
241
+ })
242
+ }
243
+ }
244
+
245
+ export async function onFinalize(callback, once) {
246
+ if (!once) runtime.hooks.finalize.push(callback)
247
+ else {
248
+ let called = false
249
+ runtime.hooks.finalize.push((signal) => {
250
+ if (!called) {
251
+ called = true
252
+ callback(signal)
253
+ }
254
+ })
255
+ }
256
+ }
257
+
258
+ export async function onFinalized(callback, once) {
259
+ if (!once) runtime.hooks.finalized.push(callback)
260
+ else {
261
+ let called = false
262
+ runtime.hooks.finalized.push((signal) => {
263
+ if (!called) {
264
+ called = true
265
+ callback(signal)
266
+ }
267
+ })
268
+ }
269
+ }
270
+
271
+ export function onSync(name, callback) {
272
+ runtime.hooks.sync.push(async (operation) => {
273
+ if (operation.name == name) {
274
+ return await callback(operation)
275
+ }
276
+ })
277
+ }
278
+
279
+ export function onValidate(operations, callback) {
280
+ const logger = useLogger()
281
+ runtime.validators.push(async (entry) => {
282
+ if (operations.indexOf(entry.operation) != -1) {
283
+ try {
284
+ const message = await callback(entry)
285
+ if (message) {
286
+ logger.warn('Validation problem: [%s] %s %s', entry.operation, entry.entity.name, message)
287
+ }
288
+ return true
289
+ } catch (err) {
290
+ logger.error('Validation error: [%s] %s %s', entry.operation, entry.entity.name, err.message)
291
+ return false
292
+ }
293
+ }
294
+ })
295
+ }
296
+
297
+ export function onComplete(callback) {
298
+ runtime.hooks.completed.push(callback)
299
+ }
package/src/manager.js ADDED
@@ -0,0 +1,119 @@
1
+ import runtime from './runtime.js'
2
+ import chokidar from 'chokidar'
3
+ import cron from 'node-cron'
4
+ import { onProcess, onFinalized } from './lifecycle.js'
5
+ import { useLogger } from './engine.js'
6
+ import { ACTION } from './constants.js'
7
+
8
+ const tasks = []
9
+
10
+ export async function createdHook(name, context) {
11
+ if (!runtime.started) return
12
+
13
+ const synced = await runtime.sync({
14
+ action: ACTION.CREATE,
15
+ name,
16
+ context
17
+ })
18
+
19
+ if (synced) {
20
+ clearTimeout(runtime.runtime.processTimeout)
21
+ runtime.runtime.processTimeout = setTimeout(() => runtime.process(), 1000)
22
+ }
23
+ }
24
+
25
+ export async function updatedHook(name, context) {
26
+ if (!runtime.started) return
27
+
28
+ const synced = await runtime.sync({
29
+ action: ACTION.UPDATE,
30
+ name,
31
+ context
32
+ })
33
+
34
+ if (synced) {
35
+ clearTimeout(runtime.runtime.processTimeout)
36
+ runtime.runtime.processTimeout = setTimeout(() => runtime.process(), 1000)
37
+ }
38
+ }
39
+
40
+ export async function triggeredHook(name, context) {
41
+ if (!runtime.started) return
42
+
43
+ const synced = await runtime.sync({
44
+ action: ACTION.TRIGGER,
45
+ name,
46
+ context
47
+ })
48
+
49
+ if (synced) {
50
+ clearTimeout(runtime.runtime.processTimeout)
51
+ runtime.runtime.processTimeout = setTimeout(() => runtime.process(), 1000)
52
+ }
53
+ }
54
+
55
+ export async function deletedHook(name, context) {
56
+ if (!runtime.started) return
57
+
58
+ const synced = await runtime.sync({
59
+ action: ACTION.DELETE,
60
+ name,
61
+ context
62
+ })
63
+
64
+ if (synced) {
65
+ clearTimeout(runtime.runtime.processTimeout)
66
+ runtime.runtime.processTimeout = setTimeout(() => runtime.process(), 1000)
67
+ }
68
+ }
69
+
70
+ export function watch(name, folder, options = { interval: 1000, binaryInterval: 3000, ignored: /[\/\\]\./, ignoreInitial: true }) {
71
+ if (runtime.options.watch !== true) return
72
+
73
+ chokidar.watch(folder, options)
74
+ .on('all', () => {
75
+ clearTimeout(runtime.runtime.processTimeout)
76
+ })
77
+ .on('add', async fullPath => {
78
+ const relativePath = fullPath.replace(`${folder}/`, '')
79
+ createdHook(name, { relativePath })
80
+ })
81
+ .on('change', async fullPath => {
82
+ const relativePath = fullPath.replace(`${folder}/`, '')
83
+ updatedHook(name, { relativePath })
84
+ })
85
+ .on('unlink', async fullPath => {
86
+ const relativePath = fullPath.replace(`${folder}/`, '')
87
+ deletedHook(name, { relativePath })
88
+ })
89
+ }
90
+
91
+ export function schedule(name, expression, context) {
92
+ if (runtime.options.watch !== true) return
93
+ const logger = useLogger()
94
+ const taks = cron.schedule(expression, async () => {
95
+ logger.info('Scheduled task executed: %s %s', name, expression)
96
+ triggeredHook(name, context)
97
+ }, {
98
+ scheduled: false
99
+ })
100
+ tasks.push(taks)
101
+ }
102
+
103
+ onProcess(() => {
104
+ if (!tasks.length) return
105
+ const logger = useLogger()
106
+ logger.debug('Stopping scheduled tasks: %d', tasks.length)
107
+ for (let task of tasks) {
108
+ task.stop()
109
+ }
110
+ })
111
+
112
+ onFinalized(() => {
113
+ if (!tasks.length) return
114
+ const logger = useLogger()
115
+ logger.debug('Starting scheduled tasks: %d', tasks.length)
116
+ for (let task of tasks) {
117
+ task.start()
118
+ }
119
+ })
@@ -0,0 +1,176 @@
1
+ import path from 'path'
2
+ import { hash } from 'hasha'
3
+ import _ from 'lodash'
4
+
5
+ export default ({
6
+ runtime,
7
+ useLogger,
8
+ onImport,
9
+ onLoaded,
10
+ onSync,
11
+ createEntity,
12
+ updateEntity,
13
+ deleteEntity,
14
+ findEntity,
15
+ findEntities,
16
+ schedule,
17
+ normalize,
18
+ trackProgress,
19
+ updateProgress,
20
+ }) => {
21
+ const format = 'api'
22
+
23
+ async function syncEntities(apiName) {
24
+ const logger = useLogger()
25
+ const syncTime = Date.now()
26
+ const {
27
+ collection = apiName,
28
+ type = 'document',
29
+ readMany,
30
+ uri = ''
31
+ } = runtime.config.api[apiName]
32
+
33
+ let synced = 0
34
+ let removed = 0
35
+ try {
36
+ const recent = new Set()
37
+ const entities = await readMany(runtime)
38
+ trackProgress(`Api sync ${apiName}`, entities.length)
39
+ for (let meta of entities) {
40
+ if (collection && type && meta.id) {
41
+ const name = path.join(collection, meta.name || meta.id.toString())
42
+ const id = path.join('/api', collection, meta.id.toString())
43
+ if (recent.has(id)) {
44
+ logger.error(meta, 'Duplicate entity found: %s', id)
45
+ continue
46
+ }
47
+ recent.add(id)
48
+ const entity = normalize({
49
+ id,
50
+ uri: `${uri}/${meta.id}`,
51
+ name,
52
+ collection,
53
+ type,
54
+ format,
55
+ meta
56
+ })
57
+
58
+ entity.checksum = await hash(JSON.stringify(entity.meta), { algorithm: 'md5' })
59
+ const current = await findEntity({ id })
60
+ if (current) {
61
+ if (entity.checksum != current.checksum) {
62
+ await updateEntity(entity)
63
+ synced++
64
+ }
65
+ } else {
66
+ await createEntity(entity)
67
+ synced++
68
+ }
69
+ }
70
+ updateProgress()
71
+ }
72
+
73
+ const entitiesToRemove = await findEntities(entity =>
74
+ entity.type == type &&
75
+ entity.format == format &&
76
+ entity.collection == collection &&
77
+ entity.time < syncTime &&
78
+ !recent.has(entity.id)
79
+ )
80
+ if (entitiesToRemove.length) trackProgress(`Api remove ${apiName}`, entitiesToRemove.length)
81
+ for (let entity of entitiesToRemove) {
82
+ deleteEntity(entity)
83
+ removed++
84
+ updateProgress()
85
+ }
86
+ if (synced || removed) {
87
+ logger.debug('Syncing api [%s] synced: %d, removed: %d', collection, synced, removed)
88
+ }
89
+ } catch (err) {
90
+ logger.error('Api sync [%s] error: %s', collection, err.message)
91
+ }
92
+ return synced > 0 || removed > 0
93
+ }
94
+
95
+ async function syncEntity(apiName, apiId) {
96
+ const logger = useLogger()
97
+ const {
98
+ collection = apiName,
99
+ type = 'document',
100
+ readOne,
101
+ uri = ''
102
+ } = runtime.config.api[apiName]
103
+
104
+ try {
105
+ const id = path.join('/api', collection, apiId.toString())
106
+ const current = await findEntity({ id })
107
+ const meta = await readOne(apiId, runtime)
108
+ if (meta?.id) {
109
+ const name = path.join(collection, meta.name || meta.id.toString())
110
+ const entity = normalize({
111
+ id,
112
+ uri: `${uri}/${meta.id}`,
113
+ name,
114
+ collection,
115
+ type,
116
+ format,
117
+ meta
118
+ })
119
+ entity.checksum = await hash(JSON.stringify(entity.meta), { algorithm: 'md5' })
120
+ if (current) {
121
+ if (entity.checksum != current.checksum) {
122
+ logger.info('Api update: %s', id)
123
+ await updateEntity(entity)
124
+ }
125
+ } else {
126
+ logger.info('Api create: %s', id)
127
+ await createEntity(entity)
128
+ }
129
+ } else {
130
+ if (current) {
131
+ logger.info('Api delete: %s', id)
132
+ await deleteEntity(entity)
133
+ }
134
+ }
135
+ } catch (err) {
136
+ logger.error('Api sync entity [%s] error: %s', collection, err.message)
137
+ }
138
+ }
139
+
140
+ onLoaded(async () => {
141
+ const logger = useLogger()
142
+ for (let apiName in runtime.config.api || {}) {
143
+ const { cron } = runtime.config.api[apiName]
144
+ if (cron) {
145
+ logger.info('Schedule api: [%s] %s', apiName, cron)
146
+ schedule(apiName, cron)
147
+ }
148
+
149
+ onSync(apiName, async ({ context }) => {
150
+ if (context?.id) {
151
+ return syncEntity(apiName, context.id)
152
+ } else {
153
+ return syncEntities(apiName)
154
+ }
155
+ })
156
+
157
+ const { origin } = new URL(runtime.config.api[apiName].uri)
158
+ onSync(origin, async ({ context }) => {
159
+ if (context.uri) {
160
+ logger.info('Syncing api: [%s] %s', apiName, context.uri)
161
+ return syncEntities(apiName)
162
+ }
163
+ })
164
+ }
165
+ })
166
+
167
+ onImport(async () => {
168
+ for (let apiName in runtime.config.api || {}) {
169
+ await syncEntities(apiName)
170
+ }
171
+ })
172
+
173
+ return {
174
+ format
175
+ }
176
+ }