@decentrl/sdk 0.0.1

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 (74) hide show
  1. package/dist/client.d.ts +36 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +192 -0
  4. package/dist/contract-manager.d.ts +23 -0
  5. package/dist/contract-manager.d.ts.map +1 -0
  6. package/dist/contract-manager.js +91 -0
  7. package/dist/define-app.d.ts +8 -0
  8. package/dist/define-app.d.ts.map +1 -0
  9. package/dist/define-app.js +7 -0
  10. package/dist/direct-transport.d.ts +69 -0
  11. package/dist/direct-transport.d.ts.map +1 -0
  12. package/dist/direct-transport.js +450 -0
  13. package/dist/errors.d.ts +7 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +10 -0
  16. package/dist/event-processor.d.ts +19 -0
  17. package/dist/event-processor.d.ts.map +1 -0
  18. package/dist/event-processor.js +93 -0
  19. package/dist/identity-manager.d.ts +22 -0
  20. package/dist/identity-manager.d.ts.map +1 -0
  21. package/dist/identity-manager.js +62 -0
  22. package/dist/identity-serialization.d.ts +5 -0
  23. package/dist/identity-serialization.d.ts.map +1 -0
  24. package/dist/identity-serialization.js +30 -0
  25. package/dist/index.d.ts +18 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +10 -0
  28. package/dist/persistence.d.ts +11 -0
  29. package/dist/persistence.d.ts.map +1 -0
  30. package/dist/persistence.js +82 -0
  31. package/dist/state-store.d.ts +12 -0
  32. package/dist/state-store.d.ts.map +1 -0
  33. package/dist/state-store.js +32 -0
  34. package/dist/sync-manager.d.ts +33 -0
  35. package/dist/sync-manager.d.ts.map +1 -0
  36. package/dist/sync-manager.js +244 -0
  37. package/dist/tag-templates.d.ts +2 -0
  38. package/dist/tag-templates.d.ts.map +1 -0
  39. package/dist/tag-templates.js +23 -0
  40. package/dist/test-helpers.d.ts +15 -0
  41. package/dist/test-helpers.d.ts.map +1 -0
  42. package/dist/test-helpers.js +65 -0
  43. package/dist/transport.d.ts +41 -0
  44. package/dist/transport.d.ts.map +1 -0
  45. package/dist/transport.js +1 -0
  46. package/dist/types.d.ts +131 -0
  47. package/dist/types.d.ts.map +1 -0
  48. package/dist/types.js +1 -0
  49. package/dist/websocket-transport.d.ts +36 -0
  50. package/dist/websocket-transport.d.ts.map +1 -0
  51. package/dist/websocket-transport.js +160 -0
  52. package/package.json +35 -0
  53. package/src/client.ts +277 -0
  54. package/src/contract-manager.test.ts +207 -0
  55. package/src/contract-manager.ts +130 -0
  56. package/src/define-app.ts +25 -0
  57. package/src/direct-transport.test.ts +460 -0
  58. package/src/direct-transport.ts +729 -0
  59. package/src/errors.ts +23 -0
  60. package/src/event-processor.ts +133 -0
  61. package/src/identity-manager.ts +91 -0
  62. package/src/identity-serialization.ts +33 -0
  63. package/src/index.ts +43 -0
  64. package/src/persistence.ts +103 -0
  65. package/src/sdk.e2e.test.ts +367 -0
  66. package/src/state-store.ts +42 -0
  67. package/src/sync-manager.test.ts +414 -0
  68. package/src/sync-manager.ts +308 -0
  69. package/src/tag-templates.test.ts +111 -0
  70. package/src/tag-templates.ts +30 -0
  71. package/src/test-helpers.ts +88 -0
  72. package/src/transport.ts +65 -0
  73. package/src/types.ts +191 -0
  74. package/src/websocket-transport.ts +233 -0
package/src/client.ts ADDED
@@ -0,0 +1,277 @@
1
+ import { ContractManager } from './contract-manager.js';
2
+ import { DirectTransport } from './direct-transport.js';
3
+ import { DecentrlSDKError } from './errors.js';
4
+ import { EventProcessor } from './event-processor.js';
5
+ import { IdentityManager } from './identity-manager.js';
6
+ import {
7
+ clearPersisted,
8
+ loadPersistedIdentity,
9
+ loadPersistedState,
10
+ persistIdentity,
11
+ persistState,
12
+ } from './persistence.js';
13
+ import { StateStore } from './state-store.js';
14
+ import { SyncManager } from './sync-manager.js';
15
+ import type { DecentrlTransport } from './transport.js';
16
+ import type {
17
+ DecentrlAppConfig,
18
+ DecentrlClientConfig,
19
+ EventDefinitions,
20
+ EventEnvelope,
21
+ EventMeta,
22
+ InferState,
23
+ PaginatedResult,
24
+ PublishOptions,
25
+ QueryOptions,
26
+ StateDefinitions,
27
+ StateListener,
28
+ SyncOptions,
29
+ } from './types.js';
30
+ import type { ConnectionStatus } from './websocket-transport.js';
31
+
32
+ export class DecentrlClient<
33
+ TEvents extends EventDefinitions,
34
+ TState extends StateDefinitions<TEvents>,
35
+ > {
36
+ private stateStore: StateStore<InferState<TState>>;
37
+ private eventProcessor: EventProcessor<TEvents, TState>;
38
+ private syncManager: SyncManager<TEvents, TState>;
39
+ private transport: DecentrlTransport;
40
+ private unsubscribePersistState: (() => void) | null = null;
41
+ private unsubscribePersistIdentity: (() => void) | null = null;
42
+
43
+ readonly identity: IdentityManager;
44
+ readonly contracts: ContractManager;
45
+
46
+ constructor(
47
+ private appConfig: DecentrlAppConfig<TEvents, TState>,
48
+ private clientConfig: DecentrlClientConfig,
49
+ ) {
50
+ // Build initial state from slice definitions
51
+ const initialState = {} as InferState<TState>;
52
+
53
+ for (const key of Object.keys(appConfig.state)) {
54
+ // TS can't statically index InferState<TState> during construction
55
+ (initialState as any)[key] = appConfig.state[key].initial;
56
+ }
57
+
58
+ // Restore persisted state if available
59
+ const persistOpts = clientConfig.persist;
60
+ let restoredState = initialState;
61
+
62
+ if (persistOpts) {
63
+ const saved = loadPersistedState<InferState<TState>>(persistOpts);
64
+
65
+ if (saved) {
66
+ // Merge: use saved values for slices that exist, fall back to initial for new slices
67
+ restoredState = { ...initialState, ...saved };
68
+ }
69
+ }
70
+
71
+ this.stateStore = new StateStore(restoredState);
72
+ this.identity = new IdentityManager();
73
+ this.contracts = new ContractManager();
74
+
75
+ // Set up transport (auto-create DirectTransport if none provided)
76
+ this.transport = clientConfig.transport ?? new DirectTransport();
77
+ this.identity.setTransport(this.transport);
78
+ this.contracts.setTransport(this.transport);
79
+
80
+ // Seed identity from transport (e.g. extension already has it after connect)
81
+ const transportIdentity = this.transport.getIdentity();
82
+
83
+ if (transportIdentity) {
84
+ try {
85
+ this.identity.load(transportIdentity);
86
+ } catch (err) {
87
+ console.debug('[Decentrl] Failed to seed identity from transport:', err);
88
+ }
89
+ }
90
+
91
+ this.eventProcessor = new EventProcessor(appConfig.events, appConfig.state, this.stateStore);
92
+
93
+ this.syncManager = new SyncManager(
94
+ this.eventProcessor,
95
+ this.contracts,
96
+ this.identity,
97
+ this.transport,
98
+ );
99
+
100
+ // Restore persisted identity
101
+ if (persistOpts) {
102
+ const savedIdentity = loadPersistedIdentity(persistOpts);
103
+
104
+ if (savedIdentity) {
105
+ try {
106
+ this.identity.load(savedIdentity);
107
+ } catch {
108
+ // Corrupted identity data — clear it
109
+ persistIdentity(persistOpts, null);
110
+ }
111
+ }
112
+
113
+ this.setupPersistence();
114
+ }
115
+ }
116
+
117
+ private setupPersistence(): void {
118
+ const persistOpts = this.clientConfig.persist;
119
+
120
+ if (!persistOpts) {
121
+ return;
122
+ }
123
+
124
+ // Auto-persist identity on changes
125
+ this.unsubscribePersistIdentity = this.identity.onChange((identity) => {
126
+ if (identity) {
127
+ persistIdentity(persistOpts, this.identity.serialize());
128
+ } else {
129
+ persistIdentity(persistOpts, null);
130
+ }
131
+ });
132
+
133
+ // Auto-persist state on changes (debounced)
134
+ let persistTimeout: ReturnType<typeof setTimeout> | null = null;
135
+ this.unsubscribePersistState = this.stateStore.subscribe((state) => {
136
+ if (persistTimeout) {
137
+ clearTimeout(persistTimeout);
138
+ }
139
+
140
+ persistTimeout = setTimeout(() => persistState(persistOpts, state), 100);
141
+ });
142
+ }
143
+
144
+ async publish<K extends string & keyof TEvents>(
145
+ eventType: K,
146
+ data: TEvents[K] extends { schema: infer S extends import('zod').ZodType }
147
+ ? import('zod').infer<S>
148
+ : never,
149
+ options?: PublishOptions,
150
+ ): Promise<void> {
151
+ // 1. Validate data against schema
152
+ this.eventProcessor.validate(eventType, data);
153
+
154
+ // 2. Build envelope
155
+ const meta: EventMeta = {
156
+ senderDid: this.identity.requireIdentity().did,
157
+ timestamp: Date.now(),
158
+ eventId: crypto.randomUUID(),
159
+ ...(options?.ephemeral ? { ephemeral: true } : {}),
160
+ };
161
+ const envelope = this.eventProcessor.buildEnvelope(eventType, data, meta);
162
+
163
+ // 3. Compute tags
164
+ const tags = this.eventProcessor.computeTags(eventType, data);
165
+
166
+ try {
167
+ // 4. Publish via transport
168
+ await this.transport.publishEvent(envelope, {
169
+ tags,
170
+ recipient: options?.recipient,
171
+ ephemeral: options?.ephemeral,
172
+ });
173
+
174
+ // 5. Optimistic local state update
175
+ this.eventProcessor.processEvent(envelope);
176
+ } catch (error) {
177
+ throw new DecentrlSDKError(
178
+ `Failed to publish event "${eventType}": ${error instanceof Error ? error.message : String(error)}`,
179
+ 'PUBLISH_FAILED',
180
+ { eventType, data, error },
181
+ );
182
+ }
183
+ }
184
+
185
+ async query(options?: QueryOptions): Promise<PaginatedResult<EventEnvelope>> {
186
+ if (!this.transport.queryEvents) {
187
+ throw new DecentrlSDKError('Transport does not support queryEvents', 'QUERY_NOT_SUPPORTED');
188
+ }
189
+
190
+ try {
191
+ return await this.transport.queryEvents(options);
192
+ } catch (error) {
193
+ if (error instanceof DecentrlSDKError) {
194
+ throw error;
195
+ }
196
+
197
+ throw new DecentrlSDKError(
198
+ `Failed to query events: ${error instanceof Error ? error.message : String(error)}`,
199
+ 'QUERY_FAILED',
200
+ { options, error },
201
+ );
202
+ }
203
+ }
204
+
205
+ onEvent(listener: (envelope: EventEnvelope) => void): () => void {
206
+ return this.eventProcessor.onEvent(listener);
207
+ }
208
+
209
+ async queryAndProcess(options?: QueryOptions): Promise<{ processed: number; total: number }> {
210
+ const result = await this.query(options);
211
+ const processed = this.eventProcessor.processBatch(result.data);
212
+
213
+ return { processed, total: result.pagination.total };
214
+ }
215
+
216
+ getState = (): InferState<TState> => {
217
+ return this.stateStore.getState();
218
+ };
219
+
220
+ subscribe = (listener: StateListener<InferState<TState>>): (() => void) => {
221
+ return this.stateStore.subscribe(listener);
222
+ };
223
+
224
+ startSync(options?: SyncOptions): void {
225
+ this.syncManager.start(options);
226
+ }
227
+
228
+ stopSync(): void {
229
+ this.syncManager.stop();
230
+ }
231
+
232
+ async sync(): Promise<void> {
233
+ await this.syncManager.tick();
234
+ }
235
+
236
+ getConnectionStatus = (): ConnectionStatus => {
237
+ return this.syncManager.getConnectionStatus();
238
+ };
239
+
240
+ onConnectionStatusChange = (listener: (status: ConnectionStatus) => void): (() => void) => {
241
+ return this.syncManager.onConnectionStatusChange(listener);
242
+ };
243
+
244
+ reset(): void {
245
+ this.syncManager.stop();
246
+ this.identity.reset();
247
+ this.contracts.reset();
248
+ this.eventProcessor.reset();
249
+
250
+ if (this.unsubscribePersistState) {
251
+ this.unsubscribePersistState();
252
+ this.unsubscribePersistState = null;
253
+ }
254
+
255
+ if (this.unsubscribePersistIdentity) {
256
+ this.unsubscribePersistIdentity();
257
+ this.unsubscribePersistIdentity = null;
258
+ }
259
+
260
+ // Rebuild initial state
261
+ const initialState = {} as InferState<TState>;
262
+
263
+ for (const key of Object.keys(this.appConfig.state)) {
264
+ (initialState as any)[key] = this.appConfig.state[key].initial;
265
+ }
266
+
267
+ this.stateStore.reset(initialState);
268
+
269
+ // Clear persisted data
270
+ if (this.clientConfig.persist) {
271
+ clearPersisted(this.clientConfig.persist);
272
+ }
273
+
274
+ // Re-subscribe persistence listeners
275
+ this.setupPersistence();
276
+ }
277
+ }
@@ -0,0 +1,207 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { ContractManager } from './contract-manager.js';
3
+ import { createMockTransport, makeContract } from './test-helpers.js';
4
+
5
+ describe('ContractManager', () => {
6
+ let manager: ContractManager;
7
+
8
+ beforeEach(() => {
9
+ manager = new ContractManager();
10
+ });
11
+
12
+ describe('getContractByDid', () => {
13
+ it('returns undefined when no contracts exist', () => {
14
+ expect(manager.getContractByDid('did:decentrl:bob')).toBeUndefined();
15
+ });
16
+
17
+ it('returns the contract for a given DID', async () => {
18
+ const contract = makeContract({
19
+ participantDid: 'did:decentrl:bob',
20
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
21
+ timestamp: Math.floor(Date.now() / 1000),
22
+ });
23
+
24
+ const transport = createMockTransport({
25
+ refreshContracts: vi.fn(async () => [contract]),
26
+ });
27
+
28
+ manager.setTransport(transport);
29
+ await manager.refresh();
30
+
31
+ const result = manager.getContractByDid('did:decentrl:bob');
32
+ expect(result).toBeDefined();
33
+ expect(result!.participantDid).toBe('did:decentrl:bob');
34
+ });
35
+
36
+ it('prefers the contract with the latest expires_at', async () => {
37
+ const now = Math.floor(Date.now() / 1000);
38
+
39
+ const oldContract = makeContract({
40
+ id: 'old',
41
+ participantDid: 'did:decentrl:bob',
42
+ expiresAt: now + 3600,
43
+ timestamp: now,
44
+ });
45
+
46
+ const newContract = makeContract({
47
+ id: 'new',
48
+ participantDid: 'did:decentrl:bob',
49
+ expiresAt: now + 7200,
50
+ timestamp: now + 100,
51
+ });
52
+
53
+ const transport = createMockTransport({
54
+ refreshContracts: vi.fn(async () => [oldContract, newContract]),
55
+ });
56
+
57
+ manager.setTransport(transport);
58
+ await manager.refresh();
59
+
60
+ const result = manager.getContractByDid('did:decentrl:bob');
61
+ expect(result!.id).toBe('new');
62
+ });
63
+ });
64
+
65
+ describe('onChange', () => {
66
+ it('fires listener on refresh', async () => {
67
+ const listener = vi.fn();
68
+ manager.onChange(listener);
69
+
70
+ const transport = createMockTransport({
71
+ refreshContracts: vi.fn(async () => []),
72
+ });
73
+ manager.setTransport(transport);
74
+
75
+ await manager.refresh();
76
+
77
+ expect(listener).toHaveBeenCalled();
78
+ });
79
+
80
+ it('returns unsubscribe function', async () => {
81
+ const listener = vi.fn();
82
+ const unsub = manager.onChange(listener);
83
+ unsub();
84
+
85
+ const transport = createMockTransport({
86
+ refreshContracts: vi.fn(async () => []),
87
+ });
88
+ manager.setTransport(transport);
89
+
90
+ await manager.refresh();
91
+
92
+ expect(listener).not.toHaveBeenCalled();
93
+ });
94
+ });
95
+
96
+ describe('processAutoRenewals (with transport)', () => {
97
+ it('delegates to transport when available', async () => {
98
+ const transport = createMockTransport();
99
+ manager.setTransport(transport);
100
+
101
+ await manager.processAutoRenewals(0.3);
102
+
103
+ expect(transport.processAutoRenewals).toHaveBeenCalledWith(0.3);
104
+ });
105
+
106
+ it('calls refresh after transport auto-renewal', async () => {
107
+ const contract = makeContract({
108
+ participantDid: 'did:decentrl:bob',
109
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
110
+ timestamp: Math.floor(Date.now() / 1000),
111
+ });
112
+
113
+ const transport = createMockTransport({
114
+ refreshContracts: vi.fn(async () => [contract]),
115
+ });
116
+ manager.setTransport(transport);
117
+
118
+ await manager.processAutoRenewals();
119
+
120
+ expect(transport.refreshContracts).toHaveBeenCalled();
121
+ });
122
+ });
123
+
124
+ describe('processContractCleanup (with transport)', () => {
125
+ it('delegates to transport when available', async () => {
126
+ const transport = createMockTransport();
127
+ manager.setTransport(transport);
128
+
129
+ await manager.processContractCleanup();
130
+
131
+ expect(transport.processContractCleanup).toHaveBeenCalled();
132
+ });
133
+
134
+ it('calls refresh after transport cleanup', async () => {
135
+ const transport = createMockTransport();
136
+ manager.setTransport(transport);
137
+
138
+ await manager.processContractCleanup();
139
+
140
+ expect(transport.refreshContracts).toHaveBeenCalled();
141
+ });
142
+ });
143
+
144
+ describe('reset', () => {
145
+ it('clears all contracts and state', async () => {
146
+ const now = Math.floor(Date.now() / 1000);
147
+ const contract = makeContract({
148
+ participantDid: 'did:decentrl:bob',
149
+ expiresAt: now + 3600,
150
+ timestamp: now,
151
+ });
152
+
153
+ const transport = createMockTransport({
154
+ refreshContracts: vi.fn(async () => [contract]),
155
+ });
156
+ manager.setTransport(transport);
157
+ await manager.refresh();
158
+
159
+ expect(manager.getActiveContracts()).toHaveLength(1);
160
+
161
+ manager.reset();
162
+
163
+ expect(manager.getActiveContracts()).toHaveLength(0);
164
+ });
165
+
166
+ it('fires onChange listeners', () => {
167
+ const listener = vi.fn();
168
+ manager.onChange(listener);
169
+
170
+ manager.reset();
171
+
172
+ expect(listener).toHaveBeenCalled();
173
+ });
174
+ });
175
+
176
+ describe('getActiveContracts', () => {
177
+ it('returns only active contracts', async () => {
178
+ const now = Math.floor(Date.now() / 1000);
179
+
180
+ const active = makeContract({
181
+ id: 'active',
182
+ participantDid: 'did:decentrl:bob',
183
+ expiresAt: now + 3600,
184
+ timestamp: now,
185
+ status: 'active',
186
+ });
187
+
188
+ const expired = makeContract({
189
+ id: 'expired',
190
+ participantDid: 'did:decentrl:carol',
191
+ expiresAt: now - 100,
192
+ timestamp: now - 7200,
193
+ status: 'expired',
194
+ });
195
+
196
+ const transport = createMockTransport({
197
+ refreshContracts: vi.fn(async () => [active, expired]),
198
+ });
199
+ manager.setTransport(transport);
200
+ await manager.refresh();
201
+
202
+ const activeContracts = manager.getActiveContracts();
203
+ expect(activeContracts).toHaveLength(1);
204
+ expect(activeContracts[0].id).toBe('active');
205
+ });
206
+ });
207
+ });
@@ -0,0 +1,130 @@
1
+ import { DecentrlSDKError } from './errors.js';
2
+ import type { DecentrlTransport } from './transport.js';
3
+ import type { PendingContractRequest, StoredSignedContract } from './types.js';
4
+
5
+ type ContractChangeListener = () => void;
6
+
7
+ export class ContractManager {
8
+ private contracts = new Map<string, StoredSignedContract>();
9
+ private listeners = new Set<ContractChangeListener>();
10
+ private cachedActiveContracts: StoredSignedContract[] = [];
11
+ private transport: DecentrlTransport | null = null;
12
+
13
+ setTransport(transport: DecentrlTransport): void {
14
+ this.transport = transport;
15
+ }
16
+
17
+ onChange(listener: ContractChangeListener): () => void {
18
+ this.listeners.add(listener);
19
+
20
+ return () => {
21
+ this.listeners.delete(listener);
22
+ };
23
+ }
24
+
25
+ private notify(): void {
26
+ this.cachedActiveContracts = Array.from(this.contracts.values()).filter(
27
+ (c) => c.status === 'active',
28
+ );
29
+
30
+ for (const listener of this.listeners) {
31
+ listener();
32
+ }
33
+ }
34
+
35
+ async request(recipientDid: string, expiresIn = 3600 * 24): Promise<void> {
36
+ if (!this.transport) {
37
+ throw new DecentrlSDKError('Transport not set', 'NO_TRANSPORT');
38
+ }
39
+
40
+ await this.transport.requestContract(recipientDid, expiresIn);
41
+ }
42
+
43
+ async getPending(): Promise<PendingContractRequest[]> {
44
+ if (!this.transport) {
45
+ throw new DecentrlSDKError('Transport not set', 'NO_TRANSPORT');
46
+ }
47
+
48
+ return this.transport.getPendingContracts();
49
+ }
50
+
51
+ async accept(
52
+ pendingId: string,
53
+ encryptedPayload: string,
54
+ requestorEphemeralPublicKey: string,
55
+ ): Promise<void> {
56
+ if (!this.transport) {
57
+ throw new DecentrlSDKError('Transport not set', 'NO_TRANSPORT');
58
+ }
59
+
60
+ await this.transport.acceptContract(pendingId, encryptedPayload, requestorEphemeralPublicKey);
61
+ await this.refresh();
62
+ }
63
+
64
+ async refresh(): Promise<void> {
65
+ if (!this.transport) {
66
+ throw new DecentrlSDKError('Transport not set', 'NO_TRANSPORT');
67
+ }
68
+
69
+ const contracts = await this.transport.refreshContracts();
70
+ this.contracts.clear();
71
+
72
+ for (const contract of contracts) {
73
+ this.contracts.set(contract.id, contract);
74
+ }
75
+
76
+ this.notify();
77
+ }
78
+
79
+ getActiveContracts(): StoredSignedContract[] {
80
+ return this.cachedActiveContracts;
81
+ }
82
+
83
+ getContractByDid(did: string): StoredSignedContract | undefined {
84
+ let best: StoredSignedContract | undefined;
85
+
86
+ for (const contract of this.contracts.values()) {
87
+ if (contract.participantDid !== did) {
88
+ continue;
89
+ }
90
+
91
+ if (!best) {
92
+ best = contract;
93
+ continue;
94
+ }
95
+
96
+ const bestExpiry = best.signedCommunicationContract?.communication_contract?.expires_at ?? 0;
97
+ const currExpiry =
98
+ contract.signedCommunicationContract?.communication_contract?.expires_at ?? 0;
99
+
100
+ if (currExpiry > bestExpiry) {
101
+ best = contract;
102
+ }
103
+ }
104
+
105
+ return best;
106
+ }
107
+
108
+ async processAutoRenewals(threshold = 0.2): Promise<void> {
109
+ if (!this.transport?.processAutoRenewals) {
110
+ return;
111
+ }
112
+
113
+ await this.transport.processAutoRenewals(threshold);
114
+ await this.refresh();
115
+ }
116
+
117
+ async processContractCleanup(): Promise<void> {
118
+ if (!this.transport?.processContractCleanup) {
119
+ return;
120
+ }
121
+
122
+ await this.transport.processContractCleanup();
123
+ await this.refresh();
124
+ }
125
+
126
+ reset(): void {
127
+ this.contracts.clear();
128
+ this.notify();
129
+ }
130
+ }
@@ -0,0 +1,25 @@
1
+ import { DecentrlClient } from './client.js';
2
+ import type {
3
+ DecentrlAppConfig,
4
+ DecentrlClientConfig,
5
+ EventDefinitions,
6
+ StateDefinitions,
7
+ } from './types.js';
8
+
9
+ export interface DecentrlApp<
10
+ TEvents extends EventDefinitions,
11
+ TState extends StateDefinitions<TEvents>,
12
+ > {
13
+ config: DecentrlAppConfig<TEvents, TState>;
14
+ createClient(clientConfig: DecentrlClientConfig): DecentrlClient<TEvents, TState>;
15
+ }
16
+
17
+ export function defineDecentrlApp<
18
+ TEvents extends EventDefinitions,
19
+ TState extends StateDefinitions<TEvents>,
20
+ >(config: DecentrlAppConfig<TEvents, TState>): DecentrlApp<TEvents, TState> {
21
+ return {
22
+ config,
23
+ createClient: (clientConfig: DecentrlClientConfig) => new DecentrlClient(config, clientConfig),
24
+ };
25
+ }