@tanstack/pacer 0.6.0 → 0.7.0
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.
- package/dist/cjs/async-debouncer.cjs +10 -6
- package/dist/cjs/async-debouncer.cjs.map +1 -1
- package/dist/cjs/async-debouncer.d.cts +2 -2
- package/dist/cjs/async-queuer.cjs +135 -106
- package/dist/cjs/async-queuer.cjs.map +1 -1
- package/dist/cjs/async-queuer.d.cts +121 -93
- package/dist/cjs/async-rate-limiter.cjs +3 -4
- package/dist/cjs/async-rate-limiter.cjs.map +1 -1
- package/dist/cjs/async-rate-limiter.d.cts +1 -2
- package/dist/cjs/async-throttler.cjs +14 -4
- package/dist/cjs/async-throttler.cjs.map +1 -1
- package/dist/cjs/async-throttler.d.cts +3 -2
- package/dist/cjs/batcher.cjs +138 -0
- package/dist/cjs/batcher.cjs.map +1 -0
- package/dist/cjs/batcher.d.cts +149 -0
- package/dist/cjs/debouncer.cjs +3 -4
- package/dist/cjs/debouncer.cjs.map +1 -1
- package/dist/cjs/debouncer.d.cts +1 -2
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/queuer.cjs +68 -41
- package/dist/cjs/queuer.cjs.map +1 -1
- package/dist/cjs/queuer.d.cts +123 -92
- package/dist/cjs/rate-limiter.cjs +3 -4
- package/dist/cjs/rate-limiter.cjs.map +1 -1
- package/dist/cjs/rate-limiter.d.cts +1 -2
- package/dist/cjs/throttler.cjs +3 -4
- package/dist/cjs/throttler.cjs.map +1 -1
- package/dist/cjs/throttler.d.cts +1 -2
- package/dist/esm/async-debouncer.d.ts +2 -2
- package/dist/esm/async-debouncer.js +10 -6
- package/dist/esm/async-debouncer.js.map +1 -1
- package/dist/esm/async-queuer.d.ts +121 -93
- package/dist/esm/async-queuer.js +135 -106
- package/dist/esm/async-queuer.js.map +1 -1
- package/dist/esm/async-rate-limiter.d.ts +1 -2
- package/dist/esm/async-rate-limiter.js +3 -4
- package/dist/esm/async-rate-limiter.js.map +1 -1
- package/dist/esm/async-throttler.d.ts +3 -2
- package/dist/esm/async-throttler.js +14 -4
- package/dist/esm/async-throttler.js.map +1 -1
- package/dist/esm/batcher.d.ts +149 -0
- package/dist/esm/batcher.js +138 -0
- package/dist/esm/batcher.js.map +1 -0
- package/dist/esm/debouncer.d.ts +1 -2
- package/dist/esm/debouncer.js +3 -4
- package/dist/esm/debouncer.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/queuer.d.ts +123 -92
- package/dist/esm/queuer.js +68 -41
- package/dist/esm/queuer.js.map +1 -1
- package/dist/esm/rate-limiter.d.ts +1 -2
- package/dist/esm/rate-limiter.js +3 -4
- package/dist/esm/rate-limiter.js.map +1 -1
- package/dist/esm/throttler.d.ts +1 -2
- package/dist/esm/throttler.js +3 -4
- package/dist/esm/throttler.js.map +1 -1
- package/package.json +11 -1
- package/src/async-debouncer.ts +12 -6
- package/src/async-queuer.ts +216 -193
- package/src/async-rate-limiter.ts +3 -4
- package/src/async-throttler.ts +18 -4
- package/src/batcher.ts +253 -0
- package/src/debouncer.ts +3 -4
- package/src/index.ts +1 -0
- package/src/queuer.ts +142 -98
- package/src/rate-limiter.ts +3 -4
- package/src/throttler.ts +3 -4
|
@@ -147,7 +147,6 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
149
|
* Updates the rate limiter options
|
|
150
|
-
* Returns the new options state
|
|
151
150
|
*/
|
|
152
151
|
setOptions(newOptions: Partial<AsyncRateLimiterOptions<TFn>>): void {
|
|
153
152
|
this._options = { ...this._options, ...newOptions }
|
|
@@ -221,7 +220,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
221
220
|
if (this._options.windowType === 'sliding') {
|
|
222
221
|
// For sliding window, we can execute if we have capacity in the current window
|
|
223
222
|
if (this._executionTimes.length < limit) {
|
|
224
|
-
await this.
|
|
223
|
+
await this.execute(...args)
|
|
225
224
|
return this._lastResult
|
|
226
225
|
}
|
|
227
226
|
} else {
|
|
@@ -231,7 +230,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
231
230
|
const isNewWindow = oldestExecution + window <= now
|
|
232
231
|
|
|
233
232
|
if (isNewWindow || this._executionTimes.length < limit) {
|
|
234
|
-
await this.
|
|
233
|
+
await this.execute(...args)
|
|
235
234
|
return this._lastResult
|
|
236
235
|
}
|
|
237
236
|
}
|
|
@@ -240,7 +239,7 @@ export class AsyncRateLimiter<TFn extends AnyAsyncFunction> {
|
|
|
240
239
|
return undefined
|
|
241
240
|
}
|
|
242
241
|
|
|
243
|
-
private async
|
|
242
|
+
private async execute(
|
|
244
243
|
...args: Parameters<TFn>
|
|
245
244
|
): Promise<ReturnType<TFn> | undefined> {
|
|
246
245
|
if (!this.getEnabled()) return
|
package/src/async-throttler.ts
CHANGED
|
@@ -114,6 +114,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
114
114
|
private _settleCount = 0
|
|
115
115
|
private _successCount = 0
|
|
116
116
|
private _timeoutId: NodeJS.Timeout | null = null
|
|
117
|
+
private _resolvePreviousPromise:
|
|
118
|
+
| ((value?: ReturnType<TFn> | undefined) => void)
|
|
119
|
+
| null = null
|
|
117
120
|
|
|
118
121
|
constructor(
|
|
119
122
|
private fn: TFn,
|
|
@@ -128,7 +131,6 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
128
131
|
|
|
129
132
|
/**
|
|
130
133
|
* Updates the throttler options
|
|
131
|
-
* Returns the new options state
|
|
132
134
|
*/
|
|
133
135
|
setOptions(newOptions: Partial<AsyncThrottlerOptions<TFn>>): void {
|
|
134
136
|
this._options = { ...this._options, ...newOptions }
|
|
@@ -181,15 +183,18 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
181
183
|
const timeSinceLastExecution = now - this._lastExecutionTime
|
|
182
184
|
const wait = this.getWait()
|
|
183
185
|
|
|
186
|
+
this.resolvePreviousPromise()
|
|
187
|
+
|
|
184
188
|
// Handle leading execution
|
|
185
189
|
if (this._options.leading && timeSinceLastExecution >= wait) {
|
|
186
|
-
await this.
|
|
190
|
+
await this.execute(...args)
|
|
187
191
|
return this._lastResult
|
|
188
192
|
} else {
|
|
189
193
|
// Store the most recent arguments for potential trailing execution
|
|
190
194
|
this._lastArgs = args
|
|
191
195
|
|
|
192
196
|
return new Promise((resolve) => {
|
|
197
|
+
this._resolvePreviousPromise = resolve
|
|
193
198
|
// Clear any existing timeout to ensure we use the latest arguments
|
|
194
199
|
if (this._timeoutId) {
|
|
195
200
|
clearTimeout(this._timeoutId)
|
|
@@ -203,8 +208,9 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
203
208
|
const timeoutDuration = wait - _timeSinceLastExecution
|
|
204
209
|
this._timeoutId = setTimeout(async () => {
|
|
205
210
|
if (this._lastArgs !== undefined) {
|
|
206
|
-
await this.
|
|
211
|
+
await this.execute(...this._lastArgs)
|
|
207
212
|
}
|
|
213
|
+
this._resolvePreviousPromise = null
|
|
208
214
|
resolve(this._lastResult)
|
|
209
215
|
}, timeoutDuration)
|
|
210
216
|
}
|
|
@@ -212,7 +218,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
|
|
215
|
-
private async
|
|
221
|
+
private async execute(
|
|
216
222
|
...args: Parameters<TFn>
|
|
217
223
|
): Promise<ReturnType<TFn> | undefined> {
|
|
218
224
|
if (!this.getEnabled() || this._isExecuting) return undefined
|
|
@@ -241,6 +247,13 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
241
247
|
return this._lastResult
|
|
242
248
|
}
|
|
243
249
|
|
|
250
|
+
private resolvePreviousPromise(): void {
|
|
251
|
+
if (this._resolvePreviousPromise) {
|
|
252
|
+
this._resolvePreviousPromise(this._lastResult)
|
|
253
|
+
this._resolvePreviousPromise = null
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
244
257
|
/**
|
|
245
258
|
* Cancels any pending execution or aborts any execution in progress
|
|
246
259
|
*/
|
|
@@ -253,6 +266,7 @@ export class AsyncThrottler<TFn extends AnyAsyncFunction> {
|
|
|
253
266
|
this._abortController.abort()
|
|
254
267
|
this._abortController = null
|
|
255
268
|
}
|
|
269
|
+
this.resolvePreviousPromise()
|
|
256
270
|
this._lastArgs = undefined
|
|
257
271
|
}
|
|
258
272
|
|
package/src/batcher.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import type { OptionalKeys } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for configuring a Batcher instance
|
|
5
|
+
*/
|
|
6
|
+
export interface BatcherOptions<TValue> {
|
|
7
|
+
/**
|
|
8
|
+
* Custom function to determine if a batch should be processed
|
|
9
|
+
* Return true to process the batch immediately
|
|
10
|
+
*/
|
|
11
|
+
getShouldExecute?: (items: Array<TValue>, batcher: Batcher<TValue>) => boolean
|
|
12
|
+
/**
|
|
13
|
+
* Maximum number of items in a batch
|
|
14
|
+
* @default Infinity
|
|
15
|
+
*/
|
|
16
|
+
maxSize?: number
|
|
17
|
+
/**
|
|
18
|
+
* Callback fired after a batch is processed
|
|
19
|
+
*/
|
|
20
|
+
onExecute?: (batcher: Batcher<TValue>) => void
|
|
21
|
+
/**
|
|
22
|
+
* Callback fired when the batcher's running state changes
|
|
23
|
+
*/
|
|
24
|
+
onIsRunningChange?: (batcher: Batcher<TValue>) => void
|
|
25
|
+
/**
|
|
26
|
+
* Callback fired after items are added to the batcher
|
|
27
|
+
*/
|
|
28
|
+
onItemsChange?: (batcher: Batcher<TValue>) => void
|
|
29
|
+
/**
|
|
30
|
+
* Whether the batcher should start processing immediately
|
|
31
|
+
* @default true
|
|
32
|
+
*/
|
|
33
|
+
started?: boolean
|
|
34
|
+
/**
|
|
35
|
+
* Maximum time in milliseconds to wait before processing a batch.
|
|
36
|
+
* If the wait duration has elapsed, the batch will be processed.
|
|
37
|
+
* If not provided, the batch will not be triggered by a timeout.
|
|
38
|
+
* @default Infinity
|
|
39
|
+
*/
|
|
40
|
+
wait?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type BatcherOptionsWithOptionalCallbacks<TValue> = OptionalKeys<
|
|
44
|
+
Required<BatcherOptions<TValue>>,
|
|
45
|
+
'onExecute' | 'onItemsChange' | 'onIsRunningChange'
|
|
46
|
+
>
|
|
47
|
+
|
|
48
|
+
const defaultOptions: BatcherOptionsWithOptionalCallbacks<any> = {
|
|
49
|
+
getShouldExecute: () => false,
|
|
50
|
+
maxSize: Infinity,
|
|
51
|
+
started: true,
|
|
52
|
+
wait: Infinity,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A class that collects items and processes them in batches.
|
|
57
|
+
*
|
|
58
|
+
* Batching is a technique for grouping multiple operations together to be processed as a single unit.
|
|
59
|
+
*
|
|
60
|
+
* The Batcher provides a flexible way to implement batching with configurable:
|
|
61
|
+
* - Maximum batch size (number of items per batch)
|
|
62
|
+
* - Time-based batching (process after X milliseconds)
|
|
63
|
+
* - Custom batch processing logic via getShouldExecute
|
|
64
|
+
* - Event callbacks for monitoring batch operations
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* const batcher = new Batcher<number>(
|
|
69
|
+
* (items) => console.log('Processing batch:', items),
|
|
70
|
+
* {
|
|
71
|
+
* maxSize: 5,
|
|
72
|
+
* wait: 2000,
|
|
73
|
+
* onExecuteBatch: (items) => console.log('Batch executed:', items)
|
|
74
|
+
* }
|
|
75
|
+
* );
|
|
76
|
+
*
|
|
77
|
+
* batcher.addItem(1);
|
|
78
|
+
* batcher.addItem(2);
|
|
79
|
+
* // After 2 seconds or when 5 items are added, whichever comes first,
|
|
80
|
+
* // the batch will be processed
|
|
81
|
+
* // batcher.execute() // manually trigger a batch
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export class Batcher<TValue> {
|
|
85
|
+
private _options: BatcherOptionsWithOptionalCallbacks<TValue>
|
|
86
|
+
private _batchExecutionCount = 0
|
|
87
|
+
private _itemExecutionCount = 0
|
|
88
|
+
private _items: Array<TValue> = []
|
|
89
|
+
private _running: boolean
|
|
90
|
+
private _timeoutId: NodeJS.Timeout | null = null
|
|
91
|
+
|
|
92
|
+
constructor(
|
|
93
|
+
private fn: (items: Array<TValue>) => void,
|
|
94
|
+
initialOptions: BatcherOptions<TValue>,
|
|
95
|
+
) {
|
|
96
|
+
this._options = { ...defaultOptions, ...initialOptions }
|
|
97
|
+
this._running = this._options.started
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Updates the batcher options
|
|
102
|
+
*/
|
|
103
|
+
setOptions(newOptions: Partial<BatcherOptions<TValue>>): void {
|
|
104
|
+
this._options = { ...this._options, ...newOptions }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns the current batcher options
|
|
109
|
+
*/
|
|
110
|
+
getOptions(): BatcherOptions<TValue> {
|
|
111
|
+
return this._options
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Adds an item to the batcher
|
|
116
|
+
* If the batch size is reached, timeout occurs, or shouldProcess returns true, the batch will be processed
|
|
117
|
+
*/
|
|
118
|
+
addItem(item: TValue): void {
|
|
119
|
+
this._items.push(item)
|
|
120
|
+
this._options.onItemsChange?.(this)
|
|
121
|
+
|
|
122
|
+
const shouldProcess =
|
|
123
|
+
this._items.length >= this._options.maxSize ||
|
|
124
|
+
this._options.getShouldExecute(this._items, this)
|
|
125
|
+
|
|
126
|
+
if (shouldProcess) {
|
|
127
|
+
this.execute()
|
|
128
|
+
} else if (
|
|
129
|
+
this._running &&
|
|
130
|
+
!this._timeoutId &&
|
|
131
|
+
this._options.wait !== Infinity
|
|
132
|
+
) {
|
|
133
|
+
this._timeoutId = setTimeout(() => this.execute(), this._options.wait)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Processes the current batch of items.
|
|
139
|
+
* This method will automatically be triggered if the batcher is running and any of these conditions are met:
|
|
140
|
+
* - The number of items reaches batchSize
|
|
141
|
+
* - The wait duration has elapsed
|
|
142
|
+
* - The getShouldExecute function returns true upon adding an item
|
|
143
|
+
*
|
|
144
|
+
* You can also call this method manually to process the current batch at any time.
|
|
145
|
+
*/
|
|
146
|
+
execute(): void {
|
|
147
|
+
if (this._timeoutId) {
|
|
148
|
+
clearTimeout(this._timeoutId)
|
|
149
|
+
this._timeoutId = null
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (this._items.length === 0) {
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const batch = this.getAllItems() // copy of the items to be processed (to prevent race conditions)
|
|
157
|
+
this._items = [] // Clear items before processing to prevent race conditions
|
|
158
|
+
this._options.onItemsChange?.(this) // Call onItemsChange to notify listeners that the items have changed
|
|
159
|
+
|
|
160
|
+
this.fn(batch)
|
|
161
|
+
this._batchExecutionCount++
|
|
162
|
+
this._itemExecutionCount += batch.length
|
|
163
|
+
this._options.onExecute?.(this)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Stops the batcher from processing batches
|
|
168
|
+
*/
|
|
169
|
+
stop(): void {
|
|
170
|
+
this._running = false
|
|
171
|
+
this._options.onIsRunningChange?.(this)
|
|
172
|
+
if (this._timeoutId) {
|
|
173
|
+
clearTimeout(this._timeoutId)
|
|
174
|
+
this._timeoutId = null
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Starts the batcher and processes any pending items
|
|
180
|
+
*/
|
|
181
|
+
start(): void {
|
|
182
|
+
this._running = true
|
|
183
|
+
this._options.onIsRunningChange?.(this)
|
|
184
|
+
if (this._items.length > 0 && !this._timeoutId) {
|
|
185
|
+
this._timeoutId = setTimeout(() => this.execute(), this._options.wait)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Returns the current number of items in the batcher
|
|
191
|
+
*/
|
|
192
|
+
getSize(): number {
|
|
193
|
+
return this._items.length
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns true if the batcher is empty
|
|
198
|
+
*/
|
|
199
|
+
getIsEmpty(): boolean {
|
|
200
|
+
return this._items.length === 0
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Returns true if the batcher is running
|
|
205
|
+
*/
|
|
206
|
+
getIsRunning(): boolean {
|
|
207
|
+
return this._running
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Returns a copy of all items currently in the batcher
|
|
212
|
+
*/
|
|
213
|
+
getAllItems(): Array<TValue> {
|
|
214
|
+
return [...this._items]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Returns the number of times batches have been processed
|
|
219
|
+
*/
|
|
220
|
+
getBatchExecutionCount(): number {
|
|
221
|
+
return this._batchExecutionCount
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Returns the total number of individual items that have been processed
|
|
226
|
+
*/
|
|
227
|
+
getItemExecutionCount(): number {
|
|
228
|
+
return this._itemExecutionCount
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Creates a batcher that processes items in batches
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```ts
|
|
237
|
+
* const batchItems = batch<number>({
|
|
238
|
+
* batchSize: 3,
|
|
239
|
+
* processBatch: (items) => console.log('Processing:', items)
|
|
240
|
+
* });
|
|
241
|
+
*
|
|
242
|
+
* batchItems(1);
|
|
243
|
+
* batchItems(2);
|
|
244
|
+
* batchItems(3); // Triggers batch processing
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export function batch<TValue>(
|
|
248
|
+
fn: (items: Array<TValue>) => void,
|
|
249
|
+
options: BatcherOptions<TValue>,
|
|
250
|
+
) {
|
|
251
|
+
const batcher = new Batcher<TValue>(fn, options)
|
|
252
|
+
return batcher.addItem.bind(batcher)
|
|
253
|
+
}
|
package/src/debouncer.ts
CHANGED
|
@@ -84,7 +84,6 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Updates the debouncer options
|
|
87
|
-
* Returns the new options state
|
|
88
87
|
*/
|
|
89
88
|
setOptions(newOptions: Partial<DebouncerOptions<TFn>>): void {
|
|
90
89
|
this._options = { ...this._options, ...newOptions }
|
|
@@ -127,7 +126,7 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
127
126
|
if (this._options.leading && this._canLeadingExecute) {
|
|
128
127
|
this._canLeadingExecute = false
|
|
129
128
|
_didLeadingExecute = true
|
|
130
|
-
this.
|
|
129
|
+
this.execute(...args)
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
// Start pending state to indicate that the debouncer is waiting for the trailing edge
|
|
@@ -142,12 +141,12 @@ export class Debouncer<TFn extends AnyFunction> {
|
|
|
142
141
|
this._timeoutId = setTimeout(() => {
|
|
143
142
|
this._canLeadingExecute = true
|
|
144
143
|
if (this._options.trailing && !_didLeadingExecute) {
|
|
145
|
-
this.
|
|
144
|
+
this.execute(...args)
|
|
146
145
|
}
|
|
147
146
|
}, this.getWait())
|
|
148
147
|
}
|
|
149
148
|
|
|
150
|
-
private
|
|
149
|
+
private execute(...args: Parameters<TFn>): void {
|
|
151
150
|
if (!this.getEnabled()) return undefined
|
|
152
151
|
this.fn(...args) // EXECUTE!
|
|
153
152
|
this._isPending = false
|
package/src/index.ts
CHANGED