@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.
Files changed (43) hide show
  1. package/.circleci/config.yml +71 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +3 -0
  4. package/.githooks/pre-push +17 -0
  5. package/.nvmrc +1 -0
  6. package/.vscode/launch.json +58 -0
  7. package/.vscode/settings.json +70 -0
  8. package/.vscode/tasks.json +112 -0
  9. package/CHANGELOG.md +16 -0
  10. package/README.md +24 -0
  11. package/package.json +84 -0
  12. package/release.config.js +7 -0
  13. package/src/.spruce/features/permission.plugin.ts +1 -0
  14. package/src/.spruce/permissions/permissions.types.ts +9 -0
  15. package/src/.spruce/schemas/chunkingEmitter/v2023_10_21/chunkPaging.schema.ts +29 -0
  16. package/src/.spruce/schemas/fields/fieldClassMap.ts +6 -0
  17. package/src/.spruce/schemas/fields/fields.types.ts +1 -0
  18. package/src/.spruce/schemas/schemas.types.ts +52 -0
  19. package/src/.spruce/schemas/spruce/v2020_07_22/choice.schema.ts +1 -0
  20. package/src/.spruce/schemas/spruce/v2020_07_22/link.schema.ts +1 -0
  21. package/src/.spruce/schemas/spruce/v2020_07_22/location.schema.ts +1 -0
  22. package/src/.spruce/schemas/spruce/v2020_07_22/message.schema.ts +1 -0
  23. package/src/.spruce/schemas/spruce/v2020_07_22/messageSource.schema.ts +1 -0
  24. package/src/.spruce/schemas/spruce/v2020_07_22/messageTarget.schema.ts +1 -0
  25. package/src/.spruce/schemas/spruce/v2020_07_22/organization.schema.ts +1 -0
  26. package/src/.spruce/schemas/spruce/v2020_07_22/person.schema.ts +1 -0
  27. package/src/.spruce/schemas/spruce/v2020_07_22/personLocation.schema.ts +1 -0
  28. package/src/.spruce/schemas/spruce/v2020_07_22/personOrganization.schema.ts +1 -0
  29. package/src/.spruce/schemas/spruce/v2020_07_22/role.schema.ts +1 -0
  30. package/src/.spruce/schemas/spruce/v2020_07_22/sendMessage.schema.ts +1 -0
  31. package/src/.spruce/schemas/spruce/v2020_07_22/skill.schema.ts +1 -0
  32. package/src/.spruce/schemas/spruce/v2020_07_22/skillCreator.schema.ts +1 -0
  33. package/src/.spruce/settings.json +14 -0
  34. package/src/__tests__/behavioral/ChunkingEmitter.test.ts +290 -0
  35. package/src/__tests__/behavioral/TestingChunkingEmitter.test.ts +163 -0
  36. package/src/__tests__/support/AbstractChunkingEmitterTest.ts +54 -0
  37. package/src/chunkingEmitter/ChunkingEmitter.ts +107 -0
  38. package/src/chunkingEmitter/MockChunkingEmitter.ts +84 -0
  39. package/src/chunkingEmitter/chunkFieldDefinition.ts +12 -0
  40. package/src/index.ts +3 -0
  41. package/src/schemas/v2023_10_21/chunkPaging.builder.ts +16 -0
  42. package/tsconfig.dist.json +36 -0
  43. 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
@@ -0,0 +1,3 @@
1
+ import { ChunkingEmitter } from './chunkingEmitter/ChunkingEmitter'
2
+ export default ChunkingEmitter
3
+ export { default as MockChunkingEmitter } from './chunkingEmitter/MockChunkingEmitter'