Skip to content
Open
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 packages/pglite-solid/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
6 changes: 6 additions & 0 deletions packages/pglite-solid/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# @electric-sql/pglite-solid

## 0.2.23

### Patch Changes
- Add support for solidjs
17 changes: 17 additions & 0 deletions packages/pglite-solid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# PGlite Solidjs Hooks

This package implements Solid hooks for [PGLite](https://pglite.dev/) on top of the [live query plugin](https://pglite.dev/docs/live-queries). Full documentation is available at [pglite.dev/docs/framework-hooks](https://pglite.dev/docs/framework-hooks#solid).

To install:

```sh
npm install @electric-sql/pglite-solid
```

The hooks this package provides are:

- [PGliteProvider](https://pglite.dev/docs/framework-hooks/solid#pgliteprovider): A Provider component to pass a PGlite instance to all child components for use with the other hooks.
- [usePGlite](https://pglite.dev/docs/framework-hooks/solid#usepglite): Retrieve the provided PGlite instance.
- [makePGliteProvider](https://pglite.dev/docs/framework-hooks/solid#makepgliteprovider): Create typed instances of `PGliteProvider` and `usePGlite`.
- [useLiveQuery](https://pglite.dev/docs/framework-hooks/solid#uselivequery): Reactively re-render your component whenever the results of a live query change
- [useLiveIncrementalQuery](https://pglite.dev/docs/framework-hooks/solid#useliveincrementalquery): Reactively re-render your component whenever the results of a live query change by offloading the diff to PGlite
23 changes: 23 additions & 0 deletions packages/pglite-solid/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import rootConfig from '../../eslint.config.js'
import solid from 'eslint-plugin-solid'
import * as tsParser from '@typescript-eslint/parser'

export default [
...rootConfig,
{
files: ['**/*.{ts,tsx}'],
...solid,
languageOptions: {
parser: tsParser,
parserOptions: {
project: 'tsconfig.json',
},
},
},
{
files: ['**/test/**'],
rules: {
'@typescript-eslint/no-unnecessary-condition': 'off',
},
},
]
71 changes: 71 additions & 0 deletions packages/pglite-solid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"name": "@electric-sql/pglite-solid",
"version": "0.2.23",
"description": "Hooks for using PGlite",
"type": "module",
"private": false,
"publishConfig": {
"access": "public"
},
"keywords": [
"postgres",
"sql",
"database",
"wasm",
"client",
"pglite",
"solid"
],
"author": "Electric DB Limited",
"homepage": "https://pglite.dev",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/electric-sql/pglite.git",
"directory": "packages/pglite-solid"
},
"scripts": {
"build": "tsup",
"check:exports": "attw . --pack --profile node16",
"test": "vitest",
"lint": "eslint ./src ./test",
"format": "prettier --write ./src ./test",
"typecheck": "tsc",
"stylecheck": "eslint ./src ./test && prettier --check ./src ./test",
"prepublishOnly": "pnpm check:exports"
},
"types": "dist/index.d.ts",
"main": "dist/index.cjs",
"module": "dist/index.js",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
"dist"
],
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.1",
"@electric-sql/pglite": "workspace:*",
"@solidjs/testing-library": "^0.8.10",
"@testing-library/dom": "^10.4.0",
"eslint-plugin-solid": "^0.14.5",
"globals": "^15.11.0",
"jsdom": "^24.1.3",
"vite-plugin-solid": "^2.11.7",
"solid-js": "^1.9.7",
"vitest": "^2.1.2"
},
"peerDependencies": {
"@electric-sql/pglite": "workspace:0.3.5",
"solid-js": "^1.8.0 || ^1.9.0"
}
}
178 changes: 178 additions & 0 deletions packages/pglite-solid/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type { LiveQuery, LiveQueryResults } from '@electric-sql/pglite/live'
import { query as buildQuery } from '@electric-sql/pglite/template'
import { usePGlite } from './provider'
import {
Accessor,
createComputed,
createMemo,
createResource,
createSignal,
onCleanup,
} from 'solid-js'

type Params = unknown[] | undefined | null
type Pagination = { limit: number; offset: number }

function useLiveQueryImpl<T = { [key: string]: unknown }>(opts: {
query: Accessor<string | LiveQuery<T> | Promise<LiveQuery<T>>>
params?: Accessor<Params>
key?: Accessor<string>
pagination?: Accessor<Pagination>
}): Accessor<Omit<LiveQueryResults<T>, 'affectedRows'> | undefined> {
const db = usePGlite()
const liveQuery = createMemo(
() => {
const originalQuery = opts.query()
if (
!(typeof originalQuery === 'string') &&
!(originalQuery instanceof Promise)
) {
return originalQuery
}

return undefined
},
undefined,
{ name: 'PGLiteLiveQueryMemo' },
)

const [results, setResults] = createSignal<LiveQueryResults<T> | undefined>(
liveQuery()?.initialResults,
{ name: 'PGLiteResultsSignal' },
)

createComputed(
() => {
const query = liveQuery()
if (query) {
setResults(query.initialResults)
}
},
undefined,
{ name: 'PGLiteLiveQueryInitialSyncComputed' },
)

const initialPagination = opts.pagination?.()
const [queryRan] = createResource(
() => ({ query: opts.query(), key: opts.key?.(), params: opts.params?.() }),
async (opts) => {
const query = opts.query
if (typeof query === 'string') {
const key = opts.key
const ret =
key != undefined
? db.live.incrementalQuery<T>({
query,
callback: setResults,
params: opts.params,
key,
})
: db.live.query({
query,
callback: setResults,
params: opts.params,
...initialPagination,
})

const res = await ret
return res
} else if (query instanceof Promise) {
const res = await query
setResults(res.initialResults)
res.subscribe(setResults)

return res
} else if (liveQuery()) {
setResults(liveQuery()!.initialResults)
liveQuery()!.subscribe(setResults)

return liveQuery()
} else {
throw new Error('Should never happen')
}
},
{ name: 'PGLiteLiveQueryResource' },
)

createComputed((oldPagination: Pagination | undefined) => {
const pagination = opts.pagination?.()

if (
pagination &&
(pagination.limit !== oldPagination?.limit ||
pagination.offset !== oldPagination?.offset)
) {
queryRan()?.refresh(pagination)
return pagination
}

return undefined
}, opts.pagination?.())

onCleanup(() => {
queryRan()?.unsubscribe()
})

const aggregatedResult = createMemo(
() => {
queryRan()
const res = results()
if (res) {
return {
rows: res.rows,
fields: res.fields,
totalCount: res.totalCount,
offset: res.offset,
limit: res.limit,
}
}

return res
},
undefined,
{ name: 'PGLiteLiveQueryResultMemo' },
)

return aggregatedResult
}

export function useLiveQuery<T = { [key: string]: unknown }>(opts: {
query: Accessor<string>
params?: Accessor<unknown[] | undefined | null>
pagination?: Accessor<Pagination>
}): Accessor<LiveQueryResults<T> | undefined>

export function useLiveQuery<T = { [key: string]: unknown }>(opts: {
query: Accessor<LiveQuery<T>>
}): Accessor<LiveQueryResults<T>>

export function useLiveQuery<T = { [key: string]: unknown }>(opts: {
query: Accessor<Promise<LiveQuery<T>>>
}): Accessor<LiveQueryResults<T> | undefined>

export function useLiveQuery<T = { [key: string]: unknown }>(opts: {
query: Accessor<string | LiveQuery<T> | Promise<LiveQuery<T>>>
params?: Accessor<unknown[] | undefined | null>
pagination?: Accessor<Pagination>
}): Accessor<LiveQueryResults<T> | undefined> {
return useLiveQueryImpl<T>(opts)
}

useLiveQuery.sql = function <T = { [key: string]: unknown }>(
strings: TemplateStringsArray,
...values: any[]
): Accessor<LiveQueryResults<T> | undefined> {
const { query, params } = buildQuery(strings, ...values)
return useLiveQueryImpl<T>({
params: () => params.map((p) => (typeof p === 'function' ? p() : p)),
query: () => query,
})
}

export function useLiveIncrementalQuery<T = { [key: string]: unknown }>(opts: {
query: Accessor<string | LiveQuery<T> | Promise<LiveQuery<T>>>
params: Accessor<unknown[] | undefined | null>
key?: Accessor<string>
}): Accessor<LiveQueryResults<T> | undefined> {
return useLiveQueryImpl<T>(opts)
}
2 changes: 2 additions & 0 deletions packages/pglite-solid/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './provider'
export * from './hooks'
42 changes: 42 additions & 0 deletions packages/pglite-solid/src/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PGliteWithLive } from '@electric-sql/pglite/live'
import { createContext, ParentProps, useContext } from 'solid-js'
import { JSX } from 'solid-js/jsx-runtime'

interface Props<T extends PGliteWithLive> extends ParentProps<{ db?: T }> {}

type PGliteProvider<T extends PGliteWithLive> = (props: Props<T>) => JSX.Element
type UsePGlite<T extends PGliteWithLive> = (db?: T) => T

interface PGliteProviderSet<T extends PGliteWithLive> {
PGliteProvider: PGliteProvider<T>
usePGlite: UsePGlite<T>
}

/**
* Create a typed set of {@link PGliteProvider} and {@link usePGlite}.
*/
function makePGliteProvider<T extends PGliteWithLive>(): PGliteProviderSet<T> {
const ctx = createContext<T | undefined>(undefined)
return {
usePGlite: ((db?: T) => {
const dbProvided = useContext(ctx)

// allow providing a db explicitly
if (db !== undefined) return db

if (!dbProvided)
throw new Error(
'No PGlite instance found, use PGliteProvider to provide one',
)

return dbProvided
}) as UsePGlite<T>,
PGliteProvider: (props: Props<T>) => {
return <ctx.Provider value={props.db}>{props.children}</ctx.Provider>
},
}
}

const { PGliteProvider, usePGlite } = makePGliteProvider<PGliteWithLive>()

export { makePGliteProvider, PGliteProvider, usePGlite }
5 changes: 5 additions & 0 deletions packages/pglite-solid/test-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { afterEach } from 'vitest'
import { cleanup } from '@solidjs/testing-library'

// https://testing-library.com/docs/solid-testing-library/api#cleanup
afterEach(() => cleanup())
Loading