bunsane 0.1.0 → 0.1.2

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 (82) hide show
  1. package/.github/workflows/deploy-docs.yml +57 -0
  2. package/LICENSE.md +1 -1
  3. package/README.md +2 -28
  4. package/TODO.md +8 -1
  5. package/bun.lock +3 -0
  6. package/config/upload.config.ts +135 -0
  7. package/core/App.ts +168 -4
  8. package/core/ArcheType.ts +122 -0
  9. package/core/BatchLoader.ts +100 -0
  10. package/core/ComponentRegistry.ts +4 -3
  11. package/core/Components.ts +2 -2
  12. package/core/Decorators.ts +15 -8
  13. package/core/Entity.ts +193 -14
  14. package/core/EntityCache.ts +15 -0
  15. package/core/EntityHookManager.ts +855 -0
  16. package/core/EntityManager.ts +12 -2
  17. package/core/ErrorHandler.ts +64 -7
  18. package/core/FileValidator.ts +284 -0
  19. package/core/Query.ts +503 -85
  20. package/core/RequestContext.ts +24 -0
  21. package/core/RequestLoaders.ts +89 -0
  22. package/core/SchedulerManager.ts +710 -0
  23. package/core/UploadManager.ts +261 -0
  24. package/core/components/UploadComponent.ts +206 -0
  25. package/core/decorators/EntityHooks.ts +190 -0
  26. package/core/decorators/ScheduledTask.ts +83 -0
  27. package/core/events/EntityLifecycleEvents.ts +177 -0
  28. package/core/processors/ImageProcessor.ts +423 -0
  29. package/core/storage/LocalStorageProvider.ts +290 -0
  30. package/core/storage/StorageProvider.ts +112 -0
  31. package/database/DatabaseHelper.ts +183 -58
  32. package/database/index.ts +5 -5
  33. package/database/sqlHelpers.ts +7 -0
  34. package/docs/README.md +149 -0
  35. package/docs/_coverpage.md +36 -0
  36. package/docs/_sidebar.md +23 -0
  37. package/docs/api/core.md +568 -0
  38. package/docs/api/hooks.md +554 -0
  39. package/docs/api/index.md +222 -0
  40. package/docs/api/query.md +678 -0
  41. package/docs/api/service.md +744 -0
  42. package/docs/core-concepts/archetypes.md +512 -0
  43. package/docs/core-concepts/components.md +498 -0
  44. package/docs/core-concepts/entity.md +314 -0
  45. package/docs/core-concepts/hooks.md +683 -0
  46. package/docs/core-concepts/query.md +588 -0
  47. package/docs/core-concepts/services.md +647 -0
  48. package/docs/examples/code-examples.md +425 -0
  49. package/docs/getting-started.md +337 -0
  50. package/docs/index.html +97 -0
  51. package/gql/Generator.ts +58 -35
  52. package/gql/decorators/Upload.ts +176 -0
  53. package/gql/helpers.ts +67 -0
  54. package/gql/index.ts +65 -31
  55. package/gql/types.ts +1 -1
  56. package/index.ts +79 -11
  57. package/package.json +19 -10
  58. package/rest/Generator.ts +3 -0
  59. package/rest/index.ts +22 -0
  60. package/service/Service.ts +1 -1
  61. package/service/ServiceRegistry.ts +10 -6
  62. package/service/index.ts +12 -1
  63. package/tests/bench/insert.bench.ts +59 -0
  64. package/tests/bench/relations.bench.ts +269 -0
  65. package/tests/bench/sorting.bench.ts +415 -0
  66. package/tests/component-hooks.test.ts +1409 -0
  67. package/tests/component.test.ts +338 -0
  68. package/tests/errorHandling.test.ts +155 -0
  69. package/tests/hooks.test.ts +666 -0
  70. package/tests/query-sorting.test.ts +101 -0
  71. package/tests/relations.test.ts +169 -0
  72. package/tests/scheduler.test.ts +724 -0
  73. package/tsconfig.json +35 -34
  74. package/types/graphql.types.ts +87 -0
  75. package/types/hooks.types.ts +141 -0
  76. package/types/scheduler.types.ts +165 -0
  77. package/types/upload.types.ts +184 -0
  78. package/upload/index.ts +140 -0
  79. package/utils/UploadHelper.ts +305 -0
  80. package/utils/cronParser.ts +366 -0
  81. package/utils/errorMessages.ts +151 -0
  82. package/core/Events.ts +0 -0
@@ -0,0 +1,1409 @@
1
+ import { describe, test, expect, beforeEach, afterEach, beforeAll } from "bun:test";
2
+ import { Entity } from "../core/Entity";
3
+ import hookManager from "../core/EntityHookManager";
4
+ import { EntityCreatedEvent, EntityUpdatedEvent, EntityDeletedEvent } from "../core/events/EntityLifecycleEvents";
5
+ import { BaseComponent, CompData, Component } from "../core/Components";
6
+ import App from "../core/App";
7
+ import { ComponentTargetHook, registerDecoratedHooks } from "../core/decorators/EntityHooks";
8
+ import ArcheType from "../core/ArcheType";
9
+
10
+ let app: App;
11
+
12
+ // Test components
13
+ @Component
14
+ class UserTag extends BaseComponent {
15
+ @CompData()
16
+ userType: string = "regular";
17
+ }
18
+
19
+ @Component
20
+ class AdminTag extends BaseComponent {
21
+ @CompData()
22
+ adminLevel: number = 1;
23
+ }
24
+
25
+ @Component
26
+ class TemporaryTag extends BaseComponent {
27
+ @CompData()
28
+ expiresAt: string = "";
29
+ }
30
+
31
+ @Component
32
+ class PostTag extends BaseComponent {
33
+ @CompData()
34
+ category: string = "general";
35
+ }
36
+
37
+ // Archetype test components
38
+ @Component
39
+ class NameComponent extends BaseComponent {
40
+ @CompData()
41
+ value: string = "";
42
+ }
43
+
44
+ @Component
45
+ class EmailComponent extends BaseComponent {
46
+ @CompData()
47
+ value: string = "";
48
+ }
49
+
50
+ @Component
51
+ class AddressComponent extends BaseComponent {
52
+ @CompData()
53
+ street: string = "";
54
+ city: string = "";
55
+ }
56
+
57
+ // Define archetypes for testing
58
+ const UserArchetype = new ArcheType([UserTag, NameComponent, EmailComponent]);
59
+ const AdminArchetype = new ArcheType([AdminTag, NameComponent, EmailComponent]);
60
+ const PostArchetype = new ArcheType([PostTag]);
61
+
62
+ beforeAll(async () => {
63
+ app = new App();
64
+ await app.waitForAppReady();
65
+ });
66
+
67
+ describe('Component-Specific Hook Targeting - Phase 1', () => {
68
+ beforeEach(() => {
69
+ hookManager.clearAllHooks();
70
+ });
71
+
72
+ afterEach(() => {
73
+ hookManager.clearAllHooks();
74
+ });
75
+
76
+ test('should execute hook only for entities with specific included component', async () => {
77
+ let hookExecuted = false;
78
+ let executedEntityId: string = "";
79
+
80
+ // Register hook that only executes for entities with UserTag
81
+ const hookId = hookManager.registerEntityHook("entity.created",
82
+ (event: EntityCreatedEvent) => {
83
+ hookExecuted = true;
84
+ executedEntityId = event.getEntity().id;
85
+ },
86
+ {
87
+ componentTarget: {
88
+ includeComponents: [UserTag]
89
+ }
90
+ }
91
+ );
92
+
93
+ expect(hookId).toBeDefined();
94
+
95
+ // Create entity with UserTag - hook should execute
96
+ const userEntity = Entity.Create();
97
+ userEntity.add(UserTag, { userType: "premium" });
98
+ await userEntity.save();
99
+
100
+ expect(hookExecuted).toBe(true);
101
+ expect(executedEntityId).toBe(userEntity.id);
102
+
103
+ // Reset for next test
104
+ hookExecuted = false;
105
+ executedEntityId = "";
106
+
107
+ // Create entity with different component - hook should NOT execute
108
+ const postEntity = Entity.Create();
109
+ postEntity.add(PostTag, { category: "news" });
110
+ await postEntity.save();
111
+
112
+ expect(hookExecuted).toBe(false);
113
+ expect(executedEntityId).toBe("");
114
+ });
115
+
116
+ test('should execute hook only for entities without specific excluded component', async () => {
117
+ let hookExecuted = false;
118
+ let executedEntityId: string = "";
119
+
120
+ // Register hook that executes for entities WITHOUT TemporaryTag
121
+ const hookId = hookManager.registerEntityHook("entity.created",
122
+ (event: EntityCreatedEvent) => {
123
+ hookExecuted = true;
124
+ executedEntityId = event.getEntity().id;
125
+ },
126
+ {
127
+ componentTarget: {
128
+ excludeComponents: [TemporaryTag]
129
+ }
130
+ }
131
+ );
132
+
133
+ expect(hookId).toBeDefined();
134
+
135
+ // Create entity without TemporaryTag - hook should execute
136
+ const userEntity = Entity.Create();
137
+ userEntity.add(UserTag, { userType: "regular" });
138
+ await userEntity.save();
139
+
140
+ expect(hookExecuted).toBe(true);
141
+ expect(executedEntityId).toBe(userEntity.id);
142
+
143
+ // Reset for next test
144
+ hookExecuted = false;
145
+ executedEntityId = "";
146
+
147
+ // Create entity with TemporaryTag - hook should NOT execute
148
+ const tempEntity = Entity.Create();
149
+ tempEntity.add(UserTag, { userType: "temp" });
150
+ tempEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
151
+ await tempEntity.save();
152
+
153
+ expect(hookExecuted).toBe(false);
154
+ expect(executedEntityId).toBe("");
155
+ });
156
+
157
+ test('should execute hook with both include and exclude component targeting', async () => {
158
+ let hookExecuted = false;
159
+ let executedEntityId: string = "";
160
+
161
+ // Register hook for entities with UserTag but WITHOUT TemporaryTag
162
+ const hookId = hookManager.registerEntityHook("entity.created",
163
+ (event: EntityCreatedEvent) => {
164
+ hookExecuted = true;
165
+ executedEntityId = event.getEntity().id;
166
+ },
167
+ {
168
+ componentTarget: {
169
+ includeComponents: [UserTag],
170
+ excludeComponents: [TemporaryTag]
171
+ }
172
+ }
173
+ );
174
+
175
+ expect(hookId).toBeDefined();
176
+
177
+ // Create entity with UserTag but no TemporaryTag - hook should execute
178
+ const userEntity = Entity.Create();
179
+ userEntity.add(UserTag, { userType: "regular" });
180
+ await userEntity.save();
181
+
182
+ expect(hookExecuted).toBe(true);
183
+ expect(executedEntityId).toBe(userEntity.id);
184
+
185
+ // Reset for next test
186
+ hookExecuted = false;
187
+ executedEntityId = "";
188
+
189
+ // Create entity with UserTag AND TemporaryTag - hook should NOT execute
190
+ const tempUserEntity = Entity.Create();
191
+ tempUserEntity.add(UserTag, { userType: "temp" });
192
+ tempUserEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
193
+ await tempUserEntity.save();
194
+
195
+ expect(hookExecuted).toBe(false);
196
+ expect(executedEntityId).toBe("");
197
+
198
+ // Reset for next test
199
+ hookExecuted = false;
200
+ executedEntityId = "";
201
+
202
+ // Create entity with only TemporaryTag - hook should NOT execute
203
+ const tempEntity = Entity.Create();
204
+ tempEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
205
+ await tempEntity.save();
206
+
207
+ expect(hookExecuted).toBe(false);
208
+ expect(executedEntityId).toBe("");
209
+ });
210
+
211
+ test('should execute hook with OR logic for multiple included components', async () => {
212
+ let hookExecuted = false;
213
+ let executedEntityId: string = "";
214
+
215
+ // Register hook for entities with UserTag OR AdminTag (requireAllIncluded = false)
216
+ const hookId = hookManager.registerEntityHook("entity.created",
217
+ (event: EntityCreatedEvent) => {
218
+ hookExecuted = true;
219
+ executedEntityId = event.getEntity().id;
220
+ },
221
+ {
222
+ componentTarget: {
223
+ includeComponents: [UserTag, AdminTag],
224
+ requireAllIncluded: false // OR logic
225
+ }
226
+ }
227
+ );
228
+
229
+ expect(hookId).toBeDefined();
230
+
231
+ // Create entity with UserTag - hook should execute
232
+ const userEntity = Entity.Create();
233
+ userEntity.add(UserTag, { userType: "regular" });
234
+ await userEntity.save();
235
+
236
+ expect(hookExecuted).toBe(true);
237
+ expect(executedEntityId).toBe(userEntity.id);
238
+
239
+ // Reset for next test
240
+ hookExecuted = false;
241
+ executedEntityId = "";
242
+
243
+ // Create entity with AdminTag - hook should execute
244
+ const adminEntity = Entity.Create();
245
+ adminEntity.add(AdminTag, { adminLevel: 2 });
246
+ await adminEntity.save();
247
+
248
+ expect(hookExecuted).toBe(true);
249
+ expect(executedEntityId).toBe(adminEntity.id);
250
+
251
+ // Reset for next test
252
+ hookExecuted = false;
253
+ executedEntityId = "";
254
+
255
+ // Create entity with neither component - hook should NOT execute
256
+ const postEntity = Entity.Create();
257
+ postEntity.add(PostTag, { category: "news" });
258
+ await postEntity.save();
259
+
260
+ expect(hookExecuted).toBe(false);
261
+ expect(executedEntityId).toBe("");
262
+ });
263
+
264
+ test('should execute hook with AND logic for multiple included components', async () => {
265
+ let hookExecuted = false;
266
+ let executedEntityId: string = "";
267
+
268
+ // Register hook for entities with BOTH UserTag AND AdminTag (requireAllIncluded = true, default)
269
+ const hookId = hookManager.registerEntityHook("entity.created",
270
+ (event: EntityCreatedEvent) => {
271
+ hookExecuted = true;
272
+ executedEntityId = event.getEntity().id;
273
+ },
274
+ {
275
+ componentTarget: {
276
+ includeComponents: [UserTag, AdminTag],
277
+ requireAllIncluded: true // AND logic (default)
278
+ }
279
+ }
280
+ );
281
+
282
+ expect(hookId).toBeDefined();
283
+
284
+ // Create entity with both UserTag and AdminTag - hook should execute
285
+ const superUserEntity = Entity.Create();
286
+ superUserEntity.add(UserTag, { userType: "admin" });
287
+ superUserEntity.add(AdminTag, { adminLevel: 3 });
288
+ await superUserEntity.save();
289
+
290
+ expect(hookExecuted).toBe(true);
291
+ expect(executedEntityId).toBe(superUserEntity.id);
292
+
293
+ // Reset for next test
294
+ hookExecuted = false;
295
+ executedEntityId = "";
296
+
297
+ // Create entity with only UserTag - hook should NOT execute
298
+ const userEntity = Entity.Create();
299
+ userEntity.add(UserTag, { userType: "regular" });
300
+ await userEntity.save();
301
+
302
+ expect(hookExecuted).toBe(false);
303
+ expect(executedEntityId).toBe("");
304
+
305
+ // Reset for next test
306
+ hookExecuted = false;
307
+ executedEntityId = "";
308
+
309
+ // Create entity with only AdminTag - hook should NOT execute
310
+ const adminEntity = Entity.Create();
311
+ adminEntity.add(AdminTag, { adminLevel: 2 });
312
+ await adminEntity.save();
313
+
314
+ expect(hookExecuted).toBe(false);
315
+ expect(executedEntityId).toBe("");
316
+ });
317
+
318
+ test('should execute hook without component targeting (backward compatibility)', async () => {
319
+ let hookExecuted = false;
320
+ let executedEntityId: string = "";
321
+
322
+ // Register hook without component targeting (should work for all entities)
323
+ const hookId = hookManager.registerEntityHook("entity.created",
324
+ (event: EntityCreatedEvent) => {
325
+ hookExecuted = true;
326
+ executedEntityId = event.getEntity().id;
327
+ }
328
+ );
329
+
330
+ expect(hookId).toBeDefined();
331
+
332
+ // Create entity with UserTag - hook should execute
333
+ const userEntity = Entity.Create();
334
+ userEntity.add(UserTag, { userType: "regular" });
335
+ await userEntity.save();
336
+
337
+ expect(hookExecuted).toBe(true);
338
+ expect(executedEntityId).toBe(userEntity.id);
339
+
340
+ // Reset for next test
341
+ hookExecuted = false;
342
+ executedEntityId = "";
343
+
344
+ // Create entity with different component - hook should still execute
345
+ const postEntity = Entity.Create();
346
+ postEntity.add(PostTag, { category: "news" });
347
+ await postEntity.save();
348
+
349
+ expect(hookExecuted).toBe(true);
350
+ expect(executedEntityId).toBe(postEntity.id);
351
+ });
352
+
353
+ test('should work with entity update events and component targeting', async () => {
354
+ let hookExecuted = false;
355
+ let executedEntityId: string = "";
356
+
357
+ // Register hook for entity updates on entities with UserTag
358
+ const hookId = hookManager.registerEntityHook("entity.updated",
359
+ (event: EntityUpdatedEvent) => {
360
+ hookExecuted = true;
361
+ executedEntityId = event.getEntity().id;
362
+ },
363
+ {
364
+ componentTarget: {
365
+ includeComponents: [UserTag]
366
+ }
367
+ }
368
+ );
369
+
370
+ expect(hookId).toBeDefined();
371
+
372
+ // Create and save entity first
373
+ const userEntity = Entity.Create();
374
+ userEntity.add(UserTag, { userType: "regular" });
375
+ await userEntity.save();
376
+
377
+ // Reset for update test
378
+ hookExecuted = false;
379
+ executedEntityId = "";
380
+
381
+ // Update the entity - hook should execute
382
+ await userEntity.set(UserTag, { userType: "premium" });
383
+ await userEntity.save();
384
+
385
+ expect(hookExecuted).toBe(true);
386
+ expect(executedEntityId).toBe(userEntity.id);
387
+
388
+ // Reset for next test
389
+ hookExecuted = false;
390
+ executedEntityId = "";
391
+
392
+ // Create and update entity without UserTag - hook should NOT execute
393
+ const postEntity = Entity.Create();
394
+ postEntity.add(PostTag, { category: "news" });
395
+ await postEntity.save();
396
+
397
+ await postEntity.set(PostTag, { category: "breaking" });
398
+ await postEntity.save();
399
+
400
+ expect(hookExecuted).toBe(false);
401
+ expect(executedEntityId).toBe("");
402
+ });
403
+ });
404
+
405
+ describe('ComponentTargetHook Decorator - Phase 2', () => {
406
+ beforeEach(() => {
407
+ hookManager.clearAllHooks();
408
+ });
409
+
410
+ afterEach(() => {
411
+ hookManager.clearAllHooks();
412
+ });
413
+
414
+ test('should register and execute decorated hook with include components', async () => {
415
+ let hookExecuted = false;
416
+ let capturedEntityId: string = "";
417
+
418
+ class TestUserService {
419
+ @ComponentTargetHook("entity.created", {
420
+ includeComponents: [UserTag]
421
+ })
422
+ async handleUserCreated(event: EntityCreatedEvent) {
423
+ hookExecuted = true;
424
+ capturedEntityId = event.getEntity().id;
425
+ }
426
+ }
427
+
428
+ const service = new TestUserService();
429
+ registerDecoratedHooks(service);
430
+
431
+ // Create entity with UserTag - hook should execute
432
+ const userEntity = Entity.Create();
433
+ userEntity.add(UserTag, { userType: "premium" });
434
+ await userEntity.save();
435
+
436
+ expect(hookExecuted).toBe(true);
437
+ expect(capturedEntityId).toBe(userEntity.id);
438
+
439
+ // Reset for next test
440
+ hookExecuted = false;
441
+ capturedEntityId = "";
442
+
443
+ // Create entity with different component - hook should NOT execute
444
+ const postEntity = Entity.Create();
445
+ postEntity.add(PostTag, { category: "news" });
446
+ await postEntity.save();
447
+
448
+ expect(hookExecuted).toBe(false);
449
+ expect(capturedEntityId).toBe("");
450
+ });
451
+
452
+ test('should register and execute decorated hook with exclude components', async () => {
453
+ let hookExecuted = false;
454
+ let capturedEntityId: string = "";
455
+
456
+ class TestUserService {
457
+ @ComponentTargetHook("entity.created", {
458
+ excludeComponents: [TemporaryTag]
459
+ })
460
+ async handleNonTemporaryEntity(event: EntityCreatedEvent) {
461
+ hookExecuted = true;
462
+ capturedEntityId = event.getEntity().id;
463
+ }
464
+ }
465
+
466
+ const service = new TestUserService();
467
+ registerDecoratedHooks(service);
468
+
469
+ // Create entity without TemporaryTag - hook should execute
470
+ const userEntity = Entity.Create();
471
+ userEntity.add(UserTag, { userType: "regular" });
472
+ await userEntity.save();
473
+
474
+ expect(hookExecuted).toBe(true);
475
+ expect(capturedEntityId).toBe(userEntity.id);
476
+
477
+ // Reset for next test
478
+ hookExecuted = false;
479
+ capturedEntityId = "";
480
+
481
+ // Create entity with TemporaryTag - hook should NOT execute
482
+ const tempEntity = Entity.Create();
483
+ tempEntity.add(UserTag, { userType: "temp" });
484
+ tempEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
485
+ await tempEntity.save();
486
+
487
+ expect(hookExecuted).toBe(false);
488
+ expect(capturedEntityId).toBe("");
489
+ });
490
+
491
+ test('should register and execute decorated hook with combined include and exclude', async () => {
492
+ let hookExecuted = false;
493
+ let capturedEntityId: string = "";
494
+
495
+ class TestUserService {
496
+ @ComponentTargetHook("entity.created", {
497
+ includeComponents: [UserTag],
498
+ excludeComponents: [TemporaryTag]
499
+ })
500
+ async handlePermanentUser(event: EntityCreatedEvent) {
501
+ hookExecuted = true;
502
+ capturedEntityId = event.getEntity().id;
503
+ }
504
+ }
505
+
506
+ const service = new TestUserService();
507
+ registerDecoratedHooks(service);
508
+
509
+ // Create entity with UserTag but no TemporaryTag - hook should execute
510
+ const userEntity = Entity.Create();
511
+ userEntity.add(UserTag, { userType: "regular" });
512
+ await userEntity.save();
513
+
514
+ expect(hookExecuted).toBe(true);
515
+ expect(capturedEntityId).toBe(userEntity.id);
516
+
517
+ // Reset for next test
518
+ hookExecuted = false;
519
+ capturedEntityId = "";
520
+
521
+ // Create entity with UserTag AND TemporaryTag - hook should NOT execute
522
+ const tempUserEntity = Entity.Create();
523
+ tempUserEntity.add(UserTag, { userType: "temp" });
524
+ tempUserEntity.add(TemporaryTag, { expiresAt: "2025-12-31" });
525
+ await tempUserEntity.save();
526
+
527
+ expect(hookExecuted).toBe(false);
528
+ expect(capturedEntityId).toBe("");
529
+ });
530
+
531
+ test('should register and execute decorated hook with OR logic', async () => {
532
+ let hookExecuted = false;
533
+ let capturedEntityId: string = "";
534
+
535
+ class TestMultiComponentService {
536
+ @ComponentTargetHook("entity.created", {
537
+ includeComponents: [UserTag, AdminTag],
538
+ requireAllIncluded: false // OR logic
539
+ })
540
+ async handleUserOrAdmin(event: EntityCreatedEvent) {
541
+ hookExecuted = true;
542
+ capturedEntityId = event.getEntity().id;
543
+ }
544
+ }
545
+
546
+ const service = new TestMultiComponentService();
547
+ registerDecoratedHooks(service);
548
+
549
+ // Create entity with UserTag - hook should execute
550
+ const userEntity = Entity.Create();
551
+ userEntity.add(UserTag, { userType: "regular" });
552
+ await userEntity.save();
553
+
554
+ expect(hookExecuted).toBe(true);
555
+ expect(capturedEntityId).toBe(userEntity.id);
556
+
557
+ // Reset for next test
558
+ hookExecuted = false;
559
+ capturedEntityId = "";
560
+
561
+ // Create entity with AdminTag - hook should execute
562
+ const adminEntity = Entity.Create();
563
+ adminEntity.add(AdminTag, { adminLevel: 2 });
564
+ await adminEntity.save();
565
+
566
+ expect(hookExecuted).toBe(true);
567
+ expect(capturedEntityId).toBe(adminEntity.id);
568
+ });
569
+
570
+ test('should register and execute decorated hook with AND logic', async () => {
571
+ let hookExecuted = false;
572
+ let capturedEntityId: string = "";
573
+
574
+ class TestSuperUserService {
575
+ @ComponentTargetHook("entity.created", {
576
+ includeComponents: [UserTag, AdminTag],
577
+ requireAllIncluded: true // AND logic (default)
578
+ })
579
+ async handleSuperUser(event: EntityCreatedEvent) {
580
+ hookExecuted = true;
581
+ capturedEntityId = event.getEntity().id;
582
+ }
583
+ }
584
+
585
+ const service = new TestSuperUserService();
586
+ registerDecoratedHooks(service);
587
+
588
+ // Create entity with both UserTag and AdminTag - hook should execute
589
+ const superUserEntity = Entity.Create();
590
+ superUserEntity.add(UserTag, { userType: "admin" });
591
+ superUserEntity.add(AdminTag, { adminLevel: 3 });
592
+ await superUserEntity.save();
593
+
594
+ expect(hookExecuted).toBe(true);
595
+ expect(capturedEntityId).toBe(superUserEntity.id);
596
+
597
+ // Reset for next test
598
+ hookExecuted = false;
599
+ capturedEntityId = "";
600
+
601
+ // Create entity with only UserTag - hook should NOT execute
602
+ const userEntity = Entity.Create();
603
+ userEntity.add(UserTag, { userType: "regular" });
604
+ await userEntity.save();
605
+
606
+ expect(hookExecuted).toBe(false);
607
+ expect(capturedEntityId).toBe("");
608
+ });
609
+
610
+ test('should work with entity update events using decorator', async () => {
611
+ let hookExecuted = false;
612
+ let capturedEntityId: string = "";
613
+
614
+ class TestUserUpdateService {
615
+ @ComponentTargetHook("entity.updated", {
616
+ includeComponents: [UserTag]
617
+ })
618
+ async handleUserUpdated(event: EntityUpdatedEvent) {
619
+ hookExecuted = true;
620
+ capturedEntityId = event.getEntity().id;
621
+ }
622
+ }
623
+
624
+ const service = new TestUserUpdateService();
625
+ registerDecoratedHooks(service);
626
+
627
+ // Create and save entity first
628
+ const userEntity = Entity.Create();
629
+ userEntity.add(UserTag, { userType: "regular" });
630
+ await userEntity.save();
631
+
632
+ // Reset for update test
633
+ hookExecuted = false;
634
+ capturedEntityId = "";
635
+
636
+ // Update the entity - hook should execute
637
+ await userEntity.set(UserTag, { userType: "premium" });
638
+ await userEntity.save();
639
+
640
+ expect(hookExecuted).toBe(true);
641
+ expect(capturedEntityId).toBe(userEntity.id);
642
+ });
643
+
644
+ test('should support multiple decorated hooks on same service', async () => {
645
+ let userHookExecuted = false;
646
+ let adminHookExecuted = false;
647
+ let userEntityId: string = "";
648
+ let adminEntityId: string = "";
649
+
650
+ class TestMultiHookService {
651
+ @ComponentTargetHook("entity.created", {
652
+ includeComponents: [UserTag]
653
+ })
654
+ async handleUserCreated(event: EntityCreatedEvent) {
655
+ userHookExecuted = true;
656
+ userEntityId = event.getEntity().id;
657
+ }
658
+
659
+ @ComponentTargetHook("entity.created", {
660
+ includeComponents: [AdminTag]
661
+ })
662
+ async handleAdminCreated(event: EntityCreatedEvent) {
663
+ adminHookExecuted = true;
664
+ adminEntityId = event.getEntity().id;
665
+ }
666
+ }
667
+
668
+ const service = new TestMultiHookService();
669
+ registerDecoratedHooks(service);
670
+
671
+ // Create user entity - only user hook should execute
672
+ const userEntity = Entity.Create();
673
+ userEntity.add(UserTag, { userType: "regular" });
674
+ await userEntity.save();
675
+
676
+ expect(userHookExecuted).toBe(true);
677
+ expect(adminHookExecuted).toBe(false);
678
+ expect(userEntityId).toBe(userEntity.id);
679
+
680
+ // Reset for next test
681
+ userHookExecuted = false;
682
+ adminHookExecuted = false;
683
+ userEntityId = "";
684
+ adminEntityId = "";
685
+
686
+ // Create admin entity - only admin hook should execute
687
+ const adminEntity = Entity.Create();
688
+ adminEntity.add(AdminTag, { adminLevel: 2 });
689
+ await adminEntity.save();
690
+
691
+ expect(userHookExecuted).toBe(false);
692
+ expect(adminHookExecuted).toBe(true);
693
+ expect(adminEntityId).toBe(adminEntity.id);
694
+ });
695
+
696
+ test('should support additional hook options with component targeting', async () => {
697
+ let hookExecuted = false;
698
+ let executionCount = 0;
699
+
700
+ class TestPriorityService {
701
+ @ComponentTargetHook("entity.created", {
702
+ includeComponents: [UserTag]
703
+ }, {
704
+ priority: 10,
705
+ name: "HighPriorityUserHook"
706
+ })
707
+ async handleUserCreated(event: EntityCreatedEvent) {
708
+ hookExecuted = true;
709
+ executionCount++;
710
+ }
711
+ }
712
+
713
+ const service = new TestPriorityService();
714
+ registerDecoratedHooks(service);
715
+
716
+ // Create entity with UserTag - hook should execute
717
+ const userEntity = Entity.Create();
718
+ userEntity.add(UserTag, { userType: "premium" });
719
+ await userEntity.save();
720
+
721
+ expect(hookExecuted).toBe(true);
722
+ expect(executionCount).toBe(1);
723
+ });
724
+ });
725
+
726
+ describe('Archetype-Based Component Targeting - Phase 2', () => {
727
+ beforeEach(() => {
728
+ hookManager.clearAllHooks();
729
+ });
730
+
731
+ afterEach(() => {
732
+ hookManager.clearAllHooks();
733
+ });
734
+
735
+ test('should execute hook for entities matching specific archetype', async () => {
736
+ let hookExecuted = false;
737
+ let capturedEntityId: string = "";
738
+
739
+ // Register hook that only executes for UserArchetype entities
740
+ const hookId = hookManager.registerEntityHook("entity.created",
741
+ (event: EntityCreatedEvent) => {
742
+ hookExecuted = true;
743
+ capturedEntityId = event.getEntity().id;
744
+ },
745
+ {
746
+ componentTarget: {
747
+ archetype: UserArchetype
748
+ }
749
+ }
750
+ );
751
+
752
+ expect(hookId).toBeDefined();
753
+
754
+ // Create entity matching UserArchetype - hook should execute
755
+ const userEntity = UserArchetype.fill({
756
+ userTag: { userType: "premium" },
757
+ nameComponent: { value: "John Doe" },
758
+ emailComponent: { value: "john@example.com" }
759
+ }).createEntity();
760
+ await userEntity.save();
761
+
762
+ expect(hookExecuted).toBe(true);
763
+ expect(capturedEntityId).toBe(userEntity.id);
764
+
765
+ // Reset for next test
766
+ hookExecuted = false;
767
+ capturedEntityId = "";
768
+
769
+ // Create entity with different archetype - hook should NOT execute
770
+ const postEntity = PostArchetype.fill({
771
+ postTag: { category: "news" }
772
+ }).createEntity();
773
+ await postEntity.save();
774
+
775
+ expect(hookExecuted).toBe(false);
776
+ expect(capturedEntityId).toBe("");
777
+
778
+ // Reset for next test
779
+ hookExecuted = false;
780
+ capturedEntityId = "";
781
+
782
+ // Create entity with UserArchetype but extra component - hook should NOT execute
783
+ const extendedUserEntity = UserArchetype.fill({
784
+ userTag: { userType: "regular" },
785
+ nameComponent: { value: "Jane Doe" },
786
+ emailComponent: { value: "jane@example.com" }
787
+ }).createEntity();
788
+ extendedUserEntity.add(AddressComponent, { street: "123 Main St", city: "Anytown" });
789
+ await extendedUserEntity.save();
790
+
791
+ expect(hookExecuted).toBe(false);
792
+ expect(capturedEntityId).toBe("");
793
+ });
794
+
795
+ test('should execute hook for entities matching any of multiple archetypes', async () => {
796
+ let hookExecuted = false;
797
+ let capturedEntityId: string = "";
798
+
799
+ // Register hook that executes for UserArchetype OR AdminArchetype entities
800
+ const hookId = hookManager.registerEntityHook("entity.created",
801
+ (event: EntityCreatedEvent) => {
802
+ hookExecuted = true;
803
+ capturedEntityId = event.getEntity().id;
804
+ },
805
+ {
806
+ componentTarget: {
807
+ archetypes: [UserArchetype, AdminArchetype]
808
+ }
809
+ }
810
+ );
811
+
812
+ expect(hookId).toBeDefined();
813
+
814
+ // Create entity matching UserArchetype - hook should execute
815
+ const userEntity = UserArchetype.fill({
816
+ userTag: { userType: "regular" },
817
+ nameComponent: { value: "John Doe" },
818
+ emailComponent: { value: "john@example.com" }
819
+ }).createEntity();
820
+ await userEntity.save();
821
+
822
+ expect(hookExecuted).toBe(true);
823
+ expect(capturedEntityId).toBe(userEntity.id);
824
+
825
+ // Reset for next test
826
+ hookExecuted = false;
827
+ capturedEntityId = "";
828
+
829
+ // Create entity matching AdminArchetype - hook should execute
830
+ const adminEntity = AdminArchetype.fill({
831
+ adminTag: { adminLevel: 2 },
832
+ nameComponent: { value: "Admin User" },
833
+ emailComponent: { value: "admin@example.com" }
834
+ }).createEntity();
835
+ await adminEntity.save();
836
+
837
+ expect(hookExecuted).toBe(true);
838
+ expect(capturedEntityId).toBe(adminEntity.id);
839
+
840
+ // Reset for next test
841
+ hookExecuted = false;
842
+ capturedEntityId = "";
843
+
844
+ // Create entity with different archetype - hook should NOT execute
845
+ const postEntity = PostArchetype.fill({
846
+ postTag: { category: "news" }
847
+ }).createEntity();
848
+ await postEntity.save();
849
+
850
+ expect(hookExecuted).toBe(false);
851
+ expect(capturedEntityId).toBe("");
852
+ });
853
+
854
+ test('should combine archetype and component targeting', async () => {
855
+ let hookExecuted = false;
856
+ let capturedEntityId: string = "";
857
+
858
+ // Register hook for UserArchetype entities with additional AdminTag
859
+ const hookId = hookManager.registerEntityHook("entity.created",
860
+ (event: EntityCreatedEvent) => {
861
+ hookExecuted = true;
862
+ capturedEntityId = event.getEntity().id;
863
+ },
864
+ {
865
+ componentTarget: {
866
+ archetype: UserArchetype,
867
+ includeComponents: [AdminTag] // Must also have AdminTag
868
+ }
869
+ }
870
+ );
871
+
872
+ expect(hookId).toBeDefined();
873
+
874
+ // Create entity matching UserArchetype with AdminTag - hook should execute
875
+ const superUserEntity = UserArchetype.fill({
876
+ userTag: { userType: "admin" },
877
+ nameComponent: { value: "Super User" },
878
+ emailComponent: { value: "super@example.com" }
879
+ }).createEntity();
880
+ superUserEntity.add(AdminTag, { adminLevel: 3 });
881
+ await superUserEntity.save();
882
+
883
+ expect(hookExecuted).toBe(true);
884
+ expect(capturedEntityId).toBe(superUserEntity.id);
885
+
886
+ // Reset for next test
887
+ hookExecuted = false;
888
+ capturedEntityId = "";
889
+
890
+ // Create entity matching UserArchetype without AdminTag - hook should NOT execute
891
+ const userEntity = UserArchetype.fill({
892
+ userTag: { userType: "regular" },
893
+ nameComponent: { value: "Regular User" },
894
+ emailComponent: { value: "regular@example.com" }
895
+ }).createEntity();
896
+ await userEntity.save();
897
+
898
+ expect(hookExecuted).toBe(false);
899
+ expect(capturedEntityId).toBe("");
900
+ });
901
+
902
+ test('should work with archetype targeting using decorator', async () => {
903
+ let hookExecuted = false;
904
+ let capturedEntityId: string = "";
905
+
906
+ class TestArchetypeService {
907
+ @ComponentTargetHook("entity.created", {
908
+ archetype: UserArchetype
909
+ })
910
+ async handleUserArchetype(event: EntityCreatedEvent) {
911
+ hookExecuted = true;
912
+ capturedEntityId = event.getEntity().id;
913
+ }
914
+ }
915
+
916
+ const service = new TestArchetypeService();
917
+ registerDecoratedHooks(service);
918
+
919
+ // Create entity matching UserArchetype - hook should execute
920
+ const userEntity = UserArchetype.fill({
921
+ userTag: { userType: "premium" },
922
+ nameComponent: { value: "John Doe" },
923
+ emailComponent: { value: "john@example.com" }
924
+ }).createEntity();
925
+ await userEntity.save();
926
+
927
+ expect(hookExecuted).toBe(true);
928
+ expect(capturedEntityId).toBe(userEntity.id);
929
+
930
+ // Reset for next test
931
+ hookExecuted = false;
932
+ capturedEntityId = "";
933
+
934
+ // Create entity with different archetype - hook should NOT execute
935
+ const postEntity = PostArchetype.fill({
936
+ postTag: { category: "news" }
937
+ }).createEntity();
938
+ await postEntity.save();
939
+
940
+ expect(hookExecuted).toBe(false);
941
+ expect(capturedEntityId).toBe("");
942
+ });
943
+
944
+ test('should work with multiple archetypes using decorator', async () => {
945
+ let hookExecuted = false;
946
+ let capturedEntityId: string = "";
947
+
948
+ class TestMultiArchetypeService {
949
+ @ComponentTargetHook("entity.created", {
950
+ archetypes: [UserArchetype, AdminArchetype]
951
+ })
952
+ async handleUserOrAdminArchetype(event: EntityCreatedEvent) {
953
+ hookExecuted = true;
954
+ capturedEntityId = event.getEntity().id;
955
+ }
956
+ }
957
+
958
+ const service = new TestMultiArchetypeService();
959
+ registerDecoratedHooks(service);
960
+
961
+ // Create entity matching UserArchetype - hook should execute
962
+ const userEntity = UserArchetype.fill({
963
+ userTag: { userType: "regular" },
964
+ nameComponent: { value: "John Doe" },
965
+ emailComponent: { value: "john@example.com" }
966
+ }).createEntity();
967
+ await userEntity.save();
968
+
969
+ expect(hookExecuted).toBe(true);
970
+ expect(capturedEntityId).toBe(userEntity.id);
971
+
972
+ // Reset for next test
973
+ hookExecuted = false;
974
+ capturedEntityId = "";
975
+
976
+ // Create entity matching AdminArchetype - hook should execute
977
+ const adminEntity = AdminArchetype.fill({
978
+ adminTag: { adminLevel: 2 },
979
+ nameComponent: { value: "Admin User" },
980
+ emailComponent: { value: "admin@example.com" }
981
+ }).createEntity();
982
+ await adminEntity.save();
983
+
984
+ expect(hookExecuted).toBe(true);
985
+ expect(capturedEntityId).toBe(adminEntity.id);
986
+ });
987
+ });
988
+
989
+ describe('Batch Processing Optimizations - Phase 2', () => {
990
+ beforeEach(() => {
991
+ hookManager.clearAllHooks();
992
+ });
993
+
994
+ afterEach(() => {
995
+ hookManager.clearAllHooks();
996
+ });
997
+
998
+ test('should efficiently process multiple events in batch with component targeting', async () => {
999
+ let executionCount = 0;
1000
+ let executedEntityIds: string[] = [];
1001
+
1002
+ // Register hook that only executes for entities with UserTag
1003
+ const hookId = hookManager.registerEntityHook("entity.created",
1004
+ (event: EntityCreatedEvent) => {
1005
+ executionCount++;
1006
+ executedEntityIds.push(event.getEntity().id);
1007
+ },
1008
+ {
1009
+ componentTarget: {
1010
+ includeComponents: [UserTag]
1011
+ }
1012
+ }
1013
+ );
1014
+
1015
+ expect(hookId).toBeDefined();
1016
+
1017
+ // Create multiple entities in batch
1018
+ const events: EntityCreatedEvent[] = [];
1019
+ const userEntities: Entity[] = [];
1020
+ const postEntities: Entity[] = [];
1021
+
1022
+ // Create 3 user entities and 2 post entities
1023
+ for (let i = 0; i < 3; i++) {
1024
+ const userEntity = Entity.Create();
1025
+ userEntity.add(UserTag, { userType: `user${i}` });
1026
+ userEntities.push(userEntity);
1027
+ events.push(new EntityCreatedEvent(userEntity));
1028
+ }
1029
+
1030
+ for (let i = 0; i < 2; i++) {
1031
+ const postEntity = Entity.Create();
1032
+ postEntity.add(PostTag, { category: `category${i}` });
1033
+ postEntities.push(postEntity);
1034
+ events.push(new EntityCreatedEvent(postEntity));
1035
+ }
1036
+
1037
+ // Execute hooks in batch
1038
+ await hookManager.executeHooksBatch(events);
1039
+
1040
+ // Hook should have executed only for user entities
1041
+ expect(executionCount).toBe(3);
1042
+ expect(executedEntityIds).toHaveLength(3);
1043
+
1044
+ // Verify the correct entities were processed
1045
+ const userEntityIds = userEntities.map(e => e.id);
1046
+ expect(executedEntityIds.sort()).toEqual(userEntityIds.sort());
1047
+ });
1048
+
1049
+ test('should optimize batch processing by pre-filtering hooks', async () => {
1050
+ let userHookExecuted = false;
1051
+ let adminHookExecuted = false;
1052
+ let postHookExecuted = false;
1053
+
1054
+ // Register multiple hooks with different component targeting
1055
+ const userHookId = hookManager.registerEntityHook("entity.created",
1056
+ (event: EntityCreatedEvent) => {
1057
+ userHookExecuted = true;
1058
+ },
1059
+ {
1060
+ componentTarget: {
1061
+ includeComponents: [UserTag]
1062
+ }
1063
+ }
1064
+ );
1065
+
1066
+ const adminHookId = hookManager.registerEntityHook("entity.created",
1067
+ (event: EntityCreatedEvent) => {
1068
+ adminHookExecuted = true;
1069
+ },
1070
+ {
1071
+ componentTarget: {
1072
+ includeComponents: [AdminTag]
1073
+ }
1074
+ }
1075
+ );
1076
+
1077
+ const postHookId = hookManager.registerEntityHook("entity.created",
1078
+ (event: EntityCreatedEvent) => {
1079
+ postHookExecuted = true;
1080
+ },
1081
+ {
1082
+ componentTarget: {
1083
+ includeComponents: [PostTag]
1084
+ }
1085
+ }
1086
+ );
1087
+
1088
+ // Create batch of events with only UserTag entities
1089
+ const events: EntityCreatedEvent[] = [];
1090
+ for (let i = 0; i < 3; i++) {
1091
+ const userEntity = Entity.Create();
1092
+ userEntity.add(UserTag, { userType: `user${i}` });
1093
+ events.push(new EntityCreatedEvent(userEntity));
1094
+ }
1095
+
1096
+ // Execute hooks in batch - only user hook should execute
1097
+ await hookManager.executeHooksBatch(events);
1098
+
1099
+ expect(userHookExecuted).toBe(true);
1100
+ expect(adminHookExecuted).toBe(false);
1101
+ expect(postHookExecuted).toBe(false);
1102
+ });
1103
+
1104
+ test('should handle mixed sync and async hooks efficiently in batch', async () => {
1105
+ let syncExecutionCount = 0;
1106
+ let asyncExecutionCount = 0;
1107
+ let syncEntityIds: string[] = [];
1108
+ let asyncEntityIds: string[] = [];
1109
+
1110
+ // Register sync hook
1111
+ const syncHookId = hookManager.registerEntityHook("entity.created",
1112
+ (event: EntityCreatedEvent) => {
1113
+ syncExecutionCount++;
1114
+ syncEntityIds.push(event.getEntity().id);
1115
+ },
1116
+ {
1117
+ componentTarget: {
1118
+ includeComponents: [UserTag]
1119
+ },
1120
+ async: false
1121
+ }
1122
+ );
1123
+
1124
+ // Register async hook
1125
+ const asyncHookId = hookManager.registerEntityHook("entity.created",
1126
+ async (event: EntityCreatedEvent) => {
1127
+ await new Promise(resolve => setTimeout(resolve, 1)); // Simulate async work
1128
+ asyncExecutionCount++;
1129
+ asyncEntityIds.push(event.getEntity().id);
1130
+ },
1131
+ {
1132
+ componentTarget: {
1133
+ includeComponents: [UserTag]
1134
+ },
1135
+ async: true
1136
+ }
1137
+ );
1138
+
1139
+ // Create batch of user entities
1140
+ const events: EntityCreatedEvent[] = [];
1141
+ const userEntities: Entity[] = [];
1142
+
1143
+ for (let i = 0; i < 3; i++) {
1144
+ const userEntity = Entity.Create();
1145
+ userEntity.add(UserTag, { userType: `user${i}` });
1146
+ userEntities.push(userEntity);
1147
+ events.push(new EntityCreatedEvent(userEntity));
1148
+ }
1149
+
1150
+ // Execute hooks in batch
1151
+ await hookManager.executeHooksBatch(events);
1152
+
1153
+ // Both hooks should have executed for all user entities
1154
+ expect(syncExecutionCount).toBe(3);
1155
+ expect(asyncExecutionCount).toBe(3);
1156
+
1157
+ const userEntityIds = userEntities.map(e => e.id).sort();
1158
+ expect(syncEntityIds.sort()).toEqual(userEntityIds);
1159
+ expect(asyncEntityIds.sort()).toEqual(userEntityIds);
1160
+ });
1161
+
1162
+ test('should maintain hook execution order in batch processing', async () => {
1163
+ let executionOrder: string[] = [];
1164
+
1165
+ // Register hooks with different priorities
1166
+ const highPriorityHookId = hookManager.registerEntityHook("entity.created",
1167
+ (event: EntityCreatedEvent) => {
1168
+ executionOrder.push(`high-${event.getEntity().id}`);
1169
+ },
1170
+ {
1171
+ componentTarget: {
1172
+ includeComponents: [UserTag]
1173
+ },
1174
+ priority: 10
1175
+ }
1176
+ );
1177
+
1178
+ const lowPriorityHookId = hookManager.registerEntityHook("entity.created",
1179
+ (event: EntityCreatedEvent) => {
1180
+ executionOrder.push(`low-${event.getEntity().id}`);
1181
+ },
1182
+ {
1183
+ componentTarget: {
1184
+ includeComponents: [UserTag]
1185
+ },
1186
+ priority: 1
1187
+ }
1188
+ );
1189
+
1190
+ // Create batch of user entities
1191
+ const events: EntityCreatedEvent[] = [];
1192
+ const userEntities: Entity[] = [];
1193
+
1194
+ for (let i = 0; i < 2; i++) {
1195
+ const userEntity = Entity.Create();
1196
+ userEntity.add(UserTag, { userType: `user${i}` });
1197
+ userEntities.push(userEntity);
1198
+ events.push(new EntityCreatedEvent(userEntity));
1199
+ }
1200
+
1201
+ // Execute hooks in batch
1202
+ await hookManager.executeHooksBatch(events);
1203
+
1204
+ // Verify execution order (high priority first)
1205
+ expect(executionOrder).toHaveLength(4);
1206
+ expect(executionOrder[0]).toMatch(/^high-/);
1207
+ expect(executionOrder[1]).toMatch(/^high-/);
1208
+ expect(executionOrder[2]).toMatch(/^low-/);
1209
+ expect(executionOrder[3]).toMatch(/^low-/);
1210
+ });
1211
+
1212
+ test('should handle archetype-based targeting efficiently in batch', async () => {
1213
+ let executionCount = 0;
1214
+ let executedEntityIds: string[] = [];
1215
+
1216
+ // Register hook for UserArchetype entities
1217
+ const hookId = hookManager.registerEntityHook("entity.created",
1218
+ (event: EntityCreatedEvent) => {
1219
+ executionCount++;
1220
+ executedEntityIds.push(event.getEntity().id);
1221
+ },
1222
+ {
1223
+ componentTarget: {
1224
+ archetype: UserArchetype
1225
+ }
1226
+ }
1227
+ );
1228
+
1229
+ // Create batch of mixed entities
1230
+ const events: EntityCreatedEvent[] = [];
1231
+ const userEntities: Entity[] = [];
1232
+ const adminEntities: Entity[] = [];
1233
+ const postEntities: Entity[] = [];
1234
+
1235
+ // Create 2 user archetype entities
1236
+ for (let i = 0; i < 2; i++) {
1237
+ const userEntity = UserArchetype.fill({
1238
+ userTag: { userType: `user${i}` },
1239
+ nameComponent: { value: `User ${i}` },
1240
+ emailComponent: { value: `user${i}@example.com` }
1241
+ }).createEntity();
1242
+ userEntities.push(userEntity);
1243
+ events.push(new EntityCreatedEvent(userEntity));
1244
+ }
1245
+
1246
+ // Create 1 admin archetype entity
1247
+ const adminEntity = AdminArchetype.fill({
1248
+ adminTag: { adminLevel: 2 },
1249
+ nameComponent: { value: "Admin User" },
1250
+ emailComponent: { value: "admin@example.com" }
1251
+ }).createEntity();
1252
+ adminEntities.push(adminEntity);
1253
+ events.push(new EntityCreatedEvent(adminEntity));
1254
+
1255
+ // Create 1 post entity
1256
+ const postEntity = PostArchetype.fill({
1257
+ postTag: { category: "news" }
1258
+ }).createEntity();
1259
+ postEntities.push(postEntity);
1260
+ events.push(new EntityCreatedEvent(postEntity));
1261
+
1262
+ // Execute hooks in batch
1263
+ await hookManager.executeHooksBatch(events);
1264
+
1265
+ // Hook should have executed only for user archetype entities
1266
+ expect(executionCount).toBe(2);
1267
+ expect(executedEntityIds).toHaveLength(2);
1268
+
1269
+ const userEntityIds = userEntities.map(e => e.id);
1270
+ expect(executedEntityIds.sort()).toEqual(userEntityIds.sort());
1271
+ });
1272
+
1273
+ test('should handle timeout and error scenarios in batch processing', async () => {
1274
+ let successCount = 0;
1275
+ let errorCount = 0;
1276
+
1277
+ // Register hook that succeeds
1278
+ const successHookId = hookManager.registerEntityHook("entity.created",
1279
+ (event: EntityCreatedEvent) => {
1280
+ successCount++;
1281
+ },
1282
+ {
1283
+ componentTarget: {
1284
+ includeComponents: [UserTag]
1285
+ }
1286
+ }
1287
+ );
1288
+
1289
+ // Register hook that throws error
1290
+ const errorHookId = hookManager.registerEntityHook("entity.created",
1291
+ (event: EntityCreatedEvent) => {
1292
+ errorCount++;
1293
+ throw new Error("Test error");
1294
+ },
1295
+ {
1296
+ componentTarget: {
1297
+ includeComponents: [UserTag]
1298
+ }
1299
+ }
1300
+ );
1301
+
1302
+ // Create batch of user entities
1303
+ const events: EntityCreatedEvent[] = [];
1304
+ for (let i = 0; i < 2; i++) {
1305
+ const userEntity = Entity.Create();
1306
+ userEntity.add(UserTag, { userType: `user${i}` });
1307
+ events.push(new EntityCreatedEvent(userEntity));
1308
+ }
1309
+
1310
+ // Execute hooks in batch - should handle errors gracefully
1311
+ await hookManager.executeHooksBatch(events);
1312
+
1313
+ // Success hook should have executed for both entities
1314
+ expect(successCount).toBe(2);
1315
+ // Error hook should have executed but thrown errors
1316
+ expect(errorCount).toBe(2);
1317
+ });
1318
+
1319
+ test('should optimize performance with large batches and component targeting', async () => {
1320
+ let executionCount = 0;
1321
+
1322
+ // Register hook with component targeting
1323
+ const hookId = hookManager.registerEntityHook("entity.created",
1324
+ (event: EntityCreatedEvent) => {
1325
+ executionCount++;
1326
+ },
1327
+ {
1328
+ componentTarget: {
1329
+ includeComponents: [UserTag]
1330
+ }
1331
+ }
1332
+ );
1333
+
1334
+ // Create large batch of mixed entities (100 total: 50 users, 50 posts)
1335
+ const events: EntityCreatedEvent[] = [];
1336
+ const userEntities: Entity[] = [];
1337
+
1338
+ for (let i = 0; i < 50; i++) {
1339
+ const userEntity = Entity.Create();
1340
+ userEntity.add(UserTag, { userType: `user${i}` });
1341
+ userEntities.push(userEntity);
1342
+ events.push(new EntityCreatedEvent(userEntity));
1343
+ }
1344
+
1345
+ for (let i = 0; i < 50; i++) {
1346
+ const postEntity = Entity.Create();
1347
+ postEntity.add(PostTag, { category: `category${i}` });
1348
+ events.push(new EntityCreatedEvent(postEntity));
1349
+ }
1350
+
1351
+ // Execute hooks in batch
1352
+ const startTime = performance.now();
1353
+ await hookManager.executeHooksBatch(events);
1354
+ const endTime = performance.now();
1355
+
1356
+ // Hook should have executed only for user entities
1357
+ expect(executionCount).toBe(50);
1358
+
1359
+ // Verify reasonable performance (should complete in reasonable time)
1360
+ const executionTime = endTime - startTime;
1361
+ expect(executionTime).toBeLessThan(1000); // Should complete in less than 1 second
1362
+ });
1363
+
1364
+ test('should work with decorator-based hooks in batch processing', async () => {
1365
+ let executionCount = 0;
1366
+ let executedEntityIds: string[] = [];
1367
+
1368
+ class TestBatchService {
1369
+ @ComponentTargetHook("entity.created", {
1370
+ includeComponents: [UserTag]
1371
+ })
1372
+ async handleUserCreated(event: EntityCreatedEvent) {
1373
+ executionCount++;
1374
+ executedEntityIds.push(event.getEntity().id);
1375
+ }
1376
+ }
1377
+
1378
+ const service = new TestBatchService();
1379
+ registerDecoratedHooks(service);
1380
+
1381
+ // Create batch of mixed entities
1382
+ const events: EntityCreatedEvent[] = [];
1383
+ const userEntities: Entity[] = [];
1384
+
1385
+ for (let i = 0; i < 3; i++) {
1386
+ const userEntity = Entity.Create();
1387
+ userEntity.add(UserTag, { userType: `user${i}` });
1388
+ userEntities.push(userEntity);
1389
+ events.push(new EntityCreatedEvent(userEntity));
1390
+ }
1391
+
1392
+ // Add non-matching entities
1393
+ for (let i = 0; i < 2; i++) {
1394
+ const postEntity = Entity.Create();
1395
+ postEntity.add(PostTag, { category: `category${i}` });
1396
+ events.push(new EntityCreatedEvent(postEntity));
1397
+ }
1398
+
1399
+ // Execute hooks in batch
1400
+ await hookManager.executeHooksBatch(events);
1401
+
1402
+ // Decorator hook should have executed only for user entities
1403
+ expect(executionCount).toBe(3);
1404
+ expect(executedEntityIds).toHaveLength(3);
1405
+
1406
+ const userEntityIds = userEntities.map(e => e.id);
1407
+ expect(executedEntityIds.sort()).toEqual(userEntityIds.sort());
1408
+ });
1409
+ });