From 4cbaaead1b9d29aad9acfb72b4bd6cf3fc4a0dd3 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Thu, 3 Dec 2020 22:31:13 +0000 Subject: [PATCH 1/3] [Flight] Add rudimentary PG binding --- packages/react-pg/README.md | 12 ++++ packages/react-pg/index.browser.js | 12 ++++ packages/react-pg/index.js | 12 ++++ packages/react-pg/index.node.js | 12 ++++ packages/react-pg/npm/index.browser.js | 7 ++ packages/react-pg/npm/index.js | 3 + packages/react-pg/npm/index.node.js | 7 ++ packages/react-pg/package.json | 27 ++++++++ packages/react-pg/src/ReactPostgres.js | 88 ++++++++++++++++++++++++++ scripts/flow/environment.js | 8 +++ scripts/rollup/bundles.js | 18 ++++++ 11 files changed, 206 insertions(+) create mode 100644 packages/react-pg/README.md create mode 100644 packages/react-pg/index.browser.js create mode 100644 packages/react-pg/index.js create mode 100644 packages/react-pg/index.node.js create mode 100644 packages/react-pg/npm/index.browser.js create mode 100644 packages/react-pg/npm/index.js create mode 100644 packages/react-pg/npm/index.node.js create mode 100644 packages/react-pg/package.json create mode 100644 packages/react-pg/src/ReactPostgres.js diff --git a/packages/react-pg/README.md b/packages/react-pg/README.md new file mode 100644 index 0000000000000..cbcc8fb09a7fe --- /dev/null +++ b/packages/react-pg/README.md @@ -0,0 +1,12 @@ +# react-pg + +This package is meant to be used alongside yet-to-be-released, experimental React features. It's unlikely to be useful in any other context. + +**Do not use in a real application.** We're publishing this early for +demonstration purposes. + +**Use it at your own risk.** + +# No, Really, It Is Unstable + +The API ~~may~~ will change wildly between versions. diff --git a/packages/react-pg/index.browser.js b/packages/react-pg/index.browser.js new file mode 100644 index 0000000000000..444c63ec765ff --- /dev/null +++ b/packages/react-pg/index.browser.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +throw new Error( + 'This entry point is not yet supported in the browser environment', +); diff --git a/packages/react-pg/index.js b/packages/react-pg/index.js new file mode 100644 index 0000000000000..ceb2071c4f055 --- /dev/null +++ b/packages/react-pg/index.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +export * from './index.node'; diff --git a/packages/react-pg/index.node.js b/packages/react-pg/index.node.js new file mode 100644 index 0000000000000..71ee93be80aa8 --- /dev/null +++ b/packages/react-pg/index.node.js @@ -0,0 +1,12 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +'use strict'; + +export * from './src/ReactPostgres'; diff --git a/packages/react-pg/npm/index.browser.js b/packages/react-pg/npm/index.browser.js new file mode 100644 index 0000000000000..6e11ea9c992e1 --- /dev/null +++ b/packages/react-pg/npm/index.browser.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-pg.browser.production.min.js'); +} else { + module.exports = require('./cjs/react-pg.browser.development.js'); +} diff --git a/packages/react-pg/npm/index.js b/packages/react-pg/npm/index.js new file mode 100644 index 0000000000000..ee510df2ad686 --- /dev/null +++ b/packages/react-pg/npm/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./index.node'); diff --git a/packages/react-pg/npm/index.node.js b/packages/react-pg/npm/index.node.js new file mode 100644 index 0000000000000..a351531e3c902 --- /dev/null +++ b/packages/react-pg/npm/index.node.js @@ -0,0 +1,7 @@ +'use strict'; + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./cjs/react-pg.node.production.min.js'); +} else { + module.exports = require('./cjs/react-pg.node.development.js'); +} diff --git a/packages/react-pg/package.json b/packages/react-pg/package.json new file mode 100644 index 0000000000000..9f8575414eb45 --- /dev/null +++ b/packages/react-pg/package.json @@ -0,0 +1,27 @@ +{ + "private": true, + "name": "react-pg", + "description": "React bindings for PostgreSQL", + "version": "0.0.0", + "repository": { + "type" : "git", + "url" : "https://github.com/facebook/react.git", + "directory": "packages/react-pg" + }, + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "index.node.js", + "index.browser.js", + "cjs/" + ], + "peerDependencies": { + "react": "^17.0.0", + "pg": "*" + }, + "browser": { + "./index.js": "./index.browser.js" + } +} diff --git a/packages/react-pg/src/ReactPostgres.js b/packages/react-pg/src/ReactPostgres.js new file mode 100644 index 0000000000000..a7dd75934ccaf --- /dev/null +++ b/packages/react-pg/src/ReactPostgres.js @@ -0,0 +1,88 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import type {Wakeable} from 'shared/ReactTypes'; + +import {unstable_getCacheForType} from 'react'; +import {Pool as PostgresPool} from 'pg'; + +const Pending = 0; +const Resolved = 1; +const Rejected = 2; + +type PendingResult = {| + status: 0, + value: Wakeable, +|}; + +type ResolvedResult = {| + status: 1, + value: mixed, +|}; + +type RejectedResult = {| + status: 2, + value: mixed, +|}; + +type Result = PendingResult | ResolvedResult | RejectedResult; + +function toResult(thenable): Result { + const result: Result = { + status: Pending, + value: thenable, + }; + thenable.then( + value => { + if (result.status === Pending) { + const resolvedResult = ((result: any): ResolvedResult); + resolvedResult.status = Resolved; + resolvedResult.value = value; + } + }, + err => { + if (result.status === Pending) { + const rejectedResult = ((result: any): RejectedResult); + rejectedResult.status = Rejected; + rejectedResult.value = err; + } + }, + ); + return result; +} + +function readResult(result: Result) { + if (result.status === Resolved) { + return result.value; + } else { + throw result.value; + } +} + +export function Pool(options: mixed) { + this.pool = new PostgresPool(options); + // Unique function per instance because it's used for cache identity. + this.createResultMap = function() { + return new Map(); + }; +} + +Pool.prototype.query = function(query: string, values?: Array) { + const pool = this.pool; + const map = unstable_getCacheForType(this.createResultMap); + // TODO: Is this sufficient? What about more complex types? + const key = JSON.stringify({query, values}); + let entry = map.get(key); + if (!entry) { + const thenable = pool.query(query, values); + entry = toResult(thenable); + map.set(key, entry); + } + return readResult(entry); +}; diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 3efe41ff344d2..905220bea816b 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -69,3 +69,11 @@ declare module 'EventListener' { declare function __webpack_chunk_load__(id: string): Promise; declare function __webpack_require__(id: string): any; + +declare module 'pg' { + declare var Pool: ( + options: mixed, + ) => { + query: (query: string, values?: Array) => void, + }; +} diff --git a/scripts/rollup/bundles.js b/scripts/rollup/bundles.js index 37fdafa3408a1..2ee1ab74a402c 100644 --- a/scripts/rollup/bundles.js +++ b/scripts/rollup/bundles.js @@ -153,6 +153,24 @@ const bundles = [ externals: ['react', 'http', 'https'], }, + /******* React PG Browser (experimental, new) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-pg/index.browser', + global: 'ReactPostgres', + externals: [], + }, + + /******* React PG Node (experimental, new) *******/ + { + bundleTypes: [NODE_DEV, NODE_PROD], + moduleType: ISOMORPHIC, + entry: 'react-pg/index.node', + global: 'ReactPostgres', + externals: ['react', 'pg'], + }, + /******* React DOM *******/ { bundleTypes: [ From ce48940d02486a58172fc83ef3361c304d7afe21 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 4 Dec 2020 16:48:18 +0000 Subject: [PATCH 2/3] Use nested Maps for parameters --- packages/react-pg/src/ReactPostgres.js | 37 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/react-pg/src/ReactPostgres.js b/packages/react-pg/src/ReactPostgres.js index a7dd75934ccaf..b3c94e01286da 100644 --- a/packages/react-pg/src/ReactPostgres.js +++ b/packages/react-pg/src/ReactPostgres.js @@ -11,6 +11,7 @@ import type {Wakeable} from 'shared/ReactTypes'; import {unstable_getCacheForType} from 'react'; import {Pool as PostgresPool} from 'pg'; +import {prepareValue} from 'pg/lib/utils'; const Pending = 0; const Resolved = 1; @@ -73,16 +74,42 @@ export function Pool(options: mixed) { }; } +function getInnerMap( + outerMap: Map, + query: string, + values?: Array, +) { + if (values == null || values.length === 0) { + return [outerMap, query]; + } + // If we have parameters, each becomes as a nesting layer for Maps. + // We want to find (or create as needed) the innermost Map, and return that. + let innerMap = outerMap; + let key = query; + for (let i = 0; i < values.length; i++) { + let nextMap = innerMap.get(key); + if (nextMap === undefined) { + nextMap = new Map(); + innerMap.set(key, nextMap); + } + innerMap = nextMap; + // Postgres bindings convert everything to strings: + // https://node-postgres.com/features/queries#parameterized-query + // We reuse their algorithm instead of reimplementing. + key = prepareValue(values[i]); + } + return [innerMap, key]; +} + Pool.prototype.query = function(query: string, values?: Array) { const pool = this.pool; - const map = unstable_getCacheForType(this.createResultMap); - // TODO: Is this sufficient? What about more complex types? - const key = JSON.stringify({query, values}); - let entry = map.get(key); + const outerMap = unstable_getCacheForType(this.createResultMap); + const [innerMap, key] = getInnerMap(outerMap, query, values); + let entry = innerMap.get(key); if (!entry) { const thenable = pool.query(query, values); entry = toResult(thenable); - map.set(key, entry); + innerMap.set(key, entry); } return readResult(entry); }; From fa404195b871b269390af239fecf54e2dd092571 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Fri, 4 Dec 2020 16:59:17 +0000 Subject: [PATCH 3/3] Inline and fix Flow --- packages/react-pg/src/ReactPostgres.js | 48 +++++++++++--------------- scripts/flow/environment.js | 6 ++++ 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/packages/react-pg/src/ReactPostgres.js b/packages/react-pg/src/ReactPostgres.js index b3c94e01286da..fc4eae302a320 100644 --- a/packages/react-pg/src/ReactPostgres.js +++ b/packages/react-pg/src/ReactPostgres.js @@ -74,38 +74,30 @@ export function Pool(options: mixed) { }; } -function getInnerMap( - outerMap: Map, - query: string, - values?: Array, -) { - if (values == null || values.length === 0) { - return [outerMap, query]; - } - // If we have parameters, each becomes as a nesting layer for Maps. - // We want to find (or create as needed) the innermost Map, and return that. - let innerMap = outerMap; +Pool.prototype.query = function(query: string, values?: Array) { + const pool = this.pool; + const outerMap = unstable_getCacheForType(this.createResultMap); + + let innerMap: Map = outerMap; let key = query; - for (let i = 0; i < values.length; i++) { - let nextMap = innerMap.get(key); - if (nextMap === undefined) { - nextMap = new Map(); - innerMap.set(key, nextMap); + if (values != null) { + // If we have parameters, each becomes as a nesting layer for Maps. + // We want to find (or create as needed) the innermost Map, and return that. + for (let i = 0; i < values.length; i++) { + let nextMap = innerMap.get(key); + if (nextMap === undefined) { + nextMap = new Map(); + innerMap.set(key, nextMap); + } + innerMap = nextMap; + // Postgres bindings convert everything to strings: + // https://node-postgres.com/features/queries#parameterized-query + // We reuse their algorithm instead of reimplementing. + key = prepareValue(values[i]); } - innerMap = nextMap; - // Postgres bindings convert everything to strings: - // https://node-postgres.com/features/queries#parameterized-query - // We reuse their algorithm instead of reimplementing. - key = prepareValue(values[i]); } - return [innerMap, key]; -} -Pool.prototype.query = function(query: string, values?: Array) { - const pool = this.pool; - const outerMap = unstable_getCacheForType(this.createResultMap); - const [innerMap, key] = getInnerMap(outerMap, query, values); - let entry = innerMap.get(key); + let entry: Result | void = innerMap.get(key); if (!entry) { const thenable = pool.query(query, values); entry = toResult(thenable); diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 905220bea816b..65d7c81da974b 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -77,3 +77,9 @@ declare module 'pg' { query: (query: string, values?: Array) => void, }; } + +declare module 'pg/lib/utils' { + declare module.exports: { + prepareValue(val: any): mixed, + }; +}