@schukai/monster 4.38.4 → 4.38.5

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.
@@ -15,149 +15,144 @@
15
15
  import { Base } from "./base.mjs";
16
16
  import { isObject } from "./is.mjs";
17
17
  import { TokenList } from "./tokenlist.mjs";
18
- import { UniqueQueue } from "./uniquequeue.mjs";
19
18
  import { instanceSymbol } from "../constants.mjs";
20
19
  export { Observer };
21
20
 
22
21
  /**
23
- * An observer manages a callback function
22
+ * Manages a callback function that is executed asynchronously when updated.
24
23
  *
25
- * The update method is called with the subject object as this pointer. For this reason
26
- * the callback should not be an arrow function, because it gets this pointer of its own context.
24
+ * The `update` method is called with the subject object as its `this` context.
25
+ * For this reason, the callback should be a regular function, not an arrow function,
26
+ * if you need access to the subject via `this`.
27
27
  *
28
- * Include this class in your project with the following code:
28
+ * @example
29
+ * // Basic usage with a regular function to access the subject
30
+ * const observer = new Observer(function() {
31
+ * console.log("Subject updated:", this); // `this` refers to `mySubject`
32
+ * });
29
33
  *
30
- * ```js
31
- * import { Observer } from "@schukai/monster/source/types/observer.mjs";
32
- * ```
34
+ * // Usage with arguments
35
+ * const greeter = new Observer(function(greeting) {
36
+ * console.log(greeting, this.name); // "Hello", "World"
37
+ * }, "Hello");
33
38
  *
34
- * The callback function is passed as the first argument to the constructor.
35
- *
36
- * ```js
37
- * new Observer(()=>{
38
- * // this is not subject
39
- * })
40
- *
41
- * new Observer(function() {
42
- * // this is subject
43
- * })
44
- * ```
45
- *
46
- * Additional arguments can be passed to the callback. To do this, simply specify them.
47
- *
48
- * ```js
49
- * Observer(function(a, b, c) {
50
- * console.log(a, b, c); // ↦ "a", 2, true
51
- * }, "a", 2, true)
52
- * ```
53
- *
54
- * The callback function must have as many parameters as arguments are given.
39
+ * const mySubject = { name: "World" };
40
+ * observer.update(mySubject);
41
+ * greeter.update(mySubject);
55
42
  *
56
43
  * @license AGPLv3
57
44
  * @since 1.0.0
58
45
  */
59
46
  class Observer extends Base {
60
- /**
61
- *
62
- * @param {function} callback
63
- * @param {*} args
64
- */
65
- constructor(callback, ...args) {
66
- super();
67
-
68
- if (typeof callback !== "function") {
69
- throw new Error("observer callback must be a function");
70
- }
71
-
72
- this.callback = callback;
73
- this.arguments = args;
74
- this.tags = new TokenList();
75
- this.queue = new UniqueQueue();
76
- }
77
-
78
- /**
79
- * This method is called by the `instanceof` operator.
80
- * @return {symbol}
81
- * @since 2.1.0
82
- */
83
- static get [instanceSymbol]() {
84
- return Symbol.for("@schukai/monster/types/observer");
85
- }
86
-
87
- /**
88
- *
89
- * @param {string} tag
90
- * @return {Observer}
91
- */
92
- addTag(tag) {
93
- this.tags.add(tag);
94
- return this;
95
- }
96
-
97
- /**
98
- *
99
- * @param {string} tag
100
- * @return {Observer}
101
- */
102
- removeTag(tag) {
103
- this.tags.remove(tag);
104
- return this;
105
- }
106
-
107
- /**
108
- *
109
- * @return {Array}
110
- */
111
- getTags() {
112
- return this.tags.entries();
113
- }
114
-
115
- /**
116
- *
117
- * @param {string} tag
118
- * @return {boolean}
119
- */
120
- hasTag(tag) {
121
- return this.tags.contains(tag);
122
- }
123
-
124
- /**
125
- *
126
- * @param {object} subject
127
- * @return {Promise}
128
- */
129
- update(subject) {
130
- const self = this;
131
-
132
- if (!isObject(subject)) {
133
- return Promise.reject("subject must be an object");
134
- }
135
-
136
- return new Promise(function (resolve, reject) {
137
- self.queue.add(subject);
138
-
139
- queueMicrotask(() => {
140
- try {
141
- // the queue and the `queueMicrotask` ensure that an object is not
142
- // informed of the same change more than once.
143
- if (self.queue.isEmpty()) {
144
- resolve();
145
- return;
146
- }
147
-
148
- const s = self.queue.poll();
149
- const result = self.callback.apply(s, self.arguments);
150
-
151
- if (isObject(result) && result instanceof Promise) {
152
- result.then(resolve).catch(reject);
153
- return;
154
- }
155
-
156
- resolve(result);
157
- } catch (e) {
158
- reject(e);
159
- }
160
- });
161
- });
162
- }
47
+ /**
48
+ * Stores promises for updates that are scheduled but not yet executed.
49
+ * This prevents multiple executions for the same subject within one microtask cycle.
50
+ * @type {Map<object, Promise<*>>}
51
+ */
52
+ #pendingUpdates = new Map();
53
+ #callback;
54
+ #arguments;
55
+ #tags = new TokenList();
56
+
57
+ /**
58
+ * @param {function} callback The function to execute on update.
59
+ * @param {...*} args Additional arguments to pass to the callback.
60
+ */
61
+ constructor(callback, ...args) {
62
+ super();
63
+
64
+ if (typeof callback !== "function") {
65
+ throw new Error("Observer callback must be a function.");
66
+ }
67
+
68
+ this.#callback = callback;
69
+ this.#arguments = args;
70
+ }
71
+
72
+ /**
73
+ * This method is called by the `instanceof` operator.
74
+ * @returns {symbol}
75
+ * @since 2.1.0
76
+ */
77
+ static get [instanceSymbol]() {
78
+ return Symbol.for("@schukai/monster/types/observer");
79
+ }
80
+
81
+ // Getter for properties for cleaner access if needed
82
+ get callback() {
83
+ return this.#callback;
84
+ }
85
+
86
+ get arguments() {
87
+ return this.#arguments;
88
+ }
89
+
90
+ /**
91
+ * Schedules the callback for execution with the given subject.
92
+ * If multiple updates for the same subject are requested in the same event loop tick,
93
+ * the callback is only executed once. All callers will receive the same promise.
94
+ *
95
+ * @param {object} subject The subject object that triggered the update.
96
+ * @returns {Promise<*>} A promise that resolves with the return value of the callback,
97
+ * or rejects if the callback throws an error.
98
+ */
99
+ update(subject) {
100
+ if (!isObject(subject)) {
101
+ return Promise.reject(new Error("Subject must be an object."));
102
+ }
103
+
104
+ if (this.#pendingUpdates.has(subject)) {
105
+ return this.#pendingUpdates.get(subject);
106
+ }
107
+
108
+ const promise = new Promise((resolve, reject) => {
109
+ queueMicrotask(async () => {
110
+ try {
111
+ const result = await this.#callback.apply(subject, this.#arguments);
112
+ resolve(result);
113
+ } catch (e) {
114
+ reject(e);
115
+ } finally {
116
+ this.#pendingUpdates.delete(subject);
117
+ }
118
+ });
119
+ });
120
+
121
+ this.#pendingUpdates.set(subject, promise);
122
+
123
+ return promise;
124
+ }
125
+
126
+ /**
127
+ * @param {string} tag
128
+ * @returns {Observer}
129
+ */
130
+ addTag(tag) {
131
+ this.#tags.add(tag);
132
+ return this;
133
+ }
134
+
135
+ /**
136
+ * @param {string} tag
137
+ * @returns {Observer}
138
+ */
139
+ removeTag(tag) {
140
+ this.#tags.remove(tag);
141
+ return this;
142
+ }
143
+
144
+ /**
145
+ * @returns {string[]}
146
+ */
147
+ getTags() {
148
+ return this.#tags.entries();
149
+ }
150
+
151
+ /**
152
+ * @param {string} tag
153
+ * @returns {boolean}
154
+ */
155
+ hasTag(tag) {
156
+ return this.#tags.contains(tag);
157
+ }
163
158
  }
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.38.3");
159
+ monsterVersion = new Version("4.38.4");
160
160
 
161
161
  return monsterVersion;
162
162
  }