From baeeaa665b16bb60bbee1ba0a7821bbf3c3ca725 Mon Sep 17 00:00:00 2001 From: Ihor Chulinda Date: Sun, 12 May 2019 21:08:11 +0200 Subject: [PATCH] feat(sync): add initial implementation --- packages/sync/.npmignore | 5 +++ packages/sync/LICENSE | 22 ++++++++++++ packages/sync/README.md | 47 ++++++++++++++++++++++++++ packages/sync/package.json | 21 ++++++++++++ packages/sync/src/index.ts | 4 +++ packages/sync/src/runInSync.ts | 17 ++++++++++ packages/sync/src/sync.ts | 57 ++++++++++++++++++++++++++++++++ packages/sync/src/syncService.ts | 49 +++++++++++++++++++++++++++ packages/sync/tsconfig.json | 10 ++++++ packages/sync/tslint.json | 11 ++++++ 10 files changed, 243 insertions(+) create mode 100644 packages/sync/.npmignore create mode 100644 packages/sync/LICENSE create mode 100644 packages/sync/README.md create mode 100644 packages/sync/package.json create mode 100644 packages/sync/src/index.ts create mode 100644 packages/sync/src/runInSync.ts create mode 100644 packages/sync/src/sync.ts create mode 100644 packages/sync/src/syncService.ts create mode 100644 packages/sync/tsconfig.json create mode 100644 packages/sync/tslint.json diff --git a/packages/sync/.npmignore b/packages/sync/.npmignore new file mode 100644 index 00000000..d0fdcb37 --- /dev/null +++ b/packages/sync/.npmignore @@ -0,0 +1,5 @@ +!/bin/**/* +!/dist/**/* +/src/ +tsconfig.json +tslint.json diff --git a/packages/sync/LICENSE b/packages/sync/LICENSE new file mode 100644 index 00000000..c8e488bc --- /dev/null +++ b/packages/sync/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Ihor Chulinda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/packages/sync/README.md b/packages/sync/README.md new file mode 100644 index 00000000..8dd43cc8 --- /dev/null +++ b/packages/sync/README.md @@ -0,0 +1,47 @@ +[npm-badge-png]: https://nodei.co/npm/metaf-sync.png?downloads=true&downloadRank=true&stars=true +[package-url]: https://npmjs.com/package/metaf-sync + +[![npm badge][npm-badge-png]][package-url] + +[![Known Vulnerabilities](https://snyk.io/test/npm/metaf-core/badge.svg)](https://snyk.io/test/npm/metaf-sync) + + +# MetaF Sync +> Sync package for **MetaF**ramework. + +> **WARNING:** it's early beta, so documentation may have mistakes, if you face any problems feel free to create [issues](https://github.com/Igmat/metaf/issues). + +## Table of Contents + + + + +- [What is it?](#what-is-it) +- [Motivation](#motivation) +- [How it works?](#how-it-works) +- [Why do I have to use it?](#why-do-i-have-to-use-it) +- [Installation](#installation) +- [Usage](#usage) + + + +## What is it? +**TBD** + +## Motivation +**TBD** + +## How it works? +**TBD** + +## Why do I have to use it? +**TBD** + +## Installation +Run: +``` +npm install metaf-core +``` + +## Usage +**TBD** diff --git a/packages/sync/package.json b/packages/sync/package.json new file mode 100644 index 00000000..8c29081b --- /dev/null +++ b/packages/sync/package.json @@ -0,0 +1,21 @@ +{ + "name": "metaf-sync", + "version": "0.2.34", + "description": "", + "main": "dist/index.js", + "private": true, + "types": "dist/index.d.ts", + "author": "Ihor Chulinda ", + "repository": { + "type": "git", + "url": "git@github.com:Igmat/metaf.git" + }, + "scripts": { + "doctoc": "doctoc README.md", + "prepublish": "npm run doctoc" + }, + "dependencies": { + "metaf-resolvable": "^0.2.33" + }, + "license": "MIT" +} diff --git a/packages/sync/src/index.ts b/packages/sync/src/index.ts new file mode 100644 index 00000000..8c06c9f1 --- /dev/null +++ b/packages/sync/src/index.ts @@ -0,0 +1,4 @@ +export * from './runInSync'; +export * from './sync'; +export * from './syncService'; + diff --git a/packages/sync/src/runInSync.ts b/packages/sync/src/runInSync.ts new file mode 100644 index 00000000..9f7e2cd6 --- /dev/null +++ b/packages/sync/src/runInSync.ts @@ -0,0 +1,17 @@ +import { PromiseCache } from './sync'; + +export const context = { + cache: new WeakMap(), +}; + +export function runInSync(app: () => void, cache = new WeakMap()) { + context.cache = cache; + try { + app(); + } catch (err) { + if (!(err instanceof PromiseCache)) throw err; + err.resolved + .then(() => runInSync(app, cache)) + .catch(innerError => { throw innerError; }); + } +} diff --git a/packages/sync/src/sync.ts b/packages/sync/src/sync.ts new file mode 100644 index 00000000..4fda2586 --- /dev/null +++ b/packages/sync/src/sync.ts @@ -0,0 +1,57 @@ +const methodsToThrow = [ + 'getPrototypeOf', + 'setPrototypeOf', + 'isExtensible', + 'preventExtensions', + 'getOwnPropertyDescriptor', + 'has', + 'get', + 'set', + 'deleteProperty', + 'defineProperty', + 'enumerate', + 'ownKeys', + 'apply', + 'construct', +]; +export class PromiseCache { + content = new WeakMap, unknown>(); + proxies = new WeakMap, unknown>(); + resolved = Promise.resolve(undefined); +} +const promiseCache = new PromiseCache(); +const trap = () => { + throw promiseCache; +}; +const pendingHandler = methodsToThrow.reduce((result, method) => ({...result, [method]: trap}), {}); +type Primitives = string | number | boolean | symbol | undefined | null; +export type PrimitivesWrapper = T extends Primitives ? { value: T } : T; +export type AsyncPrimitivesWrapper = T extends Promise + ? PrimitivesWrapper + : T; +export function sync

(promise: P): AsyncPrimitivesWrapper

{ + if (!(promise instanceof Promise)) return promise as AsyncPrimitivesWrapper

; + if (promiseCache.content.has(promise)) return promiseCache.content.get(promise) as AsyncPrimitivesWrapper

; + if (promiseCache.proxies.has(promise)) return promiseCache.proxies.get(promise) as AsyncPrimitivesWrapper

; + promiseCache.resolved = Promise.all([ + promiseCache.resolved, + promise.then(value => promiseCache.content.set(promise, wrapPrimitive(value))), + ]); + const pendingProxy = new Proxy({}, pendingHandler) as AsyncPrimitivesWrapper

; + promiseCache.proxies.set(promise, pendingProxy); + + return pendingProxy; +} + +function wrapPrimitive(value: T): PrimitivesWrapper { + if (typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' || + typeof value === 'symbol' || + typeof value === 'undefined' || + value === null) { + return { value } as PrimitivesWrapper; + } + + return value as PrimitivesWrapper; +} diff --git a/packages/sync/src/syncService.ts b/packages/sync/src/syncService.ts new file mode 100644 index 00000000..5d2e6334 --- /dev/null +++ b/packages/sync/src/syncService.ts @@ -0,0 +1,49 @@ +import { Callable, Constructable, isConstructor } from 'metaf-resolvable'; +import { context } from './runInSync'; +import { AsyncPrimitivesWrapper, sync } from './sync'; +type TypedFunction = (...args: ARGS) => T; +function isFunction(arg: unknown): arg is TypedFunction { + return typeof arg === 'function'; +} +export type SynchronizedProperty = + T extends () => infer R + ? () => AsyncPrimitivesWrapper + // FIXME: remove this hack for JSX syntax when TS will properly use createElement signature + : T extends (props: infer PROPS, ...args: infer ARGS) => infer R + ? (props: PROPS, ...args: ARGS) => AsyncPrimitivesWrapper + : T extends (...args: infer ARGS) => infer R + ? TypedFunction, ARGS> + : AsyncPrimitivesWrapper; +export type Synchronous = { + [P in keyof I]: SynchronizedProperty; +}; +function synchronizeFunction(method: TypedFunction): TypedFunction, ARGS> { + return function (this: unknown, ...args: ARGS) { + if (!context.cache.has(method)) context.cache.set(method, {}); + const functionCache = context.cache.get(method) as { [arg: string]: R }; + const key = JSON.stringify(args); + if (!functionCache.hasOwnProperty(key)) functionCache[key] = method.call(this, ...args); + + return sync(functionCache[key]); + }; +} +export function syncService(serviceConstructor: Constructable<[], I> | Callable): Synchronous { + const instance = isConstructor(serviceConstructor) + ? new serviceConstructor() + : serviceConstructor(); + const result: Partial> = {}; + // We want to wrap all properties, including inherited, + // so we don't need tslint warning as about not filtering + // forin statement to only own properties + // tslint:disable-next-line:forin + for (const key in instance) { + const property = instance[key]; + result[key] = (isFunction(property) + ? synchronizeFunction(property) + : (property instanceof Promise) + ? sync(property) + : property) as Synchronous[Extract, string>]; + } + + return result as Synchronous; +} diff --git a/packages/sync/tsconfig.json b/packages/sync/tsconfig.json new file mode 100644 index 00000000..7286d47b --- /dev/null +++ b/packages/sync/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ] +} diff --git a/packages/sync/tslint.json b/packages/sync/tslint.json new file mode 100644 index 00000000..c20fb2b9 --- /dev/null +++ b/packages/sync/tslint.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "../../tslint.json" + ], + "rules": { + "no-object-literal-type-assertion": false, + "interface-name": false, + "no-default-export": false, + "variable-name": false + } +}