@sprucelabs/mercury-chunking-emitter 1.0.4
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/.circleci/config.yml +71 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +3 -0
- package/.githooks/pre-push +17 -0
- package/.nvmrc +1 -0
- package/.vscode/launch.json +58 -0
- package/.vscode/settings.json +70 -0
- package/.vscode/tasks.json +112 -0
- package/CHANGELOG.md +16 -0
- package/README.md +24 -0
- package/package.json +84 -0
- package/release.config.js +7 -0
- package/src/.spruce/features/permission.plugin.ts +1 -0
- package/src/.spruce/permissions/permissions.types.ts +9 -0
- package/src/.spruce/schemas/chunkingEmitter/v2023_10_21/chunkPaging.schema.ts +29 -0
- package/src/.spruce/schemas/fields/fieldClassMap.ts +6 -0
- package/src/.spruce/schemas/fields/fields.types.ts +1 -0
- package/src/.spruce/schemas/schemas.types.ts +52 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/choice.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/link.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/location.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/message.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/messageSource.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/messageTarget.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/organization.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/person.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/personLocation.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/personOrganization.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/role.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/sendMessage.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/skill.schema.ts +1 -0
- package/src/.spruce/schemas/spruce/v2020_07_22/skillCreator.schema.ts +1 -0
- package/src/.spruce/settings.json +14 -0
- package/src/__tests__/behavioral/ChunkingEmitter.test.ts +290 -0
- package/src/__tests__/behavioral/TestingChunkingEmitter.test.ts +163 -0
- package/src/__tests__/support/AbstractChunkingEmitterTest.ts +54 -0
- package/src/chunkingEmitter/ChunkingEmitter.ts +107 -0
- package/src/chunkingEmitter/MockChunkingEmitter.ts +84 -0
- package/src/chunkingEmitter/chunkFieldDefinition.ts +12 -0
- package/src/index.ts +3 -0
- package/src/schemas/v2023_10_21/chunkPaging.builder.ts +16 -0
- package/tsconfig.dist.json +36 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { EventName } from '@sprucelabs/mercury-types'
|
|
2
|
+
import { FieldDefinitions, buildSchema } from '@sprucelabs/schema'
|
|
3
|
+
import { eventFaker, fake } from '@sprucelabs/spruce-test-fixtures'
|
|
4
|
+
import { test, assert, errorAssert, generateId } from '@sprucelabs/test-utils'
|
|
5
|
+
import { chunkFieldDefinition } from '../../chunkingEmitter/chunkFieldDefinition'
|
|
6
|
+
import ChunkingEmitterImpl from '../../chunkingEmitter/ChunkingEmitter'
|
|
7
|
+
import AbstractChunkingEmitterTest from '../support/AbstractChunkingEmitterTest'
|
|
8
|
+
|
|
9
|
+
@fake.login()
|
|
10
|
+
export default class ChunkingEmitterTest extends AbstractChunkingEmitterTest {
|
|
11
|
+
private static fqen2: EventName
|
|
12
|
+
private static allTargetAndPayloads: any[] = []
|
|
13
|
+
private static hitCount: number
|
|
14
|
+
protected static emitter: SpyEmitter
|
|
15
|
+
private static readonly itemFieldDefinition: FieldDefinitions = {
|
|
16
|
+
type: 'raw',
|
|
17
|
+
isArray: true,
|
|
18
|
+
options: {
|
|
19
|
+
valueType: 'any',
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected static async beforeEach() {
|
|
24
|
+
await super.beforeEach()
|
|
25
|
+
|
|
26
|
+
ChunkingEmitterImpl.Class = SpyEmitter
|
|
27
|
+
|
|
28
|
+
this.hitCount = 0
|
|
29
|
+
this.fqen = 'test.test::v2021_01_01' as EventName
|
|
30
|
+
this.fqen2 = 'test2.test3::v2022_02_02' as EventName
|
|
31
|
+
|
|
32
|
+
this.allTargetAndPayloads = []
|
|
33
|
+
|
|
34
|
+
this.mixinTestContract()
|
|
35
|
+
|
|
36
|
+
await this.resetEmitterWithChunkSize(1)
|
|
37
|
+
|
|
38
|
+
await this.fakedClient.on(this.fqen, (targetAndPayload) => {
|
|
39
|
+
this.hitCount++
|
|
40
|
+
this.allTargetAndPayloads.push(targetAndPayload)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@test()
|
|
45
|
+
protected static async throwsWithMissing() {
|
|
46
|
+
//@ts-ignore
|
|
47
|
+
const err = await assert.doesThrowAsync(() => ChunkingEmitterImpl.Emitter())
|
|
48
|
+
errorAssert.assertError(err, 'MISSING_PARAMETERS', {
|
|
49
|
+
parameters: ['client'],
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test()
|
|
54
|
+
protected static async emitThrowsWithMissing() {
|
|
55
|
+
//@ts-ignore
|
|
56
|
+
const err = await assert.doesThrowAsync(() => this.emitter.emit())
|
|
57
|
+
errorAssert.assertError(err, 'MISSING_PARAMETERS', {
|
|
58
|
+
parameters: ['eventName', 'items', 'payloadKey'],
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@test()
|
|
63
|
+
protected static async emitsNothingIfPassedNoItems() {
|
|
64
|
+
await this.emitWithItems([])
|
|
65
|
+
assert.isFalse(this.wasEventEmitted)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test()
|
|
69
|
+
protected static async emitsThePassedEvent() {
|
|
70
|
+
await this.emitWithItems([{ id: '1' }])
|
|
71
|
+
assert.isTrue(this.wasEventEmitted)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@test('passes one item to payload', [{ id: '1' }])
|
|
75
|
+
@test('passes two items to payload', [{ id: '1' }, { id: '2' }])
|
|
76
|
+
protected static async passesThePayloadThroughUsingTheKeyPassed(
|
|
77
|
+
items: Record<string, any>[]
|
|
78
|
+
) {
|
|
79
|
+
await this.resetEmitterWithChunkSize(10)
|
|
80
|
+
await this.emitWithItems(items)
|
|
81
|
+
this.assertLastEmittedItemsEqual(items)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@test()
|
|
85
|
+
protected static async canPassToDifferentPayloadKey() {
|
|
86
|
+
this.payloadKey = 'items2'
|
|
87
|
+
const item = await this.emitWithOneItem()
|
|
88
|
+
assert.isEqualDeep(this.lastEmittedPayload?.items2, [item])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test()
|
|
92
|
+
protected static async emitsNameOfEventPassed() {
|
|
93
|
+
this.fqen = this.fqen2
|
|
94
|
+
|
|
95
|
+
let wasHit = false
|
|
96
|
+
await eventFaker.on(this.fqen2, () => {
|
|
97
|
+
wasHit = true
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await this.emitWithOneItem()
|
|
101
|
+
assert.isTrue(wasHit)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@test('emits 2 chunks', 2)
|
|
105
|
+
@test('emits 3 chunks', 3)
|
|
106
|
+
protected static async emitsCorrectNumberOfChunks(total: number) {
|
|
107
|
+
const items = await this.emitWithTotalItems(total)
|
|
108
|
+
this.assertTotalEmits(total)
|
|
109
|
+
|
|
110
|
+
const expected = items.map((item) => [item])
|
|
111
|
+
assert.isEqualDeep(
|
|
112
|
+
this.allTargetAndPayloads.map((t) => t.payload.items),
|
|
113
|
+
expected
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@test()
|
|
118
|
+
protected static async doesNotStopIfOneFails() {
|
|
119
|
+
let hitCount = 0
|
|
120
|
+
await eventFaker.on(this.fqen, () => {
|
|
121
|
+
hitCount++
|
|
122
|
+
if (hitCount === 2) {
|
|
123
|
+
throw new Error('Failed')
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await this.emitWithTotalItems(10)
|
|
128
|
+
assert.isEqual(hitCount, 10)
|
|
129
|
+
this.assertTotalErrors(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@test()
|
|
133
|
+
protected static async accuratelyTracksTotalErrors() {
|
|
134
|
+
await this.makeEventThrow()
|
|
135
|
+
await this.assertEveryEmitErrors()
|
|
136
|
+
await this.assertEveryEmitErrors()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@test()
|
|
140
|
+
protected static async includesChuckingPlacementInPayloadForOneItem() {
|
|
141
|
+
await this.emitWithOneItem()
|
|
142
|
+
assert.isEqualDeep(this.lastEmittedPayload?.chunk, {
|
|
143
|
+
total: 1,
|
|
144
|
+
current: 0,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@test()
|
|
149
|
+
protected static async includesChuckingPlacementInPayloadForTwoItems() {
|
|
150
|
+
await this.emitWithTotalItems(2)
|
|
151
|
+
const expected = [
|
|
152
|
+
{ total: 2, current: 0 },
|
|
153
|
+
{ total: 2, current: 1 },
|
|
154
|
+
]
|
|
155
|
+
assert.isEqualDeep(
|
|
156
|
+
this.allTargetAndPayloads.map((t) => t.payload.chunk),
|
|
157
|
+
expected
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@test()
|
|
162
|
+
protected static async includesTargetInEmit() {
|
|
163
|
+
this.fqen = generateId() as EventName
|
|
164
|
+
|
|
165
|
+
const payloadSchema = buildSchema({
|
|
166
|
+
id: generateId(),
|
|
167
|
+
fields: {
|
|
168
|
+
items: this.itemFieldDefinition,
|
|
169
|
+
chunk: chunkFieldDefinition(),
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const targetSchema = buildSchema({
|
|
174
|
+
id: generateId(),
|
|
175
|
+
fields: {
|
|
176
|
+
personId: {
|
|
177
|
+
type: 'id',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
this.mixinEventSignatures({
|
|
183
|
+
[this.fqen]: this.buildSignature(payloadSchema, targetSchema),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
let passedTarget: Record<string, any> | undefined
|
|
187
|
+
|
|
188
|
+
//@ts-ignore
|
|
189
|
+
await eventFaker.on(this.fqen, ({ target }) => {
|
|
190
|
+
passedTarget = target
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const target = { personId: generateId() }
|
|
194
|
+
await this.emitWithOneItem(target)
|
|
195
|
+
assert.isEqualDeep(passedTarget, target)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@test()
|
|
199
|
+
protected static async defaultsToChunkSizeOfTen() {
|
|
200
|
+
this.emitter = (await ChunkingEmitterImpl.Emitter({
|
|
201
|
+
client: this.fakedClient,
|
|
202
|
+
})) as SpyEmitter
|
|
203
|
+
|
|
204
|
+
assert.isEqualDeep(this.emitter.getChunkSize(), 10)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private static async makeEventThrow() {
|
|
208
|
+
await eventFaker.on(this.fqen, () => {
|
|
209
|
+
throw new Error('Failed')
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private static assertTotalErrors(expected: number) {
|
|
214
|
+
assert.isEqual(this.emitter.getTotalErrors(), expected)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private static assertTotalEmits(total: number) {
|
|
218
|
+
assert.isEqual(this.hitCount, total)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static async emitWithTotalItems(total: number) {
|
|
222
|
+
const items = new Array(total).fill(0).map(() => this.generateItem())
|
|
223
|
+
await this.emitWithItems(items)
|
|
224
|
+
return items
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private static assertLastEmittedItemsEqual(items: Record<string, any>[]) {
|
|
228
|
+
assert.isEqualDeep(this.lastEmittedPayload?.items, items)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private static async resetEmitterWithChunkSize(chunkSize: number) {
|
|
232
|
+
this.emitter = (await ChunkingEmitterImpl.Emitter({
|
|
233
|
+
client: this.fakedClient,
|
|
234
|
+
chunkSize,
|
|
235
|
+
})) as SpyEmitter
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private static async emitWithOneItem(target?: Record<string, any>) {
|
|
239
|
+
const items = [this.generateItem()]
|
|
240
|
+
await this.emitWithItems(items, target)
|
|
241
|
+
return items[0]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private static get lastEmittedPayload() {
|
|
245
|
+
return this.lastTargetAndPayload?.payload
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static mixinTestContract() {
|
|
249
|
+
const eventSignatures = {
|
|
250
|
+
[this.fqen]: this.eventSignature,
|
|
251
|
+
[this.fqen2]: this.eventSignature,
|
|
252
|
+
}
|
|
253
|
+
this.mixinEventSignatures(eventSignatures)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private static get eventSignature() {
|
|
257
|
+
const payload = buildSchema({
|
|
258
|
+
id: generateId(),
|
|
259
|
+
fields: {
|
|
260
|
+
items: this.itemFieldDefinition,
|
|
261
|
+
items2: this.itemFieldDefinition,
|
|
262
|
+
chunk: chunkFieldDefinition(),
|
|
263
|
+
},
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return this.buildSignature(payload)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private static async assertEveryEmitErrors() {
|
|
270
|
+
await this.emitWithTotalItems(10)
|
|
271
|
+
this.assertTotalErrors(10)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private static get lastTargetAndPayload() {
|
|
275
|
+
return this.allTargetAndPayloads[this.allTargetAndPayloads.length - 1]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static get wasEventEmitted() {
|
|
279
|
+
return this.hitCount > 0
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class SpyEmitter extends ChunkingEmitterImpl {
|
|
284
|
+
public constructor(options: any) {
|
|
285
|
+
super(options)
|
|
286
|
+
}
|
|
287
|
+
public getChunkSize() {
|
|
288
|
+
return this.chunkSize
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { EventName } from '@sprucelabs/mercury-types'
|
|
2
|
+
import {
|
|
3
|
+
FieldDefinitions,
|
|
4
|
+
SchemaFieldsByName,
|
|
5
|
+
buildSchema,
|
|
6
|
+
} from '@sprucelabs/schema'
|
|
7
|
+
import { fake } from '@sprucelabs/spruce-test-fixtures'
|
|
8
|
+
import { test, assert, generateId } from '@sprucelabs/test-utils'
|
|
9
|
+
import { chunkFieldDefinition } from '../../chunkingEmitter/chunkFieldDefinition'
|
|
10
|
+
import ChunkingEmitterImpl from '../../chunkingEmitter/ChunkingEmitter'
|
|
11
|
+
import MockChunkingEmitter from '../../chunkingEmitter/MockChunkingEmitter'
|
|
12
|
+
import AbstractChunkingEmitterTest from '../support/AbstractChunkingEmitterTest'
|
|
13
|
+
|
|
14
|
+
@fake.login()
|
|
15
|
+
export default class TestingChunkingEmitterTest extends AbstractChunkingEmitterTest {
|
|
16
|
+
protected static emitter: MockChunkingEmitter
|
|
17
|
+
private static readonly itemsField: FieldDefinitions = {
|
|
18
|
+
type: 'raw',
|
|
19
|
+
isArray: true,
|
|
20
|
+
options: {
|
|
21
|
+
valueType: 'any',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected static async beforeEach(): Promise<void> {
|
|
26
|
+
await super.beforeEach()
|
|
27
|
+
ChunkingEmitterImpl.Class = MockChunkingEmitter
|
|
28
|
+
this.fqen = generateId() as EventName
|
|
29
|
+
this.emitter = (await ChunkingEmitterImpl.Emitter({
|
|
30
|
+
client: this.fakedClient,
|
|
31
|
+
})) as MockChunkingEmitter
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@test()
|
|
35
|
+
protected static async canSetSpyClass() {
|
|
36
|
+
assert.isInstanceOf(this.emitter, MockChunkingEmitter)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@test()
|
|
40
|
+
protected static async knowsIfWasHit() {
|
|
41
|
+
assert.doesThrow(() => this.emitter.assertDidEmit())
|
|
42
|
+
await this.emit()
|
|
43
|
+
this.emitter.assertDidEmit()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@test()
|
|
47
|
+
protected static async knowsEventNamePassedToIt() {
|
|
48
|
+
await this.emit()
|
|
49
|
+
assert.doesThrow(() =>
|
|
50
|
+
this.emitter.assertDidEmitEventNamed(generateId() as EventName)
|
|
51
|
+
)
|
|
52
|
+
this.emitter.assertDidEmitEventNamed(this.fqen)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@test()
|
|
56
|
+
protected static async knowsItemsEmitted() {
|
|
57
|
+
const items = [this.generateItem(), this.generateItem()]
|
|
58
|
+
this.assertDidEmitItemsThrows(items)
|
|
59
|
+
await this.emitWithItems(items)
|
|
60
|
+
this.emitter.assertEmittedItems(items)
|
|
61
|
+
this.assertDidEmitItemsThrows([])
|
|
62
|
+
this.assertDidEmitItemsThrows([this.generateItem()])
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@test()
|
|
66
|
+
protected static async canResetFactoryClassReference() {
|
|
67
|
+
ChunkingEmitterImpl.reset()
|
|
68
|
+
assert.isFalsy(ChunkingEmitterImpl.Class)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@test()
|
|
72
|
+
protected static async canAssertPayloadKey() {
|
|
73
|
+
this.payloadKey = generateId()
|
|
74
|
+
await this.emit()
|
|
75
|
+
assert.doesThrow(() => this.emitter.assertDidEmitPayloadKey('wrong'))
|
|
76
|
+
this.emitter.assertDidEmitPayloadKey(this.payloadKey)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@test()
|
|
80
|
+
protected static async throwsIfEventSignatureIsBad() {
|
|
81
|
+
const sigWithoutChunking = this.buildSignatureWithPayloadFields({
|
|
82
|
+
items: this.itemsField,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const sigWithChunking = this.buildSignatureWithPayloadFields({
|
|
86
|
+
items: this.itemsField,
|
|
87
|
+
chunk: chunkFieldDefinition(),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const fqenWithChunk = generateId() as EventName
|
|
91
|
+
this.mixinEventSignatures({
|
|
92
|
+
[this.fqen]: sigWithoutChunking,
|
|
93
|
+
[fqenWithChunk]: sigWithChunking,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const fqen = this.fqen
|
|
97
|
+
this.assertSignatureDoesNotConformToChunk(fqen)
|
|
98
|
+
this.assertSignatureDoesConfirmToChunk(fqenWithChunk)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@test()
|
|
102
|
+
protected static async throwsIfChunkIsThereButBad() {
|
|
103
|
+
this.assertChunkFieldThrowsWithBadMatch({
|
|
104
|
+
type: 'number',
|
|
105
|
+
})
|
|
106
|
+
this.assertChunkFieldThrowsWithBadMatch({
|
|
107
|
+
type: 'schema',
|
|
108
|
+
options: {
|
|
109
|
+
schema: buildSchema({
|
|
110
|
+
id: generateId(),
|
|
111
|
+
fields: {
|
|
112
|
+
test: {
|
|
113
|
+
type: 'number',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private static assertChunkFieldThrowsWithBadMatch(
|
|
122
|
+
chunkSig: FieldDefinitions
|
|
123
|
+
) {
|
|
124
|
+
const fqen = generateId() as EventName
|
|
125
|
+
const sig = this.buildSignatureWithPayloadFields({
|
|
126
|
+
items: this.itemsField,
|
|
127
|
+
chunk: chunkSig,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
this.mixinEventSignatures({
|
|
131
|
+
[fqen]: sig,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
this.assertSignatureDoesNotConformToChunk(fqen)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static assertSignatureDoesConfirmToChunk(fqenWithChunk: EventName) {
|
|
138
|
+
this.emitter.assertEventHonorsChunkingSignature(fqenWithChunk)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private static assertSignatureDoesNotConformToChunk(fqen: EventName) {
|
|
142
|
+
assert.doesThrow(() =>
|
|
143
|
+
this.emitter.assertEventHonorsChunkingSignature(fqen)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static buildSignatureWithPayloadFields(fields: SchemaFieldsByName) {
|
|
148
|
+
return this.buildSignature(
|
|
149
|
+
buildSchema({
|
|
150
|
+
id: generateId(),
|
|
151
|
+
fields,
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private static assertDidEmitItemsThrows(items: { id: string }[]) {
|
|
157
|
+
assert.doesThrow(() => this.emitter.assertEmittedItems(items))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private static async emit() {
|
|
161
|
+
return this.emitWithItems([this.generateItem()])
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MercuryTestClient } from '@sprucelabs/mercury-client'
|
|
2
|
+
import { EventName, EventSignaturesByName } from '@sprucelabs/mercury-types'
|
|
3
|
+
import { Schema } from '@sprucelabs/schema'
|
|
4
|
+
import { buildEmitTargetAndPayloadSchema } from '@sprucelabs/spruce-event-utils'
|
|
5
|
+
import { AbstractSpruceFixtureTest } from '@sprucelabs/spruce-test-fixtures'
|
|
6
|
+
import { generateId } from '@sprucelabs/test-utils'
|
|
7
|
+
import { ChunkingEmitter } from '../../chunkingEmitter/ChunkingEmitter'
|
|
8
|
+
|
|
9
|
+
export default abstract class AbstractChunkingEmitterTest extends AbstractSpruceFixtureTest {
|
|
10
|
+
protected static emitter: ChunkingEmitter
|
|
11
|
+
protected static fqen: EventName
|
|
12
|
+
protected static payloadKey: string
|
|
13
|
+
|
|
14
|
+
protected static async beforeEach() {
|
|
15
|
+
await super.beforeEach()
|
|
16
|
+
this.payloadKey = 'items'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
protected static mixinEventSignatures(
|
|
20
|
+
eventSignatures: EventSignaturesByName
|
|
21
|
+
) {
|
|
22
|
+
const client = this.fakedClient as MercuryTestClient
|
|
23
|
+
client.mixinContract({
|
|
24
|
+
eventSignatures,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected static buildSignature(payload: Schema, target?: Schema) {
|
|
29
|
+
return {
|
|
30
|
+
isGlobal: true,
|
|
31
|
+
emitPayloadSchema: buildEmitTargetAndPayloadSchema({
|
|
32
|
+
eventName: this.fqen,
|
|
33
|
+
payloadSchema: payload,
|
|
34
|
+
targetSchema: target,
|
|
35
|
+
}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected static async emitWithItems(
|
|
40
|
+
items: Record<string, unknown>[],
|
|
41
|
+
target?: Record<string, any>
|
|
42
|
+
) {
|
|
43
|
+
await this.emitter.emit({
|
|
44
|
+
eventName: this.fqen,
|
|
45
|
+
items,
|
|
46
|
+
payloadKey: this.payloadKey,
|
|
47
|
+
target,
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
protected static generateItem() {
|
|
52
|
+
return { id: generateId() }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { MercuryClient } from '@sprucelabs/mercury-client'
|
|
2
|
+
import { EventName } from '@sprucelabs/mercury-types'
|
|
3
|
+
import { assertOptions } from '@sprucelabs/schema'
|
|
4
|
+
import { buildLog } from '@sprucelabs/spruce-skill-utils'
|
|
5
|
+
|
|
6
|
+
export default class ChunkingEmitterImpl {
|
|
7
|
+
private client: MercuryClient
|
|
8
|
+
protected chunkSize: number
|
|
9
|
+
private log = buildLog('ChunkingEmitter')
|
|
10
|
+
public static Class?: new (options: ChunkingEmitterOptions) => ChunkingEmitter
|
|
11
|
+
private totalErrors = 0
|
|
12
|
+
|
|
13
|
+
protected constructor(options: ChunkingEmitterOptions) {
|
|
14
|
+
const { client, chunkSize } = assertOptions(options, ['client'])
|
|
15
|
+
this.client = client
|
|
16
|
+
this.chunkSize = chunkSize ?? 10
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public static async Emitter(options: ChunkingEmitterOptions) {
|
|
20
|
+
assertOptions(options, ['client'])
|
|
21
|
+
return new (this.Class ?? this)(options)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async emit(options: ChunkingEmitterEmitOptions) {
|
|
25
|
+
const { eventName, items, payloadKey, target } = assertOptions(options, [
|
|
26
|
+
'eventName',
|
|
27
|
+
'items',
|
|
28
|
+
'payloadKey',
|
|
29
|
+
])
|
|
30
|
+
|
|
31
|
+
this.totalErrors = 0
|
|
32
|
+
const chunks = this.splitItemsIntoChunks(items)
|
|
33
|
+
let current = 0
|
|
34
|
+
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
try {
|
|
37
|
+
let targetAndPayload: Record<string, any> = {
|
|
38
|
+
payload: {
|
|
39
|
+
[payloadKey]: chunk,
|
|
40
|
+
chunk: {
|
|
41
|
+
current: current++,
|
|
42
|
+
total: chunks.length,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (target) {
|
|
48
|
+
targetAndPayload.target = target
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await this.client.emitAndFlattenResponses(
|
|
52
|
+
eventName as EventName,
|
|
53
|
+
targetAndPayload
|
|
54
|
+
)
|
|
55
|
+
} catch (err: any) {
|
|
56
|
+
this.log.error('Failed to emit chunk', err)
|
|
57
|
+
this.totalErrors++
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static reset() {
|
|
63
|
+
this.Class = undefined
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private splitItemsIntoChunks(items: Record<string, any>[]) {
|
|
67
|
+
const chunks: Record<string, any>[][] = []
|
|
68
|
+
|
|
69
|
+
let index = 0
|
|
70
|
+
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
if (!chunks[index]) {
|
|
73
|
+
chunks[index] = []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (chunks[index].length === this.chunkSize) {
|
|
77
|
+
index++
|
|
78
|
+
chunks[index] = []
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
chunks[index].push(item)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return chunks
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public getTotalErrors(): number {
|
|
88
|
+
return this.totalErrors
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface ChunkingEmitterOptions {
|
|
93
|
+
client: MercuryClient
|
|
94
|
+
chunkSize?: number
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
type ChunkingEmitterEmitOptions = {
|
|
98
|
+
eventName: EventName
|
|
99
|
+
items: Record<string, any>[]
|
|
100
|
+
payloadKey: string
|
|
101
|
+
target?: Record<string, any>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ChunkingEmitter {
|
|
105
|
+
emit(options: ChunkingEmitterEmitOptions): Promise<void>
|
|
106
|
+
getTotalErrors(): number
|
|
107
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { MercuryTestClient } from '@sprucelabs/mercury-client'
|
|
2
|
+
import { EventName } from '@sprucelabs/mercury-types'
|
|
3
|
+
import { assert } from '@sprucelabs/test-utils'
|
|
4
|
+
import { chunkFieldDefinition } from './chunkFieldDefinition'
|
|
5
|
+
import { ChunkingEmitter } from './ChunkingEmitter'
|
|
6
|
+
|
|
7
|
+
export default class MockChunkingEmitter implements ChunkingEmitter {
|
|
8
|
+
private didEmit = false
|
|
9
|
+
private emittedEventName?: EventName
|
|
10
|
+
private emittedItems?: Record<string, unknown>[]
|
|
11
|
+
private emittedPayloadKey?: string
|
|
12
|
+
|
|
13
|
+
public async emit(options: {
|
|
14
|
+
eventName: EventName
|
|
15
|
+
items: Record<string, unknown>[]
|
|
16
|
+
payloadKey: string
|
|
17
|
+
}): Promise<void> {
|
|
18
|
+
const { eventName, items, payloadKey } = options
|
|
19
|
+
this.didEmit = true
|
|
20
|
+
this.emittedEventName = eventName
|
|
21
|
+
this.emittedItems = items
|
|
22
|
+
this.emittedPayloadKey = payloadKey
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public assertDidEmitPayloadKey(payloadKey: string) {
|
|
26
|
+
this.assertDidEmit()
|
|
27
|
+
assert.isEqual(
|
|
28
|
+
this.emittedPayloadKey,
|
|
29
|
+
payloadKey,
|
|
30
|
+
`I expected chunkingEmitter.emit() with payloadKey '${payloadKey}'! But you emitted ${
|
|
31
|
+
this.emittedPayloadKey || 'nothing'
|
|
32
|
+
}`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public getTotalErrors(): number {
|
|
37
|
+
return 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public assertEmittedItems(items: Record<string, any>[]) {
|
|
41
|
+
this.assertDidEmit()
|
|
42
|
+
assert.isEqualDeep(
|
|
43
|
+
this.emittedItems,
|
|
44
|
+
items,
|
|
45
|
+
'You did not chunkingEmitter.emit(...) the expected items!!'
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public assertDidEmit() {
|
|
50
|
+
assert.isTrue(
|
|
51
|
+
this.didEmit,
|
|
52
|
+
`You did not ever call chunkingEmitter.emit(...). That is your next step!`
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public assertDidEmitEventNamed(fqen: EventName) {
|
|
57
|
+
this.assertDidEmit()
|
|
58
|
+
assert.isEqual(
|
|
59
|
+
this.emittedEventName,
|
|
60
|
+
fqen,
|
|
61
|
+
`I expected chunkingEmitter to emit '${fqen}'! But you emitted ${
|
|
62
|
+
this.emittedEventName || 'nothing'
|
|
63
|
+
}`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public assertEventHonorsChunkingSignature(fqen: EventName) {
|
|
68
|
+
const emitter = MercuryTestClient.getInternalEmitter()
|
|
69
|
+
const contract = emitter.getContract()
|
|
70
|
+
const payload = contract.eventSignatures[fqen]?.emitPayloadSchema?.fields
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
const chunkField = payload?.payload?.options?.schema?.fields?.chunk
|
|
73
|
+
assert.isTruthy(
|
|
74
|
+
chunkField,
|
|
75
|
+
'Your event does not conform to the chunking signature. Please add a chunk field to your event payload. Add a field to your payload called chunk: chunkFieldDefinition()'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
assert.isEqualDeep(
|
|
79
|
+
chunkField,
|
|
80
|
+
chunkFieldDefinition(),
|
|
81
|
+
'Your chunk field is there but not properly formed. Use chunkFieldDefinition() to define it.'
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FieldDefinitions } from '@sprucelabs/schema'
|
|
2
|
+
import chunkPagingSchema from '#spruce/schemas/chunkingEmitter/v2023_10_21/chunkPaging.schema'
|
|
3
|
+
|
|
4
|
+
export function chunkFieldDefinition() {
|
|
5
|
+
return {
|
|
6
|
+
type: 'schema',
|
|
7
|
+
isRequired: true,
|
|
8
|
+
options: {
|
|
9
|
+
schema: chunkPagingSchema,
|
|
10
|
+
},
|
|
11
|
+
} as FieldDefinitions
|
|
12
|
+
}
|
package/src/index.ts
ADDED