Skip to content

Commit

Permalink
Introduce an inter-service communication layer
Browse files Browse the repository at this point in the history
- Extract the event dispatching mechanism into a new service
- Refactor token-related services to handle external changes
- Add support for external services to dispatch events
- Restructure token store concepts
- Restrict tracker access to the token to read-only operation
- Add a prefix to tracking-related events to disambiguate with other events
  • Loading branch information
marcospassos authored Jun 8, 2020
1 parent 05c25c1 commit dcb5c91
Show file tree
Hide file tree
Showing 51 changed files with 1,356 additions and 500 deletions.
4 changes: 2 additions & 2 deletions src/activeRecord.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Operation, Patch} from './patch';
import {JsonArray, JsonObject, JsonValue} from './json';
import {Event} from './event';
import {TrackingEvent} from './trackingEvents';
import {
addOperation,
clearOperation,
Expand All @@ -23,7 +23,7 @@ const operationSchema = {
unset: unsetOperation,
};

export default abstract class ActiveRecord<T extends Event> {
export default abstract class ActiveRecord<T extends TrackingEvent> {
private readonly operations: Operation[] = [];

public set(value: JsonValue): this;
Expand Down
12 changes: 5 additions & 7 deletions src/cache/cookieCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,11 @@ export default class CookieCache implements Cache {
return getCookie(this.cookieName);
}

public put(value: string|null): void {
if (value === null) {
unsetCookie(this.cookieName);

return;
}

public put(value: string): void {
setCookie(this.cookieName, value, this.options);
}

public clear(): void {
unsetCookie(this.cookieName);
}
}
10 changes: 6 additions & 4 deletions src/cache/fallbackCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ export default class FallbackCache implements Cache {
return null;
}

public put(value: string | null): void {
for (const cache of this.caches) {
cache.put(value);
}
public put(value: string): void {
this.caches.forEach(cache => cache.put(value));
}

public clear(): void {
this.caches.forEach(cache => cache.clear());
}
}
12 changes: 8 additions & 4 deletions src/cache/inMemoryCache.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import Cache from './index';

export default class InMemoryCache implements Cache {
private cache: string | null = null;
private cache?: string;

public constructor(cache: string | null = null) {
public constructor(cache?: string) {
this.cache = cache;
}

public get(): string | null {
return this.cache;
return this.cache ?? null;
}

public put(value: string | null): void {
public put(value: string): void {
this.cache = value;
}

public clear(): void {
delete this.cache;
}
}
13 changes: 12 additions & 1 deletion src/cache/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export default interface Cache {
get(): string|null;
put(value: string|null): void;
put(value: string): void;
clear(): void;
}

export interface CacheListener {
(value: string|null): void;
}

export interface ObservableCache extends Cache {
addListener(listener: CacheListener): void;

removeListener(listener: CacheListener): void;
}
85 changes: 85 additions & 0 deletions src/cache/localStorageCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {CacheListener, ObservableCache} from './index';

export default class LocalStorageCache implements ObservableCache {
private readonly storage: Storage;

private readonly key: string;

private value: string|null;

private readonly listeners: CacheListener[] = [];

public constructor(storage: Storage, key: string) {
this.storage = storage;
this.key = key;
this.value = storage.getItem(key);
}

public static autoSync(cache: LocalStorageCache): (() => void) {
const listener = cache.sync.bind(cache);

window.addEventListener('storage', listener);

return (): void => window.removeEventListener('storage', listener);
}

public get(): string|null {
return this.value;
}

public put(value: string): void {
this.storage.setItem(this.key, value);

if (this.value !== value) {
this.value = value;
this.notifyChange(value);
}
}

public clear(): void {
this.storage.removeItem(this.key);

if (this.value !== null) {
this.value = null;
this.notifyChange(null);
}
}

public addListener(listener: CacheListener): void {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}
}

public removeListener(listener: CacheListener): void {
const index = this.listeners.indexOf(listener);

if (index > -1) {
this.listeners.splice(index, 1);
}
}

private notifyChange(value: string|null): void {
this.listeners.forEach(listener => listener(value));
}

private sync(event: StorageEvent): void {
if (event.storageArea !== this.storage || (event.key !== null && event.key !== this.key)) {
// Ignore unrelated changes
return;
}

/*
* Retrieving the value from the store rather than the event ensures
* the cache will be in sync with the latest value set.
* In case of cascading changes, it prevents notifying listeners
* about intermediate states already outdated at this point.
*/
const value = this.storage.getItem(this.key);

if (this.value !== value) {
this.value = value;
this.notifyChange(value);
}
}
}
26 changes: 0 additions & 26 deletions src/cache/storageCache.ts

This file was deleted.

71 changes: 54 additions & 17 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import MonitoredQueue from './queue/monitoredQueue';
import CapacityRestrictedQueue from './queue/capacityRestrictedQueue';
import EncodedChannel from './channel/encodedChannel';
import BeaconSocketChannel from './channel/beaconSocketChannel';
import {Beacon} from './event';
import {Beacon} from './trackingEvents';
import {SocketChannel} from './channel/socketChannel';
import Token, {TokenProvider} from './token';
import {TokenProvider} from './token';
import Tracker from './tracker';
import Evaluator from './evaluator';
import NamespacedLogger from './logging/namespacedLogger';
Expand All @@ -24,10 +24,12 @@ import CidAssigner from './cid/index';
import CachedAssigner from './cid/cachedAssigner';
import RemoteAssigner from './cid/remoteAssigner';
import FallbackCache from './cache/fallbackCache';
import StorageCache from './cache/storageCache';
import CookieCache from './cache/cookieCache';
import {getBaseDomain} from './cookie';
import FixedCidAssigner from './cid/fixedCidAssigner';
import {EventManager, SynchronousEventManager} from './eventManager';
import {SdkEventMap} from './sdkEvents';
import LocalStorageCache from './cache/localStorageCache';

export type Configuration = {
appId: string,
Expand All @@ -47,6 +49,8 @@ export class Container {

private context?: Context;

private tokenProvider?: TokenProvider;

private tracker?: Tracker;

private evaluator?: Evaluator;
Expand All @@ -57,6 +61,10 @@ export class Container {

private beaconQueue?: MonitoredQueue<string>;

private removeTokenSyncListener?: {(): void};

private readonly eventManager = new SynchronousEventManager<SdkEventMap>();

public constructor(configuration: Configuration) {
this.configuration = configuration;
}
Expand All @@ -74,16 +82,10 @@ export class Container {
}

private createEvaluator(): Evaluator {
const context = this.getContext();

return new Evaluator({
appId: this.configuration.appId,
endpointUrl: this.configuration.evaluationEndpointUrl,
tokenProvider: new class implements TokenProvider {
public getToken(): Promise<Token | null> {
return Promise.resolve(context.getToken());
}
}(),
tokenProvider: this.getTokenProvider(),
cidAssigner: this.getCidAssigner(),
});
}
Expand All @@ -97,8 +99,11 @@ export class Container {
}

private createTracker(): Tracker {
const context = this.getContext();

const tracker = new Tracker({
context: this.getContext(),
tab: context.getTab(),
tokenProvider: this.getTokenProvider(),
logger: this.getLogger('Tracker'),
channel: this.getBeaconChannel(),
eventMetadata: this.configuration.eventMetadata || {},
Expand All @@ -112,6 +117,15 @@ export class Container {
return tracker;
}

public getTokenProvider(): TokenProvider {
if (this.tokenProvider === undefined) {
const context = this.getContext();
this.tokenProvider = {getToken: context.getToken.bind(context)};
}

return this.tokenProvider;
}

public getContext(): Context {
if (this.context === undefined) {
this.context = this.createContext();
Expand All @@ -121,11 +135,21 @@ export class Container {
}

private createContext(): Context {
return Context.load(
this.getGlobalTabStorage('context'),
this.getGlobalBrowserStorage('context'),
this.configuration.tokenScope,
);
const browserStorage = this.getLocalStorage();
const browserCache = new LocalStorageCache(browserStorage, 'croct.token');
const tabStorage = this.getSessionStorage();

this.removeTokenSyncListener = LocalStorageCache.autoSync(browserCache);

return Context.load({
tokenScope: this.configuration.tokenScope,
eventDispatcher: this.getEventManager(),
cache: {
tabId: new LocalStorageCache(tabStorage, 'croct.tab'),
tabToken: new LocalStorageCache(tabStorage, 'croct.token'),
browserToken: browserCache,
},
});
}

private getBeaconChannel(): OutputChannel<Beacon> {
Expand Down Expand Up @@ -196,7 +220,7 @@ export class Container {
logger,
),
new FallbackCache(
new StorageCache(this.getLocalStorage(), 'croct.cid'),
new LocalStorageCache(this.getLocalStorage(), 'croct.cid'),
new CookieCache('croct.cid', {
sameSite: 'strict',
domain: getBaseDomain(),
Expand Down Expand Up @@ -277,6 +301,10 @@ export class Container {
return sessionStorage;
}

public getEventManager(): EventManager<SdkEventMap> {
return this.eventManager;
}

public async dispose(): Promise<void> {
const logger = this.getLogger();

Expand All @@ -286,6 +314,12 @@ export class Container {
await this.beaconChannel.close();
}

if (this.removeTokenSyncListener) {
logger.debug('Removing token sync listener...');

this.removeTokenSyncListener();
}

if (this.tracker) {
if (this.beaconQueue) {
logger.debug('Removing queue listeners...');
Expand All @@ -302,10 +336,13 @@ export class Container {
}

delete this.context;
delete this.tokenProvider;
delete this.cidAssigner;
delete this.tracker;
delete this.evaluator;
delete this.beaconChannel;
delete this.beaconQueue;
delete this.removeTokenSyncListener;

logger.debug('Container resources released.');
}
Expand Down
Loading

0 comments on commit dcb5c91

Please sign in to comment.