-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
@web5/agent
Adding DwnServerInfo
to RPC Clients
#489
Changes from all commits
29957c2
5a2788c
14f0c46
01f2a82
e8392c0
f0d538e
03b78fa
b38ddc7
04734bb
0e9734c
95ca04e
1fbb6b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
--- | ||
"@web5/agent": patch | ||
"@web5/identity-agent": patch | ||
"@web5/proxy-agent": patch | ||
"@web5/user-agent": patch | ||
--- | ||
|
||
Add `DwnServerInfoRpc` to `Web5Rpc` for retrieving server specific info. | ||
|
||
Server Info includes: | ||
- maxFileSize | ||
- registrationRequirements | ||
- webSocketSupport | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
|
||
import ms from 'ms'; | ||
import { TtlCache } from '@web5/common'; | ||
import { DwnServerInfoCache, ServerInfo } from './server-info-types.js'; | ||
|
||
/** | ||
* Configuration parameters for creating an in-memory cache for DWN ServerInfo entries. | ||
* | ||
* Allows customization of the cache time-to-live (TTL) setting. | ||
*/ | ||
export type DwnServerInfoCacheMemoryParams = { | ||
/** | ||
* Optional. The time-to-live for cache entries, expressed as a string (e.g., '1h', '15m'). | ||
* Determines how long a cache entry should remain valid before being considered expired. | ||
* | ||
* Defaults to '15m' if not specified. | ||
*/ | ||
ttl?: string; | ||
} | ||
|
||
export class DwnServerInfoCacheMemory implements DwnServerInfoCache { | ||
private cache: TtlCache<string, ServerInfo>; | ||
|
||
constructor({ ttl = '15m' }: DwnServerInfoCacheMemoryParams= {}) { | ||
this.cache = new TtlCache({ ttl: ms(ttl) }); | ||
} | ||
|
||
/** | ||
* Retrieves a DWN ServerInfo entry from the cache. | ||
* | ||
* If the cached item has exceeded its TTL, it's scheduled for deletion and undefined is returned. | ||
* | ||
* @param dwnUrl - The DWN URL endpoint string used as the key for getting the entry. | ||
* @returns The cached DWN ServerInfo entry or undefined if not found or expired. | ||
*/ | ||
public async get(dwnUrl: string): Promise<ServerInfo| undefined> { | ||
return this.cache.get(dwnUrl); | ||
} | ||
|
||
/** | ||
* Stores a DWN ServerInfo entry in the cache with a TTL. | ||
* | ||
* @param dwnUrl - The DWN URL endpoint string used as the key for storing the entry. | ||
* @param value - The DWN ServerInfo entry to be cached. | ||
* @returns A promise that resolves when the operation is complete. | ||
*/ | ||
public async set(dwnUrl: string, value: ServerInfo): Promise<void> { | ||
this.cache.set(dwnUrl, value); | ||
} | ||
|
||
/** | ||
* Deletes a DWN ServerInfo entry from the cache. | ||
* | ||
* @param dwnUrl - The DWN URL endpoint string used as the key for deletion. | ||
* @returns A promise that resolves when the operation is complete. | ||
*/ | ||
public async delete(dwnUrl: string): Promise<void> { | ||
this.cache.delete(dwnUrl); | ||
} | ||
|
||
/** | ||
* Clears all entries from the cache. | ||
* | ||
* @returns A promise that resolves when the operation is complete. | ||
*/ | ||
public async clear(): Promise<void> { | ||
this.cache.clear(); | ||
} | ||
|
||
/** | ||
* This method is a no-op but exists to be consistent with other DWN ServerInfo Cache | ||
* implementations. | ||
* | ||
* @returns A promise that resolves immediately. | ||
*/ | ||
public async close(): Promise<void> { | ||
// No-op since there is no underlying store to close. | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { KeyValueStore } from '@web5/common'; | ||
|
||
export type ServerInfo = { | ||
/** the maximum file size the user can request to store */ | ||
maxFileSize: number, | ||
/** | ||
* an array of strings representing the server's registration requirements. | ||
* | ||
* ie. ['proof-of-work-sha256-v0', 'terms-of-service'] | ||
* */ | ||
registrationRequirements: string[], | ||
/** whether web socket support is enabled on this server */ | ||
webSocketSupport: boolean, | ||
} | ||
|
||
export interface DwnServerInfoCache extends KeyValueStore<string, ServerInfo| undefined> {} | ||
|
||
export interface DwnServerInfoRpc { | ||
/** retrieves the DWN Sever info, used to detect features such as WebSocket Subscriptions */ | ||
getServerInfo(url: string): Promise<ServerInfo>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import sinon from 'sinon'; | ||
|
||
import { expect } from 'chai'; | ||
|
||
import { DwnServerInfoCache, ServerInfo } from '../../../src/prototyping/clients/server-info-types.js'; | ||
import { DwnServerInfoCacheMemory } from '../../../src/prototyping/clients/dwn-server-info-cache-memory.js'; | ||
import { isNode } from '../../utils/runtimes.js'; | ||
|
||
describe('DwnServerInfoCache', () => { | ||
|
||
describe(`DwnServerInfoCacheMemory`, () => { | ||
let cache: DwnServerInfoCache; | ||
let clock: sinon.SinonFakeTimers; | ||
|
||
const exampleInfo:ServerInfo = { | ||
maxFileSize : 100, | ||
webSocketSupport : true, | ||
registrationRequirements : [] | ||
}; | ||
|
||
after(() => { | ||
sinon.restore(); | ||
}); | ||
|
||
beforeEach(() => { | ||
clock = sinon.useFakeTimers(); | ||
cache = new DwnServerInfoCacheMemory(); | ||
}); | ||
|
||
afterEach(async () => { | ||
await cache.clear(); | ||
await cache.close(); | ||
clock.restore(); | ||
}); | ||
|
||
it('sets server info in cache', async () => { | ||
const key1 = 'some-key1'; | ||
const key2 = 'some-key2'; | ||
await cache.set(key1, { ...exampleInfo }); | ||
await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false | ||
|
||
const result1 = await cache.get(key1); | ||
expect(result1!.webSocketSupport).to.deep.equal(true); | ||
expect(result1).to.deep.equal(exampleInfo); | ||
|
||
const result2 = await cache.get(key2); | ||
expect(result2!.webSocketSupport).to.deep.equal(false); | ||
}); | ||
|
||
it('deletes from cache', async () => { | ||
const key1 = 'some-key1'; | ||
const key2 = 'some-key2'; | ||
await cache.set(key1, { ...exampleInfo }); | ||
await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false | ||
|
||
const result1 = await cache.get(key1); | ||
expect(result1!.webSocketSupport).to.deep.equal(true); | ||
expect(result1).to.deep.equal(exampleInfo); | ||
|
||
const result2 = await cache.get(key2); | ||
expect(result2!.webSocketSupport).to.deep.equal(false); | ||
|
||
// delete one of the keys | ||
await cache.delete(key1); | ||
|
||
// check results after delete | ||
const resultAfterDelete = await cache.get(key1); | ||
expect(resultAfterDelete).to.equal(undefined); | ||
|
||
// key 2 still exists | ||
const result2AfterDelete = await cache.get(key2); | ||
expect(result2AfterDelete!.webSocketSupport).to.equal(false); | ||
}); | ||
|
||
it('clears cache', async () => { | ||
const key1 = 'some-key1'; | ||
const key2 = 'some-key2'; | ||
await cache.set(key1, { ...exampleInfo }); | ||
await cache.set(key2, { ...exampleInfo, webSocketSupport: false }); // set to false | ||
|
||
const result1 = await cache.get(key1); | ||
expect(result1!.webSocketSupport).to.deep.equal(true); | ||
expect(result1).to.deep.equal(exampleInfo); | ||
|
||
const result2 = await cache.get(key2); | ||
expect(result2!.webSocketSupport).to.deep.equal(false); | ||
|
||
// delete one of the keys | ||
await cache.clear(); | ||
|
||
// check results after delete | ||
const resultAfterDelete = await cache.get(key1); | ||
expect(resultAfterDelete).to.equal(undefined); | ||
const result2AfterDelete = await cache.get(key2); | ||
expect(result2AfterDelete).to.equal(undefined); | ||
}); | ||
|
||
it('returns undefined after ttl', async function () { | ||
// skip this test in the browser, sinon fake timers don't seem to work here | ||
// with a an await setTimeout in the test, it passes. | ||
if (!isNode) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanity: this would be the first precedence that it doesn't work in browser right? Have you tried passing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's really strange that it doesn't work. I just tried a number instead of string and get the same behavior because I didn't think to try that before. But still fails in the browsers. |
||
this.skip(); | ||
} | ||
|
||
const key = 'some-key1'; | ||
await cache.set(key, { ...exampleInfo }); | ||
|
||
const result = await cache.get(key); | ||
expect(result!.webSocketSupport).to.deep.equal(true); | ||
expect(result).to.deep.equal(exampleInfo); | ||
|
||
// wait until 15m default ttl is up | ||
await clock.tickAsync('15:01'); | ||
|
||
const resultAfter = await cache.get(key); | ||
expect(resultAfter).to.be.undefined; | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider turning this into generic, seems low hanging fruit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing about this is we already have one called MemoryStore which is why his is more specific. Maybe MemoryStoreAsync? Unsure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@thehenrytsai Yeah @shamilovtim called out something similar and I was considering this, but figured we could do a separate PR to add
TTLCacheAsync
or something of that sort to@web5/common
because we are doing this same wrapper in a couple of places to conform with an async interface.