@communecter/cocolight-api-client 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -5,6 +5,26 @@ import { createDefaultMultiServerTokenStorageStrategy } from "./utils/createDefa
5
5
  import { createDefaultTokenStorageStrategy } from "./utils/createDefaultTokenStorageStrategy.js";
6
6
  import { MultiServerTokenStorageStrategy } from "./utils/MultiServerTokenStorageStrategy.js";
7
7
  import OfflineClientManager from "./utils/OfflineClientManager.js";
8
+ import * as reactive from "./utils/reactive.js";
8
9
  import { TokenStorageStrategy } from "./utils/TokenStorage.js";
9
10
 
10
- export default { ApiClient, Api, error, tokenStorageStrategy: { createDefaultTokenStorageStrategy, TokenStorageStrategy, createDefaultMultiServerTokenStorageStrategy, MultiServerTokenStorageStrategy }, OfflineClientManager };
11
+ export default {
12
+ ApiClient,
13
+ Api,
14
+ error,
15
+
16
+ // Accès aux primitives réactives via .reactive
17
+ reactive,
18
+
19
+ // Accès direct à chaque primitive (reactive, effect, watch...) en racine
20
+ ...reactive,
21
+
22
+ tokenStorageStrategy: {
23
+ createDefaultTokenStorageStrategy,
24
+ TokenStorageStrategy,
25
+ createDefaultMultiServerTokenStorageStrategy,
26
+ MultiServerTokenStorageStrategy
27
+ },
28
+
29
+ OfflineClientManager
30
+ };
@@ -0,0 +1,279 @@
1
+ // reactive.js - Système maison combinant Signal + Proxy pour réactivité profonde et fine
2
+
3
+ import { ApiError } from "../error.js";
4
+
5
+ const effectStack = [];
6
+ const computedCache = new WeakMap();
7
+ const signalRegistry = new WeakMap();
8
+
9
+ /**
10
+ * Crée une fonction réactive qui se relance automatiquement
11
+ * lorsqu'une valeur réactive qu'elle utilise change.
12
+ * @param {Function} fn
13
+ * @returns {Function} le wrapper exécuté
14
+ */
15
+ export function effect(fn) {
16
+ const wrapper = () => {
17
+ try {
18
+ effectStack.push(wrapper);
19
+ fn();
20
+ } finally {
21
+ effectStack.pop();
22
+ }
23
+ };
24
+ wrapper();
25
+ return wrapper;
26
+ }
27
+
28
+ /**
29
+ * Crée un signal observable pour une valeur primitive ou complexe.
30
+ * @param {*} initialValue
31
+ * @returns {{value: *, subscribe: Function, __isSignal: boolean}}
32
+ */
33
+ function createSignal(initialValue) {
34
+ let value = initialValue;
35
+ const subscribers = new Set();
36
+
37
+ const signal = {
38
+ get value() {
39
+ const currentEffect = effectStack[effectStack.length - 1];
40
+ if (currentEffect) subscribers.add(currentEffect);
41
+ return value;
42
+ },
43
+ set value(newValue) {
44
+ if (value !== newValue) {
45
+ value = newValue;
46
+ subscribers.forEach(fn => fn());
47
+ }
48
+ },
49
+ subscribe(fn) {
50
+ subscribers.add(fn);
51
+ return () => subscribers.delete(fn);
52
+ },
53
+ __isSignal: true
54
+ };
55
+
56
+ return signal;
57
+ }
58
+
59
+ function isSignal(obj) {
60
+ return obj && obj.__isSignal === true;
61
+ }
62
+
63
+ function toRaw(obj) {
64
+ return isSignal(obj) ? obj.value : obj;
65
+ }
66
+
67
+ function _wrapArray(arr, path) {
68
+ const proxy = new Proxy(arr, {
69
+ get(target, prop) {
70
+ if (typeof prop === "string" && !isNaN(prop)) {
71
+ const val = target[prop];
72
+ return isReactive(val) ? val : reactive(val, path.concat(prop));
73
+ }
74
+ return Reflect.get(target, prop);
75
+ },
76
+ set(target, prop, value) {
77
+ const val = reactive(value, path.concat(prop));
78
+ target[prop] = val;
79
+ return true;
80
+ }
81
+ });
82
+ return proxy;
83
+ }
84
+
85
+ function _wrapReactive(obj, path = []) {
86
+ if (typeof obj !== "object" || obj === null) return obj;
87
+ if (Array.isArray(obj)) return _wrapArray(obj, path);
88
+ if (obj.__isReactive) return obj;
89
+
90
+ const signalMap = new Map();
91
+ const proxy = new Proxy(obj, {
92
+ // eslint-disable-next-line no-unused-vars
93
+ get(target, prop, receiver) {
94
+ if (prop === "__raw") return target;
95
+ if (prop === "__isReactive") return true;
96
+
97
+ if (!signalMap.has(prop)) {
98
+ const val = target[prop];
99
+ const reactiveVal = _wrapReactive(val, path.concat(prop));
100
+ const signal = createSignal(reactiveVal);
101
+ signalMap.set(prop, signal);
102
+ }
103
+
104
+ const signal = signalMap.get(prop);
105
+ return signal.value;
106
+ },
107
+
108
+ set(target, prop, value) {
109
+ const val = _wrapReactive(value, path.concat(prop));
110
+ if (!signalMap.has(prop)) {
111
+ const signal = createSignal(val);
112
+ signalMap.set(prop, signal);
113
+ }
114
+ signalMap.get(prop).value = val;
115
+ target[prop] = toRaw(val);
116
+ return true;
117
+ },
118
+
119
+ deleteProperty(target, prop) {
120
+ signalMap.delete(prop);
121
+ return Reflect.deleteProperty(target, prop);
122
+ }
123
+ });
124
+
125
+ signalRegistry.set(proxy, signalMap);
126
+ return proxy;
127
+ }
128
+
129
+ /**
130
+ * Rend un objet profondément réactif.
131
+ * @param {object} obj
132
+ * @returns {object}
133
+ */
134
+ export function reactive(obj) {
135
+ if (typeof obj !== "object" || obj === null) return obj;
136
+ if (obj.__isReactive) return obj;
137
+ return _wrapReactive(obj);
138
+ }
139
+
140
+ /**
141
+ * Teste si un objet est réactif.
142
+ * @param {*} obj
143
+ * @returns {boolean}
144
+ */
145
+ export function isReactive(obj) {
146
+ return !!obj?.__isReactive;
147
+ }
148
+
149
+ /**
150
+ * Crée une valeur dérivée automatiquement recalculée.
151
+ * @param {Function} fn
152
+ * @returns {{value: *}}
153
+ */
154
+ export function computed(fn) {
155
+ if (computedCache.has(fn)) return computedCache.get(fn);
156
+
157
+ const result = createSignal();
158
+ effect(() => result.value = fn());
159
+ computedCache.set(fn, result);
160
+ return result;
161
+ }
162
+
163
+ /**
164
+ * Observe les changements de valeur retournée par un getter.
165
+ * @param {Function} getter
166
+ * @param {Function} callback
167
+ */
168
+ export function watch(getter, callback) {
169
+ let oldValue;
170
+ effect(() => {
171
+ const newValue = getter();
172
+ if (newValue !== oldValue) {
173
+ callback(newValue, oldValue);
174
+ oldValue = newValue;
175
+ }
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Récupère les signaux internes d’un objet réactif.
181
+ * @param {object} obj
182
+ * @returns {Map|null}
183
+ */
184
+ export function getSignals(obj) {
185
+ if (!isReactive(obj)) return null;
186
+ return signalRegistry.get(obj);
187
+ }
188
+
189
+ // --- Helpers internes ---
190
+ function _getPathValue(obj, path) {
191
+ return path.split(".").reduce((o, k) => (o ? o[k] : undefined), obj);
192
+ }
193
+
194
+ function _setPathValue(obj, path, value) {
195
+ const keys = path.split(".");
196
+ const lastKey = keys.pop();
197
+ const parent = keys.reduce((o, k) => {
198
+ if (o[k] == null) o[k] = {};
199
+ return o[k];
200
+ }, obj);
201
+ parent[lastKey] = value;
202
+ }
203
+
204
+ function _deletePathValue(obj, path) {
205
+ const keys = path.split(".");
206
+ const lastKey = keys.pop();
207
+ const parent = keys.reduce((o, k) => o?.[k], obj);
208
+ if (parent && lastKey in parent) delete parent[lastKey];
209
+ }
210
+
211
+ function _applyTransformIfExists(path, val, entity) {
212
+ const topKey = path.split(".")[0];
213
+ const transform = entity.transforms?.[topKey];
214
+ return typeof transform === "function" ? transform(val, entity.serverData) : val;
215
+ }
216
+
217
+ /**
218
+ * Synchronise certaines clés du draft avec serverData tant qu'elles ne sont pas modifiées.
219
+ * @param {object} entityInstance
220
+ * @param {string[]} keys
221
+ * @param {{ cleanIfEqual?: boolean }} [options]
222
+ */
223
+ export function syncDraftOnServerChange(entityInstance, keys = [], { cleanIfEqual = true } = {}) {
224
+ for (const path of keys) {
225
+ effect(() => {
226
+ const rawServerVal = _getPathValue(entityInstance.serverData, path);
227
+ const transformedServerVal = _applyTransformIfExists(path, rawServerVal, entityInstance);
228
+
229
+ const draftVal = _getPathValue(entityInstance.draftData, path);
230
+ const initialVal = _getPathValue(entityInstance.initialDraftData, path);
231
+
232
+ const isModified = JSON.stringify(draftVal) !== JSON.stringify(initialVal);
233
+
234
+ if (!isModified) {
235
+ if (cleanIfEqual && JSON.stringify(draftVal) === JSON.stringify(transformedServerVal)) {
236
+ _deletePathValue(entityInstance.draftData, path);
237
+ } else {
238
+ _setPathValue(entityInstance.draftData, path, transformedServerVal);
239
+ }
240
+ }
241
+ });
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Applique `syncDraftOnServerChange()` pour tous les champs autorisés de l'entité.
247
+ * @param {object} entityInstance
248
+ */
249
+ export function autoSyncDraftFromSchema(entityInstance) {
250
+ const constants = Array.isArray(entityInstance.constructor.SCHEMA_CONSTANTS)
251
+ ? entityInstance.constructor.SCHEMA_CONSTANTS
252
+ : [entityInstance.constructor.SCHEMA_CONSTANTS];
253
+
254
+ const combinedSchema = { allOf: [], $defs: {} };
255
+
256
+ for (const key of constants) {
257
+ const sch = entityInstance.apiClient.getRequestSchema(key);
258
+ if (!sch) throw new ApiError(`Unable to find schema for ${key}.`);
259
+ if (sch.$defs) {
260
+ for (const [defKey, defVal] of Object.entries(sch.$defs)) {
261
+ if (!combinedSchema.$defs[defKey]) {
262
+ combinedSchema.$defs[defKey] = defVal;
263
+ }
264
+ }
265
+ }
266
+ combinedSchema.allOf.push(sch);
267
+ }
268
+
269
+ const data = {
270
+ ...entityInstance.serverData,
271
+ ...entityInstance.defaultFields
272
+ };
273
+
274
+ let allowed = entityInstance._extractWritableFields?.(combinedSchema, data) || [];
275
+ allowed = allowed.filter(k => !entityInstance.removeFields.includes(k));
276
+ allowed = allowed.filter(k => !["id"].includes(k));
277
+
278
+ syncDraftOnServerChange(entityInstance, allowed);
279
+ }