Skip to content

Commit

Permalink
pit: LinearPit
Browse files Browse the repository at this point in the history
  • Loading branch information
yoursunny committed Sep 30, 2019
1 parent aee2cf8 commit 7749eb8
Show file tree
Hide file tree
Showing 15 changed files with 345 additions and 19 deletions.
25 changes: 18 additions & 7 deletions packages/l3pkt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,24 @@ const digest = await data.computeImplicitDigest();
assert.equal(digest.length, 32);

// Full names are available, too.
const fullName = await data2.getFullName();
const fullName = await data2.computeFullName();
assert.equal(fullName.length, data2.name.length + 1);
assert(fullName.at(-1).is(ImplicitDigest));

// Note that these two functions are only available after encoding or decoding.
// After computation, implicit digest is cached on the Data instance,
// so we can get them without await:
const digest2 = data.getImplicitDigest();
const fullName2 = data.getFullName();
assert.equal(digest2, digest);
assert(typeof fullName2 !== "undefined");
assert.equal(fullName2!.toString(), fullName.toString());

// Note that these functions are only available after encoding or decoding.
// Calling them on a Data before encoding results in an error.
assert.throws(() => new Data().getImplicitDigest());
assert.throws(() => new Data().getFullName());
assert.rejects(new Data().computeImplicitDigest());
assert.rejects(new Data().getFullName());
assert.rejects(new Data().computeFullName());
// Also, if you modify the Data after encoding or decoding, you'll get incorrect results.
// In short, only call them right after encoding or decoding.
```
Expand All @@ -106,13 +116,14 @@ assert.equal(canSatisfySync(interest, data), true);
const interest3 = new Interest("/B");
assert.equal(canSatisfySync(interest3, data), false);
// However, it does not support implicit digest, because digest computation is async:
const data3 = new Decoder(dataWire).decode(Data);
const interestWithFullName = new Interest(fullName);
assert(typeof canSatisfySync(interestWithFullName, data) === "undefined");
assert(typeof canSatisfySync(interestWithFullName, data3) === "undefined");
// Unless the Data contains cached implicit digest:
assert.equal(canSatisfySync(interestWithFullName, data), true);

// canSatisfy returns a Promise that resolves to boolean, which can support implicit digest.
assert.equal(await canSatisfy(interestWithFullName, data), true);
assert.equal(await canSatisfy(interest, data), true);
assert.equal(await canSatisfy(interest3, data), false);
assert.equal(await canSatisfy(interestWithFullName, data3), true);
```
```ts
Expand Down
28 changes: 23 additions & 5 deletions packages/l3pkt/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SigInfo } from "./sig-info";
const FAKE_SIGINFO = new SigInfo(SigType.Sha256);
const FAKE_SIGVALUE = new Uint8Array(32);
const TOPTLV = Symbol("Data.TopTlv");
const TOPTLV_DIGEST = Symbol("Data.TopTlvDigest");

const EVD = new EvDecoder<Data>("Data", TT.Data)
.setTop((t, { tlv }) => t[TOPTLV] = tlv)
Expand Down Expand Up @@ -62,7 +63,7 @@ export class Data {
public sigValue: Uint8Array = FAKE_SIGVALUE;
public [LLSign.PENDING]?: LLSign;
public [LLVerify.SIGNED]?: Uint8Array;
public [TOPTLV]?: Uint8Array; // for implicit digest
public [TOPTLV]?: Uint8Array & {[TOPTLV_DIGEST]?: Uint8Array}; // for implicit digest

private contentType_: number = 0;
private freshnessPeriod_: number = 0;
Expand Down Expand Up @@ -113,19 +114,36 @@ export class Data {
));
}

public async computeImplicitDigest(): Promise<Uint8Array> {
public getImplicitDigest(): Uint8Array|undefined {
const topTlv = this[TOPTLV];
if (!topTlv) {
throw new Error("wire encoding is unavailable");
}
return sha256(topTlv);
return topTlv[TOPTLV_DIGEST];
}

public async computeImplicitDigest(): Promise<Uint8Array> {
let digest = this.getImplicitDigest();
if (!digest) {
digest = await sha256(this[TOPTLV]!);
this[TOPTLV]![TOPTLV_DIGEST] = digest;
}
return digest;
}

public async getFullName(): Promise<Name> {
const digest = await this.computeImplicitDigest();
public getFullName(): Name|undefined {
const digest = this.getImplicitDigest();
if (!digest) {
return undefined;
}
return this.name.append(ImplicitDigest, digest);
}

public async computeFullName(): Promise<Name> {
await this.computeImplicitDigest();
return this.getFullName()!;
}

public [LLSign.PROCESS](): Promise<void> {
return LLSign.processImpl(this,
() => Encoder.encode(this.getSignedPortion()),
Expand Down
8 changes: 6 additions & 2 deletions packages/l3pkt/src/satisfy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export function canSatisfySync(interest: Interest, data: Data): boolean|undefine

if (interest.name.length === data.name.length + 1 &&
interest.name.get(-1)!.is(ImplicitDigest)) {
return undefined;
const fullName = data.getFullName();
if (!fullName) {
return undefined;
}
return interest.name.equals(fullName);
}

return false;
Expand All @@ -32,7 +36,7 @@ export function canSatisfySync(interest: Interest, data: Data): boolean|undefine
export async function canSatisfy(interest: Interest, data: Data): Promise<boolean> {
const result = canSatisfySync(interest, data);
if (typeof result === "undefined") {
return interest.name.equals(await data.getFullName());
return interest.name.equals(await data.computeFullName());
}
return result;
}
10 changes: 10 additions & 0 deletions packages/l3pkt/test-fixture/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Name } from "@ndn/name";
import { Decoder, Encoder } from "@ndn/tlv";

import { Data } from "../src";

/** Obtain Data full name without being cached on Data packet. */
export async function getDataFullName(data: Data): Promise<Name> {
const copy = new Decoder(Encoder.encode(data)).decode(Data);
return await copy.computeFullName();
}
9 changes: 8 additions & 1 deletion packages/l3pkt/tests/data.t.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,16 @@ test("ImplicitDigest", async () => {

const wire = Encoder.encode(data);
const expectedDigest = createHash("sha256").update(wire).digest();
expect(data.getImplicitDigest()).toBeUndefined();
await expect(data.computeImplicitDigest()).resolves.toEqualUint8Array(expectedDigest);
expect(data.getImplicitDigest()).toEqualUint8Array(expectedDigest);
await expect(data.computeImplicitDigest()).resolves.toEqualUint8Array(expectedDigest);

data = new Decoder(wire).decode(Data);
const fullName = await data.getFullName();
expect(data.getFullName()).toBeUndefined();
const fullName = await data.computeFullName();
expect(fullName.toString()).toBe(`/A/${ImplicitDigest.create(expectedDigest).toString()}`);
const fullName2 = data.getFullName();
expect(fullName2).not.toBeUndefined();
expect(fullName2!.toString()).toBe(fullName.toString());
});
7 changes: 3 additions & 4 deletions packages/l3pkt/tests/satisfy.t.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Name } from "@ndn/name";

import { Encoder } from "@ndn/tlv";
import { canSatisfy, canSatisfySync, Data, Interest } from "../src";
import { getDataFullName } from "../test-fixture";

test("simple", async () => {
const interest = new Interest("/A");
Expand All @@ -25,12 +25,11 @@ test("simple", async () => {
expect(canSatisfySync(interest, data)).toBe(true);
await expect(canSatisfy(interest, data)).resolves.toBe(true);

Encoder.encode(data);
interest.name = await data.getFullName();
interest.name = await getDataFullName(data);
expect(canSatisfySync(interest, data)).toBeUndefined();
await expect(canSatisfy(interest, data)).resolves.toBe(true);

interest.canBePrefix = false;
expect(canSatisfySync(interest, data)).toBeUndefined();
expect(canSatisfySync(interest, data)).toBe(true); // digest cached on data
await expect(canSatisfy(interest, data)).resolves.toBe(true);
});
34 changes: 34 additions & 0 deletions packages/pit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@ndn/pit",
"version": "0.0.0",
"description": "NDNts: Pending Interest Table",
"keywords": [
"NDN",
"Named Data Networking"
],
"author": "Junxiao Shi <npm@mail1.yoursunny.com>",
"license": "ISC",
"main": "src/index.ts",
"files": [
"lib"
],
"sideEffects": [],
"homepage": "https://yoursunny.com/p/NDNts/",
"repository": {
"type": "git",
"url": "https://github.com/yoursunny/NDNts.git",
"directory": "packages/pit"
},
"publishConfig": {
"main": "lib/index.js",
"types": "lib/index.d.ts"
},
"dependencies": {
"@ndn/l3pkt": "workspace:*",
"@ndn/name": "workspace:*",
"strict-event-emitter-types": "^2.0.0"
},
"devDependencies": {
"delay": "^4.3.0"
}
}
3 changes: 3 additions & 0 deletions packages/pit/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./interface";
export * from "./linear";
export * from "./pending-interest";
16 changes: 16 additions & 0 deletions packages/pit/src/interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Data, Interest } from "@ndn/l3pkt";
import { PendingInterest } from "./pending-interest";

/** Pending Interest Table. */
export interface Pit {
readonly length: number;

/**
* Add an Interest.
* Return PendingInterest if accepted, or undefined if loop detected.
*/
addInterest(interest: Interest, from: PropertyKey): PendingInterest|undefined;

/** Asynchronously process incoming Data. */
processData(data: Data, from: PropertyKey): void;
}
16 changes: 16 additions & 0 deletions packages/pit/src/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Data } from "@ndn/l3pkt";

export namespace PitImpl {
export const SATISFY = Symbol("PitImpl.SATISFY");
export const REMOVE = Symbol("PitImpl.REMOVE");

export interface PendingInterest {
/** Satisfy Interest with Data. */
[SATISFY](data: Data);
}

export interface Table {
/** Remove from table. */
[REMOVE](pi: PendingInterest);
}
}
48 changes: 48 additions & 0 deletions packages/pit/src/linear.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { canSatisfySync, Data, Interest } from "@ndn/l3pkt";

import { Pit } from "./interface";
import { PitImpl } from "./internal";
import { PendingInterest } from "./pending-interest";

/**
* PIT implemented with an array.
*
* This implementation does not support Interest aggregation or loop prevention.
* Each Interest is appended to an array.
* Each Data is matched against all pending Interests.
*/
export class LinearPit implements Pit {
public get length() { return this.table.length; }

private table: PendingInterest[] = [];

public addInterest(interest: Interest): PendingInterest {
const pi = new PendingInterest(this, interest);
this.table.push(pi);
return pi;
}

public processData(data: Data): void {
let needDigest = false;
this.table = this.table.filter((pi) => {
const satisfied = canSatisfySync(pi.interest, data);
if (satisfied === true) {
pi[PitImpl.SATISFY](data);
return false;
}
needDigest = needDigest || satisfied !== false;
return true;
});
if (needDigest) {
data.computeImplicitDigest()
.then(() => this.processData(data));
}
}

public [PitImpl.REMOVE](pi: PitImpl.PendingInterest) {
const i = this.table.findIndex((item) => item === pi);
if (i >= 0) {
this.table.splice(i, 1);
}
}
}
61 changes: 61 additions & 0 deletions packages/pit/src/pending-interest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Data, Interest } from "@ndn/l3pkt";
import { EventEmitter } from "events";
import { StrictEventEmitter } from "strict-event-emitter-types";

import { PitImpl } from "./internal";

interface Events {
/** Emitted when Interest has been satisfied. */
data: Data;
/** Emitted when Interest times out. */
timeout: void;
}

type Emitter = StrictEventEmitter<EventEmitter, Events>;

const TIMEOUT = "4a96aedc-be5d-4eab-8a2b-775f13a7a982";

export class PendingInterest extends (EventEmitter as new() => Emitter) {
private timers: Record<string, number> = {};

constructor(private readonly table: PitImpl.Table, public readonly interest: Interest) {
super();
this.setTimer(TIMEOUT, interest.lifetime, () => {
this.clearTimers();
this.emit("timeout");
this.table[PitImpl.REMOVE](this);
});
}

/**
* Schedule a timer associated with this pending Interest.
* This cancels previous timer with same id.
* All timers will be canceled when the pending Interest is satisfied/expired/canceled.
*/
public setTimer(id: string, timeout: number, f: () => any) {
this.clearTimer(id);
this.timers[id] = setTimeout(f, timeout) as any;
}

/** Cancel a timer. */
public clearTimer(id: string) {
clearTimeout(this.timers[id]);
delete this.timers[id];
}

public [PitImpl.SATISFY](data: Data) {
this.clearTimers();
this.emit("data", data);
}

/** Indicate the requester no longer wants the Data. */
public cancel() {
this.clearTimers();
this.table[PitImpl.REMOVE](this);
}

private clearTimers() {
Object.values(this.timers).forEach(clearTimeout);
this.timers = {};
}
}
Loading

0 comments on commit 7749eb8

Please sign in to comment.