@thomas-siegfried/tapout 0.0.1 → 0.0.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.
Files changed (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1205 -1205
  3. package/package.json +55 -55
  4. package/src/applyBindings.ts +372 -372
  5. package/src/arrayToDomMapping.ts +300 -300
  6. package/src/attributeInterpolationMarkup.ts +91 -91
  7. package/src/bindingContext.ts +207 -207
  8. package/src/bindingEvent.ts +198 -198
  9. package/src/bindingProvider.ts +260 -260
  10. package/src/bindings.ts +963 -963
  11. package/src/compareArrays.ts +122 -122
  12. package/src/componentBinding.ts +318 -318
  13. package/src/componentDecorator.ts +62 -62
  14. package/src/components.ts +367 -367
  15. package/src/computed.ts +439 -439
  16. package/src/configure.ts +52 -52
  17. package/src/controlFlowBindings.ts +206 -206
  18. package/src/core.ts +60 -60
  19. package/src/decorators.ts +407 -407
  20. package/src/dependencyDetection.ts +76 -76
  21. package/src/disposable.ts +36 -36
  22. package/src/domData.ts +42 -42
  23. package/src/domNodeDisposal.ts +94 -94
  24. package/src/effects.ts +29 -29
  25. package/src/event.ts +173 -173
  26. package/src/expressionRewriting.ts +219 -219
  27. package/src/extenders.ts +102 -102
  28. package/src/filters.ts +91 -91
  29. package/src/index.ts +150 -150
  30. package/src/interpolationMarkup.ts +130 -130
  31. package/src/memoization.ts +71 -71
  32. package/src/namespacedBindings.ts +132 -132
  33. package/src/observable.ts +48 -48
  34. package/src/observableArray.ts +397 -397
  35. package/src/options.ts +25 -25
  36. package/src/selectExtensions.ts +68 -68
  37. package/src/slotBinding.ts +84 -84
  38. package/src/subscribable.ts +266 -266
  39. package/src/tasks.ts +79 -79
  40. package/src/templateEngine.ts +108 -108
  41. package/src/templateRendering.ts +399 -399
  42. package/src/templateRewriting.ts +94 -94
  43. package/src/templateSources.ts +134 -134
  44. package/src/utils.ts +123 -123
  45. package/src/utilsDom.ts +87 -87
  46. package/src/virtualElements.ts +153 -153
  47. package/src/wireParams.ts +49 -49
@@ -1,219 +1,219 @@
1
- import { Observable } from './observable.js';
2
- import { Computed } from './computed.js';
3
- import { isSubscribable } from './subscribable.js';
4
-
5
- // ---- Types ----
6
-
7
- export interface KeyValuePair {
8
- key?: string;
9
- value?: string;
10
- unknown?: string;
11
- }
12
-
13
- export interface PreProcessOptions {
14
- valueAccessors?: boolean;
15
- bindingParams?: boolean;
16
- getBindingHandler?: (key: string) => { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined;
17
- }
18
-
19
- export interface AllBindingsAccessor {
20
- get(key: string): unknown;
21
- has(key: string): boolean;
22
- }
23
-
24
- // ---- Two-way binding registry ----
25
-
26
- export const twoWayBindings: Record<string, boolean | string> = {};
27
-
28
- // ---- Token regex ----
29
-
30
- const javaScriptReservedWords = ['true', 'false', 'null', 'undefined'];
31
-
32
- // Matches something assignable: an isolated identifier or something ending with a property accessor.
33
- const javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
34
-
35
- const specials = ',"\'`{}()/:[\\]';
36
- const bindingToken = RegExp([
37
- '"(?:\\\\.|[^"])*"', // double-quoted string
38
- "'(?:\\\\.|[^'])*'", // single-quoted string
39
- '`(?:\\\\.|[^`])*`', // template literal
40
- '/\\*(?:[^*]|\\*+[^*/])*\\*+/', // block comment
41
- '//.*\n', // line comment
42
- '/(?:\\\\.|[^/])+/\\w*', // regex literal
43
- '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']', // multi-char token
44
- '[^\\s]', // single char
45
- ].join('|'), 'g');
46
-
47
- const divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/;
48
- const keywordRegexLookBehind: Record<string, number> = { 'in': 1, 'return': 1, 'typeof': 1 };
49
-
50
- // ---- Core functions ----
51
-
52
- function getWriteableValue(expression: string): string | false {
53
- if (javaScriptReservedWords.indexOf(expression) >= 0) return false;
54
- const match = expression.match(javaScriptAssignmentTarget);
55
- if (match === null) return false;
56
- return match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
57
- }
58
-
59
- export function parseObjectLiteral(objectLiteralString: string): KeyValuePair[] {
60
- let str = objectLiteralString.trim();
61
-
62
- // Strip surrounding braces
63
- if (str.charCodeAt(0) === 123 /* { */) str = str.slice(1, -1);
64
-
65
- // Append newline + comma so the last pair terminates uniformly
66
- str += '\n,';
67
-
68
- const toks = str.match(bindingToken);
69
- const result: KeyValuePair[] = [];
70
-
71
- if (!toks || toks.length <= 1) return result;
72
-
73
- let key: string | undefined;
74
- let values: string[] = [];
75
- let depth = 0;
76
-
77
- for (let i = 0, tok: string; (tok = toks[i]); ++i) {
78
- const c = tok.charCodeAt(0);
79
-
80
- if (c === 44 /* , */) {
81
- if (depth <= 0) {
82
- result.push(
83
- (key && values.length)
84
- ? { key, value: values.join('') }
85
- : { unknown: key || values.join('') },
86
- );
87
- key = undefined;
88
- depth = 0;
89
- values = [];
90
- continue;
91
- }
92
- } else if (c === 58 /* : */) {
93
- if (!depth && !key && values.length === 1) {
94
- key = values.pop()!;
95
- continue;
96
- }
97
- } else if (c === 47 /* / */ && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) {
98
- // Line or block comment — skip
99
- continue;
100
- } else if (c === 47 /* / */ && i && tok.length > 1) {
101
- // Disambiguate regex vs division
102
- const prevMatch = toks[i - 1].match(divisionLookBehind);
103
- if (prevMatch && !keywordRegexLookBehind[prevMatch[0]]) {
104
- str = str.substr(str.indexOf(tok) + 1);
105
- const newToks = str.match(bindingToken);
106
- if (newToks) {
107
- toks.length = 0;
108
- toks.push(...newToks);
109
- }
110
- i = -1;
111
- tok = '/';
112
- }
113
- } else if (c === 40 || c === 123 || c === 91) {
114
- // ( { [
115
- ++depth;
116
- } else if (c === 41 || c === 125 || c === 93) {
117
- // ) } ]
118
- --depth;
119
- } else if (!key && !values.length && (c === 34 || c === 39)) {
120
- // Quoted key — strip quotes
121
- tok = tok.slice(1, -1);
122
- }
123
-
124
- values.push(tok);
125
- }
126
-
127
- if (depth > 0) {
128
- throw new Error('Unbalanced parentheses, braces, or brackets');
129
- }
130
-
131
- return result;
132
- }
133
-
134
- export function preProcessBindings(
135
- bindingsStringOrKeyValueArray: string | KeyValuePair[],
136
- options?: PreProcessOptions,
137
- ): string {
138
- options = options || {};
139
-
140
- const resultStrings: string[] = [];
141
- const propertyAccessorResultStrings: string[] = [];
142
- const makeValueAccessors = options.valueAccessors;
143
- const bindingParams = options.bindingParams;
144
- const getBindingHandler = options.getBindingHandler;
145
-
146
- const keyValueArray = typeof bindingsStringOrKeyValueArray === 'string'
147
- ? parseObjectLiteral(bindingsStringOrKeyValueArray)
148
- : bindingsStringOrKeyValueArray;
149
-
150
- function processKeyValue(key: string, val: string) {
151
- let writableVal: string | false;
152
-
153
- function callPreprocessHook(
154
- obj: { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined,
155
- ): boolean {
156
- if (obj && obj.preprocess) {
157
- const result = obj.preprocess(val, key, processKeyValue);
158
- if (result !== undefined) val = result;
159
- return result !== undefined;
160
- }
161
- return true;
162
- }
163
-
164
- if (!bindingParams) {
165
- if (getBindingHandler && !callPreprocessHook(getBindingHandler(key))) return;
166
-
167
- if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
168
- const writeKey = typeof twoWayBindings[key] === 'string' ? twoWayBindings[key] as string : key;
169
- propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + '=_z}');
170
- }
171
- }
172
-
173
- if (makeValueAccessors) {
174
- val = 'function(){return ' + val + ' }';
175
- }
176
-
177
- resultStrings.push("'" + key + "':" + val);
178
- }
179
-
180
- for (const kv of keyValueArray) {
181
- processKeyValue(kv.key || kv.unknown || '', kv.value || '');
182
- }
183
-
184
- if (propertyAccessorResultStrings.length) {
185
- processKeyValue('_tap_property_writers', '{' + propertyAccessorResultStrings.join(',') + ' }');
186
- }
187
-
188
- return resultStrings.join(',');
189
- }
190
-
191
- export function keyValueArrayContainsKey(keyValueArray: KeyValuePair[], key: string): boolean {
192
- for (const kv of keyValueArray) {
193
- if (kv.key === key) return true;
194
- }
195
- return false;
196
- }
197
-
198
- export function writeValueToProperty(
199
- property: unknown,
200
- allBindings: AllBindingsAccessor,
201
- key: string,
202
- value: unknown,
203
- checkIfDifferent?: boolean,
204
- ): void {
205
- if (!property || !isSubscribable(property)) {
206
- const propWriters = allBindings.get('_tap_property_writers') as Record<string, (v: unknown) => void> | undefined;
207
- if (propWriters && propWriters[key]) {
208
- propWriters[key](value);
209
- }
210
- } else if (isWritable(property) && (!checkIfDifferent || property.peek() !== value)) {
211
- property.set(value as never);
212
- }
213
- }
214
-
215
- function isWritable(value: unknown): value is Observable<unknown> | Computed<unknown> {
216
- if (value instanceof Observable) return true;
217
- if (value instanceof Computed) return value.hasWriteFunction;
218
- return false;
219
- }
1
+ import { Observable } from './observable.js';
2
+ import { Computed } from './computed.js';
3
+ import { isSubscribable } from './subscribable.js';
4
+
5
+ // ---- Types ----
6
+
7
+ export interface KeyValuePair {
8
+ key?: string;
9
+ value?: string;
10
+ unknown?: string;
11
+ }
12
+
13
+ export interface PreProcessOptions {
14
+ valueAccessors?: boolean;
15
+ bindingParams?: boolean;
16
+ getBindingHandler?: (key: string) => { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined;
17
+ }
18
+
19
+ export interface AllBindingsAccessor {
20
+ get(key: string): unknown;
21
+ has(key: string): boolean;
22
+ }
23
+
24
+ // ---- Two-way binding registry ----
25
+
26
+ export const twoWayBindings: Record<string, boolean | string> = {};
27
+
28
+ // ---- Token regex ----
29
+
30
+ const javaScriptReservedWords = ['true', 'false', 'null', 'undefined'];
31
+
32
+ // Matches something assignable: an isolated identifier or something ending with a property accessor.
33
+ const javaScriptAssignmentTarget = /^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;
34
+
35
+ const specials = ',"\'`{}()/:[\\]';
36
+ const bindingToken = RegExp([
37
+ '"(?:\\\\.|[^"])*"', // double-quoted string
38
+ "'(?:\\\\.|[^'])*'", // single-quoted string
39
+ '`(?:\\\\.|[^`])*`', // template literal
40
+ '/\\*(?:[^*]|\\*+[^*/])*\\*+/', // block comment
41
+ '//.*\n', // line comment
42
+ '/(?:\\\\.|[^/])+/\\w*', // regex literal
43
+ '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']', // multi-char token
44
+ '[^\\s]', // single char
45
+ ].join('|'), 'g');
46
+
47
+ const divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/;
48
+ const keywordRegexLookBehind: Record<string, number> = { 'in': 1, 'return': 1, 'typeof': 1 };
49
+
50
+ // ---- Core functions ----
51
+
52
+ function getWriteableValue(expression: string): string | false {
53
+ if (javaScriptReservedWords.indexOf(expression) >= 0) return false;
54
+ const match = expression.match(javaScriptAssignmentTarget);
55
+ if (match === null) return false;
56
+ return match[1] ? ('Object(' + match[1] + ')' + match[2]) : expression;
57
+ }
58
+
59
+ export function parseObjectLiteral(objectLiteralString: string): KeyValuePair[] {
60
+ let str = objectLiteralString.trim();
61
+
62
+ // Strip surrounding braces
63
+ if (str.charCodeAt(0) === 123 /* { */) str = str.slice(1, -1);
64
+
65
+ // Append newline + comma so the last pair terminates uniformly
66
+ str += '\n,';
67
+
68
+ const toks = str.match(bindingToken);
69
+ const result: KeyValuePair[] = [];
70
+
71
+ if (!toks || toks.length <= 1) return result;
72
+
73
+ let key: string | undefined;
74
+ let values: string[] = [];
75
+ let depth = 0;
76
+
77
+ for (let i = 0, tok: string; (tok = toks[i]); ++i) {
78
+ const c = tok.charCodeAt(0);
79
+
80
+ if (c === 44 /* , */) {
81
+ if (depth <= 0) {
82
+ result.push(
83
+ (key && values.length)
84
+ ? { key, value: values.join('') }
85
+ : { unknown: key || values.join('') },
86
+ );
87
+ key = undefined;
88
+ depth = 0;
89
+ values = [];
90
+ continue;
91
+ }
92
+ } else if (c === 58 /* : */) {
93
+ if (!depth && !key && values.length === 1) {
94
+ key = values.pop()!;
95
+ continue;
96
+ }
97
+ } else if (c === 47 /* / */ && tok.length > 1 && (tok.charCodeAt(1) === 47 || tok.charCodeAt(1) === 42)) {
98
+ // Line or block comment — skip
99
+ continue;
100
+ } else if (c === 47 /* / */ && i && tok.length > 1) {
101
+ // Disambiguate regex vs division
102
+ const prevMatch = toks[i - 1].match(divisionLookBehind);
103
+ if (prevMatch && !keywordRegexLookBehind[prevMatch[0]]) {
104
+ str = str.substr(str.indexOf(tok) + 1);
105
+ const newToks = str.match(bindingToken);
106
+ if (newToks) {
107
+ toks.length = 0;
108
+ toks.push(...newToks);
109
+ }
110
+ i = -1;
111
+ tok = '/';
112
+ }
113
+ } else if (c === 40 || c === 123 || c === 91) {
114
+ // ( { [
115
+ ++depth;
116
+ } else if (c === 41 || c === 125 || c === 93) {
117
+ // ) } ]
118
+ --depth;
119
+ } else if (!key && !values.length && (c === 34 || c === 39)) {
120
+ // Quoted key — strip quotes
121
+ tok = tok.slice(1, -1);
122
+ }
123
+
124
+ values.push(tok);
125
+ }
126
+
127
+ if (depth > 0) {
128
+ throw new Error('Unbalanced parentheses, braces, or brackets');
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ export function preProcessBindings(
135
+ bindingsStringOrKeyValueArray: string | KeyValuePair[],
136
+ options?: PreProcessOptions,
137
+ ): string {
138
+ options = options || {};
139
+
140
+ const resultStrings: string[] = [];
141
+ const propertyAccessorResultStrings: string[] = [];
142
+ const makeValueAccessors = options.valueAccessors;
143
+ const bindingParams = options.bindingParams;
144
+ const getBindingHandler = options.getBindingHandler;
145
+
146
+ const keyValueArray = typeof bindingsStringOrKeyValueArray === 'string'
147
+ ? parseObjectLiteral(bindingsStringOrKeyValueArray)
148
+ : bindingsStringOrKeyValueArray;
149
+
150
+ function processKeyValue(key: string, val: string) {
151
+ let writableVal: string | false;
152
+
153
+ function callPreprocessHook(
154
+ obj: { preprocess?(val: string, key: string, addBinding: (key: string, val: string) => void): string | void } | undefined,
155
+ ): boolean {
156
+ if (obj && obj.preprocess) {
157
+ const result = obj.preprocess(val, key, processKeyValue);
158
+ if (result !== undefined) val = result;
159
+ return result !== undefined;
160
+ }
161
+ return true;
162
+ }
163
+
164
+ if (!bindingParams) {
165
+ if (getBindingHandler && !callPreprocessHook(getBindingHandler(key))) return;
166
+
167
+ if (twoWayBindings[key] && (writableVal = getWriteableValue(val))) {
168
+ const writeKey = typeof twoWayBindings[key] === 'string' ? twoWayBindings[key] as string : key;
169
+ propertyAccessorResultStrings.push("'" + writeKey + "':function(_z){" + writableVal + '=_z}');
170
+ }
171
+ }
172
+
173
+ if (makeValueAccessors) {
174
+ val = 'function(){return ' + val + ' }';
175
+ }
176
+
177
+ resultStrings.push("'" + key + "':" + val);
178
+ }
179
+
180
+ for (const kv of keyValueArray) {
181
+ processKeyValue(kv.key || kv.unknown || '', kv.value || '');
182
+ }
183
+
184
+ if (propertyAccessorResultStrings.length) {
185
+ processKeyValue('_tap_property_writers', '{' + propertyAccessorResultStrings.join(',') + ' }');
186
+ }
187
+
188
+ return resultStrings.join(',');
189
+ }
190
+
191
+ export function keyValueArrayContainsKey(keyValueArray: KeyValuePair[], key: string): boolean {
192
+ for (const kv of keyValueArray) {
193
+ if (kv.key === key) return true;
194
+ }
195
+ return false;
196
+ }
197
+
198
+ export function writeValueToProperty(
199
+ property: unknown,
200
+ allBindings: AllBindingsAccessor,
201
+ key: string,
202
+ value: unknown,
203
+ checkIfDifferent?: boolean,
204
+ ): void {
205
+ if (!property || !isSubscribable(property)) {
206
+ const propWriters = allBindings.get('_tap_property_writers') as Record<string, (v: unknown) => void> | undefined;
207
+ if (propWriters && propWriters[key]) {
208
+ propWriters[key](value);
209
+ }
210
+ } else if (isWritable(property) && (!checkIfDifferent || property.peek() !== value)) {
211
+ property.set(value as never);
212
+ }
213
+ }
214
+
215
+ function isWritable(value: unknown): value is Observable<unknown> | Computed<unknown> {
216
+ if (value instanceof Observable) return true;
217
+ if (value instanceof Computed) return value.hasWriteFunction;
218
+ return false;
219
+ }
package/src/extenders.ts CHANGED
@@ -1,102 +1,102 @@
1
- import type { Subscribable } from './subscribable.js';
2
- import { valuesArePrimitiveAndEqual } from './subscribable.js';
3
- import { schedule, cancel } from './tasks.js';
4
-
5
- export type ExtenderHandler = (target: Subscribable, value: unknown) => Subscribable | void;
6
-
7
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
8
- export interface ExtenderMap {
9
- notify: 'always';
10
- rateLimit: number | RateLimitOptions;
11
- deferred: true;
12
- }
13
-
14
- export type ExtenderOptions = Partial<ExtenderMap> & Record<string, unknown>;
15
-
16
- const registry: Record<string, ExtenderHandler> = {};
17
-
18
- export function registerExtender(name: string, handler: ExtenderHandler): void {
19
- registry[name] = handler;
20
- }
21
-
22
- export function getExtenderHandler(name: string): ExtenderHandler | undefined {
23
- return registry[name];
24
- }
25
-
26
- export function throttle(callback: () => void, timeout: number): () => void {
27
- let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
28
- return function () {
29
- if (!timeoutInstance) {
30
- timeoutInstance = setTimeout(function () {
31
- timeoutInstance = undefined;
32
- callback();
33
- }, timeout);
34
- }
35
- };
36
- }
37
-
38
- export function debounce(callback: () => void, timeout: number): () => void {
39
- let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
40
- return function () {
41
- clearTimeout(timeoutInstance);
42
- timeoutInstance = setTimeout(callback, timeout);
43
- };
44
- }
45
-
46
- export interface RateLimitOptions {
47
- timeout: number;
48
- method?: 'notifyWhenChangesStop' | ((callback: () => void, timeout: number) => () => void);
49
- }
50
-
51
- registerExtender('notify', (target, notifyWhen) => {
52
- target.equalityComparer = notifyWhen === 'always'
53
- ? undefined
54
- : valuesArePrimitiveAndEqual;
55
- });
56
-
57
- registerExtender('rateLimit', (target, options) => {
58
- let timeout: number;
59
- let method: RateLimitOptions['method'] | undefined;
60
-
61
- if (typeof options === 'number') {
62
- timeout = options;
63
- } else {
64
- const opts = options as RateLimitOptions;
65
- timeout = opts.timeout;
66
- method = opts.method;
67
- }
68
-
69
- target._deferUpdates = false;
70
-
71
- const limitFunction = typeof method === 'function'
72
- ? method
73
- : method === 'notifyWhenChangesStop' ? debounce : throttle;
74
-
75
- target.limit((callback) => limitFunction(callback, timeout));
76
- });
77
-
78
- registerExtender('deferred', (target, options) => {
79
- if (options !== true) {
80
- throw new Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");
81
- }
82
-
83
- if (!target._deferUpdates) {
84
- target._deferUpdates = true;
85
- target.limit((callback) => {
86
- let handle: number;
87
- let ignoreUpdates = false;
88
- return () => {
89
- if (!ignoreUpdates) {
90
- cancel(handle);
91
- handle = schedule(callback);
92
- try {
93
- ignoreUpdates = true;
94
- target.notifySubscribers(undefined as never, 'dirty');
95
- } finally {
96
- ignoreUpdates = false;
97
- }
98
- }
99
- };
100
- });
101
- }
102
- });
1
+ import type { Subscribable } from './subscribable.js';
2
+ import { valuesArePrimitiveAndEqual } from './subscribable.js';
3
+ import { schedule, cancel } from './tasks.js';
4
+
5
+ export type ExtenderHandler = (target: Subscribable, value: unknown) => Subscribable | void;
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
8
+ export interface ExtenderMap {
9
+ notify: 'always';
10
+ rateLimit: number | RateLimitOptions;
11
+ deferred: true;
12
+ }
13
+
14
+ export type ExtenderOptions = Partial<ExtenderMap> & Record<string, unknown>;
15
+
16
+ const registry: Record<string, ExtenderHandler> = {};
17
+
18
+ export function registerExtender(name: string, handler: ExtenderHandler): void {
19
+ registry[name] = handler;
20
+ }
21
+
22
+ export function getExtenderHandler(name: string): ExtenderHandler | undefined {
23
+ return registry[name];
24
+ }
25
+
26
+ export function throttle(callback: () => void, timeout: number): () => void {
27
+ let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
28
+ return function () {
29
+ if (!timeoutInstance) {
30
+ timeoutInstance = setTimeout(function () {
31
+ timeoutInstance = undefined;
32
+ callback();
33
+ }, timeout);
34
+ }
35
+ };
36
+ }
37
+
38
+ export function debounce(callback: () => void, timeout: number): () => void {
39
+ let timeoutInstance: ReturnType<typeof setTimeout> | undefined;
40
+ return function () {
41
+ clearTimeout(timeoutInstance);
42
+ timeoutInstance = setTimeout(callback, timeout);
43
+ };
44
+ }
45
+
46
+ export interface RateLimitOptions {
47
+ timeout: number;
48
+ method?: 'notifyWhenChangesStop' | ((callback: () => void, timeout: number) => () => void);
49
+ }
50
+
51
+ registerExtender('notify', (target, notifyWhen) => {
52
+ target.equalityComparer = notifyWhen === 'always'
53
+ ? undefined
54
+ : valuesArePrimitiveAndEqual;
55
+ });
56
+
57
+ registerExtender('rateLimit', (target, options) => {
58
+ let timeout: number;
59
+ let method: RateLimitOptions['method'] | undefined;
60
+
61
+ if (typeof options === 'number') {
62
+ timeout = options;
63
+ } else {
64
+ const opts = options as RateLimitOptions;
65
+ timeout = opts.timeout;
66
+ method = opts.method;
67
+ }
68
+
69
+ target._deferUpdates = false;
70
+
71
+ const limitFunction = typeof method === 'function'
72
+ ? method
73
+ : method === 'notifyWhenChangesStop' ? debounce : throttle;
74
+
75
+ target.limit((callback) => limitFunction(callback, timeout));
76
+ });
77
+
78
+ registerExtender('deferred', (target, options) => {
79
+ if (options !== true) {
80
+ throw new Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");
81
+ }
82
+
83
+ if (!target._deferUpdates) {
84
+ target._deferUpdates = true;
85
+ target.limit((callback) => {
86
+ let handle: number;
87
+ let ignoreUpdates = false;
88
+ return () => {
89
+ if (!ignoreUpdates) {
90
+ cancel(handle);
91
+ handle = schedule(callback);
92
+ try {
93
+ ignoreUpdates = true;
94
+ target.notifySubscribers(undefined as never, 'dirty');
95
+ } finally {
96
+ ignoreUpdates = false;
97
+ }
98
+ }
99
+ };
100
+ });
101
+ }
102
+ });