@thomas-siegfried/tapout 0.0.1 → 0.0.3

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 (52) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1207 -1205
  3. package/decorator-config.md +220 -0
  4. package/dist/wireParams.d.ts.map +1 -1
  5. package/dist/wireParams.js +27 -4
  6. package/dist/wireParams.js.map +1 -1
  7. package/llms.txt +360 -0
  8. package/package.json +57 -55
  9. package/src/applyBindings.ts +372 -372
  10. package/src/arrayToDomMapping.ts +300 -300
  11. package/src/attributeInterpolationMarkup.ts +91 -91
  12. package/src/bindingContext.ts +207 -207
  13. package/src/bindingEvent.ts +198 -198
  14. package/src/bindingProvider.ts +260 -260
  15. package/src/bindings.ts +963 -963
  16. package/src/compareArrays.ts +122 -122
  17. package/src/componentBinding.ts +318 -318
  18. package/src/componentDecorator.ts +62 -62
  19. package/src/components.ts +367 -367
  20. package/src/computed.ts +439 -439
  21. package/src/configure.ts +52 -52
  22. package/src/controlFlowBindings.ts +206 -206
  23. package/src/core.ts +60 -60
  24. package/src/decorators.ts +407 -407
  25. package/src/dependencyDetection.ts +76 -76
  26. package/src/disposable.ts +36 -36
  27. package/src/domData.ts +42 -42
  28. package/src/domNodeDisposal.ts +94 -94
  29. package/src/effects.ts +29 -29
  30. package/src/event.ts +173 -173
  31. package/src/expressionRewriting.ts +219 -219
  32. package/src/extenders.ts +102 -102
  33. package/src/filters.ts +91 -91
  34. package/src/index.ts +150 -150
  35. package/src/interpolationMarkup.ts +130 -130
  36. package/src/memoization.ts +71 -71
  37. package/src/namespacedBindings.ts +132 -132
  38. package/src/observable.ts +48 -48
  39. package/src/observableArray.ts +397 -397
  40. package/src/options.ts +25 -25
  41. package/src/selectExtensions.ts +68 -68
  42. package/src/slotBinding.ts +84 -84
  43. package/src/subscribable.ts +266 -266
  44. package/src/tasks.ts +79 -79
  45. package/src/templateEngine.ts +108 -108
  46. package/src/templateRendering.ts +399 -399
  47. package/src/templateRewriting.ts +94 -94
  48. package/src/templateSources.ts +134 -134
  49. package/src/utils.ts +123 -123
  50. package/src/utilsDom.ts +87 -87
  51. package/src/virtualElements.ts +153 -153
  52. package/src/wireParams.ts +69 -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
+ });