UNPKG

14.6 kBJavaScriptView Raw
1/*
2 * == Luar ==
3 * Extremely small, fast and compatible (IE 9 and up) reactive programming
4 * library, similar to Hyperactiv.
5 *
6 * Version 1.4.2
7 *
8 * MIT License
9 *
10 * Copyright (c) 2020 Lua MacDougall
11 *
12 * Permission is hereby granted, free of charge, to any person obtaining a copy
13 * of this software and associated documentation files (the "Software"), to deal
14 * in the Software without restriction, including without limitation the rights
15 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16 * copies of the Software, and to permit persons to whom the Software is
17 * furnished to do so, subject to the following conditions:
18 *
19 * The above copyright notice and this permission notice shall be included in
20 * all copies or substantial portions of the Software.
21 *
22 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 * SOFTWARE.
29 */
30
31var Luar = (function () {
32
33 /**
34 * Checks if `val` is a valid JavaScript object
35 * @param {any} val Value to check
36 * @returns {boolean} Indicates whether `val` is an object
37 */
38 function isObject(val) {
39 return val && typeof val === "object" && !Array.isArray(val);
40 }
41 /**
42 * Checks if `val` is a valid JavaScript function
43 * @param {any} val Value to check
44 * @returns {boolean} Indicates whether `val` is a function
45 */
46 function isFunction(val) {
47 return typeof val === "function";
48 }
49
50 /**
51 * Checks if `obj` has the property `key`, ignoring prototypes
52 * @param {Object} obj Object to call hasOwnProperty on
53 * @param {string} key Key to call hasOwnProperty with
54 * @returns {boolean} Indicates whether `obj` has a property named `key`
55 */
56 function hasOwnProperty(obj, key) {
57 return Object.prototype.hasOwnProperty.call(obj, key);
58 }
59
60 /**
61 * Creates a "Map" (object with no prototype)
62 * @returns {Object} Empty object with no prototype
63 */
64 function createMap() {
65 return Object.create(null);
66 }
67
68
69 /**
70 * Gets the name of a function
71 * @param {any} fn Function to get name from
72 * @returns {any} Function name, or "anonymous" if the function's name is
73 * invalid. May not be a string!
74 */
75 function getFunctionName(fn) {
76 if (isFunction(fn) && fn.name) {
77 return fn.name;
78 } else {
79 return "anonymous";
80 }
81 }
82
83 /**
84 * Generates an error/warning message
85 * @param {string} header Error message header
86 * @param {string} body Error message body
87 * @param {boolean} [warn] Use "WRN" instead of "ERR"?
88 * @returns {string} Formatted error message string
89 */
90 function createErrorMessage(header, body, warn) {
91 return "[Luar] " + (warn ? "WRN " : "ERR ") + header + "\n" + body;
92 }
93
94
95 /**
96 * Hint property added to observed objects, used to check if an object has
97 * already been observed
98 */
99 var observeHint = "__luar";
100 /**
101 * Hint property to add to computed tasks, indicating that they shouldn't be
102 * called again and should be removed from dependencies
103 */
104 var disposeHint = "__disposed";
105 /**
106 * Defines a hint property on an object
107 * @param {Object} obj Object to define the hint property on
108 * @param {string} hint Hint key name
109 */
110 function defineHint(obj, hint) {
111 Object.defineProperty(obj, hint, {
112 enumerable: false,
113 value: true
114 });
115 }
116
117
118 /**
119 * Maximum number of computed tasks allowed in `computedList`, exceeding this
120 * value will cause an overflow error to be thrown
121 */
122 var computedLimit = 2000;
123
124 /**
125 * List of currently executing computed tasks
126 */
127 var computedList = [];
128 /**
129 * Index into `computedList`, points to the currently executing computed task
130 */
131 var computedI = 0;
132
133 /**
134 * Mutex-like lock flag for `computedProcess` to prevent multiple executions of
135 * the computed task list
136 */
137 var computedLock = false;
138
139 /**
140 * Throws an error indicating an overflow of `computedList`, with descriptions
141 * of the last 10 pending/executed computed tasks
142 */
143 function computedOverflow() {
144 // Get the names of the last 10 computed tasks in `computedList`
145 var taskNames = [];
146 for (var i = computedList.length - 11; i < computedList.length; i++) {
147 taskNames[taskNames.length] = i + ": " + getFunctionName(computedList[i]);
148 }
149
150 // Throw a descriptive error containing the last 10 task function names
151 throw new Error(createErrorMessage(
152 "Maximum computed task stack length exceeded (overflow!)",
153 "Last 10 tasks in the stack:\n" +
154 taskNames.join("\n")
155 ));
156 }
157
158 /**
159 * Executes all pending computed tasks in `computedList`
160 */
161 function computedProcess() {
162 // Attempt to lock the computed processing state (return if already executing)
163 if (computedLock) return;
164 computedLock = true;
165
166 // Catch errors from rogue computed tasks
167 try {
168 // Loop through all pending-execution computed tasks
169 for (; computedI < computedList.length; computedI++) {
170 var task = computedList[computedI];
171
172 // If the task has not been `dispose(task)`'ed, run it!
173 if (!hasOwnProperty(task, disposeHint)) {
174 task();
175 }
176
177 // If we've gone overboard and executed too many tasks, throw an overflow
178 // error
179 if (computedI > computedLimit) {
180 computedOverflow();
181 }
182 }
183 } finally {
184 // Reset `computedList`/`computedI`
185 computedList = [];
186 computedI = 0;
187 // Unlock the lock for the next `computedNotify` call
188 computedLock = false;
189 }
190 }
191
192 /**
193 * Adds `task` to `computedList` if it hasn't already been added, then start
194 * processing through `computedList` if processing isn't already running
195 * @param {Function} task Computed task function to queue
196 */
197 function computedNotify(task) {
198 // Make sure that this task isn't already in `computedList`
199 for (var i = computedI; i < computedList.length; i++) {
200 if (computedList[i] === task) return;
201 }
202
203 // Add this task to `computedList`
204 computedList[computedList.length] = task;
205
206 // Start processing, if it isn't already running
207 computedProcess();
208 }
209
210
211 /**
212 * Gets the value stored in a reactive property on a reactive object. This
213 * function will also register the current computed task as a dependency, if it
214 * is not already registered
215 * @param {Object} shadowValMap observeObject's shadowValMap
216 * @param {Object} dependencyMap observeObject's dependencyMap
217 * @param {string} key Key of the reactive property to get
218 * @returns {any} Value stored in the reactive property (shadowValMap)
219 */
220 function reactiveGet(
221 shadowValMap, dependencyMap,
222 key
223 ) {
224 // registerComputed makes sure that the current computed task is registered
225 // as a dependency of this reactive property, since it was accessed while
226 // the computed task was running
227 registerComputed: {
228 // Get the current computed task (exit if we are not in a computed task)
229 var task = computedList[computedI];
230 if (!task) break registerComputed;
231
232 // Get the dependency list for this key (create it if it does not exist)
233 var dependencyList = dependencyMap[key];
234 if (!dependencyList) dependencyMap[key] = dependencyList = [];
235
236 // Make sure that this computed task is not already registered as a
237 // dependency
238 for (var i = 0; i < dependencyList.length; i++) {
239 if (dependencyList[i] === task) break registerComputed;
240 }
241
242 // Not a dependency! Add this task to the list of dependencies
243 dependencyList[dependencyList.length] = task;
244 }
245
246 // Return the value stored in this reactive property
247 return shadowValMap[key];
248 }
249
250 /**
251 * Sets the value stored in a reactive property, automatically notifying any
252 * dependant computed tasks
253 * @param {Object} shadowValMap observeObject's shadowValMap
254 * @param {Object} dependencyMap observeObject's dependencyMap
255 * @param {string} key Key of the reactive property to update
256 * @param {any} val Value to set on the reactive property
257 */
258 function reactiveSet(
259 shadowValMap, dependencyMap,
260 key, val
261 ) {
262 // If the value we were given is an object, make sure it is reactive
263 if (isObject(val)) observeObject(val);
264
265 // Set the value of this reactive property
266 shadowValMap[key] = val;
267
268 // Retrieve the dependency list for this reactive property
269 var dependencyList = dependencyMap[key];
270 if (!dependencyList) return;
271
272 // Notify/update all the dependent computed tasks
273 for (var i = 0; i < dependencyList.length; i++) {
274 var task = dependencyList[i];
275 if (!task) continue;
276
277 // If this task has been disposed ...
278 if (hasOwnProperty(task, disposeHint)) {
279 // ... then remove it from the dependency list ...
280 dependencyList[i] = null;
281 } else {
282 // ... or notify it of an update if it is still valid
283 computedNotify(task);
284 }
285 }
286 }
287
288 /**
289 * Creates a reactive property descriptor (added to `descriptorMap`) for `key`
290 * of `originalObject`
291 * @param {Object} originalObj Object to retrieve value of `key` from
292 * @param {Object} descriptorMap observeObject's descriptorMap, new entry for
293 * this `key` will be created
294 * @param {Object} shadowValMap observeObject's shadowValMap
295 * @param {Object} dependencyMap observeObject's dependencyMap
296 * @param {string} key Key of the original property to reactify
297 */
298 function reactiveCreate(
299 originalObj, descriptorMap, shadowValMap, dependencyMap,
300 key
301 ) {
302 // Create the getter/setter descriptor pair for this reactive property
303 descriptorMap[key] = {
304 get: function () {
305 return reactiveGet(shadowValMap, dependencyMap, key);
306 },
307 set: function (newVal) {
308 reactiveSet(shadowValMap, dependencyMap, key, newVal);
309 }
310 };
311
312 // Get the current value of the original property
313 var val = originalObj[key];
314 // If the value is an object, make sure it is reactive
315 if (isObject(val)) observeObject(val);
316
317 // Copy the value into the shadow map, for usage in reactiveGet and
318 // reactiveSet
319 shadowValMap[key] = val;
320 }
321
322 /**
323 * Makes the properties of an object reactive, if it has not already been made
324 * reactive
325 * @param {Object} obj Object to reactify
326 */
327 function observeObject(obj) {
328 // Don't re-observe this object if it has already been observed (has observe
329 // hint)
330 if (hasOwnProperty(obj, observeHint)) return;
331 // No hint! Add the hint so the object fails the check if observeObject is
332 // called on it a second time
333 defineHint(obj, observeHint);
334
335 // shadowValMap is a shadow object that contains all the actual values of all
336 // the reactive properties
337 var shadowValMap = createMap();
338 // dependencyMap is a map of reactive property keys -> lists of dependent
339 // computed tasks
340 var dependencyMap = createMap();
341 // descriptorMap is passed to Object.defineProperties and contains all the
342 // property descriptors for each reactive property
343 var descriptorMap = createMap();
344
345 // Loop through all the properties of the object
346 for (var key in obj) {
347 // Make sure that this property is actually part of the object
348 if (!hasOwnProperty(obj, key) || key === "__proto__") continue;
349 // Make this property reactive
350 reactiveCreate(obj, descriptorMap, shadowValMap, dependencyMap, key);
351 }
352
353 // Apply all the generated property descriptors at once
354 Object.defineProperties(obj, descriptorMap);
355 }
356
357
358 /** See documentation for this function in: index.d.ts */
359 /* export */ function observe(obj) {
360 // New in version 1.4.0, functions can now be observed! But only explicitly,
361 // functions will not be observed if they exist as children of a reactive
362 // object
363 if (!isObject(obj) && !isFunction(obj)) {
364 throw new Error(createErrorMessage(
365 "Attempted to observe a value that is not an object",
366 "observe(obj) expects \"obj\" to be an object, got \"" + obj + "\""
367 ));
368 }
369
370 observeObject(obj);
371 return obj;
372 }
373
374 /** See documentation for this function in: index.d.ts */
375 /* export */ function computed(task) {
376 if (!isFunction(task)) {
377 throw new Error(createErrorMessage(
378 "Attempted to register a value that is not a function as a computed task",
379 "computed(task) expects \"task\" to to be a function, got \"" + task + "\""
380 ));
381 }
382
383 if (computedLock) {
384 console.warn(createErrorMessage(
385 "Creating computed tasks from within another computed task is not recommended",
386 "Offending computed task: " + getFunctionName(computedList[computedI]) +
387 "\nNewly created computed task: " + getFunctionName(task),
388 true
389 ));
390 }
391
392 computedNotify(task);
393 return task;
394 }
395
396 /** See documentation for this function in: index.d.ts */
397 /* export */ function dispose(task) {
398 if (task == null) {
399 task = computedList[computedI];
400 if (!task) {
401 throw new Error(createErrorMessage(
402 "Attempted to dispose of current computed task while no computed task is running",
403 "dispose(task) was called without \"task\" but there is currently no executing computed task to dispose of"
404 ));
405 }
406 }
407
408 else if (!isFunction(task)) {
409 throw new Error(createErrorMessage(
410 "Attempted to dispose of a value that is not a function",
411 "dispose(task) expects \"task\" to be a function, got \"" + task + "\""
412 ));
413 }
414
415 defineHint(task, disposeHint);
416 }
417
418
419 /**
420 * Object containing all of Luar's public functions, which will be exported as
421 * `Luar` or through `module.exports` if `module` is valid
422 */
423 var localExports = { observe: observe, computed: computed, dispose: dispose };
424
425 // Set localExports on the current module if we are in a CommonJS environment
426 /* istanbul ignore else */
427 if (typeof module !== "undefined") {
428 module.exports = localExports;
429 }
430
431 // Return the localExports which will be set as `Luar`, for browser-like
432 // environments
433 return localExports;
434
435})();