@xyo-network/diviner-temporal-indexing-memory 4.2.1 → 5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xyo-network/diviner-temporal-indexing-memory",
3
- "version": "4.2.1",
3
+ "version": "5.0.0",
4
4
  "description": "Primary SDK for using XYO Protocol 2.0",
5
5
  "homepage": "https://xyo.network",
6
6
  "bugs": {
@@ -28,45 +28,49 @@
28
28
  },
29
29
  "module": "dist/neutral/index.mjs",
30
30
  "types": "dist/neutral/index.d.ts",
31
+ "files": [
32
+ "dist",
33
+ "src"
34
+ ],
31
35
  "dependencies": {
32
- "@xylabs/array": "^4.14.1",
33
- "@xylabs/assert": "^4.14.1",
34
- "@xylabs/exists": "^4.14.1",
35
- "@xyo-network/archivist-model": "^4.2.1",
36
- "@xyo-network/archivist-wrapper": "^4.2.1",
37
- "@xyo-network/boundwitness-model": "^4.2.1",
38
- "@xyo-network/boundwitness-validator": "^4.2.1",
39
- "@xyo-network/diviner-abstract": "^4.2.1",
40
- "@xyo-network/diviner-boundwitness-abstract": "^4.2.1",
41
- "@xyo-network/diviner-boundwitness-model": "^4.2.1",
42
- "@xyo-network/diviner-indexing-memory": "^4.2.1",
43
- "@xyo-network/diviner-indexing-model": "^4.2.1",
44
- "@xyo-network/diviner-jsonpath-aggregate-memory": "^4.2.1",
45
- "@xyo-network/diviner-jsonpath-model": "^4.2.1",
46
- "@xyo-network/diviner-model": "^4.2.1",
47
- "@xyo-network/diviner-payload-model": "^4.2.1",
48
- "@xyo-network/diviner-temporal-indexing-model": "^4.2.1",
49
- "@xyo-network/diviner-wrapper": "^4.2.1",
50
- "@xyo-network/module-model": "^4.2.1",
51
- "@xyo-network/payload-builder": "^4.2.1",
52
- "@xyo-network/payload-model": "^4.2.1",
53
- "@xyo-network/payload-utils": "^4.2.1",
54
- "@xyo-network/witness-timestamp": "^4.2.1"
36
+ "@xylabs/array": "^5.0.0",
37
+ "@xylabs/assert": "^5.0.0",
38
+ "@xylabs/exists": "^5.0.0",
39
+ "@xyo-network/archivist-model": "^5.0.0",
40
+ "@xyo-network/archivist-wrapper": "^5.0.0",
41
+ "@xyo-network/boundwitness-model": "^5.0.0",
42
+ "@xyo-network/boundwitness-validator": "^5.0.0",
43
+ "@xyo-network/diviner-abstract": "^5.0.0",
44
+ "@xyo-network/diviner-boundwitness-abstract": "^5.0.0",
45
+ "@xyo-network/diviner-boundwitness-model": "^5.0.0",
46
+ "@xyo-network/diviner-indexing-memory": "^5.0.0",
47
+ "@xyo-network/diviner-indexing-model": "^5.0.0",
48
+ "@xyo-network/diviner-jsonpath-aggregate-memory": "^5.0.0",
49
+ "@xyo-network/diviner-jsonpath-model": "^5.0.0",
50
+ "@xyo-network/diviner-model": "^5.0.0",
51
+ "@xyo-network/diviner-payload-model": "^5.0.0",
52
+ "@xyo-network/diviner-temporal-indexing-model": "^5.0.0",
53
+ "@xyo-network/diviner-wrapper": "^5.0.0",
54
+ "@xyo-network/module-model": "^5.0.0",
55
+ "@xyo-network/payload-builder": "^5.0.0",
56
+ "@xyo-network/payload-model": "^5.0.0",
57
+ "@xyo-network/payload-utils": "^5.0.0",
58
+ "@xyo-network/witness-timestamp": "^5.0.0"
55
59
  },
56
60
  "devDependencies": {
57
- "@xylabs/delay": "^4.14.1",
58
- "@xylabs/hex": "^4.14.1",
59
- "@xylabs/object": "^4.14.1",
60
- "@xylabs/promise": "^4.14.1",
61
- "@xylabs/ts-scripts-yarn3": "^7.0.1",
62
- "@xylabs/tsconfig": "^7.0.1",
63
- "@xylabs/vitest-extended": "^4.14.1",
64
- "@xyo-network/archivist-memory": "^4.2.1",
65
- "@xyo-network/boundwitness-builder": "^4.2.1",
66
- "@xyo-network/manifest": "^4.2.1",
67
- "@xyo-network/module-factory-locator": "^4.2.1",
68
- "@xyo-network/node-memory": "^4.2.1",
69
- "@xyo-network/wallet": "^4.2.1",
61
+ "@xylabs/delay": "^5.0.0",
62
+ "@xylabs/hex": "^5.0.0",
63
+ "@xylabs/object": "^5.0.0",
64
+ "@xylabs/promise": "^5.0.0",
65
+ "@xylabs/ts-scripts-yarn3": "^7.0.2",
66
+ "@xylabs/tsconfig": "^7.0.2",
67
+ "@xylabs/vitest-extended": "^5.0.0",
68
+ "@xyo-network/archivist-memory": "^5.0.0",
69
+ "@xyo-network/boundwitness-builder": "^5.0.0",
70
+ "@xyo-network/manifest": "^5.0.0",
71
+ "@xyo-network/module-factory-locator": "^5.0.0",
72
+ "@xyo-network/node-memory": "^5.0.0",
73
+ "@xyo-network/wallet": "^5.0.0",
70
74
  "typescript": "^5.8.3",
71
75
  "vitest": "^3.2.4"
72
76
  },
@@ -0,0 +1,244 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import type { SchemaToJsonPathTransformExpressionsDictionary } from '@xyo-network/diviner-jsonpath-model'
4
+ import type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
5
+ import { isPayloadDivinerQueryPayload, PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model'
6
+ import type { TemporalIndexingDivinerDivinerQueryToIndexQueryDivinerConfig } from '@xyo-network/diviner-temporal-indexing-model'
7
+ import {
8
+ TemporalIndexingDivinerDivinerQueryToIndexQueryDivinerConfigSchema,
9
+ TemporalIndexingDivinerResultIndexSchema,
10
+ } from '@xyo-network/diviner-temporal-indexing-model'
11
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
12
+ import type { Payload } from '@xyo-network/payload-model'
13
+ import { isPayloadOfSchemaType } from '@xyo-network/payload-model'
14
+ import {
15
+ beforeAll,
16
+ describe, expect, it,
17
+ } from 'vitest'
18
+
19
+ import { TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner } from '../Diviner.ts'
20
+
21
+ type QueryType = PayloadDivinerQueryPayload<{ status?: number; success?: boolean; url: string }>
22
+
23
+ describe('TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner', () => {
24
+ const url = 'https://xyo.network'
25
+ let diviner: TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner
26
+ describe('divine', () => {
27
+ describe('with with no config specified', () => {
28
+ const queries: QueryType[] = [
29
+ {
30
+ schema: PayloadDivinerQuerySchema,
31
+ url,
32
+ },
33
+ {
34
+ limit: 10,
35
+ order: 'asc',
36
+ schema: PayloadDivinerQuerySchema,
37
+ },
38
+ ]
39
+ const expected: PayloadDivinerQueryPayload[] = [
40
+ {
41
+ limit: 1,
42
+ order: 'desc',
43
+ schema: 'network.xyo.diviner.payload.query',
44
+ schemas: [TemporalIndexingDivinerResultIndexSchema],
45
+ } as unknown as PayloadDivinerQueryPayload,
46
+ {
47
+ limit: 10,
48
+ order: 'asc',
49
+ schema: 'network.xyo.diviner.payload.query',
50
+ schemas: [TemporalIndexingDivinerResultIndexSchema],
51
+ } as unknown as PayloadDivinerQueryPayload,
52
+ ]
53
+ beforeAll(async () => {
54
+ diviner = await TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner.create({ account: 'random' })
55
+ })
56
+ const cases: [QueryType, PayloadDivinerQueryPayload][] = queries.map((query, i) => [query, expected[i]])
57
+ describe('with single query', () => {
58
+ it.each(cases)('transforms query using default settings', async (query, expected) => {
59
+ const results = await diviner.divine([query])
60
+ const actual = results.filter(isPayloadDivinerQueryPayload)
61
+ expect(actual).toBeArrayOfSize(1)
62
+ expect(PayloadBuilder.omitMeta(actual[0])).toEqual(PayloadBuilder.omitMeta(expected))
63
+ })
64
+ })
65
+ describe('with multiple queries', () => {
66
+ it('transforms queries using default settings', async () => {
67
+ const results = await diviner.divine(queries)
68
+ const actual = results.filter(isPayloadDivinerQueryPayload)
69
+ expect(actual).toBeArrayOfSize(expected.length)
70
+ expect(actual.map(i => PayloadBuilder.omitMeta(i))).toEqual(expected.map(i => PayloadBuilder.omitMeta(i)))
71
+ })
72
+ })
73
+ })
74
+ describe('with config fields specified', () => {
75
+ const divinerQuerySchema = 'network.xyo.test.source.query'
76
+ const indexQuerySchema = 'network.xyo.test.destination.query'
77
+ const indexSchema = 'network.xyo.test.index.schema'
78
+ const queries = [
79
+ {
80
+ schema: divinerQuerySchema,
81
+ url,
82
+ },
83
+ {
84
+ limit: 10,
85
+ order: 'asc',
86
+ schema: divinerQuerySchema,
87
+ status: 200,
88
+ success: true,
89
+ url,
90
+ },
91
+ {
92
+ limit: 10,
93
+ schema: divinerQuerySchema,
94
+ url,
95
+ },
96
+ {
97
+ schema: divinerQuerySchema,
98
+ url,
99
+ },
100
+ {
101
+ order: 'asc',
102
+ schema: divinerQuerySchema,
103
+ url,
104
+ },
105
+ {
106
+ schema: divinerQuerySchema,
107
+ status: 200,
108
+ url,
109
+ },
110
+ {
111
+ schema: divinerQuerySchema,
112
+ success: true,
113
+ url,
114
+ },
115
+ {
116
+ schema: divinerQuerySchema,
117
+ success: false,
118
+ url,
119
+ },
120
+ ]
121
+ const expected = [
122
+ {
123
+ limit: 1,
124
+ order: 'desc',
125
+ schema: indexQuerySchema,
126
+ schemas: [indexSchema],
127
+ url,
128
+ },
129
+ {
130
+ limit: 10,
131
+ order: 'asc',
132
+ schema: indexQuerySchema,
133
+ schemas: [indexSchema],
134
+ status: 200,
135
+ success: true,
136
+ url,
137
+ },
138
+ {
139
+ limit: 10,
140
+ order: 'desc',
141
+ schema: indexQuerySchema,
142
+ schemas: [indexSchema],
143
+ url,
144
+ },
145
+ {
146
+ limit: 1,
147
+ order: 'desc',
148
+ schema: indexQuerySchema,
149
+ schemas: [indexSchema],
150
+ url,
151
+ },
152
+ {
153
+ limit: 1,
154
+ order: 'asc',
155
+ schema: indexQuerySchema,
156
+ schemas: [indexSchema],
157
+ url,
158
+ },
159
+ {
160
+ limit: 1,
161
+ order: 'desc',
162
+ schema: indexQuerySchema,
163
+ schemas: [indexSchema],
164
+ status: 200,
165
+ url,
166
+ },
167
+ {
168
+ limit: 1,
169
+ order: 'desc',
170
+ schema: indexQuerySchema,
171
+ schemas: [indexSchema],
172
+ success: true,
173
+ url,
174
+ },
175
+ {
176
+ limit: 1,
177
+ order: 'desc',
178
+ schema: indexQuerySchema,
179
+ schemas: [indexSchema],
180
+ success: false,
181
+ url,
182
+ },
183
+ ]
184
+ const cases: [Payload, Payload][] = queries.map((query, i) => [query, expected[i]])
185
+ const schemaTransforms: SchemaToJsonPathTransformExpressionsDictionary = {
186
+ [divinerQuerySchema]: [
187
+ {
188
+ destinationField: 'url',
189
+ sourcePathExpression: '$.url',
190
+ },
191
+ {
192
+ defaultValue: 1,
193
+ destinationField: 'limit',
194
+ sourcePathExpression: '$.limit',
195
+ },
196
+ {
197
+ // defaultValue: 0,
198
+ destinationField: 'cursor',
199
+ sourcePathExpression: '$.cursor',
200
+ },
201
+ {
202
+ defaultValue: 'desc',
203
+ destinationField: 'order',
204
+ sourcePathExpression: '$.order',
205
+ },
206
+ {
207
+ destinationField: 'status',
208
+ sourcePathExpression: '$.status',
209
+ },
210
+ {
211
+ destinationField: 'success',
212
+ sourcePathExpression: '$.success',
213
+ },
214
+ ],
215
+ }
216
+ const config: TemporalIndexingDivinerDivinerQueryToIndexQueryDivinerConfig = {
217
+ divinerQuerySchema,
218
+ indexQuerySchema,
219
+ indexSchema,
220
+ schema: TemporalIndexingDivinerDivinerQueryToIndexQueryDivinerConfigSchema,
221
+ schemaTransforms,
222
+ }
223
+ beforeAll(async () => {
224
+ diviner = await TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner.create({ account: 'random', config })
225
+ })
226
+ describe('with single query', () => {
227
+ it.each(cases)('transforms query using default settings', async (query, expected) => {
228
+ const results = await diviner.divine([query])
229
+ const actual = results.filter(isPayloadOfSchemaType<Payload>(indexQuerySchema))
230
+ expect(actual).toBeArrayOfSize(1)
231
+ expect(PayloadBuilder.omitMeta(actual[0])).toEqual(PayloadBuilder.omitMeta(expected))
232
+ })
233
+ })
234
+ describe('with multiple queries', () => {
235
+ it('transforms queries using default settings', async () => {
236
+ const results = await diviner.divine(queries)
237
+ const actual = results.filter(isPayloadOfSchemaType<Payload>(indexQuerySchema))
238
+ expect(actual).toBeArrayOfSize(expected.length)
239
+ expect(actual.map(i => PayloadBuilder.omitMeta(i))).toEqual(expected.map(i => PayloadBuilder.omitMeta(i)))
240
+ })
241
+ })
242
+ })
243
+ })
244
+ })
@@ -0,0 +1,247 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
4
+ import type { BoundWitness } from '@xyo-network/boundwitness-model'
5
+ import type { SchemaToJsonPathTransformExpressionsDictionary } from '@xyo-network/diviner-jsonpath-model'
6
+ import { isTemporalIndexingDivinerResultIndex } from '@xyo-network/diviner-temporal-indexing-model'
7
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
8
+ import type { Payload } from '@xyo-network/payload-model'
9
+ import type { TimeStamp } from '@xyo-network/witness-timestamp'
10
+ import { TimestampSchema } from '@xyo-network/witness-timestamp'
11
+ import {
12
+ beforeAll,
13
+ describe, expect, it,
14
+ } from 'vitest'
15
+
16
+ import { TemporalIndexingDivinerIndexCandidateToIndexDiviner } from '../Diviner.ts'
17
+
18
+ type ImageThumbnail = Payload<{
19
+ http?: {
20
+ ipAddress?: string
21
+ status?: number
22
+ }
23
+ // schema: 'network.xyo.image.thumbnail'
24
+ sourceUrl: string
25
+ url?: string
26
+ }>
27
+
28
+ describe('TemporalIndexCandidateToImageThumbnailIndexDiviner', () => {
29
+ describe('divine', () => {
30
+ const timestampA = 1_234_567_890
31
+ const timestampPayloadA: TimeStamp = { schema: TimestampSchema, timestamp: timestampA }
32
+ const imageThumbnailPayloadA: ImageThumbnail = {
33
+ http: { status: 200 },
34
+ schema: 'network.xyo.image.thumbnail',
35
+ sourceUrl: 'https://xyo.network',
36
+ url: 'data',
37
+ }
38
+ const timestampB = 1_234_567_891
39
+ const timestampPayloadB: TimeStamp = { schema: TimestampSchema, timestamp: timestampB }
40
+ const imageThumbnailPayloadB: ImageThumbnail = {
41
+ http: { status: 500 },
42
+ schema: 'network.xyo.image.thumbnail',
43
+ sourceUrl: 'https://xyo.network',
44
+ }
45
+ const timestampC = 1_234_567_892
46
+ const timestampPayloadC: TimeStamp = { schema: TimestampSchema, timestamp: timestampC }
47
+ const imageThumbnailPayloadC: ImageThumbnail = {
48
+
49
+ http: { ipAddress: '192.169.1.1' },
50
+ schema: 'network.xyo.image.thumbnail',
51
+ sourceUrl: 'https://www.google.com',
52
+ }
53
+ describe('with single schema transform', () => {
54
+ const validateSingleResult = async (
55
+ input: [boundWitness: BoundWitness, timestamp: TimeStamp, thumbnail: ImageThumbnail],
56
+ result: Payload[],
57
+ ) => {
58
+ const [boundWitness, timestamp, thumbnail] = input
59
+ const payloadDictionary = await PayloadBuilder.toHashMap([boundWitness, timestamp, thumbnail])
60
+ expect(result).toBeArrayOfSize(1)
61
+ expect(result.filter(isTemporalIndexingDivinerResultIndex)).toBeArrayOfSize(1)
62
+ const index = result.find(isTemporalIndexingDivinerResultIndex)
63
+ // eslint-disable-next-line sonarjs/no-alphabetical-sort
64
+ expect(index?.$sources.sort()).toEqual(Object.keys(payloadDictionary).toSorted())
65
+ expect(index?.timestamp).toBe(timestamp.timestamp)
66
+ expect((index as { url?: string })?.url).toBe(thumbnail.sourceUrl)
67
+ expect((index as { status?: number })?.status).toBe(thumbnail.http?.status)
68
+ }
69
+ beforeAll(async () => {
70
+ diviner = await TemporalIndexingDivinerIndexCandidateToIndexDiviner.create({
71
+ account: 'random',
72
+ config: {
73
+ schema: TemporalIndexingDivinerIndexCandidateToIndexDiviner.defaultConfigSchema,
74
+ schemaTransforms,
75
+ },
76
+ })
77
+ })
78
+ const schemaTransforms: SchemaToJsonPathTransformExpressionsDictionary = {
79
+ 'network.xyo.image.thumbnail': [
80
+ { destinationField: 'url', sourcePathExpression: '$.sourceUrl' },
81
+ { destinationField: 'status', sourcePathExpression: '$.http.status' },
82
+ ],
83
+ 'network.xyo.timestamp': [{ destinationField: 'timestamp', sourcePathExpression: '$.timestamp' }],
84
+ }
85
+ let diviner: TemporalIndexingDivinerIndexCandidateToIndexDiviner
86
+
87
+ const cases: [TimeStamp, ImageThumbnail][] = [
88
+ [timestampPayloadA, imageThumbnailPayloadA],
89
+ [timestampPayloadB, imageThumbnailPayloadB],
90
+ [timestampPayloadC, imageThumbnailPayloadC],
91
+ ]
92
+ describe('with single result', () => {
93
+ it.each(cases)('transforms single result', async (timestamp, thumbnail) => {
94
+ const [boundWitness] = await new BoundWitnessBuilder().payloads([timestamp, thumbnail]).build()
95
+ const result = await diviner.divine([boundWitness, timestamp, thumbnail])
96
+ await validateSingleResult([boundWitness, timestamp, thumbnail], result)
97
+ })
98
+ it('transforms BW with multiple results inside', async () => {
99
+ const payloads = cases.flat()
100
+ const [boundWitness] = await new BoundWitnessBuilder().payloads(payloads).build()
101
+ const results = await diviner.divine([boundWitness, ...payloads])
102
+ expect(results).toBeArrayOfSize(Math.pow(cases.length, cases[0].length))
103
+ let resultIndex = 0
104
+ for (let i = 0; i < cases.length; i++) {
105
+ const thumbnail = cases[i][1]
106
+ // eslint-disable-next-line unicorn/no-for-loop
107
+ for (let j = 0; j < cases.length; j++) {
108
+ const timestamp = cases[j][0]
109
+ const result = results[resultIndex]
110
+ await validateSingleResult([boundWitness, timestamp, thumbnail], [result])
111
+ resultIndex++
112
+ }
113
+ }
114
+ })
115
+ it.each(cases)('handles sparse inputs', async (thumbnail, timestamp) => {
116
+ const [boundWitness] = await new BoundWitnessBuilder().payloads([timestamp, thumbnail]).build()
117
+ expect(await diviner.divine([thumbnail, timestamp])).toBeArrayOfSize(0)
118
+ expect(await diviner.divine([boundWitness, timestamp])).toBeArrayOfSize(0)
119
+ expect(await diviner.divine([boundWitness, thumbnail])).toBeArrayOfSize(0)
120
+ })
121
+ })
122
+ describe('with multiple results', () => {
123
+ it('transforms multiple results', async () => {
124
+ const data: [BoundWitness, TimeStamp, ImageThumbnail][] = await Promise.all(
125
+ cases.map(async (payloads) => {
126
+ const [bw] = await new BoundWitnessBuilder().payloads(payloads).build()
127
+ return [bw, ...payloads]
128
+ }),
129
+ )
130
+ const results = await diviner.divine(data.flat())
131
+ expect(results).toBeArrayOfSize(cases.length)
132
+
133
+ await Promise.all(
134
+ data.map(async (input, i) => {
135
+ const result = results[i]
136
+ await validateSingleResult(input, [result])
137
+ }),
138
+ )
139
+ })
140
+ it('handles sparse inputs', async () => {
141
+ const [bw] = await new BoundWitnessBuilder().payloads(cases[0]).build()
142
+ const results = await diviner.divine([bw, ...cases.flat()])
143
+ expect(results).toBeArrayOfSize(1)
144
+ await validateSingleResult([bw, ...cases[0]], results)
145
+ })
146
+ it('handles missing inputs', async () => {
147
+ const [bw] = await new BoundWitnessBuilder().payloads([...cases[0]]).build()
148
+ const results = await diviner.divine([bw, ...cases[0].slice(0, -1)])
149
+ expect(results).toBeArrayOfSize(0)
150
+ })
151
+ })
152
+ })
153
+ describe('with multiple schema transforms', () => {
154
+ const validateMultiResult = async (
155
+ input: [boundWitness: BoundWitness, timestamp: TimeStamp, thumbnail: ImageThumbnail, payload: Payload],
156
+ result: Payload[],
157
+ ) => {
158
+ const [boundWitness, timestamp, thumbnail, payload] = input
159
+ const payloadDictionary = await PayloadBuilder.toHashMap([boundWitness, timestamp, thumbnail, payload])
160
+ expect(result).toBeArrayOfSize(1)
161
+ expect(result.filter(isTemporalIndexingDivinerResultIndex)).toBeArrayOfSize(1)
162
+ const index = result.find(isTemporalIndexingDivinerResultIndex)
163
+ // eslint-disable-next-line sonarjs/no-alphabetical-sort
164
+ expect(index?.$sources.sort()).toEqual(Object.keys(payloadDictionary).toSorted())
165
+ expect(index?.timestamp).toBe(timestamp.timestamp)
166
+ expect((index as { url?: string })?.url).toBe((payload as { sourceUrl?: string }).sourceUrl)
167
+ expect((index as { status?: number })?.status).toBe(thumbnail.http?.status)
168
+ }
169
+ beforeAll(async () => {
170
+ diviner = await TemporalIndexingDivinerIndexCandidateToIndexDiviner.create({
171
+ account: 'random',
172
+ config: {
173
+ schema: TemporalIndexingDivinerIndexCandidateToIndexDiviner.defaultConfigSchema,
174
+ schemaTransforms,
175
+ },
176
+ })
177
+ })
178
+ const schemaTransforms: SchemaToJsonPathTransformExpressionsDictionary = {
179
+ 'network.xyo.image.thumbnail': [{ destinationField: 'status', sourcePathExpression: '$.http.status' }],
180
+ 'network.xyo.image.thumbnail.other': [{ destinationField: 'url', sourcePathExpression: '$.sourceUrl' }],
181
+ 'network.xyo.timestamp': [{ destinationField: 'timestamp', sourcePathExpression: '$.timestamp' }],
182
+ }
183
+ let diviner: TemporalIndexingDivinerIndexCandidateToIndexDiviner
184
+
185
+ const cases: [TimeStamp, ImageThumbnail, Payload<{ sourceUrl: string }>][] = [
186
+ [
187
+ timestampPayloadA,
188
+ { ...imageThumbnailPayloadA, sourceUrl: '' },
189
+ { schema: 'network.xyo.image.thumbnail.other', sourceUrl: imageThumbnailPayloadA.sourceUrl },
190
+ ],
191
+ [
192
+ timestampPayloadB,
193
+ { ...imageThumbnailPayloadB, sourceUrl: '' },
194
+ { schema: 'network.xyo.image.thumbnail.other', sourceUrl: imageThumbnailPayloadB.sourceUrl },
195
+ ],
196
+ [
197
+ timestampPayloadC,
198
+ { ...imageThumbnailPayloadC, sourceUrl: '' },
199
+ { schema: 'network.xyo.image.thumbnail.other', sourceUrl: imageThumbnailPayloadC.sourceUrl },
200
+ ],
201
+ ]
202
+ describe('with single result', () => {
203
+ it.each(cases)('transforms single result', async (timestamp, thumbnail, payload) => {
204
+ const [boundWitness] = await new BoundWitnessBuilder().payloads([timestamp, thumbnail, payload]).build()
205
+ const result = await diviner.divine([boundWitness, timestamp, thumbnail, payload])
206
+ await validateMultiResult([boundWitness, timestamp, thumbnail, payload], result)
207
+ })
208
+ it.each(cases)('handles sparse inputs', async (thumbnail, timestamp, payload) => {
209
+ const [boundWitness] = await new BoundWitnessBuilder().payloads([timestamp, thumbnail, payload]).build()
210
+ expect(await diviner.divine([thumbnail, timestamp])).toBeArrayOfSize(0)
211
+ expect(await diviner.divine([boundWitness, timestamp])).toBeArrayOfSize(0)
212
+ expect(await diviner.divine([boundWitness, thumbnail])).toBeArrayOfSize(0)
213
+ })
214
+ })
215
+ describe('with multiple results', () => {
216
+ it('transforms multiple results', async () => {
217
+ const data: [BoundWitness, TimeStamp, ImageThumbnail, Payload][] = await Promise.all(
218
+ cases.map(async (payloads) => {
219
+ const [bw] = await new BoundWitnessBuilder().payloads(payloads).build()
220
+ return [bw, ...payloads]
221
+ }),
222
+ )
223
+ const results = await diviner.divine(data.flat())
224
+ expect(results).toBeArrayOfSize(cases.length)
225
+
226
+ await Promise.all(
227
+ data.map(async (input, i) => {
228
+ const result = results[i]
229
+ await validateMultiResult(input, [result])
230
+ }),
231
+ )
232
+ })
233
+ it('handles sparse inputs', async () => {
234
+ const [bw] = await new BoundWitnessBuilder().payloads(cases[0]).build()
235
+ const results = await diviner.divine([bw, ...cases.flat()])
236
+ expect(results).toBeArrayOfSize(1)
237
+ await validateMultiResult([bw, ...cases[0]], results)
238
+ })
239
+ it('handles missing inputs', async () => {
240
+ const [bw] = await new BoundWitnessBuilder().payloads([...cases[0]]).build()
241
+ const results = await diviner.divine([bw, ...cases[0].slice(0, -1)])
242
+ expect(results).toBeArrayOfSize(0)
243
+ })
244
+ })
245
+ })
246
+ })
247
+ })
@@ -0,0 +1,103 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
4
+ import { PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model'
5
+ import type { Payload } from '@xyo-network/payload-model'
6
+ import {
7
+ beforeAll,
8
+ describe, expect, it,
9
+ } from 'vitest'
10
+
11
+ import { TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner } from '../Diviner.ts'
12
+
13
+ type QueryType = Payload<PayloadDivinerQueryPayload & Payload<{ status?: number; success?: boolean; url: string }>>
14
+
15
+ describe('TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner', () => {
16
+ const url = 'https://xyo.network'
17
+ const queries: QueryType[] = [
18
+ {
19
+ schema: PayloadDivinerQuerySchema,
20
+ url,
21
+ },
22
+ {
23
+ schema: PayloadDivinerQuerySchema,
24
+ url,
25
+ },
26
+ ]
27
+ const indexes = [
28
+ [
29
+ {
30
+ schema: 'TODO',
31
+ sources: [],
32
+ status: 200,
33
+ success: true,
34
+ timestamp: 1_234_567_890,
35
+ url,
36
+ },
37
+ ],
38
+ [
39
+ {
40
+ schema: 'TODO',
41
+ sources: [],
42
+ status: 200,
43
+ success: true,
44
+ timestamp: 1_234_567_891,
45
+ url,
46
+ },
47
+ {
48
+ schema: 'TODO',
49
+ sources: [],
50
+ status: 500,
51
+ success: false,
52
+ timestamp: 1_234_567_892,
53
+ url,
54
+ },
55
+ ],
56
+ ]
57
+ let diviner: TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner
58
+ beforeAll(async () => {
59
+ diviner = await TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner.create({ account: 'random' })
60
+ })
61
+ const cases: [QueryType, Payload[]][] = queries.map((query, i) => [query, indexes[i]])
62
+ describe('divine', () => {
63
+ describe('with single url in index result', () => {
64
+ it.each(cases)('transforms single url index results', async (imageThumbnailDivinerQuery, imageThumbnailResultIndex) => {
65
+ const results = await diviner.divine([imageThumbnailDivinerQuery, ...imageThumbnailResultIndex])
66
+ expect(results).toBeArrayOfSize(imageThumbnailResultIndex.length)
67
+ expect(results).toBeArrayOfSize(imageThumbnailResultIndex.length)
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-array-for-each
69
+ results.forEach((result: any, i) => {
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ const index = imageThumbnailResultIndex[i] as any
72
+ expect(result.url).toBe(imageThumbnailDivinerQuery.url)
73
+ expect(result.success).toBe(index.success)
74
+ expect(result.timestamp).toBe(index.timestamp)
75
+ expect(result.status).toBe(index.status)
76
+ expect(result.schema).toBe('TODO')
77
+ })
78
+ })
79
+ })
80
+ describe('with multiple urls in index result', () => {
81
+ it('transforms multiple url index results', async () => {
82
+ const indexesLength = indexes.flat().length
83
+ const results = await diviner.divine([...queries, ...indexes.flat()])
84
+ expect(results).toBeArrayOfSize(indexesLength)
85
+ expect(results).toBeArrayOfSize(indexesLength)
86
+ let resultsIterator = 0
87
+ for (const [i, { url }] of queries.entries()) {
88
+ const indexSet = indexes[i]
89
+ for (const index of indexSet) {
90
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ const result = results[resultsIterator] as any
92
+ expect(result.url).toBe(url)
93
+ expect(result.success).toBe(index.success)
94
+ expect(result.timestamp).toBe(index.timestamp)
95
+ expect(result.status).toBe(index.status)
96
+ expect(result.schema).toBe('TODO')
97
+ resultsIterator = ++resultsIterator
98
+ }
99
+ }
100
+ })
101
+ })
102
+ })
103
+ })
@@ -0,0 +1,167 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { filterAs } from '@xylabs/array'
4
+ import { assertEx } from '@xylabs/assert'
5
+ import { delay } from '@xylabs/delay'
6
+ import type { MemoryArchivist } from '@xyo-network/archivist-memory'
7
+ import { asArchivistInstance } from '@xyo-network/archivist-model'
8
+ import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
9
+ import { isBoundWitness } from '@xyo-network/boundwitness-model'
10
+ import type { IndexingDivinerState } from '@xyo-network/diviner-indexing-model'
11
+ import { asDivinerInstance } from '@xyo-network/diviner-model'
12
+ import type { PackageManifestPayload } from '@xyo-network/manifest'
13
+ import { ManifestWrapper } from '@xyo-network/manifest'
14
+ import { ModuleFactoryLocator } from '@xyo-network/module-factory-locator'
15
+ import type { ModuleState } from '@xyo-network/module-model'
16
+ import { isModuleState, ModuleStateSchema } from '@xyo-network/module-model'
17
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
18
+ import type { Payload, WithStorageMeta } from '@xyo-network/payload-model'
19
+ import { asOptionalStorageMeta } from '@xyo-network/payload-model'
20
+ import { HDWallet } from '@xyo-network/wallet'
21
+ import type { TimeStamp } from '@xyo-network/witness-timestamp'
22
+ import { isTimestamp, TimestampSchema } from '@xyo-network/witness-timestamp'
23
+ import {
24
+ beforeAll, describe, expect, it,
25
+ } from 'vitest'
26
+
27
+ import { TemporalIndexingDivinerStateToIndexCandidateDiviner } from '../Diviner.ts'
28
+ import TemporalStateToIndexCandidateDivinerManifest from './TemporalStateToIndexCandidateDiviner.json' with { type: 'json' }
29
+
30
+ /**
31
+ * @group slow
32
+ */
33
+ describe('TemporalStateToIndexCandidateDiviner', () => {
34
+ const sourceUrl = 'https://placekitten.com/200/300'
35
+ const thumbnailHttpSuccess = {
36
+ http: { status: 200 },
37
+ schema: 'network.xyo.image.thumbnail',
38
+ sourceHash: '7f39363514d9d9b958a5a993edeba35cb44f912c7072ed9ddd628728ac0fd681',
39
+ sourceUrl,
40
+ url: 'data:image/png;base64,===',
41
+ }
42
+
43
+ const thumbnailHttpFail = {
44
+ http: {
45
+
46
+ ipAddress: '104.17.96.13',
47
+ status: 429,
48
+ },
49
+ schema: 'network.xyo.image.thumbnail',
50
+ sourceUrl,
51
+ }
52
+
53
+ const thumbnailCodeFail = {
54
+ http: { code: 'FAILED' },
55
+ schema: 'network.xyo.image.thumbnail',
56
+ sourceUrl,
57
+ }
58
+
59
+ const thumbnailWitnessFail = {
60
+
61
+ http: { ipAddress: '104.17.96.13' },
62
+ schema: 'network.xyo.image.thumbnail',
63
+ sourceUrl,
64
+ }
65
+
66
+ let testCases: WithStorageMeta<Payload>[][] = []
67
+ let archivist: MemoryArchivist
68
+ let sut: TemporalIndexingDivinerStateToIndexCandidateDiviner
69
+
70
+ beforeAll(async () => {
71
+ const wallet = await HDWallet.random()
72
+ const locator = new ModuleFactoryLocator()
73
+ locator.register(TemporalIndexingDivinerStateToIndexCandidateDiviner.factory())
74
+ const manifest = TemporalStateToIndexCandidateDivinerManifest as PackageManifestPayload
75
+ const manifestWrapper = new ManifestWrapper(manifest, wallet, locator)
76
+ const node = await manifestWrapper.loadNodeFromIndex(0)
77
+ await node.start()
78
+
79
+ const privateModules = manifest.nodes[0].modules?.private ?? []
80
+ const publicModules = manifest.nodes[0].modules?.public ?? []
81
+ const mods = await node.resolve('*')
82
+ expect(mods.length).toBe(privateModules.length + publicModules.length + 1)
83
+
84
+ // Insert previously witnessed payloads into thumbnail archivist
85
+ const httpSuccessTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: 1 }
86
+ const [httpSuccessBoundWitness, httpSuccessPayloads] = await new BoundWitnessBuilder()
87
+ .payloads([thumbnailHttpSuccess, httpSuccessTimestamp])
88
+ .build()
89
+ const httpFailTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: 2 }
90
+ const [httpFailBoundWitness, httpFailPayloads] = await (new BoundWitnessBuilder().payloads([thumbnailHttpFail, httpFailTimestamp])).build()
91
+
92
+ const witnessFailTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: 3 }
93
+ const [witnessFailBoundWitness, witnessFailPayloads] = await new BoundWitnessBuilder()
94
+ .payloads([thumbnailWitnessFail, witnessFailTimestamp])
95
+ .build()
96
+
97
+ const codeFailTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: 4 }
98
+ const [codeFailBoundWitness, codeFailPayloads] = await (new BoundWitnessBuilder().payloads([thumbnailCodeFail, codeFailTimestamp])).build()
99
+
100
+ archivist = assertEx(asArchivistInstance<MemoryArchivist>(await node.resolve('ImageThumbnailArchivist')))
101
+ const testCasesToCreate = [
102
+ [httpSuccessBoundWitness, ...httpSuccessPayloads],
103
+ [httpFailBoundWitness, ...httpFailPayloads],
104
+ [witnessFailBoundWitness, ...witnessFailPayloads],
105
+ [codeFailBoundWitness, ...codeFailPayloads],
106
+ ]
107
+
108
+ for (const [bw, ...payloads] of testCasesToCreate) {
109
+ const createdTestCase = []
110
+ for (const payload of [bw, ...payloads]) {
111
+ await delay(2)
112
+ const [signedPayload] = await archivist.insert([payload])
113
+ createdTestCase.push(signedPayload)
114
+ }
115
+ testCases.push(createdTestCase)
116
+ }
117
+
118
+ sut = assertEx(
119
+ asDivinerInstance(await node.resolve('TemporalStateToIndexCandidateDiviner')),
120
+ ) as TemporalIndexingDivinerStateToIndexCandidateDiviner
121
+ })
122
+
123
+ describe('divine', () => {
124
+ describe('with no previous state', () => {
125
+ it('returns next state and batch results', async () => {
126
+ const results = await sut.divine()
127
+ expect(results.length).toBe(testCases.flat().length + 1)
128
+ const state = results.find(isModuleState<IndexingDivinerState>)
129
+ expect(state).toBeDefined()
130
+ const last = filterAs(results, asOptionalStorageMeta)
131
+ .map(p => p as WithStorageMeta<Payload>)
132
+ .sort(PayloadBuilder.compareStorageMeta)
133
+ .at(-1)
134
+ expect(last).toBeDefined()
135
+ expect(state?.state.cursor).toBe(last?._sequence)
136
+ })
137
+ })
138
+ describe('with previous state', () => {
139
+ it.each([1, 2, 3])('returns next state and batch results', async (batch) => {
140
+ const all = (await archivist.all()).sort(PayloadBuilder.compareStorageMeta)
141
+ const batchOffset = all.at((3 * batch) - 1)
142
+ expect(batchOffset).toBeDefined()
143
+ // Test across all offsets
144
+ const cursor = assertEx(batchOffset)._sequence
145
+ const lastState: ModuleState<IndexingDivinerState> = { schema: ModuleStateSchema, state: { cursor } }
146
+ const results = await sut.divine([lastState])
147
+
148
+ // Validate expected results length
149
+ // [BW, ImageThumbnail, TimeStamp] + 1 [ModuleState]
150
+ const expectedResults = testCases.slice(batch).flat().length + 1
151
+ expect(results.length).toBe(expectedResults)
152
+
153
+ // Validate expected state
154
+ const nextState = results.find(isModuleState<IndexingDivinerState>)
155
+ expect(nextState).toBeDefined()
156
+ expect(nextState?.state?.cursor).toBeDefined()
157
+ expect(nextState?.state.cursor).toBe(testCases?.at(-1)?.at(-1)?._sequence)
158
+
159
+ // Validate expected individual results
160
+ const bws = results.filter(isBoundWitness)
161
+ expect(bws.length).toBeGreaterThan(0)
162
+ const timestamps = results.filter(isTimestamp)
163
+ expect(timestamps.length).toBeGreaterThan(0)
164
+ })
165
+ })
166
+ })
167
+ })
@@ -0,0 +1,214 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { filterAs } from '@xylabs/array'
4
+ import { assertEx } from '@xylabs/assert'
5
+ import { delay } from '@xylabs/delay'
6
+ import { AsObjectFactory } from '@xylabs/object'
7
+ import type { MemoryArchivist } from '@xyo-network/archivist-memory'
8
+ import { asArchivistInstance } from '@xyo-network/archivist-model'
9
+ import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
10
+ import { asBoundWitness } from '@xyo-network/boundwitness-model'
11
+ import { asDivinerInstance } from '@xyo-network/diviner-model'
12
+ import type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
13
+ import { PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model'
14
+ import { isTemporalIndexingDivinerResultIndex } from '@xyo-network/diviner-temporal-indexing-model'
15
+ import type { PackageManifestPayload } from '@xyo-network/manifest'
16
+ import { ManifestWrapper } from '@xyo-network/manifest'
17
+ import { ModuleFactoryLocator } from '@xyo-network/module-factory-locator'
18
+ import type {
19
+ Labels, ModuleState, StateDictionary,
20
+ } from '@xyo-network/module-model'
21
+ import { isModuleState } from '@xyo-network/module-model'
22
+ import type { MemoryNode } from '@xyo-network/node-memory'
23
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
24
+ import type { Payload } from '@xyo-network/payload-model'
25
+ import { HDWallet } from '@xyo-network/wallet'
26
+ import type { TimeStamp } from '@xyo-network/witness-timestamp'
27
+ import { TimestampSchema } from '@xyo-network/witness-timestamp'
28
+ import {
29
+ beforeAll,
30
+ describe, expect, it,
31
+ } from 'vitest'
32
+
33
+ import { TemporalIndexingDiviner } from '../Diviner.ts'
34
+ import { TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner } from '../DivinerQueryToIndexQueryDiviner/index.ts'
35
+ import { TemporalIndexingDivinerIndexCandidateToIndexDiviner } from '../IndexCandidateToIndexDiviner/index.ts'
36
+ import { TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner } from '../IndexQueryResponseToDivinerQueryResponseDiviner/index.ts'
37
+ import { TemporalIndexingDivinerStateToIndexCandidateDiviner } from '../StateToIndexCandidateDiviner/index.ts'
38
+ import imageThumbnailDivinerManifest from './TemporalDiviner.json' with { type: 'json' }
39
+
40
+ type ImageThumbnail = Payload<{
41
+ http?: {
42
+ code?: string
43
+ ipAddress?: string
44
+ status?: number
45
+ }
46
+ // schema: 'network.xyo.image.thumbnail'
47
+ sourceHash?: string
48
+ sourceUrl: string
49
+ url?: string
50
+ }>
51
+
52
+ type Query = PayloadDivinerQueryPayload & { status?: number; success?: boolean; url?: string }
53
+
54
+ /**
55
+ * @group slow
56
+ */
57
+ describe('TemporalIndexingDiviner - Multiple', () => {
58
+ const sourceUrl = 'https://placekitten.com/200/300'
59
+ const thumbnailHttpSuccess: ImageThumbnail = {
60
+ http: { status: 200 },
61
+ schema: 'network.xyo.image.thumbnail',
62
+ sourceHash: '7f39363514d9d9b958a5a993edeba35cb44f912c7072ed9ddd628728ac0fd681',
63
+ sourceUrl,
64
+ url: 'data:image/png;base64,===',
65
+ }
66
+
67
+ const thumbnailHttpFail: ImageThumbnail = {
68
+ http: {
69
+
70
+ ipAddress: '104.17.96.13',
71
+ status: 429,
72
+ },
73
+ schema: 'network.xyo.image.thumbnail',
74
+ sourceUrl,
75
+ }
76
+
77
+ const thumbnailCodeFail: ImageThumbnail = {
78
+ http: { code: 'FAILED' },
79
+ schema: 'network.xyo.image.thumbnail',
80
+ sourceUrl,
81
+ }
82
+
83
+ const thumbnailWitnessFail: ImageThumbnail = {
84
+
85
+ http: { ipAddress: '104.17.96.13' },
86
+ schema: 'network.xyo.image.thumbnail',
87
+ sourceUrl,
88
+ }
89
+ const witnessedThumbnails = [thumbnailHttpSuccess, thumbnailHttpFail, thumbnailCodeFail, thumbnailWitnessFail]
90
+
91
+ let sut: TemporalIndexingDiviner
92
+ let node: MemoryNode
93
+
94
+ beforeAll(async () => {
95
+ const labels: Labels = { 'network.xyo.image.thumbnail': 'diviner' }
96
+ const wallet = await HDWallet.random()
97
+ const locator = new ModuleFactoryLocator()
98
+ locator.register(TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner.factory(), labels)
99
+ locator.register(TemporalIndexingDivinerIndexCandidateToIndexDiviner.factory(), labels)
100
+ locator.register(TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner.factory(), labels)
101
+ locator.register(TemporalIndexingDivinerStateToIndexCandidateDiviner.factory(), labels)
102
+ locator.register(TemporalIndexingDiviner.factory(), labels)
103
+ const manifest = imageThumbnailDivinerManifest as PackageManifestPayload
104
+ const manifestWrapper = new ManifestWrapper(manifest, wallet, locator)
105
+ node = await manifestWrapper.loadNodeFromIndex(0)
106
+ await node.start()
107
+
108
+ // Insert previously witnessed payloads into thumbnail archivist
109
+ const timestamp: TimeStamp = { schema: TimestampSchema, timestamp: Date.now() }
110
+ const [boundWitness, payloads] = await new BoundWitnessBuilder().payloads([timestamp, ...witnessedThumbnails]).build()
111
+
112
+ const thumbnailArchivist = assertEx(asArchivistInstance<MemoryArchivist>(await node.resolve('ImageThumbnailArchivist')))
113
+ await thumbnailArchivist.insert([boundWitness, ...payloads])
114
+
115
+ sut = assertEx(asDivinerInstance(await node.resolve('ImageThumbnailDiviner'))) as TemporalIndexingDiviner
116
+
117
+ // Allow enough time for diviner to divine
118
+ await delay(1000)
119
+ }, 40_000)
120
+ describe('diviner state', () => {
121
+ let stateArchivist: MemoryArchivist
122
+ beforeAll(async () => {
123
+ const mod = await node.resolve('AddressStateArchivist')
124
+ stateArchivist = assertEx(asArchivistInstance<MemoryArchivist>(mod))
125
+ })
126
+ it('has expected bound witnesses', async () => {
127
+ const payloads = await stateArchivist.all()
128
+ const stateBoundWitnesses = filterAs(payloads, asBoundWitness)
129
+ expect(stateBoundWitnesses).toBeArrayOfSize(1)
130
+ for (const stateBoundWitness of stateBoundWitnesses) {
131
+ expect(stateBoundWitness).toBeObject()
132
+ expect(stateBoundWitness.addresses).toBeArrayOfSize(1)
133
+ expect(stateBoundWitness.addresses).toContain(sut.address)
134
+ }
135
+ })
136
+ it('has expected state', async () => {
137
+ const payloads = await stateArchivist.all()
138
+ try {
139
+ const asModuleState = AsObjectFactory.create<ModuleState<StateDictionary>>(isModuleState)
140
+ const statePayloads = filterAs(payloads, asModuleState)
141
+ expect(statePayloads).toBeArrayOfSize(1)
142
+ expect(statePayloads.at(-1)).toBeObject()
143
+ const statePayload = assertEx(statePayloads.at(-1))
144
+ expect(statePayload.state).toBeObject()
145
+ expect(statePayload.state?.cursor).toBeDefined()
146
+ } catch (ex) {
147
+ console.error('State payloads:', payloads)
148
+ throw ex
149
+ }
150
+ })
151
+ })
152
+ describe('diviner index', () => {
153
+ let indexArchivist: MemoryArchivist
154
+ beforeAll(async () => {
155
+ const mod = await node.resolve('ImageThumbnailDivinerIndexArchivist')
156
+ indexArchivist = assertEx(asArchivistInstance<MemoryArchivist>(mod))
157
+ })
158
+ // NOTE: We're not signing indexes for performance reasons
159
+ it.skip('has expected bound witnesses', async () => {
160
+ const payloads = await indexArchivist.all()
161
+ const indexBoundWitnesses = filterAs(payloads, asBoundWitness)
162
+ expect(indexBoundWitnesses).toBeArrayOfSize(1)
163
+ const indexBoundWitness = indexBoundWitnesses[0]
164
+ expect(indexBoundWitness).toBeObject()
165
+ expect(indexBoundWitness.addresses).toBeArrayOfSize(1)
166
+ expect(indexBoundWitness.addresses).toContain(sut.address)
167
+ })
168
+ it('has expected index', async () => {
169
+ const payloads = await indexArchivist.all()
170
+ const indexPayloads = payloads.filter(isTemporalIndexingDivinerResultIndex)
171
+ expect(indexPayloads).toBeArrayOfSize((witnessedThumbnails).length)
172
+ })
173
+ })
174
+ describe('with no thumbnail for the provided URL', () => {
175
+ const url = 'https://does.not.exist.io'
176
+ const schema = PayloadDivinerQuerySchema
177
+ it('returns nothing', async () => {
178
+ const query: Query = { schema, url }
179
+ const result = await sut.divine([query])
180
+ expect(result).toBeArrayOfSize(0)
181
+ })
182
+ })
183
+ describe('with thumbnails for the provided URL', () => {
184
+ const url = sourceUrl
185
+ const schema = PayloadDivinerQuerySchema
186
+ describe('with no filter criteria', () => {
187
+ it('returns the most recent result', async () => {
188
+ const query: Query = { schema, url }
189
+ const results = await sut.divine([query])
190
+ const result = results.find(isTemporalIndexingDivinerResultIndex)
191
+ expect(result).toBeDefined()
192
+ const payload = assertEx((witnessedThumbnails).at(-1))
193
+ const expected = await PayloadBuilder.dataHash(payload)
194
+ expect(result?.$sources).toContain(expected)
195
+ })
196
+ })
197
+ describe('with filter criteria', () => {
198
+ describe('for status code', () => {
199
+ const cases: ImageThumbnail[] = [thumbnailHttpSuccess, thumbnailHttpFail]
200
+ it.each(cases)('returns the most recent instance of that status code', async (payload) => {
201
+ const { status } = payload.http ?? {}
202
+ const query: Query = {
203
+ schema, status, url,
204
+ }
205
+ const results = await sut.divine([query])
206
+ const result = results.find(isTemporalIndexingDivinerResultIndex)
207
+ expect(result).toBeDefined()
208
+ const expected = await PayloadBuilder.dataHash(payload)
209
+ expect(result?.$sources).toContain(expected)
210
+ })
211
+ })
212
+ })
213
+ })
214
+ })
@@ -0,0 +1,225 @@
1
+ import '@xylabs/vitest-extended'
2
+
3
+ import { filterAs } from '@xylabs/array'
4
+ import { assertEx } from '@xylabs/assert'
5
+ import { delay } from '@xylabs/delay'
6
+ import type { MemoryArchivist } from '@xyo-network/archivist-memory'
7
+ import { asArchivistInstance } from '@xyo-network/archivist-model'
8
+ import { BoundWitnessBuilder } from '@xyo-network/boundwitness-builder'
9
+ import { asBoundWitness } from '@xyo-network/boundwitness-model'
10
+ import { asDivinerInstance } from '@xyo-network/diviner-model'
11
+ import type { PayloadDivinerQueryPayload } from '@xyo-network/diviner-payload-model'
12
+ import { PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model'
13
+ import { isTemporalIndexingDivinerResultIndex } from '@xyo-network/diviner-temporal-indexing-model'
14
+ import type { PackageManifestPayload } from '@xyo-network/manifest'
15
+ import { ManifestWrapper } from '@xyo-network/manifest'
16
+ import { ModuleFactoryLocator } from '@xyo-network/module-factory-locator'
17
+ import type { Labels } from '@xyo-network/module-model'
18
+ import { asModuleState } from '@xyo-network/module-model'
19
+ import type { MemoryNode } from '@xyo-network/node-memory'
20
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
21
+ import type { Payload } from '@xyo-network/payload-model'
22
+ import { HDWallet } from '@xyo-network/wallet'
23
+ import type { TimeStamp } from '@xyo-network/witness-timestamp'
24
+ import { TimestampSchema } from '@xyo-network/witness-timestamp'
25
+ import {
26
+ beforeAll,
27
+ describe, expect, it,
28
+ } from 'vitest'
29
+
30
+ import { TemporalIndexingDiviner } from '../Diviner.ts'
31
+ import { TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner } from '../DivinerQueryToIndexQueryDiviner/index.ts'
32
+ import { TemporalIndexingDivinerIndexCandidateToIndexDiviner } from '../IndexCandidateToIndexDiviner/index.ts'
33
+ import { TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner } from '../IndexQueryResponseToDivinerQueryResponseDiviner/index.ts'
34
+ import { TemporalIndexingDivinerStateToIndexCandidateDiviner } from '../StateToIndexCandidateDiviner/index.ts'
35
+ import imageThumbnailDivinerManifest from './TemporalDiviner.json' with { type: 'json' }
36
+
37
+ type ImageThumbnail = Payload<{
38
+ http?: {
39
+ code?: string
40
+ ipAddress?: string
41
+ status?: number
42
+ }
43
+ // schema: 'network.xyo.image.thumbnail'
44
+ sourceHash?: string
45
+ sourceUrl: string
46
+ url?: string
47
+ }>
48
+
49
+ type Query = PayloadDivinerQueryPayload & { status?: number; success?: boolean; url?: string }
50
+
51
+ /**
52
+ * @group slow
53
+ */
54
+ describe('TemporalIndexingDiviner', () => {
55
+ const sourceUrl = 'https://placekitten.com/200/300'
56
+ const thumbnailHttpSuccess: ImageThumbnail = {
57
+ http: { status: 200 },
58
+ schema: 'network.xyo.image.thumbnail',
59
+ sourceHash: '7f39363514d9d9b958a5a993edeba35cb44f912c7072ed9ddd628728ac0fd681',
60
+ sourceUrl,
61
+ url: 'data:image/png;base64,===',
62
+ }
63
+
64
+ const thumbnailHttpFail: ImageThumbnail = {
65
+ http: {
66
+
67
+ ipAddress: '104.17.96.13',
68
+ status: 429,
69
+ },
70
+ schema: 'network.xyo.image.thumbnail',
71
+ sourceUrl,
72
+ }
73
+
74
+ const thumbnailCodeFail: ImageThumbnail = {
75
+ http: { code: 'FAILED' },
76
+ schema: 'network.xyo.image.thumbnail',
77
+ sourceUrl,
78
+ }
79
+
80
+ const thumbnailWitnessFail: ImageThumbnail = {
81
+
82
+ http: { ipAddress: '104.17.96.13' },
83
+ schema: 'network.xyo.image.thumbnail',
84
+ sourceUrl,
85
+ }
86
+ const witnessedThumbnails = [thumbnailHttpSuccess, thumbnailHttpFail, thumbnailCodeFail, thumbnailWitnessFail]
87
+
88
+ let sut: TemporalIndexingDiviner
89
+ let node: MemoryNode
90
+
91
+ beforeAll(async () => {
92
+ const labels: Labels = { 'network.xyo.image.thumbnail': 'diviner' }
93
+ const wallet = await HDWallet.random()
94
+ const locator = new ModuleFactoryLocator()
95
+ locator.register(TemporalIndexingDivinerDivinerQueryToIndexQueryDiviner.factory(), labels)
96
+ locator.register(TemporalIndexingDivinerIndexCandidateToIndexDiviner.factory(), labels)
97
+ locator.register(TemporalIndexingDivinerIndexQueryResponseToDivinerQueryResponseDiviner.factory(), labels)
98
+ locator.register(TemporalIndexingDivinerStateToIndexCandidateDiviner.factory(), labels)
99
+ locator.register(TemporalIndexingDiviner.factory(), labels)
100
+ const manifest = imageThumbnailDivinerManifest as PackageManifestPayload
101
+ const manifestWrapper = new ManifestWrapper(manifest, wallet, locator)
102
+ node = await manifestWrapper.loadNodeFromIndex(0)
103
+ await node.start()
104
+
105
+ // Insert previously witnessed payloads into thumbnail archivist
106
+ const httpSuccessTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: Date.now() }
107
+ const [httpSuccessBoundWitness, httpSuccessPayloads] = await new BoundWitnessBuilder()
108
+ .payloads([thumbnailHttpSuccess, httpSuccessTimestamp])
109
+ .build()
110
+ const httpFailTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: Date.now() }
111
+ const [httpFailBoundWitness, httpFailPayloads] = await new BoundWitnessBuilder().payloads([thumbnailHttpFail, httpFailTimestamp]).build()
112
+
113
+ const witnessFailTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: Date.now() }
114
+ const [witnessFailBoundWitness, witnessFailPayloads] = await new BoundWitnessBuilder()
115
+ .payloads([thumbnailWitnessFail, witnessFailTimestamp])
116
+ .build()
117
+
118
+ const codeFailTimestamp: TimeStamp = { schema: TimestampSchema, timestamp: Date.now() }
119
+ const [codeFailBoundWitness, codeFailPayloads] = await new BoundWitnessBuilder().payloads([thumbnailCodeFail, codeFailTimestamp]).build()
120
+
121
+ const thumbnailArchivist = assertEx(asArchivistInstance<MemoryArchivist>(await node.resolve('ImageThumbnailArchivist')))
122
+ await thumbnailArchivist.insert([
123
+ httpSuccessBoundWitness,
124
+ ...httpSuccessPayloads,
125
+ httpFailBoundWitness,
126
+ ...httpFailPayloads,
127
+ witnessFailBoundWitness,
128
+ ...witnessFailPayloads,
129
+ codeFailBoundWitness,
130
+ ...codeFailPayloads,
131
+ ])
132
+
133
+ sut = assertEx(asDivinerInstance(await node.resolve('ImageThumbnailDiviner'))) as TemporalIndexingDiviner
134
+
135
+ // Allow enough time for diviner to divine
136
+ await delay(1000)
137
+ }, 40_000)
138
+ describe('diviner state', () => {
139
+ let stateArchivist: MemoryArchivist
140
+ beforeAll(async () => {
141
+ const mod = await node.resolve('AddressStateArchivist')
142
+ stateArchivist = assertEx(asArchivistInstance<MemoryArchivist>(mod))
143
+ })
144
+ it('has expected bound witnesses', async () => {
145
+ const payloads = await stateArchivist.all()
146
+ const stateBoundWitnesses = filterAs(payloads, asBoundWitness)
147
+ expect(stateBoundWitnesses).toBeArrayOfSize(1)
148
+ for (const stateBoundWitness of stateBoundWitnesses) {
149
+ expect(stateBoundWitness).toBeObject()
150
+ expect(stateBoundWitness.addresses).toBeArrayOfSize(1)
151
+ expect(stateBoundWitness.addresses).toContain(sut.address)
152
+ }
153
+ })
154
+ it('has expected state', async () => {
155
+ const payloads = await stateArchivist.all()
156
+ const statePayloads = filterAs(payloads, asModuleState)
157
+ expect(statePayloads).toBeArrayOfSize(1)
158
+ expect(statePayloads.at(-1)).toBeObject()
159
+ const statePayload = assertEx(statePayloads.at(-1))
160
+ expect(statePayload.state).toBeObject()
161
+ expect(statePayload.state?.cursor).toBeDefined()
162
+ })
163
+ })
164
+ describe('diviner index', () => {
165
+ let indexArchivist: MemoryArchivist
166
+ beforeAll(async () => {
167
+ const mod = await node.resolve('ImageThumbnailDivinerIndexArchivist')
168
+ indexArchivist = assertEx(asArchivistInstance<MemoryArchivist>(mod))
169
+ })
170
+ // NOTE: We're not signing indexes for performance reasons
171
+ it.skip('has expected bound witnesses', async () => {
172
+ const payloads = await indexArchivist.all()
173
+ const indexBoundWitnesses = filterAs(payloads, asBoundWitness)
174
+ expect(indexBoundWitnesses).toBeArrayOfSize(1)
175
+ const indexBoundWitness = indexBoundWitnesses[0]
176
+ expect(indexBoundWitness).toBeObject()
177
+ expect(indexBoundWitness.addresses).toBeArrayOfSize(1)
178
+ expect(indexBoundWitness.addresses).toContain(sut.address)
179
+ })
180
+ it('has expected index', async () => {
181
+ const payloads = await indexArchivist.all()
182
+ const indexPayloads = payloads.filter(isTemporalIndexingDivinerResultIndex)
183
+ expect(indexPayloads).toBeArrayOfSize(witnessedThumbnails.length)
184
+ })
185
+ })
186
+ describe('with no thumbnail for the provided URL', () => {
187
+ const url = 'https://does.not.exist.io'
188
+ const schema = PayloadDivinerQuerySchema
189
+ it('returns nothing', async () => {
190
+ const query: Query = { schema, url }
191
+ const result = await sut.divine([query])
192
+ expect(result).toBeArrayOfSize(0)
193
+ })
194
+ })
195
+ describe('with thumbnails for the provided URL', () => {
196
+ const url = sourceUrl
197
+ const schema = PayloadDivinerQuerySchema
198
+ describe('with no filter criteria', () => {
199
+ it('returns the most recent result', async () => {
200
+ const query: Query = { schema, url }
201
+ const results = await sut.divine([query])
202
+ const result = results.find(isTemporalIndexingDivinerResultIndex)
203
+ expect(result).toBeDefined()
204
+ const expected = await PayloadBuilder.dataHash(thumbnailCodeFail)
205
+ expect(result?.$sources).toContain(expected)
206
+ })
207
+ })
208
+ describe('with filter criteria', () => {
209
+ describe('for status code', () => {
210
+ const cases: ImageThumbnail[] = [thumbnailHttpSuccess, thumbnailHttpFail]
211
+ it.each(cases)('returns the most recent instance of that status code', async (payload) => {
212
+ const { status } = payload.http ?? {}
213
+ const query: Query = {
214
+ schema, status, url,
215
+ }
216
+ const results = await sut.divine([query])
217
+ const result = results.find(isTemporalIndexingDivinerResultIndex)
218
+ expect(result).toBeDefined()
219
+ const expected = await PayloadBuilder.dataHash(payload)
220
+ expect(result?.$sources).toContain(expected)
221
+ })
222
+ })
223
+ })
224
+ })
225
+ })