@xyo-network/sentinel 2.43.37
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/LICENSE +165 -0
- package/README.md +13 -0
- package/dist/cjs/Automation.js +5 -0
- package/dist/cjs/Automation.js.map +1 -0
- package/dist/cjs/Queries/Report.js +5 -0
- package/dist/cjs/Queries/Report.js.map +1 -0
- package/dist/cjs/Queries/index.js +5 -0
- package/dist/cjs/Queries/index.js.map +1 -0
- package/dist/cjs/Sentinel.js +134 -0
- package/dist/cjs/Sentinel.js.map +1 -0
- package/dist/cjs/SentinelIntervalAutomationWrapper.js +44 -0
- package/dist/cjs/SentinelIntervalAutomationWrapper.js.map +1 -0
- package/dist/cjs/SentinelModel.js +3 -0
- package/dist/cjs/SentinelModel.js.map +1 -0
- package/dist/cjs/SentinelRunner.js +102 -0
- package/dist/cjs/SentinelRunner.js.map +1 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/docs.json +16084 -0
- package/dist/esm/Automation.js +2 -0
- package/dist/esm/Automation.js.map +1 -0
- package/dist/esm/Queries/Report.js +2 -0
- package/dist/esm/Queries/Report.js.map +1 -0
- package/dist/esm/Queries/index.js +2 -0
- package/dist/esm/Queries/index.js.map +1 -0
- package/dist/esm/Sentinel.js +102 -0
- package/dist/esm/Sentinel.js.map +1 -0
- package/dist/esm/SentinelIntervalAutomationWrapper.js +37 -0
- package/dist/esm/SentinelIntervalAutomationWrapper.js.map +1 -0
- package/dist/esm/SentinelModel.js +2 -0
- package/dist/esm/SentinelModel.js.map +1 -0
- package/dist/esm/SentinelRunner.js +86 -0
- package/dist/esm/SentinelRunner.js.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/Automation.d.ts +28 -0
- package/dist/types/Automation.d.ts.map +1 -0
- package/dist/types/Queries/Report.d.ts +7 -0
- package/dist/types/Queries/Report.d.ts.map +1 -0
- package/dist/types/Queries/index.d.ts +6 -0
- package/dist/types/Queries/index.d.ts.map +1 -0
- package/dist/types/Sentinel.d.ts +35 -0
- package/dist/types/Sentinel.d.ts.map +1 -0
- package/dist/types/SentinelIntervalAutomationWrapper.d.ts +10 -0
- package/dist/types/SentinelIntervalAutomationWrapper.d.ts.map +1 -0
- package/dist/types/SentinelModel.d.ts +11 -0
- package/dist/types/SentinelModel.d.ts.map +1 -0
- package/dist/types/SentinelRunner.d.ts +25 -0
- package/dist/types/SentinelRunner.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +67 -0
- package/src/Automation.ts +39 -0
- package/src/Queries/Report.ts +8 -0
- package/src/Queries/index.ts +11 -0
- package/src/Sentinel.ts +141 -0
- package/src/SentinelIntervalAutomationWrapper.ts +45 -0
- package/src/SentinelModel.ts +11 -0
- package/src/SentinelRunner.ts +104 -0
- package/src/index.ts +5 -0
- package/src/spec/Sentinel.spec.ts +215 -0
- package/src/spec/SentinelRunner.spec.ts +47 -0
- package/typedoc.json +10 -0
package/src/Sentinel.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { assertEx } from '@xylabs/assert'
|
|
2
|
+
import { Account } from '@xyo-network/account'
|
|
3
|
+
import { AbstractArchivist, ArchivingModule, ArchivingModuleConfig } from '@xyo-network/archivist'
|
|
4
|
+
import { ArchivistWrapper } from '@xyo-network/archivist-wrapper'
|
|
5
|
+
import { XyoBoundWitness } from '@xyo-network/boundwitness-model'
|
|
6
|
+
import {
|
|
7
|
+
AbstractModuleConfig,
|
|
8
|
+
ModuleParams,
|
|
9
|
+
ModuleQueryResult,
|
|
10
|
+
QueryBoundWitnessWrapper,
|
|
11
|
+
XyoErrorBuilder,
|
|
12
|
+
XyoQueryBoundWitness,
|
|
13
|
+
} from '@xyo-network/module'
|
|
14
|
+
import { XyoPayload } from '@xyo-network/payload-model'
|
|
15
|
+
import { AbstractWitness, WitnessWrapper } from '@xyo-network/witness'
|
|
16
|
+
import compact from 'lodash/compact'
|
|
17
|
+
import uniq from 'lodash/uniq'
|
|
18
|
+
|
|
19
|
+
import { SentinelQuery, SentinelReportQuerySchema } from './Queries'
|
|
20
|
+
import { SentinelModule } from './SentinelModel'
|
|
21
|
+
|
|
22
|
+
export type SentinelConfigSchema = 'network.xyo.sentinel.config'
|
|
23
|
+
export const SentinelConfigSchema: SentinelConfigSchema = 'network.xyo.sentinel.config'
|
|
24
|
+
|
|
25
|
+
export type SentinelConfig = ArchivingModuleConfig<{
|
|
26
|
+
onReportEnd?: (boundWitness?: XyoBoundWitness, errors?: Error[]) => void
|
|
27
|
+
onReportStart?: () => void
|
|
28
|
+
onWitnessReportEnd?: (witness: WitnessWrapper, error?: Error) => void
|
|
29
|
+
onWitnessReportStart?: (witness: WitnessWrapper) => void
|
|
30
|
+
schema: SentinelConfigSchema
|
|
31
|
+
witnesses?: string[]
|
|
32
|
+
}>
|
|
33
|
+
|
|
34
|
+
export class Sentinel extends ArchivingModule<SentinelConfig> implements SentinelModule {
|
|
35
|
+
static override configSchema: SentinelConfigSchema
|
|
36
|
+
|
|
37
|
+
public history: XyoBoundWitness[] = []
|
|
38
|
+
private _archivists: ArchivistWrapper[] | undefined
|
|
39
|
+
private _witnesses: WitnessWrapper[] | undefined
|
|
40
|
+
|
|
41
|
+
static override async create(params?: Partial<ModuleParams<SentinelConfig>>): Promise<Sentinel> {
|
|
42
|
+
return (await super.create(params)) as Sentinel
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public addWitness(address: string[]) {
|
|
46
|
+
this.config.witnesses = uniq([...address, ...(this.config.witnesses ?? [])])
|
|
47
|
+
this._witnesses = undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async getArchivists() {
|
|
51
|
+
const addresses = this.config?.archivists ? (Array.isArray(this.config.archivists) ? this.config?.archivists : [this.config.archivists]) : []
|
|
52
|
+
this._archivists =
|
|
53
|
+
this._archivists ||
|
|
54
|
+
((await this.resolver?.resolve({ address: addresses })) as AbstractArchivist[]).map((witness) => new ArchivistWrapper(witness))
|
|
55
|
+
|
|
56
|
+
return this._archivists
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public async getWitnesses() {
|
|
60
|
+
const addresses = this.config?.witnesses ? (Array.isArray(this.config.witnesses) ? this.config?.witnesses : [this.config.witnesses]) : []
|
|
61
|
+
this._witnesses =
|
|
62
|
+
this._witnesses || ((await this.resolver?.resolve({ address: addresses })) as AbstractWitness[]).map((witness) => new WitnessWrapper(witness))
|
|
63
|
+
|
|
64
|
+
return this._witnesses
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public override queries(): string[] {
|
|
68
|
+
return [SentinelReportQuerySchema, ...super.queries()]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override async query<T extends XyoQueryBoundWitness = XyoQueryBoundWitness, TConfig extends AbstractModuleConfig = AbstractModuleConfig>(
|
|
72
|
+
query: T,
|
|
73
|
+
payloads?: XyoPayload[],
|
|
74
|
+
queryConfig?: TConfig,
|
|
75
|
+
): Promise<ModuleQueryResult> {
|
|
76
|
+
const wrapper = QueryBoundWitnessWrapper.parseQuery<SentinelQuery>(query, payloads)
|
|
77
|
+
const typedQuery = wrapper.query
|
|
78
|
+
assertEx(this.queryable(query, payloads, queryConfig))
|
|
79
|
+
const queryAccount = new Account()
|
|
80
|
+
const resultPayloads: XyoPayload[] = []
|
|
81
|
+
try {
|
|
82
|
+
switch (typedQuery.schemaName) {
|
|
83
|
+
case SentinelReportQuerySchema: {
|
|
84
|
+
const reportResult = await this.report(payloads)
|
|
85
|
+
resultPayloads.push(...(reportResult[0], reportResult[1]))
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
return super.query(query, payloads)
|
|
90
|
+
}
|
|
91
|
+
} catch (ex) {
|
|
92
|
+
const error = ex as Error
|
|
93
|
+
resultPayloads.push(new XyoErrorBuilder([wrapper.hash], error.message).build())
|
|
94
|
+
}
|
|
95
|
+
return await this.bindResult(resultPayloads, queryAccount)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public removeArchivist(address: string[]) {
|
|
99
|
+
this.config.archivists = (this.config.archivists ?? []).filter((archivist) => !address.includes(archivist))
|
|
100
|
+
this._archivists = undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public removeWitness(address: string[]) {
|
|
104
|
+
this.config.witnesses = (this.config.witnesses ?? []).filter((witness) => !address.includes(witness))
|
|
105
|
+
this._witnesses = undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public async report(payloads: XyoPayload[] = []): Promise<[XyoBoundWitness, XyoPayload[]]> {
|
|
109
|
+
const errors: Error[] = []
|
|
110
|
+
this.config?.onReportStart?.()
|
|
111
|
+
const allWitnesses = [...(await this.getWitnesses())]
|
|
112
|
+
const allPayloads: XyoPayload[] = []
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const generatedPayloads = compact(await this.generatePayloads(allWitnesses))
|
|
116
|
+
const combinedPayloads = [...generatedPayloads, ...payloads]
|
|
117
|
+
allPayloads.push(...combinedPayloads)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
errors.push(e as Error)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const [newBoundWitness] = await this.bindResult(allPayloads)
|
|
123
|
+
this.history.push(assertEx(newBoundWitness))
|
|
124
|
+
this.config?.onReportEnd?.(newBoundWitness, errors.length > 0 ? errors : undefined)
|
|
125
|
+
return [newBoundWitness, allPayloads]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public async tryReport(payloads: XyoPayload[] = []): Promise<[XyoBoundWitness | null, XyoPayload[]]> {
|
|
129
|
+
try {
|
|
130
|
+
return await this.report(payloads)
|
|
131
|
+
} catch (ex) {
|
|
132
|
+
const error = ex as Error
|
|
133
|
+
this.logger?.warn(`report failed [${error.message}]`)
|
|
134
|
+
return [null, []]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async generatePayloads(witnesses: WitnessWrapper[]): Promise<XyoPayload[]> {
|
|
139
|
+
return (await Promise.all(witnesses?.map(async (witness) => await witness.observe()))).flat()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { PayloadWrapper } from '@xyo-network/payload-wrapper'
|
|
2
|
+
|
|
3
|
+
import { SentinelIntervalAutomationPayload } from './Automation'
|
|
4
|
+
|
|
5
|
+
export class SentinelIntervalAutomationWrapper<
|
|
6
|
+
T extends SentinelIntervalAutomationPayload = SentinelIntervalAutomationPayload,
|
|
7
|
+
> extends PayloadWrapper<T> {
|
|
8
|
+
protected get frequencyMillis() {
|
|
9
|
+
if (this.payload.frequency === undefined) return Infinity
|
|
10
|
+
switch (this.payload.frequencyUnits ?? 'hour') {
|
|
11
|
+
case 'minute':
|
|
12
|
+
return this.payload.frequency * 60 * 1000
|
|
13
|
+
case 'hour':
|
|
14
|
+
return this.payload.frequency * 60 * 60 * 1000
|
|
15
|
+
case 'day':
|
|
16
|
+
return this.payload.frequency * 24 * 60 * 60 * 1000
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected get remaining() {
|
|
21
|
+
//if remaining is not defined, we assume Infinity
|
|
22
|
+
return this.payload.remaining ?? Infinity
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public next() {
|
|
26
|
+
this.payload.start = this.payload.start + this.frequencyMillis
|
|
27
|
+
this.consumeRemaining()
|
|
28
|
+
this.checkEnd()
|
|
29
|
+
return this
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected checkEnd() {
|
|
33
|
+
if (this.payload.start > (this.payload.end ?? Infinity)) {
|
|
34
|
+
this.payload.start = Infinity
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
protected consumeRemaining(count = 1) {
|
|
39
|
+
this.payload.remaining = this.remaining - count
|
|
40
|
+
|
|
41
|
+
if (this.payload.remaining <= 0) {
|
|
42
|
+
this.payload.start = Infinity
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { XyoBoundWitness } from '@xyo-network/boundwitness-model'
|
|
2
|
+
import { Module } from '@xyo-network/module-model'
|
|
3
|
+
import { XyoPayload } from '@xyo-network/payload-model'
|
|
4
|
+
import { Promisable } from '@xyo-network/promise'
|
|
5
|
+
|
|
6
|
+
export interface SentinelModel {
|
|
7
|
+
report: (payloads?: XyoPayload[]) => Promisable<[XyoBoundWitness, XyoPayload[]]>
|
|
8
|
+
tryReport: (payloads?: XyoPayload[]) => Promisable<[XyoBoundWitness | null, XyoPayload[]]>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SentinelModule extends Module, SentinelModel {}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assertEx } from '@xylabs/assert'
|
|
2
|
+
import { XyoBoundWitness } from '@xyo-network/boundwitness-model'
|
|
3
|
+
import { XyoPayload } from '@xyo-network/payload-model'
|
|
4
|
+
import { PayloadWrapper } from '@xyo-network/payload-wrapper'
|
|
5
|
+
|
|
6
|
+
import { SentinelAutomationPayload, SentinelIntervalAutomationPayload } from './Automation'
|
|
7
|
+
import { SentinelIntervalAutomationWrapper } from './SentinelIntervalAutomationWrapper'
|
|
8
|
+
import { SentinelModel } from './SentinelModel'
|
|
9
|
+
|
|
10
|
+
export type OnSentinelRunnerTriggerResult = (result: [XyoBoundWitness | null, XyoPayload[]]) => void
|
|
11
|
+
|
|
12
|
+
export class SentinelRunner {
|
|
13
|
+
protected _automations: Record<string, SentinelAutomationPayload> = {}
|
|
14
|
+
protected onTriggerResult: OnSentinelRunnerTriggerResult | undefined
|
|
15
|
+
protected sentinel: SentinelModel
|
|
16
|
+
protected timeoutId?: NodeJS.Timer
|
|
17
|
+
|
|
18
|
+
constructor(sentinel: SentinelModel, automations?: SentinelAutomationPayload[], onTriggerResult?: OnSentinelRunnerTriggerResult) {
|
|
19
|
+
this.sentinel = sentinel
|
|
20
|
+
this.onTriggerResult = onTriggerResult
|
|
21
|
+
automations?.forEach((automation) => this.add(automation))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public get automations() {
|
|
25
|
+
return this._automations
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private get next() {
|
|
29
|
+
return Object.values(this._automations).reduce<SentinelIntervalAutomationPayload | undefined>((previous, current) => {
|
|
30
|
+
if (current.type === 'interval') {
|
|
31
|
+
return current.start < (previous?.start ?? Infinity) ? current : previous
|
|
32
|
+
}
|
|
33
|
+
}, undefined)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async add(automation: SentinelAutomationPayload, restart = true) {
|
|
37
|
+
const hash = new PayloadWrapper(automation).hash
|
|
38
|
+
this._automations[hash] = automation
|
|
39
|
+
if (restart) await this.restart()
|
|
40
|
+
return hash
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public find(hash: string) {
|
|
44
|
+
Object.entries(this._automations).find(([key]) => key === hash)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async remove(hash: string, restart = true) {
|
|
48
|
+
delete this._automations[hash]
|
|
49
|
+
if (restart) await this.restart()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public removeAll() {
|
|
53
|
+
this.stop()
|
|
54
|
+
this._automations = {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async restart() {
|
|
58
|
+
this.stop()
|
|
59
|
+
await this.start()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async start() {
|
|
63
|
+
assertEx(this.timeoutId === undefined, 'Already started')
|
|
64
|
+
const automation = this.next
|
|
65
|
+
if (automation) {
|
|
66
|
+
const delay = automation.start - Date.now()
|
|
67
|
+
if (delay < 0) {
|
|
68
|
+
//automation is due, just do it
|
|
69
|
+
await this.trigger(automation)
|
|
70
|
+
} else {
|
|
71
|
+
this.timeoutId = setTimeout(
|
|
72
|
+
async () => {
|
|
73
|
+
this.timeoutId = undefined
|
|
74
|
+
await this.start()
|
|
75
|
+
},
|
|
76
|
+
delay > 0 ? delay : 0,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public stop() {
|
|
83
|
+
if (this.timeoutId) {
|
|
84
|
+
clearTimeout(this.timeoutId)
|
|
85
|
+
this.timeoutId = undefined
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public async update(hash: string, automation: SentinelAutomationPayload, restart = true) {
|
|
90
|
+
await this.remove(hash, false)
|
|
91
|
+
await this.add(automation, false)
|
|
92
|
+
if (restart) await this.restart()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async trigger(automation: SentinelIntervalAutomationPayload) {
|
|
96
|
+
const wrapper = new SentinelIntervalAutomationWrapper(automation)
|
|
97
|
+
await this.remove(wrapper.hash, false)
|
|
98
|
+
wrapper.next()
|
|
99
|
+
await this.add(wrapper.payload, false)
|
|
100
|
+
const triggerResult = await this.sentinel.tryReport()
|
|
101
|
+
this.onTriggerResult?.(triggerResult)
|
|
102
|
+
await this.start()
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { AbstractArchivist, Archivist, MemoryArchivist } from '@xyo-network/archivist'
|
|
2
|
+
import { XyoBoundWitness, XyoBoundWitnessSchema } from '@xyo-network/boundwitness-model'
|
|
3
|
+
import { BoundWitnessValidator } from '@xyo-network/boundwitness-validator'
|
|
4
|
+
import { BoundWitnessWrapper } from '@xyo-network/boundwitness-wrapper'
|
|
5
|
+
import { Hasher } from '@xyo-network/core'
|
|
6
|
+
import { IdWitness, IdWitnessConfigSchema } from '@xyo-network/id-plugin'
|
|
7
|
+
import { ModuleParams, SimpleModuleResolver } from '@xyo-network/module'
|
|
8
|
+
import { XyoNodeSystemInfoWitness, XyoNodeSystemInfoWitnessConfigSchema } from '@xyo-network/node-system-info-plugin'
|
|
9
|
+
import { XyoPayload, XyoPayloadSchema } from '@xyo-network/payload-model'
|
|
10
|
+
import { PayloadWrapper } from '@xyo-network/payload-wrapper'
|
|
11
|
+
import { AbstractWitness } from '@xyo-network/witness'
|
|
12
|
+
import { XyoAdhocWitness, XyoAdhocWitnessConfigSchema } from '@xyo-network/witnesses'
|
|
13
|
+
|
|
14
|
+
import { Sentinel, SentinelConfig, SentinelConfigSchema } from '../Sentinel'
|
|
15
|
+
|
|
16
|
+
describe('Sentinel', () => {
|
|
17
|
+
test('all [simple sentinel send]', async () => {
|
|
18
|
+
const archivist = await MemoryArchivist.create()
|
|
19
|
+
|
|
20
|
+
const witnesses: AbstractWitness[] = [
|
|
21
|
+
await IdWitness.create({ config: { salt: 'test', schema: IdWitnessConfigSchema } }),
|
|
22
|
+
await XyoNodeSystemInfoWitness.create({
|
|
23
|
+
config: {
|
|
24
|
+
nodeValues: {
|
|
25
|
+
osInfo: '*',
|
|
26
|
+
},
|
|
27
|
+
schema: XyoNodeSystemInfoWitnessConfigSchema,
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const config: SentinelConfig = {
|
|
33
|
+
archivists: [archivist.address],
|
|
34
|
+
schema: SentinelConfigSchema,
|
|
35
|
+
witnesses: witnesses.map((witness) => witness.address),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const resolver = new SimpleModuleResolver()
|
|
39
|
+
resolver.add(archivist)
|
|
40
|
+
witnesses.forEach((witness) => resolver.add(witness))
|
|
41
|
+
|
|
42
|
+
const sentinel = await Sentinel.create({ config, resolver })
|
|
43
|
+
expect(await sentinel.getArchivists()).toBeArrayOfSize(1)
|
|
44
|
+
expect(await sentinel.getWitnesses()).toBeArrayOfSize(2)
|
|
45
|
+
const adhocWitness = await XyoAdhocWitness.create({
|
|
46
|
+
config: {
|
|
47
|
+
payload: {
|
|
48
|
+
schema: 'network.xyo.test.array',
|
|
49
|
+
testArray: [1, 2, 3],
|
|
50
|
+
testBoolean: true,
|
|
51
|
+
testNull: null,
|
|
52
|
+
testNullObject: { t: null, x: undefined },
|
|
53
|
+
testNumber: 5,
|
|
54
|
+
testObject: { t: 1 },
|
|
55
|
+
testSomeNullObject: { s: 1, t: null, x: undefined },
|
|
56
|
+
testString: 'hi',
|
|
57
|
+
testUndefined: undefined,
|
|
58
|
+
},
|
|
59
|
+
schema: XyoAdhocWitnessConfigSchema,
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const adhocObserved = await adhocWitness.observe([adhocWitness.config.payload])
|
|
64
|
+
|
|
65
|
+
const report1Result = await sentinel.report(adhocObserved)
|
|
66
|
+
const report1 = BoundWitnessWrapper.parse(report1Result[0])
|
|
67
|
+
expect(report1.schemaName).toBe(XyoBoundWitnessSchema)
|
|
68
|
+
expect(report1.payloadHashes).toBeArrayOfSize(3)
|
|
69
|
+
const report2 = BoundWitnessWrapper.parse((await sentinel.report())[0])
|
|
70
|
+
expect(report2.schemaName).toBeDefined()
|
|
71
|
+
expect(report2.payloadHashes).toBeArrayOfSize(2)
|
|
72
|
+
expect(report2.hash !== report1.hash).toBe(true)
|
|
73
|
+
expect(report2.prev(sentinel.address)).toBeDefined()
|
|
74
|
+
expect(report2.prev(sentinel.address)).toBe(report1.hash)
|
|
75
|
+
expect(report1.valid).toBe(true)
|
|
76
|
+
expect(report2.valid).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
describe('report', () => {
|
|
79
|
+
describe('reports witnesses when supplied in', () => {
|
|
80
|
+
let archivistA: AbstractArchivist
|
|
81
|
+
let archivistB: AbstractArchivist
|
|
82
|
+
let witnessA: AbstractWitness
|
|
83
|
+
let witnessB: AbstractWitness
|
|
84
|
+
const assertSentinelReport = (sentinelReport: [XyoBoundWitness, XyoPayload[]]) => {
|
|
85
|
+
expect(sentinelReport).toBeArrayOfSize(2)
|
|
86
|
+
const [bw, payloads] = sentinelReport
|
|
87
|
+
expect(new BoundWitnessValidator(bw).validate()).toBeArrayOfSize(0)
|
|
88
|
+
expect(payloads).toBeArrayOfSize(2)
|
|
89
|
+
}
|
|
90
|
+
const assertArchivistStateMatchesSentinelReport = async (sentinelReport: [XyoBoundWitness, XyoPayload[]], archivists: Archivist[]) => {
|
|
91
|
+
const [, payloads] = sentinelReport
|
|
92
|
+
for (const archivist of archivists) {
|
|
93
|
+
const archivistPayloads = await archivist.all?.()
|
|
94
|
+
expect(archivistPayloads).toBeArrayOfSize(payloads.length + 1)
|
|
95
|
+
const sentinelPayloads = payloads.map((payload) => {
|
|
96
|
+
const wrapped = new PayloadWrapper(payload)
|
|
97
|
+
return { ...payload, _hash: wrapped.hash, _timestamp: expect.toBeNumber() }
|
|
98
|
+
})
|
|
99
|
+
expect(archivistPayloads).toContainValues(sentinelPayloads)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
beforeEach(async () => {
|
|
103
|
+
const paramsA = {
|
|
104
|
+
config: {
|
|
105
|
+
payload: { nonce: Math.floor(Math.random() * 9999999), schema: 'network.xyo.test' },
|
|
106
|
+
schema: XyoAdhocWitnessConfigSchema,
|
|
107
|
+
targetSchema: XyoPayloadSchema,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
const paramsB = {
|
|
111
|
+
config: {
|
|
112
|
+
payload: { nonce: Math.floor(Math.random() * 9999999), schema: 'network.xyo.test' },
|
|
113
|
+
schema: XyoAdhocWitnessConfigSchema,
|
|
114
|
+
targetSchema: XyoPayloadSchema,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
witnessA = await XyoAdhocWitness.create(paramsA)
|
|
118
|
+
witnessB = await XyoAdhocWitness.create(paramsB)
|
|
119
|
+
archivistA = await MemoryArchivist.create()
|
|
120
|
+
archivistB = await MemoryArchivist.create()
|
|
121
|
+
})
|
|
122
|
+
it('config', async () => {
|
|
123
|
+
const resolver = new SimpleModuleResolver()
|
|
124
|
+
resolver.add([witnessA, witnessB, archivistA, archivistB])
|
|
125
|
+
const params: ModuleParams<SentinelConfig> = {
|
|
126
|
+
config: {
|
|
127
|
+
archivists: [archivistA.address, archivistB.address],
|
|
128
|
+
schema: 'network.xyo.sentinel.config',
|
|
129
|
+
witnesses: [witnessA.address, witnessB.address],
|
|
130
|
+
},
|
|
131
|
+
resolver,
|
|
132
|
+
}
|
|
133
|
+
const sentinel = await Sentinel.create(params)
|
|
134
|
+
const result = await sentinel.report()
|
|
135
|
+
assertSentinelReport(result)
|
|
136
|
+
await assertArchivistStateMatchesSentinelReport(result, [archivistA, archivistB])
|
|
137
|
+
})
|
|
138
|
+
it('config & inline', async () => {
|
|
139
|
+
const resolver = new SimpleModuleResolver()
|
|
140
|
+
resolver.add([witnessA, archivistA, archivistB])
|
|
141
|
+
const params: ModuleParams<SentinelConfig> = {
|
|
142
|
+
config: {
|
|
143
|
+
archivists: [archivistA.address, archivistB.address],
|
|
144
|
+
schema: 'network.xyo.sentinel.config',
|
|
145
|
+
witnesses: [witnessA.address],
|
|
146
|
+
},
|
|
147
|
+
resolver,
|
|
148
|
+
}
|
|
149
|
+
const sentinel = await Sentinel.create(params)
|
|
150
|
+
const observed = await witnessB.observe()
|
|
151
|
+
expect(observed).toBeArrayOfSize(1)
|
|
152
|
+
const result = await sentinel.report(observed)
|
|
153
|
+
assertSentinelReport(result)
|
|
154
|
+
await assertArchivistStateMatchesSentinelReport(result, [archivistA, archivistB])
|
|
155
|
+
})
|
|
156
|
+
it('inline', async () => {
|
|
157
|
+
const resolver = new SimpleModuleResolver()
|
|
158
|
+
resolver.add([archivistA, archivistB])
|
|
159
|
+
const params: ModuleParams<SentinelConfig> = {
|
|
160
|
+
config: {
|
|
161
|
+
archivists: [archivistA.address, archivistB.address],
|
|
162
|
+
schema: 'network.xyo.sentinel.config',
|
|
163
|
+
witnesses: [],
|
|
164
|
+
},
|
|
165
|
+
resolver,
|
|
166
|
+
}
|
|
167
|
+
const sentinel = await Sentinel.create(params)
|
|
168
|
+
const observedA = await witnessA.observe()
|
|
169
|
+
expect(observedA).toBeArrayOfSize(1)
|
|
170
|
+
const observedB = await witnessB.observe()
|
|
171
|
+
expect(observedB).toBeArrayOfSize(1)
|
|
172
|
+
const result = await sentinel.report([...observedA, ...observedB])
|
|
173
|
+
assertSentinelReport(result)
|
|
174
|
+
expect((await archivistA.get([Hasher.hash(observedA)])).length).toBe(1)
|
|
175
|
+
expect((await archivistA.get([Hasher.hash(observedB)])).length).toBe(1)
|
|
176
|
+
expect((await archivistB.get([Hasher.hash(observedA)])).length).toBe(1)
|
|
177
|
+
expect((await archivistB.get([Hasher.hash(observedB)])).length).toBe(1)
|
|
178
|
+
await assertArchivistStateMatchesSentinelReport(result, [archivistA, archivistB])
|
|
179
|
+
})
|
|
180
|
+
it('reports errors', async () => {
|
|
181
|
+
const paramsA = {
|
|
182
|
+
config: {
|
|
183
|
+
payload: { nonce: Math.floor(Math.random() * 9999999), schema: 'network.xyo.test' },
|
|
184
|
+
schema: XyoAdhocWitnessConfigSchema,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
class FailingWitness extends XyoAdhocWitness {
|
|
188
|
+
override async observe(): Promise<XyoPayload[]> {
|
|
189
|
+
await Promise.reject(Error('observation failed'))
|
|
190
|
+
return [{ schema: 'fake.result' }]
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const witnessA = await FailingWitness.create(paramsA)
|
|
194
|
+
|
|
195
|
+
const resolver = new SimpleModuleResolver()
|
|
196
|
+
resolver.add([witnessA, witnessB, archivistA, archivistB])
|
|
197
|
+
const params: ModuleParams<SentinelConfig> = {
|
|
198
|
+
config: {
|
|
199
|
+
archivists: [archivistA.address, archivistB.address],
|
|
200
|
+
onReportEnd(_, errors) {
|
|
201
|
+
expect(errors?.length).toBe(1)
|
|
202
|
+
expect(errors?.[0]?.message).toBe('observation failed')
|
|
203
|
+
},
|
|
204
|
+
schema: 'network.xyo.sentinel.config',
|
|
205
|
+
witnesses: [witnessA.address, witnessB.address],
|
|
206
|
+
},
|
|
207
|
+
resolver,
|
|
208
|
+
}
|
|
209
|
+
const sentinel = await Sentinel.create(params)
|
|
210
|
+
await sentinel.report()
|
|
211
|
+
return
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { XyoBoundWitnessSchema } from '@xyo-network/boundwitness-model'
|
|
2
|
+
import { SimpleModuleResolver } from '@xyo-network/module'
|
|
3
|
+
import { IdSchema, IdWitness, IdWitnessConfigSchema } from '@xyo-network/plugins'
|
|
4
|
+
import { AbstractWitness } from '@xyo-network/witness'
|
|
5
|
+
|
|
6
|
+
import { SentinelIntervalAutomationPayload, AutomationSchema } from '../Automation'
|
|
7
|
+
import { Sentinel, SentinelConfig, SentinelConfigSchema } from '../Sentinel'
|
|
8
|
+
import { OnSentinelRunnerTriggerResult, SentinelRunner } from '../SentinelRunner'
|
|
9
|
+
|
|
10
|
+
describe('SentinelRunner', () => {
|
|
11
|
+
let sentinel: Sentinel
|
|
12
|
+
let config: SentinelConfig
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
const witnesses: AbstractWitness[] = [await IdWitness.create({ config: { salt: 'test', schema: IdWitnessConfigSchema } })]
|
|
16
|
+
const resolver = new SimpleModuleResolver()
|
|
17
|
+
witnesses.forEach((witness) => resolver.add(witness))
|
|
18
|
+
|
|
19
|
+
config = {
|
|
20
|
+
schema: SentinelConfigSchema,
|
|
21
|
+
witnesses: witnesses.map((witness) => witness.address),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
sentinel = await Sentinel.create({ config, resolver })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should output interval results', async () => {
|
|
28
|
+
const intervalAutomation: SentinelIntervalAutomationPayload = {
|
|
29
|
+
frequency: 1,
|
|
30
|
+
frequencyUnits: 'minute',
|
|
31
|
+
remaining: 1,
|
|
32
|
+
schema: AutomationSchema,
|
|
33
|
+
start: Date.now() - 1,
|
|
34
|
+
type: 'interval',
|
|
35
|
+
witnesses: config.witnesses,
|
|
36
|
+
}
|
|
37
|
+
const onTriggerResult: OnSentinelRunnerTriggerResult = (results) => {
|
|
38
|
+
expect(results.length).toBe(2)
|
|
39
|
+
expect(results[0]?.schema).toBe(XyoBoundWitnessSchema)
|
|
40
|
+
expect(results[1].length).toBe(1)
|
|
41
|
+
expect(results[1]?.[0].schema).toBe(IdSchema)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const runner = new SentinelRunner(sentinel, [intervalAutomation], onTriggerResult)
|
|
45
|
+
await runner.start()
|
|
46
|
+
})
|
|
47
|
+
})
|