Skip to content
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

Enforce and execute purge conditions #98

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export { MemoryStore } from './memory/memory-store';
export { Device, User, UserName } from "./model/user";
export { ObservableSource, ObservableSource as ObservableSourceImpl, SpecificationListener } from './observable/observable';
export { ObservableCollection } from './observer/observer';
export { PurgeConditions } from './purge/purgeConditions';
export { Declaration } from './specification/declaration';
export { describeDeclaration, describeSpecification } from './specification/description';
export { buildFeeds } from './specification/feed-builder';
Expand Down
19 changes: 17 additions & 2 deletions src/jinaga-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ import { FactManager } from "./managers/factManager";
import { Network, NetworkNoOp } from "./managers/NetworkManager";
import { MemoryStore } from "./memory/memory-store";
import { ObservableSource } from "./observable/observable";
import { PurgeConditions } from "./purge/purgeConditions";
import { Specification } from "./specification/specification";
import { Storage } from "./storage";

export type JinagaBrowserConfig = {
httpEndpoint?: string,
wsEndpoint?: string,
indexedDb?: string,
httpTimeoutSeconds?: number,
httpAuthenticationProvider?: AuthenticationProvider
httpAuthenticationProvider?: AuthenticationProvider,
purgeConditions?: (p: PurgeConditions) => PurgeConditions
}

export class JinagaBrowser {
Expand All @@ -37,7 +40,8 @@ export class JinagaBrowser {
const fork = createFork(config, store, webClient);
const authentication = createAuthentication(config, webClient);
const network = createNetwork(webClient);
const factManager = new FactManager(fork, observableSource, store, network);
const purgeConditions = createPurgeConditions(config);
const factManager = new FactManager(fork, observableSource, store, network, purgeConditions);
return new Jinaga(authentication, factManager, syncStatusNotifier);
}
}
Expand Down Expand Up @@ -129,4 +133,15 @@ function createNetwork(
else {
return new NetworkNoOp();
}
}

function createPurgeConditions(
config: JinagaBrowserConfig
): Specification[] {
if (config.purgeConditions) {
return config.purgeConditions(new PurgeConditions([])).specifications;
}
else {
return [];
}
}
17 changes: 15 additions & 2 deletions src/jinaga-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { FactManager } from './managers/factManager';
import { Network, NetworkDistribution, NetworkNoOp } from './managers/NetworkManager';
import { MemoryStore } from './memory/memory-store';
import { ObservableSource } from './observable/observable';
import { PurgeConditions } from "./purge/purgeConditions";
import { Model } from './specification/model';
import { Specification } from "./specification/specification";
import { FactEnvelope, Storage } from './storage';

export type JinagaTestConfig = {
Expand All @@ -20,7 +22,8 @@ export type JinagaTestConfig = {
distribution?: (d: DistributionRules) => DistributionRules,
user?: {},
device?: {},
initialState?: {}[]
initialState?: {}[],
purgeConditions?: (p: PurgeConditions) => PurgeConditions
}

export class JinagaTest {
Expand All @@ -32,7 +35,8 @@ export class JinagaTest {
const fork = new PassThroughFork(store);
const authentication = this.createAuthentication(config, store);
const network = this.createNetwork(config, store);
const factManager = new FactManager(fork, observableSource, store, network);
const purgeConditions = this.createPurgeConditions(config);
const factManager = new FactManager(fork, observableSource, store, network, purgeConditions);
return new Jinaga(authentication, factManager, syncStatusNotifier);
}

Expand Down Expand Up @@ -67,6 +71,15 @@ export class JinagaTest {
}
}

static createPurgeConditions(config: JinagaTestConfig): Specification[] {
if (config.purgeConditions) {
return config.purgeConditions(new PurgeConditions([])).specifications;
}
else {
return [];
}
}

private static getUserFact(config: JinagaTestConfig) {
return config.user ? dehydrateFact(config.user)[0] : null;
}
Expand Down
13 changes: 12 additions & 1 deletion src/managers/factManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Fork } from "../fork/fork";
import { ObservableSource, SpecificationListener } from "../observable/observable";
import { Observer, ObserverImpl, ResultAddedFunc } from "../observer/observer";
import { isSpecificationCompliant } from "../purge/purgeCompliance";
import { Specification } from "../specification/specification";
import { FactEnvelope, FactRecord, FactReference, ProjectedResult, Storage } from "../storage";
import { Network, NetworkManager } from "./NetworkManager";
Expand All @@ -12,7 +13,8 @@ export class FactManager {
private readonly fork: Fork,
private readonly observableSource: ObservableSource,
private readonly store: Storage,
network: Network
network: Network,
private readonly purgeConditions: Specification[]
) {
this.networkManager = new NetworkManager(network, store,
factsAdded => this.observableSource.notify(factsAdded));
Expand All @@ -39,14 +41,23 @@ export class FactManager {
}

async read(start: FactReference[], specification: Specification): Promise<ProjectedResult[]> {
if (!isSpecificationCompliant(specification, this.purgeConditions)) {
throw new Error("Specification is not compliant with purge conditions.");
}
return await this.store.read(start, specification);
}

async fetch(start: FactReference[], specification: Specification) {
if (!isSpecificationCompliant(specification, this.purgeConditions)) {
throw new Error("Specification is not compliant with purge conditions.");
}
await this.networkManager.fetch(start, specification);
}

async subscribe(start: FactReference[], specification: Specification) {
if (!isSpecificationCompliant(specification, this.purgeConditions)) {
throw new Error("Specification is not compliant with purge conditions.");
}
return await this.networkManager.subscribe(start, specification);
}

Expand Down
128 changes: 128 additions & 0 deletions src/purge/purgeCompliance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { Condition, Match, Role, Specification } from "../specification/specification";

export function isSpecificationCompliant(specification: Specification, purgeConditions: Specification[]) {
return specification.matches.every(m => isMatchCompliant(m, purgeConditions));
}

function isMatchCompliant(match: Match, purgeConditions: Specification[]) {
var failedUnknown = purgeConditions.some(pc =>
pc.given[0].type === match.unknown.type &&
!hasCondition(match.conditions, pc)
);
if (failedUnknown) {
return false;
}

var failedIntermediate = purgeConditions.some(pc =>
match.conditions.some(c => hasIntermediateType(c, pc.given[0].type))
)
if (failedIntermediate) {
return false;
}

// TODO: We need to check the existential conditions.
return true;
}

function hasCondition(conditions: Condition[], purgeCondition: Specification) {
return conditions.some(c => conditionMatches(c, purgeCondition));
}

function conditionMatches(condition: Condition, purgeCondition: Specification) {
if (condition.type === "existential") {
if (condition.exists) {
// We only match negative existential conditions.
return false;
}
// Compare the matches of the condition with the matches of the purge condition.
if (condition.matches.length !== purgeCondition.matches.length) {
return false;
}
return condition.matches.every((m, i) => matchesAreEquivalent(m, purgeCondition.matches[i]));
}
}

function matchesAreEquivalent(match: Match, purgeMatch: Match): unknown {
if (match.unknown.type !== purgeMatch.unknown.type) {
return false;
}
if (match.conditions.length !== purgeMatch.conditions.length) {
return false;
}
return match.conditions.every((c, i) => conditionsAreEquivalent(c, purgeMatch.conditions[i]));
}

function conditionsAreEquivalent(condition: Condition, purgeCondition: Condition) {
if (condition.type === "path") {
if (purgeCondition.type !== "path") {
return false;
}
if (condition.rolesLeft.length !== purgeCondition.rolesLeft.length) {
return false;
}
if (condition.rolesRight.length !== purgeCondition.rolesRight.length) {
return false;
}
return condition.rolesLeft.every((r, i) => rolesAreEquivalent(r, purgeCondition.rolesLeft[i]))
&& condition.rolesRight.every((r, i) => rolesAreEquivalent(r, purgeCondition.rolesRight[i]));
}
else if (condition.type === "existential") {
if (purgeCondition.type !== "existential") {
return false;
}
if (condition.exists !== purgeCondition.exists) {
return false;
}
if (condition.matches.length !== purgeCondition.matches.length) {
return false;
}
return condition.matches.every((m, i) => matchesAreEquivalent(m, purgeCondition.matches[i]));
}
}

function rolesAreEquivalent(role: Role, purgeRole: Role) {
return role.predecessorType === purgeRole.predecessorType &&
role.name === purgeRole.name;
}

function hasIntermediateType(condition: Condition, type: string) {
if (condition.type === "path") {
var leftOnly = condition.rolesRight.length === 0;
var rightOnly = condition.rolesLeft.length === 0;

// If we only have left roles, then ignore the last role on the right.
// If any of the roles is the type we're looking for, then we have an intermediate type.
if (leftOnly) {
var found = condition.rolesLeft.some((r, i) =>
r.predecessorType === type &&
i < condition.rolesLeft.length - 1);
if (found) {
return true;
}
}
else {
var found = condition.rolesLeft.some(r => r.predecessorType === type);
if (found) {
return true;
}
}

// If we only have right roles, then ignore the last role on the left.
// If any of the roles is the type we're looking for, then we have an intermediate type.
if (rightOnly) {
var found = condition.rolesRight.some((r, i) =>
r.predecessorType === type &&
i < condition.rolesRight.length - 1);
if (found) {
return true;
}
}
else {
var found = condition.rolesRight.some(r => r.predecessorType === type);
if (found) {
return true;
}
}
}
return false;
}
19 changes: 19 additions & 0 deletions src/purge/purgeConditions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SpecificationOf } from "../specification/model";
import { Specification } from "../specification/specification";

export class PurgeConditions {
constructor(
public specifications: Specification[]
) { }

whenExists<T, U>(specification: SpecificationOf<T, U>): PurgeConditions {
return new PurgeConditions([
...this.specifications,
specification.specification
]);
}

with(fn: (p: PurgeConditions) => PurgeConditions): PurgeConditions {
return fn(this);
}
}
Loading
Loading