storacha / w3up

⁂ w3up protocol implementation
https://github.com/storacha-network/specs
Other
59 stars 21 forks source link

Future API Sketch #1156

Open Gozala opened 11 months ago

Gozala commented 11 months ago

Me @alanshaw, @vasco-santos and @travis had been talking about revisiting API for the client at the labweek and have got some feedback from the workshop which was validating of the one we were entertaining, I thought I'd sketch it here so we can discuss it further. Before I proceed let me call out some high level goals I have in my mind.

  1. Client state should be fully exposed and really minimal. I think it should just be a "delegation store" + "connection config".
  2. Client itself should be just a "view" class over the state "model" that simply exposes static functions operating on the "model" as methods as a convenience DSL.
    • E.g. queries over proofs should not be a clients methods (especially private) they should be just static functions over proof store and we could choose to expose or conceal them.
    • I should be able to create various "view" classes simply by wrapping over "model" and not worry that there is some state that may be out of sync.
  3. We should have views for relevant entities in the system e.g.
    • AgentView - Convenience wrapper of what I have access to on this agent.
    • AccountView - Agent may have access to arbitrary number of accounts represented via AccountView which should simply be another view over the "proofs store". When you login you get a new AccountView (holding account to agent delegation) if you do want to persist reference to the account across the agent sessions you do agent.add(account).
      • SpaceView - Agent may have access to arbitrary number of spaces represented via SpaceView which should also be views over "proofs store". When you create a space you get a new OwnedSpaceView. If you want to retain access to space across devices and sessions you'd call account.add(space). If you do not want to add space to the account you can add it it e.g. agent.local.add(space) or something like that so we emphasize the fact that space is detached from any account.
      • You could also have account.remove(space) which simply revokes delegation from space to account.
      • UploadView and StoreView will be just be views that expose corresponding upload/* and store/* capabilities of the space. I don't mean subclasses here, simply views over the same model. And space could probably have .upload and and .store fields with the corresponding views.
  4. View methods should simply query proofs in the model to find relevant delegations and invoke them if they have or fail right away if they don't.
Gozala commented 11 months ago

This is roughly the interface I had in mind when talking about it


interface ConnecionModel {
  /**
   * Local keypair use to sign messages send over the connection.
   */
  readonly issuer: Ucanto.Signer
  /**
   * DID of the destination.
   */
  readonly audience: Ucanto.Principal
  /**
   * Wire transport encoding.
   */
  readonly codec: Ucanto.Transport.OutboundCodec
  /**
   * Ucanto Transport channel.
   */
  readonly channel: Ucanto.Transport.Channel
}

interface Model {
  /**
   * Connection to the (remote) agent a.k.a service.
   */
  connection: ConnecionModel
  /**
   * Set of delegations that can be used to invoke capabilities over `connection`.
   */
  authorization: Store<Ucanto.Delegation>
}

interface AgentView {
  model: Model

  accounts: AgentAccounts
  login(email: string): Promise<AccountView>
}

interface AgentAccounts extends Iterable<AccountView> {
  get(email: string): AccountView
  add(account: AccountView): Promise<AccountView>
  remove(account: AccountView): Promise<void>

}

interface AccountView {
  spaces: Spaces
  plan: AccountPlan
}

interface AccountPlan {
  get(): PlanView
}

interface PlanView {
  subscriptions: Subscriptions
}

interface Subscriptions extends Iterable<Subscription> {
  get(id: string): Subscription
  add(subscription: Subscription): Promise<Subscription>
  remove(subscription: Subscription): Promise<void>
}

interface Subscription {
  consumer: SpaceDID
}

interface Spaces extends Iterable<SpaceView> {
  get(name: string): SpaceView
  add(space: SpaceView): Promise<SpaceView>
  remove(space: SpaceView): Promise<void>
}

interface SpaceView {
  upload: UploadView
  store: StoreView

  createFileUploader(): FileUploader
  createDirectoryUploader(): DirectoryUploader
  createArchiveUploader(): ArchiveUploader
}

export interface Uploader {
  /**
   * Writes contents of the upload to the space without adding it to the upload
   * list.
   */
  store(space?: SpaceView): Promise<Upload>
  /**
   * Writes content of the upload to the space and adds it to the upload list.
   */
  upload(space?: SpaceView): Promise<Upload>
}

interface FileUploader extends Uploader {
}

interface DirectoryUploader extends Uploader {
}

interface ArchiveUploader extends Uploader {
}

interface Upload {
  root: Link
  shards: CARLink[]
}

interface UploadView {
  add(upload: Upload): Promise<Upload>
  remove(upload: Upload): Promise<Unit>
  list(): Promise<Upload[]>
}

interface StoreView {
  add(shard: Archive): Promise<ArchiveWriter>
  remove(shard: Archive): Promise<Unit>
  list(): Promise<Archive[]>
}

interface Archive {
  link: Link

  size: number
}

interface ArchiveWriter {
  write(bytes: Uint8Array): Promise<void>
}
alanshaw commented 11 months ago

This looks great, really nice to be able to design something like this now we have our domain model figured out. I realise just a sketch but here's some notes on missing/wanted things so we don't forget:

hannahhoward commented 2 months ago

@alanshaw I've heard about the client refactor, and I'd love to hear from you what you see as the goal of this refactor, in qualitative terms.

Essentially what makes worth it to spend 2 weeks at minimum to implement all this plus the pain of getting folks to upgrade? I think it would super useful also for @Peeja as someone with deep frontend experience to have strong input on this.

I will say for myself by far one of the hardest parts of working in the w3up repo is the abundance of packages and the fact that a typical operational function will cross potentially multiple different packages. (and on the server side, into the w3infra repo and back)

Also, would it help to defer this for a bit until @hakierka and @Peeja have a few weeks experience working on the code and /or helping clients so they can have more informed input?

alanshaw commented 2 months ago

I'd love to hear from you what you see as the goal of this refactor, in qualitative terms.

I've likely missed a few things here...

FYI we have a planning ticket open for this also: https://github.com/storacha-network/project-tracking/issues/2

My ask in discord was not for this to be worked on by @hakierka or @Peeja just yet, I just wondered, given the proposed API and the in-flight PR, if, (in their opinion!) the proposed API might be better than the current, given they are new to the project, just learning the ropes and experiencing the pains other users might also be experiencing. I also think their opinion is valuable given their expertise. This would provide us with some more validation and hence would give us more reason to work on it.