@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.
Files changed (63) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +13 -0
  3. package/dist/cjs/Automation.js +5 -0
  4. package/dist/cjs/Automation.js.map +1 -0
  5. package/dist/cjs/Queries/Report.js +5 -0
  6. package/dist/cjs/Queries/Report.js.map +1 -0
  7. package/dist/cjs/Queries/index.js +5 -0
  8. package/dist/cjs/Queries/index.js.map +1 -0
  9. package/dist/cjs/Sentinel.js +134 -0
  10. package/dist/cjs/Sentinel.js.map +1 -0
  11. package/dist/cjs/SentinelIntervalAutomationWrapper.js +44 -0
  12. package/dist/cjs/SentinelIntervalAutomationWrapper.js.map +1 -0
  13. package/dist/cjs/SentinelModel.js +3 -0
  14. package/dist/cjs/SentinelModel.js.map +1 -0
  15. package/dist/cjs/SentinelRunner.js +102 -0
  16. package/dist/cjs/SentinelRunner.js.map +1 -0
  17. package/dist/cjs/index.js +9 -0
  18. package/dist/cjs/index.js.map +1 -0
  19. package/dist/docs.json +16084 -0
  20. package/dist/esm/Automation.js +2 -0
  21. package/dist/esm/Automation.js.map +1 -0
  22. package/dist/esm/Queries/Report.js +2 -0
  23. package/dist/esm/Queries/Report.js.map +1 -0
  24. package/dist/esm/Queries/index.js +2 -0
  25. package/dist/esm/Queries/index.js.map +1 -0
  26. package/dist/esm/Sentinel.js +102 -0
  27. package/dist/esm/Sentinel.js.map +1 -0
  28. package/dist/esm/SentinelIntervalAutomationWrapper.js +37 -0
  29. package/dist/esm/SentinelIntervalAutomationWrapper.js.map +1 -0
  30. package/dist/esm/SentinelModel.js +2 -0
  31. package/dist/esm/SentinelModel.js.map +1 -0
  32. package/dist/esm/SentinelRunner.js +86 -0
  33. package/dist/esm/SentinelRunner.js.map +1 -0
  34. package/dist/esm/index.js +6 -0
  35. package/dist/esm/index.js.map +1 -0
  36. package/dist/types/Automation.d.ts +28 -0
  37. package/dist/types/Automation.d.ts.map +1 -0
  38. package/dist/types/Queries/Report.d.ts +7 -0
  39. package/dist/types/Queries/Report.d.ts.map +1 -0
  40. package/dist/types/Queries/index.d.ts +6 -0
  41. package/dist/types/Queries/index.d.ts.map +1 -0
  42. package/dist/types/Sentinel.d.ts +35 -0
  43. package/dist/types/Sentinel.d.ts.map +1 -0
  44. package/dist/types/SentinelIntervalAutomationWrapper.d.ts +10 -0
  45. package/dist/types/SentinelIntervalAutomationWrapper.d.ts.map +1 -0
  46. package/dist/types/SentinelModel.d.ts +11 -0
  47. package/dist/types/SentinelModel.d.ts.map +1 -0
  48. package/dist/types/SentinelRunner.d.ts +25 -0
  49. package/dist/types/SentinelRunner.d.ts.map +1 -0
  50. package/dist/types/index.d.ts +6 -0
  51. package/dist/types/index.d.ts.map +1 -0
  52. package/package.json +67 -0
  53. package/src/Automation.ts +39 -0
  54. package/src/Queries/Report.ts +8 -0
  55. package/src/Queries/index.ts +11 -0
  56. package/src/Sentinel.ts +141 -0
  57. package/src/SentinelIntervalAutomationWrapper.ts +45 -0
  58. package/src/SentinelModel.ts +11 -0
  59. package/src/SentinelRunner.ts +104 -0
  60. package/src/index.ts +5 -0
  61. package/src/spec/Sentinel.spec.ts +215 -0
  62. package/src/spec/SentinelRunner.spec.ts +47 -0
  63. package/typedoc.json +10 -0
@@ -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,5 @@
1
+ export * from './Automation'
2
+ export * from './Sentinel'
3
+ export * from './SentinelIntervalAutomationWrapper'
4
+ export * from './SentinelModel'
5
+ export * from './SentinelRunner'
@@ -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
+ })
package/typedoc.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "intentionallyNotExported": [
3
+ "XyoBoundWitness",
4
+ "XyoAccount",
5
+ "XyoArchivistApi",
6
+ "XyoWitness",
7
+ "XyoPayloadFull",
8
+ "XyoWitnessConfig"
9
+ ]
10
+ }