Skip to content
Merged

SSR #37

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
}
],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": false }],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"no-console": ["error", { "allow": ["warn", "error"] }]
},
"settings": {
Expand Down
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: [
'**/__tests__/**/?(*.)+(spec|test).ts?(x)', '**/?(*.)+(spec|test).ts?(x)',
],
globalSetup: './setup.js'
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"main": "./dist/index.js",
"module": "./esm/index.js",
"esnext": "./esnext/index.js",
"types": "./dist/index.d.ts",
"types": "./esm/index.d.ts",
"sideEffects": false,
"repository": {
"type": "git",
Expand Down Expand Up @@ -62,6 +62,7 @@
},
"devDependencies": {
"@asuka/di": "^0.2.0",
"@types/express": "^4.17.0",
"@types/jest": "^24.0.16",
"@types/lodash": "^4.14.136",
"@types/node": "^12.6.9",
Expand Down
3 changes: 3 additions & 0 deletions setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = function setupTestEnv() {
process.env.ENABLE_AYANAMI_SSR = 'false'
}
41 changes: 40 additions & 1 deletion src/core/ayanami.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,50 @@
import { Observable } from 'rxjs'
import { Observable, noop } from 'rxjs'

import { ActionOfAyanami } from './types'
import { combineWithIkari, destroyIkariFrom } from './ikari'
import { moduleNameKey, globalKey } from '../ssr/ssr-module'
import { isSSREnabled } from '../ssr/flag'

const globalScope =
typeof self !== 'undefined'
? self
: typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}

export abstract class Ayanami<State> {
abstract defaultState: State

// @internal
ssrLoadKey = Symbol('SSR_LOADED')

// @internal
scopeName!: string

constructor() {
if (!isSSREnabled()) {
const name = Object.getPrototypeOf(this)[moduleNameKey]
if (!name) {
return
}
// @ts-ignore
const globalCache = globalScope[globalKey]

if (globalCache) {
const moduleCache = globalCache[name]
if (moduleCache) {
Reflect.defineMetadata(this.ssrLoadKey, true, this)
Object.defineProperty(this, 'defaultState', {
get: () => moduleCache[this.scopeName],
set: noop,
})
}
}
}
}

destroy() {
destroyIkariFrom(this)
}
Expand Down
5 changes: 3 additions & 2 deletions src/core/decorators/action-related.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { Ayanami } from '../ayanami'
import { ConstructorOf } from '../types'

export function createActionDecorator(symbols: ActionSymbols) {
return () => ({ constructor }: any, propertyKey: string) => {
addActionName(symbols, constructor, propertyKey)
return () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
addActionName(symbols, target.constructor, propertyKey)
return descriptor
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/core/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createActionDecorator } from './action-related'
export * from './action-related'

interface DecoratorReturnType<V> {
(target: any, propertyKey: string, descriptor: { value?: V }): void
(target: any, propertyKey: string, descriptor: { value?: V }): PropertyDescriptor
}

export const ImmerReducer: <S = any>() => DecoratorReturnType<
Expand All @@ -23,4 +23,4 @@ export const Effect: <A = any, S = any>() => DecoratorReturnType<
(action: Observable<A>, state$: Observable<S>) => Observable<EffectAction>
> = createActionDecorator(effectSymbols)

export const DefineAction = createActionDecorator(defineActionSymbols)
export const DefineAction: () => any = createActionDecorator(defineActionSymbols)
46 changes: 30 additions & 16 deletions src/core/ikari.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { merge, Observable, Subject, Subscription, NEVER } from 'rxjs'
import { map, catchError } from 'rxjs/operators'
import { map, catchError, takeUntil, filter } from 'rxjs/operators'
import { mapValues } from 'lodash'
import produce from 'immer'

Expand All @@ -14,9 +14,11 @@ import {
EffectActionFactories,
} from './types'
import { Ayanami } from './ayanami'
import { BasicState, getEffectActionFactories, getOriginalFunctions } from './utils'
import { createState, getEffectActionFactories, getOriginalFunctions } from './utils'
import { logStateAction } from '../redux-devtools-extension'
import { ikariSymbol } from './symbols'
import { TERMINATE_ACTION } from '../ssr/terminate'
import { isSSREnabled } from '../ssr/flag'

interface Config<State> {
nameForLog: string
Expand Down Expand Up @@ -74,13 +76,13 @@ export function destroyIkariFrom<S>(ayanami: Ayanami<S>): void {
}

export class Ikari<State> {
static createAndBindAt<S>(target: { defaultState: S }, config: Config<S>): Ikari<S> {
static createAndBindAt<S>(target: Ayanami<S>, config: Config<S>): Ikari<S> {
const createdIkari = this.getFrom(target)

if (createdIkari) {
return createdIkari
} else {
const ikari = new Ikari(config)
const ikari = new Ikari(target, config)
Reflect.defineMetadata(ikariSymbol, ikari, target)
return ikari
}
Expand All @@ -90,18 +92,18 @@ export class Ikari<State> {
return Reflect.getMetadata(ikariSymbol, target)
}

state: BasicState<State>
state = createState(this.config.defaultState)

effectActionFactories: EffectActionFactories
effectActionFactories = this.config.effectActionFactories

triggerActions: TriggerActions = {}

subscription = new Subscription()

constructor(private readonly config: Readonly<Config<State>>) {
this.effectActionFactories = config.effectActionFactories
this.state = new BasicState<State>(config.defaultState)
// @internal
terminate$ = new Subject<typeof TERMINATE_ACTION | null>()

constructor(readonly ayanami: Ayanami<State>, private readonly config: Readonly<Config<State>>) {
const [effectActions$, effectActions] = setupEffectActions(
this.config.effects,
this.state.state$,
Expand All @@ -124,8 +126,18 @@ export class Ikari<State> {
...mapValues(this.config.defineActions, ({ next }) => next),
}

let effectActionsWithTerminate$: Observable<Action<any>>

if (!isSSREnabled()) {
effectActionsWithTerminate$ = effectActions$
} else {
effectActionsWithTerminate$ = effectActions$.pipe(
takeUntil(this.terminate$.pipe(filter((action) => action === null))),
)
}

this.subscription.add(
effectActions$.subscribe((action) => {
effectActionsWithTerminate$.subscribe((action) => {
this.log(action)
this.handleAction(action)
}),
Expand All @@ -152,12 +164,10 @@ export class Ikari<State> {
}

private log = ({ originalActionName, effectAction, reducerAction }: Action<State>) => {
if (effectAction) {
if (effectAction && effectAction !== TERMINATE_ACTION) {
logStateAction(this.config.nameForLog, {
params: effectAction.params,
actionName: `${originalActionName}/👉${effectAction.ayanami.constructor.name}/️${
effectAction.actionName
}`,
actionName: `${originalActionName}/👉${effectAction.ayanami.constructor.name}/️${effectAction.actionName}`,
})
}

Expand All @@ -172,8 +182,12 @@ export class Ikari<State> {

private handleAction = ({ effectAction, reducerAction }: Action<State>) => {
if (effectAction) {
const { ayanami, actionName, params } = effectAction
combineWithIkari(ayanami).triggerActions[actionName](params)
if (effectAction !== TERMINATE_ACTION) {
const { ayanami, actionName, params } = effectAction
combineWithIkari(ayanami).triggerActions[actionName](params)
} else {
this.terminate$.next(effectAction)
}
}

if (reducerAction) {
Expand Down
19 changes: 5 additions & 14 deletions src/core/scope/__test__/scope.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,9 @@ import 'reflect-metadata'
import { Injectable } from '@asuka/di'

import { getInstanceWithScope, TransientScope, SameScope } from '../'
import { createNewInstance, createOrGetInstanceInScope } from '../utils'
import { createOrGetInstanceInScope } from '../utils'

describe('Scope spec:', () => {
describe('createNewInstance', () => {
it('should always return new instance', () => {
class Test {}

expect(createNewInstance(Test)).toBeInstanceOf(Test)
expect(createNewInstance(Test) === createNewInstance(Test)).toBeFalsy()
})
})

describe('createOrGetInstanceInScope', () => {
class Test {}
const scope = 'Scope'
Expand Down Expand Up @@ -102,7 +93,7 @@ describe('Scope spec:', () => {
}

it('should return same instance if is same scope', () => {
const scope = 'scope'
const scope = Symbol('scope')
const b = getInstanceWithScope(B, scope)
const c = getInstanceWithScope(C, scope)

Expand All @@ -111,9 +102,9 @@ describe('Scope spec:', () => {
})

it('should return different instance if is different scope', () => {
const b = getInstanceWithScope(B, 'b')
const c1 = getInstanceWithScope(C, 'c1')
const c2 = getInstanceWithScope(C, 'c2')
const b = getInstanceWithScope(B, Symbol('b'))
const c1 = getInstanceWithScope(C, Symbol('c1'))
const c2 = getInstanceWithScope(C, Symbol('c2'))

expect(b.a).toBeInstanceOf(A)
expect(c1.a).toBeInstanceOf(A)
Expand Down
19 changes: 6 additions & 13 deletions src/core/scope/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { InjectableFactory, ValueProvider } from '@asuka/di'
import { InjectableFactory } from '@asuka/di'

import { ConstructorOf } from '../types'
import { ScopeConfig } from './type'
import { createNewInstance, createOrGetInstanceInScope } from './utils'
import { getSameScopeInjectionParams, SameScope } from './same-scope-decorator'
import { createOrGetInstanceInScope, createScopeWithRequest } from './utils'
import { SameScope } from './same-scope-decorator'

export { ScopeConfig, SameScope }
export { ScopeConfig, SameScope, createScopeWithRequest }

export const TransientScope = Symbol('scope:transient')

Expand All @@ -15,19 +15,12 @@ export function getInstanceWithScope<T>(
constructor: ConstructorOf<T>,
scope: ScopeConfig['scope'] = SingletonScope,
): T {
const providers = getSameScopeInjectionParams(constructor).map(
(sameScopeInjectionParam): ValueProvider => ({
provide: sameScopeInjectionParam,
useValue: getInstanceWithScope(sameScopeInjectionParam, scope),
}),
)

switch (scope) {
case SingletonScope:
return InjectableFactory.getInstance(constructor)
case TransientScope:
return createNewInstance(constructor, providers)
return InjectableFactory.initialize(constructor)
default:
return createOrGetInstanceInScope(constructor, scope, providers)
return createOrGetInstanceInScope(constructor, scope)
}
}
20 changes: 5 additions & 15 deletions src/core/scope/same-scope-decorator.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
const SameScopeMetadataKey = Symbol('SameScopeInjectionParams')
export const SameScopeMetadataKey = Symbol('SameScopeInjectionParams')

export function getSameScopeInjectionParams(target: any): any[] {
export const SameScope = () => (target: any, _propertyKey: string, parameterIndex: number) => {
let sameScopeInjectionParams: boolean[] = []
if (Reflect.hasMetadata(SameScopeMetadataKey, target)) {
return Reflect.getMetadata(SameScopeMetadataKey, target)
sameScopeInjectionParams = Reflect.getMetadata(SameScopeMetadataKey, target)
} else {
const sameScopeInjectionParams: any[] = []
Reflect.defineMetadata(SameScopeMetadataKey, sameScopeInjectionParams, target)
return sameScopeInjectionParams
}
}

function addSameScopeInjectionParam(target: any, param: object) {
const sameScopeInjectionParams = getSameScopeInjectionParams(target)
sameScopeInjectionParams.push(param)
}

export const SameScope = () => (target: any, _propertyKey: string, parameterIndex: number) => {
const param = Reflect.getMetadata('design:paramtypes', target)[parameterIndex]
addSameScopeInjectionParam(target, param)
sameScopeInjectionParams[parameterIndex] = true
}
Loading