@xyo-network/diviner-image-thumbnail 2.75.9

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 (38) hide show
  1. package/LICENSE +165 -0
  2. package/README.md +13 -0
  3. package/dist/docs.json +20993 -0
  4. package/dist/node/Diviner/Diviner.d.cts +39 -0
  5. package/dist/node/Diviner/Diviner.d.cts.map +1 -0
  6. package/dist/node/Diviner/Diviner.d.mts +39 -0
  7. package/dist/node/Diviner/Diviner.d.mts.map +1 -0
  8. package/dist/node/Diviner/Diviner.d.ts +39 -0
  9. package/dist/node/Diviner/Diviner.d.ts.map +1 -0
  10. package/dist/node/Diviner/Diviner.js +229 -0
  11. package/dist/node/Diviner/Diviner.js.map +1 -0
  12. package/dist/node/Diviner/Diviner.mjs +210 -0
  13. package/dist/node/Diviner/Diviner.mjs.map +1 -0
  14. package/dist/node/Diviner/index.d.cts +2 -0
  15. package/dist/node/Diviner/index.d.cts.map +1 -0
  16. package/dist/node/Diviner/index.d.mts +2 -0
  17. package/dist/node/Diviner/index.d.mts.map +1 -0
  18. package/dist/node/Diviner/index.d.ts +2 -0
  19. package/dist/node/Diviner/index.d.ts.map +1 -0
  20. package/dist/node/Diviner/index.js +231 -0
  21. package/dist/node/Diviner/index.js.map +1 -0
  22. package/dist/node/Diviner/index.mjs +210 -0
  23. package/dist/node/Diviner/index.mjs.map +1 -0
  24. package/dist/node/index.d.cts +2 -0
  25. package/dist/node/index.d.cts.map +1 -0
  26. package/dist/node/index.d.mts +2 -0
  27. package/dist/node/index.d.mts.map +1 -0
  28. package/dist/node/index.d.ts +2 -0
  29. package/dist/node/index.d.ts.map +1 -0
  30. package/dist/node/index.js +231 -0
  31. package/dist/node/index.js.map +1 -0
  32. package/dist/node/index.mjs +210 -0
  33. package/dist/node/index.mjs.map +1 -0
  34. package/package.json +95 -0
  35. package/src/Diviner/Diviner.ts +259 -0
  36. package/src/Diviner/index.ts +1 -0
  37. package/src/index.ts +1 -0
  38. package/typedoc.json +5 -0
@@ -0,0 +1,259 @@
1
+ import { assertEx } from '@xylabs/assert'
2
+ import { exists } from '@xylabs/exists'
3
+ import { AbstractDiviner } from '@xyo-network/abstract-diviner'
4
+ import { asArchivistInstance, withArchivistModule } from '@xyo-network/archivist-model'
5
+ import { ArchivistWrapper } from '@xyo-network/archivist-wrapper'
6
+ import { isBoundWitness } from '@xyo-network/boundwitness-model'
7
+ import { PayloadHasher } from '@xyo-network/core'
8
+ import { BoundWitnessDivinerQueryPayload, BoundWitnessDivinerQuerySchema } from '@xyo-network/diviner-boundwitness-model'
9
+ import { asDivinerInstance, DivinerConfigSchema } from '@xyo-network/diviner-model'
10
+ import { PayloadDivinerQueryPayload, PayloadDivinerQuerySchema } from '@xyo-network/diviner-payload-model'
11
+ import { DivinerWrapper } from '@xyo-network/diviner-wrapper'
12
+ import {
13
+ ImageThumbnailDivinerConfig,
14
+ ImageThumbnailDivinerConfigSchema,
15
+ ImageThumbnailDivinerParams,
16
+ ImageThumbnailResult,
17
+ ImageThumbnailResultIndexSchema,
18
+ ImageThumbnailSchema,
19
+ isImageThumbnail,
20
+ isImageThumbnailResult,
21
+ } from '@xyo-network/image-thumbnail-payload-plugin'
22
+ import { isModuleState, ModuleState, ModuleStateSchema, StateDictionary } from '@xyo-network/module-model'
23
+ import { PayloadBuilder } from '@xyo-network/payload-builder'
24
+ import { Payload } from '@xyo-network/payload-model'
25
+ import { isUrlPayload, UrlPayload } from '@xyo-network/url-payload-plugin'
26
+ import { isTimestamp, TimestampSchema } from '@xyo-network/witness-timestamp'
27
+
28
+ export type ImageThumbnailDivinerState = StateDictionary & {
29
+ offset: number
30
+ }
31
+
32
+ type ConfigStoreKey = 'indexStore' | 'stateStore' | 'thumbnailStore'
33
+
34
+ type ConfigStore = Extract<keyof ImageThumbnailDivinerConfig, ConfigStoreKey>
35
+
36
+ /**
37
+ * The fields that will need to be indexed on in the underlying store
38
+ */
39
+ type QueryableImageThumbnailResultProperties = Extract<keyof ImageThumbnailResult, 'url' | 'timestamp' | 'status'>
40
+
41
+ /**
42
+ * The query that will be used to retrieve the results from the underlying store
43
+ */
44
+ type ImageThumbnailResultQuery = PayloadDivinerQueryPayload & { schemas: [ImageThumbnailSchema] } & Pick<
45
+ ImageThumbnailResult,
46
+ QueryableImageThumbnailResultProperties
47
+ >
48
+
49
+ const moduleName = 'ImageThumbnailDiviner'
50
+
51
+ export class ImageThumbnailDiviner<TParams extends ImageThumbnailDivinerParams = ImageThumbnailDivinerParams> extends AbstractDiviner<TParams> {
52
+ static override configSchemas = [ImageThumbnailDivinerConfigSchema, DivinerConfigSchema]
53
+
54
+ private _pollId?: string | number | NodeJS.Timeout
55
+
56
+ get payloadDivinerLimit() {
57
+ return this.config.payloadDivinerLimit ?? 1_0000
58
+ }
59
+
60
+ get pollFrequency() {
61
+ return this.config.pollFrequency ?? 10_000
62
+ }
63
+
64
+ protected backgroundDivine = async (): Promise<void> => {
65
+ // Load last state
66
+ const lastState = (await this.retrieveState()) ?? { offset: 0 }
67
+ const { offset } = lastState
68
+ // Get next batch of results
69
+ const boundWitnessDiviner = await this.getBoundWitnessDivinerForStore('thumbnailStore')
70
+ const query = new PayloadBuilder<BoundWitnessDivinerQueryPayload>({ schema: BoundWitnessDivinerQuerySchema }).fields({
71
+ limit: this.payloadDivinerLimit,
72
+ offset,
73
+ order: 'asc',
74
+ payload_schemas: [ImageThumbnailSchema, TimestampSchema],
75
+ })
76
+ const batch = await boundWitnessDiviner.divine([query])
77
+ if (batch.length === 0) return
78
+ const imageThumbnailTimestampTuples = batch
79
+ .filter(isBoundWitness)
80
+ .map((bw) => {
81
+ const imageThumbnailIndexes = bw.payload_schemas?.map((schema, index) => (schema === ImageThumbnailSchema ? index : undefined)).filter(exists)
82
+ const timestampIndex = bw.payload_schemas?.findIndex((schema) => schema === TimestampSchema)
83
+ if (!imageThumbnailIndexes.length || timestampIndex === -1) return undefined
84
+ const imageThumbnails = bw.payload_hashes.map((hash, index) => (imageThumbnailIndexes.includes(index) ? hash : undefined)).filter(exists)
85
+ const timestamp = bw.payload_hashes?.[timestampIndex]
86
+ return imageThumbnails.map((imageThumbnail) => [imageThumbnail, timestamp] as const)
87
+ })
88
+ .flat()
89
+ .filter(exists)
90
+ const archivist = await this.getArchivistForStore('thumbnailStore')
91
+ const payloadTuples = (
92
+ await Promise.all(
93
+ imageThumbnailTimestampTuples.map(async ([imageThumbnailHash, timestampHash]) => {
94
+ const results = await archivist.get([imageThumbnailHash, timestampHash])
95
+ const imageThumbnailPayload = results.find(isImageThumbnail)
96
+ const timestampPayload = results.find(isTimestamp)
97
+ if (!imageThumbnailPayload || !timestampPayload) return undefined
98
+ const calculatedImageThumbnailHash = await PayloadHasher.hashAsync(imageThumbnailPayload)
99
+ const calculatedTimestampHash = await PayloadHasher.hashAsync(timestampPayload)
100
+ if (imageThumbnailHash !== calculatedImageThumbnailHash || timestampHash !== calculatedTimestampHash) return undefined
101
+ return [imageThumbnailHash, imageThumbnailPayload, timestampHash, timestampPayload] as const
102
+ }),
103
+ )
104
+ ).filter(exists)
105
+ // Build index results
106
+ const indexedResults = payloadTuples.map(([thumbnailHash, thumbnailPayload, timestampHash, timestampPayload]) => {
107
+ const { sourceUrl: url } = thumbnailPayload
108
+ const { timestamp } = timestampPayload
109
+ const status = thumbnailPayload.http?.status ?? -1
110
+ const sources = [thumbnailHash, timestampHash]
111
+ const result = new PayloadBuilder<ImageThumbnailResult>({ schema: ImageThumbnailResultIndexSchema })
112
+ .fields({ sources, status, timestamp, url })
113
+ .build()
114
+ return result
115
+ })
116
+ // Insert index results
117
+ const indexArchivist = await this.getArchivistForStore('indexStore')
118
+ await indexArchivist.insert(indexedResults)
119
+ // Update state
120
+ const nextOffset = offset + batch.length + 1
121
+ const currentState = { ...lastState, offset: nextOffset }
122
+ await this.commitState(currentState)
123
+ }
124
+
125
+ /**
126
+ * Commit the internal state of the Diviner process. This is similar
127
+ * to a transaction completion in a database and should only be called
128
+ * when results have been successfully persisted to the appropriate
129
+ * external stores.
130
+ */
131
+ protected async commitState(state: ImageThumbnailDivinerState) {
132
+ const stateStore = assertEx(this.config.stateStore?.archivist, `${moduleName}: No stateStore configured`)
133
+ const module = assertEx(await this.resolve(stateStore), `${moduleName}: Failed to resolve stateStore`)
134
+ await withArchivistModule(module, async (archivist) => {
135
+ const mod = ArchivistWrapper.wrap(archivist, this.account)
136
+ const payload = new PayloadBuilder<ModuleState<ImageThumbnailDivinerState>>({ schema: ModuleStateSchema }).fields({ state }).build()
137
+ await mod.insert([payload])
138
+ })
139
+ }
140
+
141
+ protected override async divineHandler(payloads: Payload[] = []): Promise<ImageThumbnailResult[]> {
142
+ const urls = payloads.filter(isUrlPayload)
143
+ const diviner = await this.getPayloadDivinerForStore('indexStore')
144
+ const results = (
145
+ await Promise.all(
146
+ urls.map(async (payload) => {
147
+ const { url, status: payloadStatus } = payload as UrlPayload & { status: number }
148
+ const status = payloadStatus ?? 200
149
+ const query = new PayloadBuilder<ImageThumbnailResultQuery>({ schema: PayloadDivinerQuerySchema })
150
+ // TODO: Expose status, limit (and possibly offset) to caller. Currently only exposing URL
151
+ .fields({ limit: 1, offset: 0, order: 'desc', status, url })
152
+ .build()
153
+ return await diviner.divine([query])
154
+ }),
155
+ )
156
+ )
157
+ .flat()
158
+ .filter(isImageThumbnailResult)
159
+ return results
160
+ }
161
+
162
+ protected async getArchivistForStore(store: ConfigStore, wrap?: boolean) {
163
+ const name = assertEx(this.config?.[store]?.archivist, () => `${moduleName}: Config for ${store}.archivist not specified`)
164
+ const mod = assertEx(await this.resolve(name), () => `${moduleName}: Failed to resolve ${store}.archivist`)
165
+ return wrap ? ArchivistWrapper.wrap(mod, this.account) : asArchivistInstance(mod, () => `${moduleName}: ${store}.archivist is not an Archivist`)
166
+ }
167
+
168
+ protected async getBoundWitnessDivinerForStore(store: ConfigStore, wrap?: boolean) {
169
+ const name = assertEx(this.config?.[store]?.boundWitnessDiviner, () => `${moduleName}: Config for ${store}.boundWitnessDiviner not specified`)
170
+ const mod = assertEx(await this.resolve(name), () => `${moduleName}: Failed to resolve ${store}.boundWitnessDiviner`)
171
+ return wrap
172
+ ? DivinerWrapper.wrap(mod, this.account)
173
+ : asDivinerInstance(mod, () => `${moduleName}: ${store}.boundWitnessDiviner is not a Diviner`)
174
+ }
175
+
176
+ protected async getPayloadDivinerForStore(store: ConfigStore, wrap?: boolean) {
177
+ const name = assertEx(this.config?.[store]?.payloadDiviner, () => `${moduleName}: Config for ${store}.payloadDiviner not specified`)
178
+ const mod = assertEx(await this.resolve(name), () => `${moduleName}: Failed to resolve ${store}.payloadDiviner`)
179
+ return wrap ? DivinerWrapper.wrap(mod, this.account) : asDivinerInstance(mod, () => `${moduleName}: ${store}.payloadDiviner is not a Diviner`)
180
+ }
181
+
182
+ /**
183
+ * Retrieves the last state of the Diviner process. Used to recover state after
184
+ * preemptions, reboots, etc.
185
+ */
186
+ protected async retrieveState(): Promise<ImageThumbnailDivinerState | undefined> {
187
+ let hash: string = ''
188
+ const diviner = await this.getBoundWitnessDivinerForStore('stateStore')
189
+ const query = new PayloadBuilder<BoundWitnessDivinerQueryPayload>({ schema: BoundWitnessDivinerQuerySchema }).fields({
190
+ address: this.account.address,
191
+ limit: 1,
192
+ offset: 0,
193
+ order: 'desc',
194
+ payload_schemas: [ModuleStateSchema],
195
+ })
196
+ const boundWitnesses = await diviner.divine([query])
197
+ if (boundWitnesses.length > 0) {
198
+ const boundWitness = boundWitnesses[0]
199
+ if (isBoundWitness(boundWitness)) {
200
+ // Find the index for this address in the BoundWitness that is a ModuleState
201
+ hash = boundWitness.addresses
202
+ .map((address, index) => ({ address, index }))
203
+ .filter(({ address }) => address === this.account.address)
204
+ .reduce(
205
+ (prev, curr) => (boundWitness.payload_schemas?.[curr?.index] === ModuleStateSchema ? boundWitness.payload_hashes[curr?.index] : prev),
206
+ '',
207
+ )
208
+ }
209
+ }
210
+
211
+ // If we able to located the last state
212
+ if (hash) {
213
+ // Get last state
214
+ const stateStoreArchivist = assertEx(this.config.stateStore?.archivist, `${moduleName}: No stateStore archivist configured`)
215
+ await withArchivistModule(
216
+ assertEx(await this.resolve(stateStoreArchivist), `${moduleName}: Failed to resolve stateStore archivist`),
217
+ async (mod) => {
218
+ const archivist = ArchivistWrapper.wrap(mod, this.account)
219
+ const payloads = await archivist.get([hash])
220
+ if (payloads.length > 0) {
221
+ const payload = payloads[0]
222
+ if (isModuleState(payload)) {
223
+ return payload.state
224
+ }
225
+ }
226
+ },
227
+ )
228
+ }
229
+ return undefined
230
+ }
231
+
232
+ protected override async startHandler(): Promise<boolean> {
233
+ await super.startHandler()
234
+ this.poll()
235
+ return true
236
+ }
237
+
238
+ protected override async stopHandler(_timeout?: number | undefined): Promise<boolean> {
239
+ if (this._pollId) {
240
+ clearTimeout(this._pollId)
241
+ this._pollId = undefined
242
+ }
243
+ return await super.stopHandler()
244
+ }
245
+
246
+ private poll() {
247
+ this._pollId = setTimeout(async () => {
248
+ try {
249
+ await this.backgroundDivine()
250
+ } catch (e) {
251
+ console.log(e)
252
+ } finally {
253
+ if (this._pollId) clearTimeout(this._pollId)
254
+ this._pollId = undefined
255
+ this.poll()
256
+ }
257
+ }, this.pollFrequency)
258
+ }
259
+ }
@@ -0,0 +1 @@
1
+ export * from './Diviner'
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './Diviner'
package/typedoc.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://typedoc.org/schema.json",
3
+ "entryPoints": ["./src/index.ts"],
4
+ "tsconfig": "./tsconfig.typedoc.json"
5
+ }