data-primals-engine 1.2.1 → 1.2.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.
@@ -13,9 +13,12 @@ import crypto from "node:crypto";
13
13
  import * as tar from "tar";
14
14
  import {promisify} from "node:util";
15
15
  import {calculateTotalUserStorageUsage, hasPermission} from "./user.js";
16
+ import {Logger} from "../gameObject.js";
17
+ import {deleteFromS3, getUserS3Config, uploadToS3} from "./bucket.js";
16
18
 
17
19
  const pbkdf2Async = promisify(crypto.pbkdf2);
18
20
 
21
+ let engine, logger;
19
22
  const fsPromises = fs.promises;
20
23
 
21
24
  // Encryption settings
@@ -64,78 +67,116 @@ export const addFile = async (file, user) => {
64
67
  throw new Error(i18n.t("api.data.serverStorageFull", "Le serveur a atteint sa capacité de stockage maximale. Veuillez réessayer plus tard."));
65
68
  }
66
69
 
67
- const collection = getCollection("files");
68
-
69
70
  // Générer un GUID pour le fichier
70
71
  const guid = uuidv4();
71
- const filename = guid + path.extname(file.originalname || file.name); // Préserver l'extension
72
- const filepath = path.join(process.cwd(), 'uploads', 'private', filename);
73
-
74
- try {
75
- // Enregistrer le fichier sur le serveur
76
- await fsPromises.mkdir(path.join(process.cwd(), 'uploads', 'private'), {recursive: true});
77
- await fsPromises.writeFile(filepath, file.buffer || fs.readFileSync(file.path)); // Utiliser buffer si disponible
78
- if( file.path && fs.existsSync(file.path) ){
79
- fs.unlinkSync(file.path);
72
+ const s3Config = await getUserS3Config(user);
73
+ const extension = getFileExtension(file.name);
74
+ const newFilename = `${guid}.${extension}`;
75
+
76
+ const fileData = {
77
+ guid: guid,
78
+ filename: file.name,
79
+ size: file.size,
80
+ mimeType: file.type,
81
+ createdAt: new Date(),
82
+ user: user.username,
83
+ mainUser: user._user
84
+ };
85
+
86
+ if (s3Config && s3Config.bucketName && s3Config.accessKeyId && s3Config.secretAccessKey) { // Correction: bucketName au lieu de bucket
87
+ try {
88
+ // Correction: Appel manquant à la fonction de téléversement
89
+ await uploadToS3(s3Config, file.path, newFilename);
90
+
91
+ fileData.storage = 's3';
92
+ fileData.filename = newFilename; // Le nom sur S3
93
+ logger.info(`Fichier ${newFilename} téléversé sur le bucket S3 ${s3Config.bucketName}.`);
94
+ } catch (error) {
95
+ logger.info(`Le téléversement S3 a échoué pour ${file.name}: ${error.message}`, 'error');
96
+ throw new Error("Le téléversement S3 a échoué.");
97
+ } finally {
98
+ // Nettoyer le fichier temporaire uploadé par express-formidable
99
+ await fsPromises.unlink(file.path).catch(e => logger.info(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
80
100
  }
101
+ } else {
102
+ // Sauvegarde locale
103
+ const uploadDir = path.join(process.cwd(), "uploads", "private");
104
+ if (!fs.existsSync(uploadDir)) {
105
+ fs.mkdirSync(uploadDir, { recursive: true });
106
+ }
107
+ const newPath = path.join(uploadDir, newFilename);
108
+
109
+ try {
110
+ // express-formidable place déjà le fichier dans un répertoire temporaire. Nous n'avons qu'éplacer.
111
+ await fsPromises.rename(file.path, newPath);
112
+ fileData.storage = 'local';
113
+ fileData.filename = newFilename; // Le nom dans le dossier uploads
114
+ fileData.path = newPath; // Le chemin complet pour les fichiers locaux
115
+ logger.info(`Fichier ${newFilename} sauvegardé localement dans ${newPath}.`);
116
+ } catch (error) {
117
+ logger.info(`Le déplacement du fichier local a échoué pour ${file.name}: ${error.message}`, 'error');
118
+ // Essayer de nettoyer le fichier temporaire même si le renommage échoue
119
+ await fsPromises.unlink(file.path).catch(e => logger.info(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
120
+ throw new Error("Le stockage du fichier local a échoué.");
121
+ }
122
+ }
81
123
 
82
- // Enregistrer les métadonnées du fichier
83
- const fileMetadata = {
84
- guid,
85
- filename: file.originalname || file.name, // Conserver le nom original
86
- mimeType: file.type,
87
- size: file.size,
88
- mainUser: user._user,
89
- user: user.username,
90
- timestamp: new Date()
91
- };
92
-
93
- // Insérer le fichier dans une collection dédiée (par exemple, "privateFiles")
94
- const result = await collection.insertOne({...fileMetadata, _model: "privateFile"});
95
- if (!result.insertedId) throw new Error("Échec de l'indexation du fichier.");
124
+ const filesCollection = await getCollection("files");
125
+ await filesCollection.insertOne(fileData);
96
126
 
97
- return guid;
127
+ return guid;
128
+ };
98
129
 
99
- } catch (error) {
100
- // Nettoyage en cas d'erreur
101
- if (fs.existsSync(filepath)) {
102
- fs.unlinkSync(filepath);
103
- }
104
- throw new Error(`Erreur lors de l'ajout du fichier: ${error.message}`);
105
- }
130
+ /**
131
+ * Récupère les métadonnées d'un fichier depuis la base de données.
132
+ * @param {string} guid - Le GUID du fichier.
133
+ * @returns {Promise<object|null>} L'objet de métadonnées du fichier ou null si non trouvé.
134
+ */
135
+ export const getFile = async (guid) => {
136
+ const filesCollection = await getCollection("files");
137
+ return await filesCollection.findOne({ guid });
106
138
  };
107
139
 
108
140
  export const removeFile = async (guid, user) => {
109
141
  if (!guid) return false;
110
142
  if (!isGUID(guid)) throw new Error("Le GUID du fichier n'est pas valide.");
111
143
 
112
- const collection = getCollection("files");
113
-
114
- // Trouver le fichier et vérifier l'autorisation (propriétaire ou admin)
115
- const file = await collection.findOne({ guid });
116
- if (!file) throw new Error("Fichier non trouvé (" + guid + ")");
117
-
118
- if (user.username !== 'demo' && isLocalUser(user) && !await hasPermission(["API_ADMIN", "API_EDIT_DATA", "API_EDIT_DATA_privateFile", `API_EDIT_DATA_privateFile_${guid}`], user) ) {
119
- if (file._user !== (user._user || user.username)) {
120
- throw new Error("Vous n'êtes pas autorisé à supprimer ce fichier.");
121
- }
144
+ const fileData = await getFile(guid);
145
+ if (!fileData) {
146
+ logger.info(`Tentative de suppression d'un fichier inexistant avec le GUID : ${guid}`, 'warn');
147
+ return;
122
148
  }
123
149
 
124
- try {
125
- // Supprimer le fichier de la base de données
126
- const deleteResult = await collection.deleteOne({ _model: "privateFile", guid });
127
- if (deleteResult.deletedCount !== 1) throw new Error("Échec de la suppression de l'indexation du fichier.");
128
-
129
- // Supprimer le fichier du serveur
130
- const filepath = path.join(process.cwd(), 'uploads', 'private', guid)+'.'+getFileExtension(file.filename);
131
- if (fs.existsSync(filepath)) {
132
- fs.unlinkSync(filepath);
150
+ if (fileData.storage === 's3') {
151
+ const s3Config = await getUserS3Config(user);
152
+ if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
153
+ try {
154
+ await deleteFromS3(s3Config, fileData.filename);
155
+ logger.info(`Fichier ${fileData.filename} supprimé du bucket S3 ${s3Config.bucketName}.`);
156
+ } catch (error) {
157
+ logger.info(`La suppression S3 a échoué pour ${fileData.filename}: ${error.message}`, 'error');
158
+ throw new Error("La suppression S3 a échoué.");
159
+ }
160
+ } else {
161
+ logger.info(`Configuration S3 non trouvée pour l'utilisateur, impossible de supprimer le fichier ${fileData.filename} de S3.`, 'error');
162
+ throw new Error("Configuration S3 non trouvée, impossible de supprimer le fichier.");
163
+ }
164
+ } else if (fileData.storage === 'local') {
165
+ try {
166
+ if (fileData.path && fs.existsSync(fileData.path)) {
167
+ await fsPromises.unlink(fileData.path);
168
+ logger.info(`Fichier local ${fileData.path} supprimé.`);
169
+ } else {
170
+ logger.info(`Fichier local non trouvé au chemin ${fileData.path}, mais suppression de l'enregistrement en BDD.`, 'warn');
171
+ }
172
+ } catch (error) {
173
+ logger.info(`La suppression du fichier local a échoué pour ${fileData.path}: ${error.message}`, 'error');
174
+ throw new Error("La suppression du fichier local a échoué.");
133
175
  }
134
-
135
- return { success: true, message: "Fichier supprimé avec succès." };
136
- } catch (error) {
137
- throw new Error(`Erreur lors de la suppression du fichier: ${error.message}`);
138
176
  }
177
+
178
+ const filesCollection = await getCollection("files");
179
+ await filesCollection.deleteOne({ guid });
139
180
  };
140
181
 
141
182
 
@@ -190,7 +231,7 @@ export async function decryptFile(filePath, password) {
190
231
  }
191
232
  }
192
233
 
193
- let engine;
194
234
  export async function onInit(defaultEngine) {
195
235
  engine = defaultEngine;
236
+ logger = engine.getComponent(Logger);
196
237
  }
@@ -0,0 +1,147 @@
1
+ // src/modules/file.js
2
+ import { getCollection } from "./mongodb.js";
3
+ import { uuidv4, getFileExtension } from "../core.js";
4
+ import { getUserS3Config, uploadToS3, deleteFromS3 } from "./bucket.js";
5
+ import { maxFileSize } from "../constants.js";
6
+ import path from "node:path";
7
+ import fs from "node:fs";
8
+ import { promises as fsPromises } from "node:fs";
9
+ import { Logger } from "../gameObject.js";
10
+
11
+ let logger;
12
+
13
+ const getLogger = () => {
14
+ if (!logger) {
15
+ logger = new Logger();
16
+ }
17
+ return logger;
18
+ };
19
+
20
+ /**
21
+ * Ajoute un fichier au système. Le téléverse sur S3 si configuré, sinon le sauvegarde localement.
22
+ * @param {object} file - L'objet fichier provenant de express-formidable.
23
+ * @param {object} user - L'objet utilisateur.
24
+ * @returns {Promise<string>} Le GUID du fichier nouvellement ajouté.
25
+ */
26
+ export const addFile = async (file, user) => {
27
+ if (!file) {
28
+ throw new Error("Aucun fichier fourni.");
29
+ }
30
+ if (file.size > maxFileSize) {
31
+ throw new Error(`La taille du fichier dépasse la limite de ${maxFileSize / 1024 / 1024} Mo.`);
32
+ }
33
+
34
+ const guid = uuidv4();
35
+ const extension = getFileExtension(file.name);
36
+ const newFilename = `${guid}.${extension}`;
37
+ const s3Config = await getUserS3Config(user);
38
+
39
+ const fileData = {
40
+ _id: guid,
41
+ guid: guid,
42
+ originalFilename: file.name,
43
+ size: file.size,
44
+ mimetype: file.type,
45
+ createdAt: new Date(),
46
+ user: user.username // Bonne pratique : garder une trace de l'uploader
47
+ };
48
+
49
+ if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
50
+ try {
51
+ // Correction: Appel manquant à la fonction de téléversement
52
+ await uploadToS3(s3Config, file.path, newFilename);
53
+
54
+ fileData.storage = 's3';
55
+ fileData.filename = newFilename; // Le nom sur S3
56
+ getLogger().log(`Fichier ${newFilename} téléversé sur le bucket S3 ${s3Config.bucketName}.`);
57
+ } catch (error) {
58
+ getLogger().log(`Le téléversement S3 a échoué pour ${file.name}: ${error.message}`, 'error');
59
+ throw new Error("Le téléversement S3 a échoué.");
60
+ } finally {
61
+ // Nettoyer le fichier temporaire uploadé par express-formidable
62
+ await fsPromises.unlink(file.path).catch(e => getLogger().log(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
63
+ }
64
+ } else {
65
+ // Sauvegarde locale
66
+ const uploadDir = path.join(process.cwd(), "uploads");
67
+ if (!fs.existsSync(uploadDir)) {
68
+ fs.mkdirSync(uploadDir, { recursive: true });
69
+ }
70
+ const newPath = path.join(uploadDir, newFilename);
71
+
72
+ try {
73
+ // express-formidable place déjà le fichier dans un répertoire temporaire. Nous n'avons qu'éplacer.
74
+ await fsPromises.rename(file.path, newPath);
75
+ fileData.storage = 'local';
76
+ fileData.filename = newFilename; // Le nom dans le dossier uploads
77
+ fileData.path = newPath; // Le chemin complet pour les fichiers locaux
78
+ getLogger().log(`Fichier ${newFilename} sauvegardé localement dans ${newPath}.`);
79
+ } catch (error) {
80
+ getLogger().log(`Le déplacement du fichier local a échoué pour ${file.name}: ${error.message}`, 'error');
81
+ // Essayer de nettoyer le fichier temporaire même si le renommage échoue
82
+ await fsPromises.unlink(file.path).catch(e => getLogger().log(`Échec de la suppression du fichier temporaire ${file.path}: ${e.message}`, 'error'));
83
+ throw new Error("Le stockage du fichier local a échoué.");
84
+ }
85
+ }
86
+
87
+ const filesCollection = await getCollection("files");
88
+ await filesCollection.insertOne(fileData);
89
+
90
+ return guid;
91
+ };
92
+
93
+ /**
94
+ * Récupère les métadonnées d'un fichier depuis la base de données.
95
+ * @param {string} guid - Le GUID du fichier.
96
+ * @returns {Promise<object|null>} L'objet de métadonnées du fichier ou null si non trouvé.
97
+ */
98
+ export const getFile = async (guid) => {
99
+ const filesCollection = await getCollection("files");
100
+ return await filesCollection.findOne({ guid });
101
+ };
102
+
103
+ /**
104
+ * Supprime un fichier du système (S3 ou local) et son enregistrement en base de données.
105
+ * @param {string} guid - Le GUID du fichier à supprimer.
106
+ * @param {object} user - L'objet utilisateur.
107
+ * @returns {Promise<void>}
108
+ */
109
+ export const deleteFile = async (guid, user) => {
110
+ const fileData = await getFile(guid);
111
+ if (!fileData) {
112
+ getLogger().log(`Tentative de suppression d'un fichier inexistant avec le GUID : ${guid}`, 'warn');
113
+ return;
114
+ }
115
+
116
+ if (fileData.storage === 's3') {
117
+ const s3Config = await getUserS3Config(user);
118
+ if (s3Config && s3Config.bucketName) { // Correction: bucketName au lieu de bucket
119
+ try {
120
+ await deleteFromS3(s3Config, fileData.filename);
121
+ getLogger().log(`Fichier ${fileData.filename} supprimé du bucket S3 ${s3Config.bucketName}.`);
122
+ } catch (error) {
123
+ getLogger().log(`La suppression S3 a échoué pour ${fileData.filename}: ${error.message}`, 'error');
124
+ throw new Error("La suppression S3 a échoué.");
125
+ }
126
+ } else {
127
+ getLogger().log(`Configuration S3 non trouvée pour l'utilisateur, impossible de supprimer le fichier ${fileData.filename} de S3.`, 'error');
128
+ throw new Error("Configuration S3 non trouvée, impossible de supprimer le fichier.");
129
+ }
130
+ } else if (fileData.storage === 'local') {
131
+ try {
132
+ if (fileData.path && fs.existsSync(fileData.path)) {
133
+ await fsPromises.unlink(fileData.path);
134
+ getLogger().log(`Fichier local ${fileData.path} supprimé.`);
135
+ } else {
136
+ getLogger().log(`Fichier local non trouvé au chemin ${fileData.path}, mais suppression de l'enregistrement en BDD.`, 'warn');
137
+ }
138
+ } catch (error) {
139
+ getLogger().log(`La suppression du fichier local a échoué pour ${fileData.path}: ${error.message}`, 'error');
140
+ throw new Error("La suppression du fichier local a échoué.");
141
+ }
142
+ }
143
+
144
+ const filesCollection = await getCollection("files");
145
+ await filesCollection.deleteOne({ guid });
146
+ getLogger().log(`Enregistrement du fichier ${guid} supprimé de la base de données.`);
147
+ };
@@ -70,7 +70,7 @@ beforeAll(async () => {
70
70
  fs.readdirSync(backupDir).forEach(file => {
71
71
  fs.unlinkSync(path.join(backupDir, file));
72
72
  });
73
- vi.stubEnv('S3_CONFIG_ENCRYPTION_KEY', '00000000000000000000000000000000');
73
+ vi.stubEnv('ENCRYPTION_KEY', '00000000000000000000000000000000');
74
74
  vi.stubEnv('OPENAI_API_KEY', '00000000000000000000000000000000');
75
75
  // You might need to create a model first if your dumpUserData requires it
76
76
  await createModel(testModelDefinition);
@@ -0,0 +1,202 @@
1
+ // events.test.js
2
+ import { Event } from '../src/events.js';
3
+ import {vitest} from "vitest";
4
+ import {expect, describe, it, beforeEach, beforeAll, afterAll} from 'vitest';
5
+
6
+ describe('Event System', () => {
7
+ beforeEach(() => {
8
+ // Clear all events before each test to ensure a clean state
9
+ for (const system in Event) {
10
+ if (Event.hasOwnProperty(system) && typeof Event[system] === 'object') {
11
+ for (const eventName in Event[system]) {
12
+ if (Event[system].hasOwnProperty(eventName)) {
13
+ delete Event[system][eventName];
14
+ }
15
+ }
16
+ }
17
+ }
18
+ });
19
+
20
+ it('should allow listening for and triggering an event', () => {
21
+ const mockCallback = vitest.fn();
22
+ const eventName = 'testEvent';
23
+
24
+ Event.Listen(eventName, mockCallback);
25
+ Event.Trigger(eventName);
26
+
27
+ expect(mockCallback).toHaveBeenCalledTimes(1);
28
+ });
29
+ it('should pass arguments to the event callback', () => {
30
+ const mockCallback = vitest.fn();
31
+ const eventName = 'testEventWithArgs';
32
+ const arg1 = 'hello';
33
+ const arg2 = 123;
34
+
35
+ Event.Listen(eventName, mockCallback, "priority", "medium");
36
+ Event.Trigger(eventName, "priority", "medium", arg1, arg2);
37
+
38
+ expect(mockCallback).toHaveBeenCalledTimes(1);
39
+ expect(mockCallback).toHaveBeenCalledWith(arg1, arg2);
40
+ });
41
+
42
+ it('should allow removing a specific callback', () => {
43
+ const mockCallback1 = vitest.fn();
44
+ const mockCallback2 = vitest.fn();
45
+ const eventName = 'testEventRemoveCallback';
46
+
47
+ Event.Listen(eventName, mockCallback1);
48
+ Event.Listen(eventName, mockCallback2);
49
+ Event.RemoveCallback(eventName, mockCallback1);
50
+ Event.Trigger(eventName);
51
+
52
+ expect(mockCallback1).not.toHaveBeenCalled();
53
+ expect(mockCallback2).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it('should not trigger a callback if the event is not listened to', () => {
57
+ const mockCallback = vitest.fn();
58
+ const eventName = 'nonExistentEvent';
59
+
60
+ Event.Trigger(eventName);
61
+
62
+ expect(mockCallback).not.toHaveBeenCalled();
63
+ });
64
+
65
+ it('should handle different systems and layers', () => {
66
+ const mockCallbackPriority = vitest.fn();
67
+ const mockCallbackLog = vitest.fn();
68
+ const eventName = 'testEventMultiSystem';
69
+
70
+ Event.Listen(eventName, mockCallbackPriority, 'priority', 'high');
71
+ Event.Listen(eventName, mockCallbackLog, 'log', 'info');
72
+
73
+ Event.Trigger(eventName, 'priority', 'high');
74
+ expect(mockCallbackPriority).toHaveBeenCalledTimes(1);
75
+ expect(mockCallbackLog).not.toHaveBeenCalled();
76
+
77
+ vitest.clearAllMocks(); // Reset mock counts
78
+
79
+ Event.Trigger(eventName, 'log', 'info');
80
+ expect(mockCallbackPriority).not.toHaveBeenCalled();
81
+ expect(mockCallbackLog).toHaveBeenCalledTimes(1);
82
+ });
83
+
84
+ it('should throw an error if an invalid system or layer is used with Listen', () => {
85
+ expect(() => {
86
+ Event.Listen('invalidEvent', () => {}, 'invalidSystem', 'invalidLayer');
87
+ }).toThrowError(/System 'invalidSystem' does not exist./);
88
+
89
+ expect(() => {
90
+ Event.Listen('invalidEvent', () => {}, 'priority', 'invalidLayer');
91
+ }).toThrowError(/Layer 'invalidLayer' does not exist/);
92
+ });
93
+
94
+ describe('Event.Trigger result merging', () => {
95
+ it('should merge objects from multiple callbacks using spread operator', () => {
96
+ const eventName = 'mergeObjectEvent';
97
+ const callback1 = vitest.fn().mockReturnValue({ a: 1, b: 2 });
98
+ const callback2 = vitest.fn().mockReturnValue({ b: 3, c: 4 }); // b will be overwritten
99
+
100
+ Event.Listen(eventName, callback1);
101
+ Event.Listen(eventName, callback2);
102
+
103
+ const result = Event.Trigger(eventName);
104
+
105
+ expect(result).toEqual({ a: 1, b: 3, c: 4 });
106
+ });
107
+
108
+ it('should concatenate arrays from multiple callbacks', () => {
109
+ const eventName = 'mergeArrayEvent';
110
+ const callback1 = vitest.fn().mockReturnValue([1, 2]);
111
+ const callback2 = vitest.fn().mockReturnValue([3, 4]);
112
+
113
+ Event.Listen(eventName, callback1);
114
+ Event.Listen(eventName, callback2);
115
+
116
+ const result = Event.Trigger(eventName);
117
+
118
+ expect(result).toEqual([1, 2, 3, 4]);
119
+ });
120
+
121
+ it('should concatenate strings from multiple callbacks', () => {
122
+ const eventName = 'mergeStringEvent';
123
+ const callback1 = vitest.fn().mockReturnValue('Hello ');
124
+ const callback2 = vitest.fn().mockReturnValue('World');
125
+
126
+ Event.Listen(eventName, callback1);
127
+ Event.Listen(eventName, callback2);
128
+
129
+ const result = Event.Trigger(eventName);
130
+
131
+ expect(result).toBe('Hello World');
132
+ });
133
+
134
+ it('should add numbers from multiple callbacks', () => {
135
+ const eventName = 'mergeNumberEvent';
136
+ const callback1 = vitest.fn().mockReturnValue(10);
137
+ const callback2 = vitest.fn().mockReturnValue(20);
138
+
139
+ Event.Listen(eventName, callback1);
140
+ Event.Listen(eventName, callback2);
141
+
142
+ const result = Event.Trigger(eventName);
143
+
144
+ expect(result).toBe(30);
145
+ });
146
+
147
+ it('should perform a logical AND on booleans from multiple callbacks', () => {
148
+ const eventName = 'mergeBooleanEvent';
149
+
150
+ // Case 1: true && true -> true
151
+ const cb_true1 = vitest.fn().mockReturnValue(true);
152
+ const cb_true2 = vitest.fn().mockReturnValue(true);
153
+ Event.Listen(eventName, cb_true1);
154
+ Event.Listen(eventName, cb_true2);
155
+ expect(Event.Trigger(eventName)).toBe(true);
156
+ Event.RemoveAllCallbacks(eventName); // Clean up for next case
157
+
158
+ // Case 2: true && false -> false
159
+ const cb_false1 = vitest.fn().mockReturnValue(false);
160
+ Event.Listen(eventName, cb_true1);
161
+ Event.Listen(eventName, cb_false1);
162
+ expect(Event.Trigger(eventName)).toBe(false);
163
+ Event.RemoveAllCallbacks(eventName);
164
+
165
+ // Case 3: false && true -> false
166
+ Event.Listen(eventName, cb_false1);
167
+ Event.Listen(eventName, cb_true1);
168
+ expect(Event.Trigger(eventName)).toBe(false);
169
+ });
170
+
171
+ it('should handle mixed-type return values', () => {
172
+ const eventName = 'mergeMixedEvent';
173
+ const callback1 = vitest.fn().mockReturnValue(100); // number
174
+ const callback2 = vitest.fn().mockReturnValue({ a: 1 }); // object
175
+
176
+ Event.Listen(eventName, callback1);
177
+ Event.Listen(eventName, callback2);
178
+
179
+ const result = Event.Trigger(eventName);
180
+
181
+ // The logic initializes `ret` with the first value (100).
182
+ // When the second callback returns an object, it re-initializes `ret` to {}
183
+ // and merges the object. The final result is the object.
184
+ expect(result).toEqual({ a: 1 });
185
+ });
186
+
187
+ it('should ignore null and undefined return values while merging', () => {
188
+ const eventName = 'ignoreNullsEvent';
189
+ const callback1 = vitest.fn().mockReturnValue(null);
190
+ const callback2 = vitest.fn().mockReturnValue({ data: 'important' });
191
+ const callback3 = vitest.fn().mockReturnValue(undefined);
192
+
193
+ Event.Listen(eventName, callback1);
194
+ Event.Listen(eventName, callback2);
195
+ Event.Listen(eventName, callback3);
196
+
197
+ const result = Event.Trigger(eventName);
198
+
199
+ expect(result).toEqual({ data: 'important' });
200
+ });
201
+ });
202
+ });