@xyo-network/bridge-pub-sub 5.3.20 → 5.3.24
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/package.json +52 -29
- package/src/AbstractModuleHost/AbstractModuleHost.ts +0 -12
- package/src/AbstractModuleHost/index.ts +0 -1
- package/src/AsyncQueryBus/AsyncQueryBusBase.ts +0 -163
- package/src/AsyncQueryBus/AsyncQueryBusClient.ts +0 -190
- package/src/AsyncQueryBus/AsyncQueryBusHost.ts +0 -305
- package/src/AsyncQueryBus/ModuleHost/ModuleHost.ts +0 -30
- package/src/AsyncQueryBus/ModuleHost/index.ts +0 -1
- package/src/AsyncQueryBus/ModuleProxy/ModuleProxy.ts +0 -104
- package/src/AsyncQueryBus/ModuleProxy/index.ts +0 -1
- package/src/AsyncQueryBus/index.ts +0 -5
- package/src/AsyncQueryBus/model/BaseConfig.ts +0 -17
- package/src/AsyncQueryBus/model/ClientConfig.ts +0 -11
- package/src/AsyncQueryBus/model/HostConfig.ts +0 -29
- package/src/AsyncQueryBus/model/IntersectConfig.ts +0 -13
- package/src/AsyncQueryBus/model/Params.ts +0 -18
- package/src/AsyncQueryBus/model/QueryStatus.ts +0 -2
- package/src/AsyncQueryBus/model/index.ts +0 -6
- package/src/Config.ts +0 -22
- package/src/Params.ts +0 -9
- package/src/PubSubBridge.ts +0 -287
- package/src/PubSubBridgeModuleResolver.ts +0 -78
- package/src/Schema.ts +0 -4
- package/src/index.ts +0 -7
package/src/PubSubBridge.ts
DELETED
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Address, assertEx,
|
|
3
|
-
exists,
|
|
4
|
-
forget,
|
|
5
|
-
isAddress, toSafeJsonString,
|
|
6
|
-
} from '@xylabs/sdk-js'
|
|
7
|
-
import { AbstractBridge } from '@xyo-network/bridge-abstract'
|
|
8
|
-
import {
|
|
9
|
-
BridgeExposeOptions,
|
|
10
|
-
BridgeModule,
|
|
11
|
-
BridgeUnexposeOptions,
|
|
12
|
-
QueryFulfillFinishedEventArgs,
|
|
13
|
-
QueryFulfillStartedEventArgs,
|
|
14
|
-
QuerySendFinishedEventArgs,
|
|
15
|
-
QuerySendStartedEventArgs,
|
|
16
|
-
} from '@xyo-network/bridge-model'
|
|
17
|
-
import {
|
|
18
|
-
AddressPayload,
|
|
19
|
-
AddressSchema,
|
|
20
|
-
creatableModule,
|
|
21
|
-
ModuleFilterOptions,
|
|
22
|
-
ModuleIdentifier,
|
|
23
|
-
ModuleInstance,
|
|
24
|
-
resolveAddressToInstance,
|
|
25
|
-
resolveAddressToInstanceUp,
|
|
26
|
-
ResolveHelper,
|
|
27
|
-
} from '@xyo-network/module-model'
|
|
28
|
-
import { asNodeInstance } from '@xyo-network/node-model'
|
|
29
|
-
import { isPayloadOfSchemaType, Schema } from '@xyo-network/payload-model'
|
|
30
|
-
import { Mutex } from 'async-mutex'
|
|
31
|
-
import { LRUCache } from 'lru-cache'
|
|
32
|
-
|
|
33
|
-
import { AsyncQueryBusClient, AsyncQueryBusHost } from './AsyncQueryBus/index.ts'
|
|
34
|
-
import { PubSubBridgeConfigSchema } from './Config.ts'
|
|
35
|
-
import { PubSubBridgeParams } from './Params.ts'
|
|
36
|
-
import { PubSubBridgeModuleResolver } from './PubSubBridgeModuleResolver.ts'
|
|
37
|
-
|
|
38
|
-
const moduleName = 'PubSubBridge'
|
|
39
|
-
|
|
40
|
-
@creatableModule()
|
|
41
|
-
export class PubSubBridge<TParams extends PubSubBridgeParams = PubSubBridgeParams> extends AbstractBridge<TParams> implements BridgeModule<TParams> {
|
|
42
|
-
static override readonly configSchemas: Schema[] = [...super.configSchemas, PubSubBridgeConfigSchema]
|
|
43
|
-
static override readonly defaultConfigSchema: Schema = PubSubBridgeConfigSchema
|
|
44
|
-
|
|
45
|
-
protected _configRootAddress: Address = '' as Address
|
|
46
|
-
protected _configStateStoreArchivist: string = ''
|
|
47
|
-
protected _configStateStoreBoundWitnessDiviner: string = ''
|
|
48
|
-
protected _exposedAddresses: Address[] = []
|
|
49
|
-
protected _lastState?: LRUCache<string, number>
|
|
50
|
-
|
|
51
|
-
private _busClient?: AsyncQueryBusClient
|
|
52
|
-
private _busHost?: AsyncQueryBusHost
|
|
53
|
-
private _discoverRootsMutex = new Mutex()
|
|
54
|
-
private _resolver?: PubSubBridgeModuleResolver
|
|
55
|
-
|
|
56
|
-
override get resolver(): PubSubBridgeModuleResolver {
|
|
57
|
-
this._resolver
|
|
58
|
-
= this._resolver
|
|
59
|
-
?? new PubSubBridgeModuleResolver({
|
|
60
|
-
additionalSigners: this.additionalSigners,
|
|
61
|
-
archiving: { ...this.archiving, resolveArchivists: this.resolveArchivingArchivists.bind(this) },
|
|
62
|
-
bridge: this,
|
|
63
|
-
busClient: assertEx(this.busClient(), () => 'busClient not configured'),
|
|
64
|
-
onQuerySendFinished: (args: Omit<QuerySendFinishedEventArgs, 'mod'>) => {
|
|
65
|
-
forget(this.emit('querySendFinished', { mod: this, ...args }))
|
|
66
|
-
},
|
|
67
|
-
onQuerySendStarted: (args: Omit<QuerySendStartedEventArgs, 'mod'>) => {
|
|
68
|
-
forget(this.emit('querySendStarted', { mod: this, ...args }))
|
|
69
|
-
},
|
|
70
|
-
root: this,
|
|
71
|
-
wrapperAccount: this.account,
|
|
72
|
-
})
|
|
73
|
-
return this._resolver
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
protected get moduleName() {
|
|
77
|
-
return this.modName ?? moduleName
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async connect(id: ModuleIdentifier, maxDepth = 5): Promise<Address | undefined> {
|
|
81
|
-
const transformedId = assertEx(await ResolveHelper.transformModuleIdentifier(id), () => `Unable to transform module identifier: ${id}`)
|
|
82
|
-
// check if already connected
|
|
83
|
-
const existingInstance = await this.resolve<ModuleInstance>(transformedId)
|
|
84
|
-
if (existingInstance) {
|
|
85
|
-
return existingInstance.address
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// use the resolver to create the proxy instance
|
|
89
|
-
const [instance] = await this.resolver.resolveHandler<ModuleInstance>(id)
|
|
90
|
-
return await this.connectInstance(instance, maxDepth)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async disconnect(id: ModuleIdentifier): Promise<Address | undefined> {
|
|
94
|
-
const transformedId = assertEx(await ResolveHelper.transformModuleIdentifier(id), () => `Unable to transform module identifier: ${id}`)
|
|
95
|
-
const instance = await this.resolve<ModuleInstance>(transformedId)
|
|
96
|
-
if (instance) {
|
|
97
|
-
this.downResolver.remove(instance.address)
|
|
98
|
-
return instance.address
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async exposeChild(mod: ModuleInstance, options?: BridgeExposeOptions | undefined): Promise<ModuleInstance[]> {
|
|
103
|
-
const { maxDepth = 5 } = options ?? {}
|
|
104
|
-
console.log(`exposeChild: ${mod.address} ${mod?.id} ${maxDepth}`)
|
|
105
|
-
const host = assertEx(this.busHost(), () => 'Not configured as a host')
|
|
106
|
-
host.expose(mod)
|
|
107
|
-
const children = maxDepth > 0 ? ((await mod.publicChildren?.()) ?? []) : []
|
|
108
|
-
this.logger?.log(`childrenToExpose [${mod.id}][${mod.address}]: ${toSafeJsonString(children.map(child => child.id))}`)
|
|
109
|
-
const exposedChildren = (await Promise.all(children.map(child => this.exposeChild(child, { maxDepth: maxDepth - 1, required: false }))))
|
|
110
|
-
.flat()
|
|
111
|
-
.filter(exists)
|
|
112
|
-
const allExposed = [mod, ...exposedChildren]
|
|
113
|
-
|
|
114
|
-
for (const exposedMod of allExposed) this.logger?.log(`exposed: ${exposedMod.address} [${mod.id}]`)
|
|
115
|
-
|
|
116
|
-
return allExposed
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async exposeHandler(address: Address, options?: BridgeExposeOptions | undefined): Promise<ModuleInstance[]> {
|
|
120
|
-
const { required = true } = options ?? {}
|
|
121
|
-
const mod = await resolveAddressToInstanceUp(this, address)
|
|
122
|
-
console.log(`exposeHandler: ${address} ${mod?.id}`)
|
|
123
|
-
if (required && !mod) {
|
|
124
|
-
throw new Error(`Unable to find required module: ${address}`)
|
|
125
|
-
}
|
|
126
|
-
if (mod) {
|
|
127
|
-
return this.exposeChild(mod, options)
|
|
128
|
-
}
|
|
129
|
-
return []
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
exposedHandler(): Address[] {
|
|
133
|
-
const exposedSet = this.busHost()?.exposedAddresses
|
|
134
|
-
return exposedSet ? [...exposedSet] : []
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async getRoots(force?: boolean): Promise<ModuleInstance[]> {
|
|
138
|
-
return await this._discoverRootsMutex.runExclusive(async () => {
|
|
139
|
-
if (this._roots === undefined || force) {
|
|
140
|
-
const rootAddresses = (
|
|
141
|
-
await Promise.all(
|
|
142
|
-
(this.config.roots ?? []).map(async (id) => {
|
|
143
|
-
try {
|
|
144
|
-
return await ResolveHelper.transformModuleIdentifier(id)
|
|
145
|
-
} catch (ex) {
|
|
146
|
-
this.logger?.warn('Unable to transform module identifier:', id, ex)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
}),
|
|
150
|
-
)
|
|
151
|
-
).filter(exists)
|
|
152
|
-
const rootInstances = (
|
|
153
|
-
await Promise.all(
|
|
154
|
-
rootAddresses.map(async (root) => {
|
|
155
|
-
try {
|
|
156
|
-
return await this.resolver.resolveHandler<ModuleInstance>(root)
|
|
157
|
-
} catch (ex) {
|
|
158
|
-
this.logger?.warn('Unable to resolve root:', root, ex)
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
}),
|
|
162
|
-
)
|
|
163
|
-
)
|
|
164
|
-
.flat()
|
|
165
|
-
.filter(exists)
|
|
166
|
-
for (const instance of rootInstances) {
|
|
167
|
-
this.downResolver.add(instance)
|
|
168
|
-
}
|
|
169
|
-
this._roots = rootInstances
|
|
170
|
-
}
|
|
171
|
-
return this._roots
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** @deprecated do not pass undefined. If trying to get all, pass '*' */
|
|
176
|
-
override async resolve(): Promise<ModuleInstance[]>
|
|
177
|
-
override async resolve<T extends ModuleInstance = ModuleInstance>(all: '*', options?: ModuleFilterOptions<T>): Promise<T[]>
|
|
178
|
-
override async resolve<T extends ModuleInstance = ModuleInstance>(id: ModuleIdentifier, options?: ModuleFilterOptions<T>): Promise<T | undefined>
|
|
179
|
-
|
|
180
|
-
override async resolve<T extends ModuleInstance = ModuleInstance>(
|
|
181
|
-
id: ModuleIdentifier = '*',
|
|
182
|
-
options: ModuleFilterOptions<T> = {},
|
|
183
|
-
): Promise<T | T[] | undefined> {
|
|
184
|
-
const roots = (this._roots ?? []) as T[]
|
|
185
|
-
const workingSet = (options.direction === 'up' ? [this as ModuleInstance] : [...roots, this]) as T[]
|
|
186
|
-
if (id === '*') {
|
|
187
|
-
const remainingDepth = (options.maxDepth ?? 1) - 1
|
|
188
|
-
return remainingDepth <= 0
|
|
189
|
-
? workingSet
|
|
190
|
-
: (
|
|
191
|
-
[...workingSet, ...(await Promise.all(roots.map(mod => mod.resolve('*', { ...options, maxDepth: remainingDepth })))).flat()]
|
|
192
|
-
)
|
|
193
|
-
}
|
|
194
|
-
switch (typeof id) {
|
|
195
|
-
case 'string': {
|
|
196
|
-
const parts = id.split(':')
|
|
197
|
-
const first = assertEx(parts.shift(), () => 'Missing first part')
|
|
198
|
-
const firstInstance: ModuleInstance | undefined
|
|
199
|
-
= isAddress(first)
|
|
200
|
-
? ((await resolveAddressToInstance(this, first, undefined, [], options.direction)) as T)
|
|
201
|
-
: this._roots?.find(mod => mod.id === first)
|
|
202
|
-
return (parts.length === 0 ? firstInstance : firstInstance?.resolve(parts.join(':'), options)) as T | undefined
|
|
203
|
-
}
|
|
204
|
-
default: {
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
override async startHandler() {
|
|
211
|
-
this.busHost()?.start()
|
|
212
|
-
await super.startHandler()
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
async unexposeHandler(id: ModuleIdentifier, options?: BridgeUnexposeOptions | undefined): Promise<ModuleInstance[]> {
|
|
216
|
-
const { maxDepth = 2, required = true } = options ?? {}
|
|
217
|
-
const host = assertEx(this.busHost(), () => 'Not configured as a host')
|
|
218
|
-
const mod = await host.unexpose(id, required)
|
|
219
|
-
if (mod) {
|
|
220
|
-
const children = maxDepth > 0 ? ((await mod.publicChildren?.()) ?? []) : []
|
|
221
|
-
const exposedChildren = (
|
|
222
|
-
await Promise.all(children.map(child => this.unexposeHandler(child.address, { maxDepth: maxDepth - 1, required: false })))
|
|
223
|
-
)
|
|
224
|
-
.flat()
|
|
225
|
-
.filter(exists)
|
|
226
|
-
return [mod, ...exposedChildren]
|
|
227
|
-
}
|
|
228
|
-
return []
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
protected busClient() {
|
|
232
|
-
if (!this._busClient && this.config.client) {
|
|
233
|
-
this._busClient = new AsyncQueryBusClient({
|
|
234
|
-
config: this.config.client,
|
|
235
|
-
logger: this.logger,
|
|
236
|
-
rootModule: this,
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
return this._busClient
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
protected busHost() {
|
|
243
|
-
if (!this._busHost && this.config.host) {
|
|
244
|
-
this._busHost = new AsyncQueryBusHost({
|
|
245
|
-
config: this.config.host,
|
|
246
|
-
logger: this.logger,
|
|
247
|
-
onQueryFulfillFinished: (args: Omit<QueryFulfillFinishedEventArgs, 'mod'>) => {
|
|
248
|
-
if (this.archiving && this.isAllowedArchivingQuery(args.query.schema)) {
|
|
249
|
-
forget(this.storeToArchivists(args.result?.flat() ?? []))
|
|
250
|
-
}
|
|
251
|
-
forget(this.emit('queryFulfillFinished', { mod: this, ...args }))
|
|
252
|
-
},
|
|
253
|
-
onQueryFulfillStarted: (args: Omit<QueryFulfillStartedEventArgs, 'mod'>) => {
|
|
254
|
-
if (this.archiving && this.isAllowedArchivingQuery(args.query.schema)) {
|
|
255
|
-
forget(this.storeToArchivists([args.query, ...(args.payloads ?? [])]))
|
|
256
|
-
}
|
|
257
|
-
forget(this.emit('queryFulfillStarted', { mod: this, ...args }))
|
|
258
|
-
},
|
|
259
|
-
rootModule: this,
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
return this._busHost
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
protected async connectInstance(instance?: ModuleInstance, maxDepth = 5): Promise<Address | undefined> {
|
|
266
|
-
if (instance) {
|
|
267
|
-
this.downResolver.add(instance)
|
|
268
|
-
if (maxDepth > 0) {
|
|
269
|
-
const node = asNodeInstance(instance)
|
|
270
|
-
if (node) {
|
|
271
|
-
const state = await node.state()
|
|
272
|
-
const children = (state?.filter(isPayloadOfSchemaType<AddressPayload>(AddressSchema)).map(s => s.address) ?? []).filter(
|
|
273
|
-
a => a !== instance.address,
|
|
274
|
-
)
|
|
275
|
-
await Promise.all(children.map(child => this.connect(child, maxDepth - 1)))
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
this.logger?.log(`Connect: ${instance.id}`)
|
|
279
|
-
return instance.address
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
protected override async stopHandler() {
|
|
284
|
-
await super.stopHandler()
|
|
285
|
-
this.busHost()?.stop()
|
|
286
|
-
}
|
|
287
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { Address, CreatableName } from '@xylabs/sdk-js'
|
|
2
|
-
import { assertEx, isAddress } from '@xylabs/sdk-js'
|
|
3
|
-
import { Account } from '@xyo-network/account'
|
|
4
|
-
import type { BridgeModuleResolverParams } from '@xyo-network/bridge-abstract'
|
|
5
|
-
import { AbstractBridgeModuleResolver, wrapModuleWithType } from '@xyo-network/bridge-abstract'
|
|
6
|
-
import type { ConfigPayload } from '@xyo-network/config-payload-plugin'
|
|
7
|
-
import { ConfigSchema } from '@xyo-network/config-payload-plugin'
|
|
8
|
-
import type {
|
|
9
|
-
ModuleConfig,
|
|
10
|
-
ModuleFilterOptions,
|
|
11
|
-
ModuleIdentifier,
|
|
12
|
-
ModuleInstance,
|
|
13
|
-
} from '@xyo-network/module-model'
|
|
14
|
-
import {
|
|
15
|
-
asModuleInstance,
|
|
16
|
-
ModuleConfigSchema,
|
|
17
|
-
ResolveHelper,
|
|
18
|
-
} from '@xyo-network/module-model'
|
|
19
|
-
import { Mutex } from 'async-mutex'
|
|
20
|
-
import { LRUCache } from 'lru-cache'
|
|
21
|
-
|
|
22
|
-
import type { AsyncQueryBusClient, AsyncQueryBusModuleProxyParams } from './AsyncQueryBus/index.ts'
|
|
23
|
-
import { AsyncQueryBusModuleProxy } from './AsyncQueryBus/index.ts'
|
|
24
|
-
|
|
25
|
-
export interface PubSubBridgeModuleResolverParams extends BridgeModuleResolverParams {
|
|
26
|
-
busClient: AsyncQueryBusClient
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export class PubSubBridgeModuleResolver extends AbstractBridgeModuleResolver<PubSubBridgeModuleResolverParams> {
|
|
30
|
-
protected _resolvedCache = new LRUCache<Address, ModuleInstance>({ max: 1000 })
|
|
31
|
-
protected _resolvedCacheMutex = new Mutex()
|
|
32
|
-
|
|
33
|
-
override async resolveHandler<T extends ModuleInstance = ModuleInstance>(id: ModuleIdentifier, options?: ModuleFilterOptions<T>): Promise<T[]> {
|
|
34
|
-
const parentResult = await super.resolveHandler(id, options)
|
|
35
|
-
if (parentResult.length > 0) {
|
|
36
|
-
return parentResult
|
|
37
|
-
}
|
|
38
|
-
const idParts = id.split(':')
|
|
39
|
-
const untransformedFirstPart = assertEx(idParts.shift(), () => 'Missing module identifier')
|
|
40
|
-
const firstPart = await ResolveHelper.transformModuleIdentifier(untransformedFirstPart)
|
|
41
|
-
assertEx(isAddress(firstPart), () => `Invalid module address: ${firstPart}`)
|
|
42
|
-
const remainderParts = idParts.join(':')
|
|
43
|
-
const instance: T = await this._resolvedCacheMutex.runExclusive(async () => {
|
|
44
|
-
const cachedMod = this._resolvedCache.get(firstPart as Address)
|
|
45
|
-
if (cachedMod) {
|
|
46
|
-
const result = idParts.length <= 0 ? cachedMod : cachedMod.resolve(remainderParts, { ...options, maxDepth: (options?.maxDepth ?? 5) - 1 })
|
|
47
|
-
return result as T
|
|
48
|
-
}
|
|
49
|
-
const account = await Account.random()
|
|
50
|
-
const finalParams: AsyncQueryBusModuleProxyParams = {
|
|
51
|
-
name: 'PubSubBridgeModuleResolver' as CreatableName,
|
|
52
|
-
account,
|
|
53
|
-
archiving: this.params.archiving,
|
|
54
|
-
busClient: this.params.busClient,
|
|
55
|
-
config: { schema: ModuleConfigSchema },
|
|
56
|
-
host: this,
|
|
57
|
-
moduleAddress: firstPart as Address,
|
|
58
|
-
onQuerySendFinished: this.params.onQuerySendFinished,
|
|
59
|
-
onQuerySendStarted: this.params.onQuerySendStarted,
|
|
60
|
-
}
|
|
61
|
-
const proxy = await AsyncQueryBusModuleProxy.create(finalParams)
|
|
62
|
-
const state = await proxy.state()
|
|
63
|
-
const configSchema = (state.find(payload => payload.schema === ConfigSchema) as ConfigPayload | undefined)?.config
|
|
64
|
-
const config = assertEx(
|
|
65
|
-
state.find(payload => payload.schema === configSchema),
|
|
66
|
-
() => 'Unable to locate config',
|
|
67
|
-
) as ModuleConfig
|
|
68
|
-
proxy.setConfig(config)
|
|
69
|
-
await proxy.start?.()
|
|
70
|
-
const wrapped = wrapModuleWithType(proxy, account) as unknown as T
|
|
71
|
-
assertEx(asModuleInstance<T>(wrapped, {}), () => `Failed to asModuleInstance [${id}]`)
|
|
72
|
-
this._resolvedCache.set(wrapped.address, wrapped)
|
|
73
|
-
return wrapped as ModuleInstance as T
|
|
74
|
-
})
|
|
75
|
-
const result = remainderParts.length > 0 ? await instance.resolve(remainderParts, options) : instance
|
|
76
|
-
return result ? [result] : []
|
|
77
|
-
}
|
|
78
|
-
}
|
package/src/Schema.ts
DELETED
package/src/index.ts
DELETED