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 |
|
31 | var 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 | })();
|