Skip to content

Commit

Permalink
Merge pull request #19 from earthstar-project/sideloading
Browse files Browse the repository at this point in the history
Implement Sideloading spec
  • Loading branch information
sgwilym authored May 29, 2024
2 parents 55ad89f + 3b39e5c commit beb54aa
Show file tree
Hide file tree
Showing 5 changed files with 561 additions and 12 deletions.
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ This module is published on JSR. Please see the
[@earthstar/willow page](https://jsr.io/@earthstar/willow) for installation
instructions.

API documentation can be found [here](https://jsr.io/@earthstar/willow).
API documentation can be found [here](https://jsr.io/@earthstar/willow/doc).

## Overview

`willow-js` includes a `Store` compliant with the
[Willow Data model](https://willowprotocol.org/specs/data-model/index.html#data_model),
and a `WgpsMessenger` which syncs data between two stores via the
[Willow General Purpose Sync Protocol](https://willowprotocol.org/specs/sync/index.html#sync).
`willow-js` includes:

- A `Store` compliant with the
[Willow Data model](https://willowprotocol.org/specs/data-model/index.html#data_model),
- A `WgpsMessenger` which syncs data between two stores via the
[Willow General Purpose Sync Protocol](https://willowprotocol.org/specs/sync/index.html#sync),
- and `createDrop` and `ingestDrop` compliant with the
[Willow Sideloading protocol](https://willowprotocol.org/specs/sideloading/index.html#sideloading).

This is a low-level module for people to build their own protocols on top of,
like [Earthstar](https://earthstar-project.org). It is an _extremely_ generic
Expand Down Expand Up @@ -99,6 +103,8 @@ of optional enhancements yet to be implemented:

- 🌶 Make WgpsMessenger send payloads below `maximum_payload_size` during
reconciliation via `ReconciliationSendPayload`
- 🌶 Make it possible to configure an upper byte length limit over which payloads
are not requested.
- 🌶 Make the threshold at which 3d range-based reconciliation stop comparing
fingerprints and just return entries user-configurable.
- 🌶 A WebRTC `Transport`.
Expand All @@ -115,10 +121,6 @@ of optional enhancements yet to be implemented:
- 🌶🌶🌶 Make WgpsMessenger intelligently free handles no longer in use via
`ControlFree`

### Miscellaneous

- 🌶 NPM compatible distribution.

## Dev

Deno is used the development runtime. Run `deno task test` to run tests.
Expand Down
19 changes: 16 additions & 3 deletions mod.universal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/**
* [Willow](https://willowprotocol.org) is a family of protocols for peer-to-peer data stores. This module provides implementations of the [Willow Data Model](https://willowprotocol.org/specs/data-model/index.html#data_model) and the [Willow General Purpose Sync Protocol](https://willowprotocol.org/specs/sync/index.html#sync).
*
* This module — just like the specs it is based on — is highly parametrised. It exports many low-level primitives for others to build their own protocols with.
*
* For more information on how to configure these parameters, please see the README.
* This module — just like the specs it is based on — is highly parametrised. It exports many low-level primitives for others to build their own protocols with. For more information on how to configure these parameters, please see the README.
*
* Implementations of the following Willow specifications are available:
* - {@linkcode Store} - [Willow Data Model](https://willowprotocol.org/specs/data-model/index.html#data_model)
* - {@linkcode WgpsMessenger } - [Willow General Purpose Sync protocol](https://willowprotocol.org/specs/sync/index.html#sync)
* - {@linkcode createDrop}, {@linkcode ingestDrop} - [Willow Sideloading protocol](https://willowprotocol.org/specs/sideloading/index.html#sideloading)
* @module
*/

Expand Down Expand Up @@ -77,6 +80,16 @@ export * from "./src/wgps/transports/websocket.ts";

export { type PaiScheme } from "./src/wgps/pai/types.ts";

// Sideloading

export {
createDrop,
DropContentsStream,
type DropContentsStreamOpts,
type DropOpts,
} from "./src/sideload/create_drop.ts";
export { ingestDrop, type IngestDropOpts } from "./src/sideload/ingest_drop.ts";

// Errors

export * from "./src/errors.ts";
216 changes: 216 additions & 0 deletions src/sideload/create_drop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import {
type AreaOfInterest,
bigintToBytes,
defaultEntry,
encodeEntryRelativeEntry,
type PathScheme,
} from "@earthstar/willow-utils";
import type { Store } from "../store/store.ts";
import type {
NamespaceScheme,
PayloadScheme,
SubspaceScheme,
} from "../store/types.ts";

/** Get the size of all the entries included by an {@linkcode AreaOfInterest} with complete payloads. */
async function getDropContentSize<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint,
>(
store: Store<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint
>,
areaOfInterest: AreaOfInterest<SubspaceId>,
): Promise<bigint> {
let count = 0n;

for await (
const [entry, payload] of store.query(
areaOfInterest,
"subspace",
)
) {
if (!payload) {
continue;
}

if (await payload.length() !== entry.payloadLength) {
continue;
}

count += 1n;
}

return count;
}

export type DropContentsStreamOpts<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint,
> = {
store: Store<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint
>;
areaOfInterest: AreaOfInterest<SubspaceId>;
schemes: {
namespace: NamespaceScheme<NamespaceId>;
subspace: SubspaceScheme<SubspaceId>;
payload: PayloadScheme<PayloadDigest>;
path: PathScheme;
};
encodeAuthorisationToken: (token: AuthorisationToken) => Uint8Array;
};

/** Produces the **unencrypted** encoded `contents` for a [drop](https://willowprotocol.org/specs/sideloading/index.html#drop) including the contents of a particular {@linkcode AreaOfInterest}. */
export class DropContentsStream<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint,
> extends ReadableStream<Uint8Array> {
constructor(
opts: DropContentsStreamOpts<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint
>,
) {
super({
start: async (controller) => {
const size = await getDropContentSize(opts.store, opts.areaOfInterest);

controller.enqueue(bigintToBytes(size));

let prevEntry = defaultEntry(
opts.schemes.namespace.defaultNamespaceId,
opts.schemes.subspace.minimalSubspaceId,
opts.schemes.payload.defaultDigest,
);

for await (
const [entry, payload, token] of opts.store.query(
opts.areaOfInterest,
"subspace",
)
) {
if (!payload) {
continue;
}

if (await payload.length() !== entry.payloadLength) {
continue;
}

const encodedEntry = encodeEntryRelativeEntry(
{
encodeNamespace: opts.schemes.namespace.encode,
encodeSubspace: opts.schemes.subspace.encode,
encodePayloadDigest: opts.schemes.payload.encode,
pathScheme: opts.schemes.path,
orderSubspace: opts.schemes.subspace.order,
isEqualNamespace: opts.schemes.namespace.isEqual,
},
entry,
prevEntry,
);

prevEntry = entry;

controller.enqueue(encodedEntry);

const encodedToken = opts.encodeAuthorisationToken(token);

controller.enqueue(encodedToken);

for await (const chunk of await payload.stream()) {
controller.enqueue(chunk);
}
}

controller.close();
},
});
}
}

/** Required options for creating an encrypted [drop](https://willowprotocol.org/specs/sideloading/index.html#drop). */
export type DropOpts<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint,
> =
& DropContentsStreamOpts<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint
>
& {
encryptTransform: TransformStream<Uint8Array, Uint8Array>;
};

/** Creates an encrypted [drop](https://willowprotocol.org/specs/sideloading/index.html#drop) containing all entries for a given {@linkcode AreaOfInterest} within a {@linkcode Store}.
*
* @returns A {@linkcode ReadableStream} which outputs the bytes of the drop.
*/
export function createDrop<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint,
>(
opts: DropOpts<
NamespaceId,
SubspaceId,
PayloadDigest,
AuthorisationOpts,
AuthorisationToken,
Prefingerprint,
Fingerprint
>,
): ReadableStream<Uint8Array> {
const dropContentStream = new DropContentsStream(opts);

dropContentStream.pipeTo(opts.encryptTransform.writable);

return opts.encryptTransform.readable;
}
Loading

0 comments on commit beb54aa

Please sign in to comment.