diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000..d98fe17111
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+
+# sst
+.sst
\ No newline at end of file
diff --git a/examples/nextjs-example/app/shape-proxy/route.ts b/examples/nextjs-example/app/shape-proxy/route.ts
index aadfae4d3a..0e66e0506f 100644
--- a/examples/nextjs-example/app/shape-proxy/route.ts
+++ b/examples/nextjs-example/app/shape-proxy/route.ts
@@ -41,4 +41,4 @@ export async function GET(request: Request) {
})
}
return resp
-}
+}
\ No newline at end of file
diff --git a/examples/yjs/.eslintignore b/examples/yjs/.eslintignore
new file mode 100644
index 0000000000..362a28a121
--- /dev/null
+++ b/examples/yjs/.eslintignore
@@ -0,0 +1,2 @@
+/build/**
+sst.config.ts
diff --git a/examples/yjs/.eslintrc.cjs b/examples/yjs/.eslintrc.cjs
new file mode 100644
index 0000000000..1e2f4aa287
--- /dev/null
+++ b/examples/yjs/.eslintrc.cjs
@@ -0,0 +1,44 @@
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true,
+ node: true,
+ },
+ extends: [
+ `eslint:recommended`,
+ `plugin:@typescript-eslint/recommended`,
+ `plugin:prettier/recommended`,
+ 'plugin:@next/next/recommended',
+ ],
+ parserOptions: {
+ ecmaVersion: 2022,
+ requireConfigFile: false,
+ sourceType: `module`,
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ parser: `@typescript-eslint/parser`,
+ plugins: [`prettier`],
+ rules: {
+ quotes: [`error`, `backtick`],
+ "no-unused-vars": `off`,
+ "@typescript-eslint/no-unused-vars": [
+ `error`,
+ {
+ argsIgnorePattern: `^_`,
+ varsIgnorePattern: `^_`,
+ caughtErrorsIgnorePattern: `^_`,
+ },
+ ],
+ "@next/next/no-img-element": "off"
+ },
+ ignorePatterns: [
+ `**/node_modules/**`,
+ `**/dist/**`,
+ `tsup.config.ts`,
+ `vitest.config.ts`,
+ `.eslintrc.js`,
+ `**/*.css`,
+ ],
+};
diff --git a/examples/yjs/.gitignore b/examples/yjs/.gitignore
new file mode 100644
index 0000000000..7a06c3b2bd
--- /dev/null
+++ b/examples/yjs/.gitignore
@@ -0,0 +1,10 @@
+dist
+.env.local
+
+# Turborepo
+.turbo
+
+# next.js
+/.next/
+/out/
+next-env.d.ts
diff --git a/examples/yjs/.prettierrc b/examples/yjs/.prettierrc
new file mode 100644
index 0000000000..eaff0359ca
--- /dev/null
+++ b/examples/yjs/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "trailingComma": "es5",
+ "semi": false,
+ "tabWidth": 2
+}
diff --git a/examples/yjs/Dockerfile b/examples/yjs/Dockerfile
new file mode 100644
index 0000000000..a1f6b8a52e
--- /dev/null
+++ b/examples/yjs/Dockerfile
@@ -0,0 +1,31 @@
+FROM node:lts-alpine AS base
+
+# Stage 1: Install dependencies
+FROM base AS deps
+WORKDIR /app
+
+RUN npm install -g pnpm
+
+COPY pnpm-*.yaml ./
+COPY package.json ./
+COPY tsconfig.build.json ./
+COPY packages/typescript-client packages/typescript-client/
+COPY packages/react-hooks packages/react-hooks/
+COPY examples/yjs-provider/ examples/yjs-provider/
+
+# Install dependencies
+RUN pnpm install --frozen-lockfile
+RUN pnpm run -r build
+
+
+# Need to make production image more clean
+FROM node:lts-alpine AS prod
+WORKDIR /app
+
+ENV NODE_ENV=production
+COPY --from=deps /app/ ./
+
+WORKDIR /app/examples/yjs-provider/
+
+EXPOSE 3000
+CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/examples/yjs/README.md b/examples/yjs/README.md
new file mode 100644
index 0000000000..4c943b7ac2
--- /dev/null
+++ b/examples/yjs/README.md
@@ -0,0 +1,28 @@
+# Yjs Electric provider
+
+This example showcases a multiplayer [Codemirror](https://codemirror.net/) editor with [YJS](https://github.com/yjs/yjs) and [ElectricSQL](https://electric-sql.com/). All data is synchronized through [Postgres](https://www.postgresql.org/), eliminating the need for additional real-time infrastructure.
+
+Y-Electric is a [YJS connection provider](https://docs.yjs.dev/ecosystem/connection-provider) that comes with offline support, integrates with [database providers](https://docs.yjs.dev/ecosystem/database-provider) and also handles [Presence/Awareness](https://docs.yjs.dev/api/about-awareness) data. It works with the entire YJS ecosystem and with you existing apps too!
+
+> We're releasing The Y-Electric backend as a package soon!
+
+## How to run
+
+Make sure you've installed all dependencies for the monorepo and built the packages (from the monorepo root directory):
+
+```shell
+pnpm install
+pnpm run -r build
+```
+
+Start the docker containers (in this directory):
+
+```shell
+pnpm backend:up
+```
+
+Start the dev server:
+
+```shell
+pnpm dev
+```
\ No newline at end of file
diff --git a/examples/yjs/app/api/operation/route.ts b/examples/yjs/app/api/operation/route.ts
new file mode 100644
index 0000000000..3d562538e2
--- /dev/null
+++ b/examples/yjs/app/api/operation/route.ts
@@ -0,0 +1,73 @@
+import { Pool } from "pg"
+import { NextResponse } from "next/server"
+import { neon } from "@neondatabase/serverless"
+
+// hybrid implementation for connection pool and serverless with neon
+
+const connectionString =
+ process.env.NEON_DATABASE_URL ||
+ process.env.DATABASE_URL ||
+ `postgresql://postgres:password@localhost:54321/electric`
+
+const sql = process.env.NEON_DATABASE_URL ? neon(connectionString) : undefined
+
+const pool = !process.env.NEON_DATABASE_URL
+ ? new Pool({ connectionString })
+ : undefined
+
+export async function POST(request: Request) {
+ try {
+ const { room, op, clientId } = await getRequestParams(request)
+ if (!clientId) {
+ await saveOperation(room, op)
+ } else {
+ await saveAwarenessOperation(room, op, clientId)
+ }
+ return NextResponse.json({})
+ } catch (e) {
+ const resp = e instanceof Error ? e.message : e
+ return NextResponse.json(resp, { status: 400 })
+ }
+}
+
+async function saveOperation(room: string, op: string) {
+ const q = `INSERT INTO ydoc_operations (room, op) VALUES ($1, decode($2, 'base64'))`
+ const params = [room, op]
+ await runQuery(q, params)
+}
+
+async function saveAwarenessOperation(
+ room: string,
+ op: string,
+ clientId: string
+) {
+ const q = `INSERT INTO ydoc_awareness (room, clientId, op) VALUES ($1, $2, decode($3, 'base64'))
+ ON CONFLICT (clientId, room)
+ DO UPDATE SET op = decode($3, 'base64'), updated = now()`
+ const params = [room, clientId, op]
+ await runQuery(q, params)
+}
+
+async function getRequestParams(
+ request: Request
+): Promise<{ room: string; op: string; clientId?: string }> {
+ const { room, op, clientId } = await request.json()
+ if (!room) {
+ throw new Error(`'room' is required`)
+ }
+ if (!op) {
+ throw new Error(`'op' is required`)
+ }
+
+ return { room, op, clientId }
+}
+
+async function runQuery(q: string, params: string[]) {
+ if (pool) {
+ await pool.query(q, params)
+ } else if (sql) {
+ await sql(q, params)
+ } else {
+ throw new Error(`No database driver provided`)
+ }
+}
diff --git a/examples/yjs/app/electric-editor.tsx b/examples/yjs/app/electric-editor.tsx
new file mode 100644
index 0000000000..f280e1a8c8
--- /dev/null
+++ b/examples/yjs/app/electric-editor.tsx
@@ -0,0 +1,115 @@
+"use client"
+
+import { useEffect, useRef, useState } from "react"
+
+import * as Y from "yjs"
+import { yCollab, yUndoManagerKeymap } from "y-codemirror.next"
+import { ElectricProvider } from "./y-electric"
+import { Awareness } from "y-protocols/awareness"
+
+import { EditorState } from "@codemirror/state"
+import { EditorView, basicSetup } from "codemirror"
+import { keymap } from "@codemirror/view"
+import { javascript } from "@codemirror/lang-javascript"
+
+import * as random from "lib0/random"
+import { IndexeddbPersistence } from "y-indexeddb"
+
+const room = `electric-demo`
+
+const usercolors = [
+ { color: `#30bced`, light: `#30bced33` },
+ { color: `#6eeb83`, light: `#6eeb8333` },
+ { color: `#ffbc42`, light: `#ffbc4233` },
+ { color: `#ecd444`, light: `#ecd44433` },
+ { color: `#ee6352`, light: `#ee635233` },
+ { color: `#9ac2c9`, light: `#9ac2c933` },
+]
+
+const userColor = usercolors[random.uint32() % usercolors.length]
+const ydoc = new Y.Doc()
+
+const isServer = typeof window === `undefined`
+
+const awareness = !isServer ? new Awareness(ydoc) : undefined
+awareness?.setLocalStateField(`user`, {
+ name: userColor.color,
+ color: userColor.color,
+ colorLight: userColor.light,
+})
+
+const network = !isServer
+ ? new ElectricProvider(
+ new URL(`/shape-proxy`, window?.location.origin).href,
+ room,
+ ydoc,
+ {
+ connect: true,
+ awareness,
+ persistence: new IndexeddbPersistence(room, ydoc),
+ }
+ )
+ : undefined
+
+export default function ElectricEditor() {
+ const editor = useRef(null)
+
+ const [connectivityStatus, setConnectivityStatus] = useState<
+ `connected` | `disconnected`
+ >(`connected`)
+
+ const toggle = () => {
+ if (!network) {
+ return
+ }
+ const toggleStatus =
+ connectivityStatus === `connected` ? `disconnected` : `connected`
+ setConnectivityStatus(toggleStatus)
+ toggleStatus === `connected` ? network.connect() : network.disconnect()
+ }
+
+ useEffect(() => {
+ if (typeof window === `undefined`) {
+ return
+ }
+
+ const ytext = ydoc.getText(room)
+
+ const state = EditorState.create({
+ doc: ytext.toString(),
+ extensions: [
+ keymap.of([...yUndoManagerKeymap]),
+ basicSetup,
+ javascript(),
+ EditorView.lineWrapping,
+ yCollab(ytext, awareness),
+ ],
+ })
+
+ const view = new EditorView({ state, parent: editor.current ?? undefined })
+
+ return () => view.destroy()
+ })
+
+ return (
+
+
+
+ This is a demo of Yjs using
+ {` `}
+ {` `}
+ Electric for
+ syncing.
+
+
+ The content of this editor is shared with every client that visits this
+ domain.
+
+
+
+ )
+}
diff --git a/examples/yjs/app/layout.tsx b/examples/yjs/app/layout.tsx
new file mode 100644
index 0000000000..ddc3e573cc
--- /dev/null
+++ b/examples/yjs/app/layout.tsx
@@ -0,0 +1,16 @@
+export const metadata = {
+ title: `Yjs <> Electric`,
+ description: `Yjs synching with Electric`,
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/examples/yjs/app/page.tsx b/examples/yjs/app/page.tsx
new file mode 100644
index 0000000000..b31810d8f9
--- /dev/null
+++ b/examples/yjs/app/page.tsx
@@ -0,0 +1,8 @@
+"use server"
+
+import React from "react"
+import ElectricEditor from "./electric-editor"
+
+const Page = async () =>
+
+export default Page
diff --git a/examples/yjs/app/shape-proxy/[...table]/route.ts b/examples/yjs/app/shape-proxy/[...table]/route.ts
new file mode 100644
index 0000000000..d389389091
--- /dev/null
+++ b/examples/yjs/app/shape-proxy/[...table]/route.ts
@@ -0,0 +1,44 @@
+export async function GET(request: Request) {
+ const url = new URL(request.url)
+ const originUrl = new URL(
+ process.env.ELECTRIC_URL
+ ? `${process.env.ELECTRIC_URL}/v1/shape/`
+ : `http://localhost:3000/v1/shape/`
+ )
+
+ url.searchParams.forEach((value, key) => {
+ originUrl.searchParams.set(key, value)
+ })
+
+ if (process.env.DATABASE_ID) {
+ originUrl.searchParams.set(`database_id`, process.env.DATABASE_ID)
+ }
+
+ const headers = new Headers()
+ if (process.env.ELECTRIC_TOKEN) {
+ originUrl.searchParams.set(`token`, process.env.ELECTRIC_TOKEN)
+ }
+
+ const newRequest = new Request(originUrl.toString(), {
+ method: `GET`,
+ headers,
+ })
+
+ // When proxying long-polling requests, content-encoding & content-length are added
+ // erroneously (saying the body is gzipped when it's not) so we'll just remove
+ // them to avoid content decoding errors in the browser.
+ //
+ // Similar-ish problem to https://github.com/wintercg/fetch/issues/23
+ let resp = await fetch(newRequest)
+ if (resp.headers.get(`content-encoding`)) {
+ const headers = new Headers(resp.headers)
+ headers.delete(`content-encoding`)
+ headers.delete(`content-length`)
+ resp = new Response(resp.body, {
+ status: resp.status,
+ statusText: resp.statusText,
+ headers,
+ })
+ }
+ return resp
+}
diff --git a/examples/yjs/app/utils.ts b/examples/yjs/app/utils.ts
new file mode 100644
index 0000000000..780dd50921
--- /dev/null
+++ b/examples/yjs/app/utils.ts
@@ -0,0 +1,42 @@
+import { toBase64 } from "lib0/buffer"
+import * as decoding from "lib0/decoding"
+
+const hexStringToUint8Array = (hexString: string) => {
+ const cleanHexString = hexString.startsWith(`\\x`)
+ ? hexString.slice(2)
+ : hexString
+ return new Uint8Array(
+ cleanHexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))
+ )
+}
+
+export const parseToUint8Array = {
+ bytea: hexStringToUint8Array,
+}
+
+export const parseToBase64 = {
+ bytea: (hexString: string) => {
+ const uint8Array = hexStringToUint8Array(hexString)
+ return toBase64(uint8Array)
+ },
+}
+
+export const parseToDecoder = {
+ bytea: (hexString: string) => {
+ const uint8Array = hexStringToUint8Array(hexString)
+ return decoding.createDecoder(uint8Array)
+ },
+}
+
+export const parseToDecoderLazy = {
+ bytea: (hexString: string) => () => {
+ const uint8Array = hexStringToUint8Array(hexString)
+ return decoding.createDecoder(uint8Array)
+ },
+}
+
+export const paserToTimestamptz = {
+ timestamptz: (timestamp: string) => {
+ return new Date(timestamp)
+ },
+}
diff --git a/examples/yjs/app/y-electric.ts b/examples/yjs/app/y-electric.ts
new file mode 100644
index 0000000000..50ec608ad4
--- /dev/null
+++ b/examples/yjs/app/y-electric.ts
@@ -0,0 +1,409 @@
+import { toBase64 } from "lib0/buffer"
+import * as encoding from "lib0/encoding"
+import * as decoding from "lib0/decoding"
+import * as syncProtocol from "y-protocols/sync"
+import * as awarenessProtocol from "y-protocols/awareness"
+import { ObservableV2 } from "lib0/observable"
+import * as env from "lib0/environment"
+import * as Y from "yjs"
+import {
+ isChangeMessage,
+ isControlMessage,
+ Message,
+ Offset,
+ ShapeStream,
+} from "@electric-sql/client"
+import { parseToDecoder, parseToDecoderLazy, paserToTimestamptz } from "./utils"
+import { IndexeddbPersistence } from "y-indexeddb"
+
+type OperationMessage = {
+ op: decoding.Decoder
+}
+
+type AwarenessMessage = {
+ op: () => decoding.Decoder
+ clientId: string
+ room: string
+ updated: Date
+}
+
+type ObservableProvider = {
+ sync: (state: boolean) => void
+ synced: (state: boolean) => void
+ status: (status: {
+ status: `connecting` | `connected` | `disconnected`
+ }) => void
+ // eslint-disable-next-line quotes
+ "connection-close": () => void
+}
+
+// from yjs docs, need to check if is configurable
+const awarenessPingPeriod = 30000 //ms
+
+const messageSync = 0
+
+export class ElectricProvider extends ObservableV2 {
+ private baseUrl: string
+ private roomName: string
+ private doc: Y.Doc
+ public awareness?: awarenessProtocol.Awareness
+
+ private operationsStream?: ShapeStream
+ private awarenessStream?: ShapeStream
+
+ private shouldConnect: boolean
+ private connected: boolean
+ private _synced: boolean
+
+ private modifiedWhileOffline: boolean
+ private lastSyncedStateVector?: Uint8Array
+
+ private updateHandler: (update: Uint8Array, origin: unknown) => void
+ private awarenessUpdateHandler?: (
+ changed: { added: number[]; updated: number[]; removed: number[] },
+ origin: string
+ ) => void
+ private disconnectShapeHandler?: () => void
+ private exitHandler?: () => void
+
+ private persistence?: IndexeddbPersistence
+ private loaded: boolean
+ private resume: {
+ operations?: { offset: Offset; handle: string }
+ awareness?: { offset: Offset; handle: string }
+ } = {}
+
+ private awarenessState: Record | null = null
+
+ constructor(
+ serverUrl: string,
+ roomName: string,
+ doc: Y.Doc,
+ options: {
+ awareness?: awarenessProtocol.Awareness
+ connect?: boolean
+ persistence?: IndexeddbPersistence
+ } // TODO: make it generic, we can load it outside the provider
+ ) {
+ super()
+
+ this.baseUrl = serverUrl + `/v1/shape`
+ this.roomName = roomName
+
+ this.doc = doc
+ this.awareness = options.awareness
+
+ this.connected = false
+ this._synced = false
+ this.shouldConnect = options.connect ?? false
+
+ this.modifiedWhileOffline = false
+
+ this.persistence = options.persistence
+ this.loaded = this.persistence === undefined
+
+ this.updateHandler = (update: Uint8Array, origin: unknown) => {
+ if (origin !== this) {
+ this.sendOperation(update)
+ }
+ }
+ this.doc.on(`update`, this.updateHandler)
+
+ if (this.awareness) {
+ this.awarenessUpdateHandler = ({ added, updated, removed }, origin) => {
+ if (origin === `local`) {
+ const changedClients = added.concat(updated).concat(removed)
+ this.sendAwareness(changedClients)
+ }
+ }
+ this.awareness.on(`update`, this.awarenessUpdateHandler)
+ }
+
+ if (env.isNode && typeof process !== `undefined`) {
+ this.exitHandler = () => {
+ process.on(`exit`, () => this.destroy())
+ }
+ }
+
+ if (!this.loaded) {
+ this.loadSyncState()
+ } else if (options.connect) {
+ this.connect()
+ }
+ }
+
+ get synced() {
+ return this._synced
+ }
+
+ set synced(state) {
+ if (this._synced !== state) {
+ this._synced = state
+ this.emit(`synced`, [state])
+ this.emit(`sync`, [state])
+ }
+ }
+
+ async loadSyncState() {
+ if (!this.persistence) {
+ throw Error(`Can't load sync state without persistence backend`)
+ }
+ const operationsHandle = await this.persistence.get(`operations_handle`)
+ const operationsOffset = await this.persistence.get(`operations_offset`)
+
+ const awarenessHandle = await this.persistence.get(`awareness_handle`)
+ const awarenessOffset = await this.persistence.get(`awareness_offset`)
+
+ const lastSyncedStateVector = await this.persistence.get(
+ `last_synced_state_vector`
+ )
+
+ this.lastSyncedStateVector = lastSyncedStateVector
+ this.modifiedWhileOffline = this.lastSyncedStateVector !== undefined
+
+ this.resume = {
+ operations: {
+ handle: operationsHandle,
+ offset: operationsOffset,
+ },
+
+ // TODO: we might miss some awareness updates since last pings
+ awareness: {
+ handle: awarenessHandle,
+ offset: awarenessOffset,
+ },
+ }
+
+ this.loaded = true
+ if (this.shouldConnect) {
+ this.connect()
+ }
+ }
+
+ destroy() {
+ this.disconnect()
+ this.doc.off(`update`, this.updateHandler)
+ this.awareness?.off(`update`, this.awarenessUpdateHandler!)
+ if (env.isNode && typeof process !== `undefined`) {
+ process.off(`exit`, this.exitHandler!)
+ }
+ super.destroy()
+ }
+
+ disconnect() {
+ this.shouldConnect = false
+
+ if (this.awareness && this.connected) {
+ this.awarenessState = this.awareness.getLocalState()
+
+ awarenessProtocol.removeAwarenessStates(
+ this.awareness,
+ Array.from(this.awareness.getStates().keys()).filter(
+ (client) => client !== this.doc.clientID
+ ),
+ this
+ )
+
+ // try to notify other clients that we are disconnected
+ awarenessProtocol.removeAwarenessStates(
+ this.awareness,
+ [this.doc.clientID],
+ `local`
+ )
+ }
+
+ if (this.disconnectShapeHandler) {
+ this.disconnectShapeHandler()
+ }
+ }
+
+ connect() {
+ this.shouldConnect = true
+ if (!this.connected && !this.operationsStream) {
+ this.setupShapeStream()
+ }
+
+ if (this.awareness && this.awarenessState !== null) {
+ this.awareness.setLocalState(this.awarenessState)
+ this.awarenessState = null
+ }
+ }
+
+ private sendOperation(update: Uint8Array) {
+ if (!this.connected) {
+ this.modifiedWhileOffline = true
+ return Promise.resolve()
+ }
+
+ const encoder = encoding.createEncoder()
+ syncProtocol.writeUpdate(encoder, update)
+ const op = toBase64(encoding.toUint8Array(encoder))
+ const room = this.roomName
+
+ return fetch(`/api/operation`, {
+ method: `POST`,
+ body: JSON.stringify({ room, op }),
+ })
+ }
+
+ private sendAwareness(changedClients: number[]) {
+ const encoder = encoding.createEncoder()
+ encoding.writeVarUint8Array(
+ encoder,
+ awarenessProtocol.encodeAwarenessUpdate(this.awareness!, changedClients)
+ )
+ const op = toBase64(encoding.toUint8Array(encoder))
+
+ if (this.connected) {
+ const room = this.roomName
+ const clientId = `${this.doc.clientID}`
+
+ return fetch(`/api/operation`, {
+ method: `POST`,
+ body: JSON.stringify({ clientId, room, op }),
+ })
+ }
+ }
+
+ private setupShapeStream() {
+ if (this.shouldConnect && !this.operationsStream) {
+ this.connected = false
+ this.synced = false
+
+ console.log(`Setting up shape stream ${JSON.stringify(this.resume)}`)
+
+ this.operationsStream = new ShapeStream({
+ url: this.baseUrl,
+ params: {
+ table: `ydoc_operations`,
+ where: `room = '${this.roomName}'`,
+ },
+ parser: parseToDecoder,
+ subscribe: true,
+ ...this.resume.operations,
+ })
+
+ this.awarenessStream = new ShapeStream({
+ url: this.baseUrl,
+ params: {
+ where: `room = '${this.roomName}'`,
+ table: `ydoc_awareness`,
+ },
+ parser: { ...parseToDecoderLazy, ...paserToTimestamptz },
+ ...this.resume.awareness,
+ })
+
+ const updateShapeState = (
+ name: `operations` | `awareness`,
+ offset: Offset,
+ handle: string
+ ) => {
+ this.resume[name] = { offset, handle }
+ this.persistence?.set(`${name}_offset`, offset)
+ this.persistence?.set(`${name}_handle`, handle)
+ }
+
+ const handleSyncMessage = (messages: Message[]) => {
+ messages.forEach((message) => {
+ if (isChangeMessage(message) && message.value.op) {
+ const decoder = message.value.op
+ const encoder = encoding.createEncoder()
+ encoding.writeVarUint(encoder, messageSync)
+ syncProtocol.readSyncMessage(decoder, encoder, this.doc, this)
+ } else if (
+ isControlMessage(message) &&
+ message.headers.control === `up-to-date`
+ ) {
+ this.synced = true
+
+ updateShapeState(
+ `operations`,
+ this.operationsStream!.lastOffset,
+ this.operationsStream!.shapeHandle
+ )
+ }
+ })
+ }
+
+ const unsubscribeSyncHandler =
+ this.operationsStream.subscribe(handleSyncMessage)
+
+ const handleAwarenessMessage = (
+ messages: Message[]
+ ) => {
+ const minTime = new Date(Date.now() - awarenessPingPeriod)
+ messages.forEach((message) => {
+ if (isChangeMessage(message) && message.value.op) {
+ if (message.value.updated < minTime) {
+ return
+ }
+
+ const decoder = message.value.op()
+ awarenessProtocol.applyAwarenessUpdate(
+ this.awareness!,
+ decoding.readVarUint8Array(decoder),
+ this
+ )
+ }
+ })
+
+ updateShapeState(
+ `awareness`,
+ this.awarenessStream!.lastOffset,
+ this.awarenessStream!.shapeHandle
+ )
+ }
+
+ const unsubscribeAwarenessHandler = this.awarenessStream.subscribe(
+ handleAwarenessMessage
+ )
+
+ this.disconnectShapeHandler = () => {
+ this.operationsStream = undefined
+ this.awarenessStream = undefined
+
+ if (this.connected) {
+ this.lastSyncedStateVector = Y.encodeStateVector(this.doc)
+ this.persistence?.set(
+ `last_synced_state_vector`,
+ this.lastSyncedStateVector
+ )
+
+ this.connected = false
+ this.synced = false
+ this.emit(`status`, [{ status: `disconnected` }])
+ }
+
+ unsubscribeSyncHandler()
+ unsubscribeAwarenessHandler()
+ this.disconnectShapeHandler = undefined
+ this.emit(`connection-close`, [])
+ }
+
+ const pushLocalChangesUnsubscribe = this.operationsStream!.subscribe(
+ () => {
+ this.connected = true
+
+ if (this.modifiedWhileOffline) {
+ const pendingUpdates = Y.encodeStateAsUpdate(
+ this.doc,
+ this.lastSyncedStateVector
+ )
+ const encoderState = encoding.createEncoder()
+ syncProtocol.writeUpdate(encoderState, pendingUpdates)
+
+ this.sendOperation(pendingUpdates).then(() => {
+ this.lastSyncedStateVector = undefined
+ this.modifiedWhileOffline = false
+ this.persistence?.del(`last_synced_state_vector`)
+ this.emit(`status`, [{ status: `connected` }])
+ })
+ }
+ pushLocalChangesUnsubscribe()
+ }
+ )
+
+ this.emit(`status`, [{ status: `connecting` }])
+ }
+ }
+}
diff --git a/examples/yjs/db/migrations/01-create_yjs_tables.sql b/examples/yjs/db/migrations/01-create_yjs_tables.sql
new file mode 100644
index 0000000000..1e9d43e165
--- /dev/null
+++ b/examples/yjs/db/migrations/01-create_yjs_tables.sql
@@ -0,0 +1,27 @@
+CREATE TABLE ydoc_operations(
+ id SERIAL PRIMARY KEY,
+ room TEXT,
+ op BYTEA NOT NULL
+);
+
+CREATE TABLE ydoc_awareness(
+ clientId TEXT,
+ room TEXT,
+ op BYTEA NOT NULL,
+ updated TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (clientId, room)
+);
+
+CREATE OR REPLACE FUNCTION delete_old_rows()
+RETURNS TRIGGER AS $$
+BEGIN
+ DELETE FROM ydoc_awareness
+ WHERE updated < NOW() - INTERVAL '2 minutes';
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER delete_old_rows_trigger
+AFTER INSERT OR UPDATE ON ydoc_awareness
+FOR EACH STATEMENT
+EXECUTE FUNCTION delete_old_rows();
diff --git a/examples/yjs/index.html b/examples/yjs/index.html
new file mode 100644
index 0000000000..1c8f62ecc9
--- /dev/null
+++ b/examples/yjs/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+ Yjs Electric provider
+
+
+
+
\ No newline at end of file
diff --git a/examples/yjs/next.config.mjs b/examples/yjs/next.config.mjs
new file mode 100644
index 0000000000..e559ceb6ca
--- /dev/null
+++ b/examples/yjs/next.config.mjs
@@ -0,0 +1,7 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {
+}
+
+export default nextConfig
\ No newline at end of file
diff --git a/examples/yjs/package.json b/examples/yjs/package.json
new file mode 100644
index 0000000000..3475e8b5d2
--- /dev/null
+++ b/examples/yjs/package.json
@@ -0,0 +1,50 @@
+{
+ "name": "@electric-examples/yjs-provider",
+ "private": true,
+ "version": "0.0.1",
+ "author": "ElectricSQL",
+ "license": "Apache-2.0",
+ "type": "module",
+ "scripts": {
+ "backend:up": "PROJECT_NAME=yjs-provider pnpm -C ../../ run example-backend:up && pnpm db:migrate",
+ "backend:down": "PROJECT_NAME=yjs-provider pnpm -C ../../ run example-backend:down",
+ "db:migrate": "dotenv -e ../../.env.dev -- pnpm exec pg-migrations apply --directory ./db/migrations",
+ "dev": "next dev --turbo -p 5173",
+ "build": "next build",
+ "start": "next start",
+ "lint": "eslint . --ext js,ts,tsx --report-unused-disable-directives --max-warnings 0",
+ "stylecheck": "eslint . --quiet",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@codemirror/lang-javascript": "^6.2.2",
+ "@codemirror/state": "^6.4.1",
+ "@codemirror/view": "^6.32.0",
+ "@electric-sql/client": "workspace:*",
+ "@electric-sql/react": "workspace:*",
+ "@neondatabase/serverless": "^0.10.4",
+ "codemirror": "^6.0.1",
+ "lib0": "^0.2.96",
+ "next": "^14.2.9",
+ "pg": "^8.13.1",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "sst": "^3.3.35",
+ "y-codemirror.next": "0.3.5",
+ "y-indexeddb": "^9.0.12",
+ "y-protocols": "1.0.6",
+ "yjs": "^13.6.18"
+ },
+ "devDependencies": {
+ "@databases/pg-migrations": "^5.0.3",
+ "@next/eslint-plugin-next": "^14.2.5",
+ "@types/pg": "^8.11.6",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "dotenv": "^16.4.5",
+ "eslint": "^8.57.0",
+ "typescript": "^5.5.3",
+ "vite": "^5.3.4"
+ }
+}
diff --git a/examples/yjs/public/favicon.ico b/examples/yjs/public/favicon.ico
new file mode 100644
index 0000000000..e55095b983
Binary files /dev/null and b/examples/yjs/public/favicon.ico differ
diff --git a/examples/yjs/public/robots.txt b/examples/yjs/public/robots.txt
new file mode 100644
index 0000000000..e9e57dc4d4
--- /dev/null
+++ b/examples/yjs/public/robots.txt
@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:
diff --git a/examples/yjs/sst-env.d.ts b/examples/yjs/sst-env.d.ts
new file mode 100644
index 0000000000..95ab3fbb9b
--- /dev/null
+++ b/examples/yjs/sst-env.d.ts
@@ -0,0 +1,19 @@
+/* This file is auto-generated by SST. Do not edit. */
+/* tslint:disable */
+/* eslint-disable */
+/* deno-fmt-ignore-file */
+import "sst"
+export {}
+declare module "sst" {
+ export interface Resource {
+ "yjs-service-production": {
+ "service": string
+ "type": "sst.aws.Service"
+ "url": string
+ }
+ "yjs-vpc-production": {
+ "bastion": string
+ "type": "sst.aws.Vpc"
+ }
+ }
+}
diff --git a/examples/yjs/sst.config.ts b/examples/yjs/sst.config.ts
new file mode 100644
index 0000000000..f4d9bfc3bb
--- /dev/null
+++ b/examples/yjs/sst.config.ts
@@ -0,0 +1,172 @@
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
+
+import { execSync } from "child_process"
+
+const isProduction = () => $app.stage.toLocaleLowerCase() === `production`
+
+export default $config({
+ app(input) {
+ return {
+ name: `yjs`,
+ removal:
+ input?.stage.toLocaleLowerCase() === `production` ? `retain` : `remove`,
+ home: `aws`,
+ providers: {
+ cloudflare: `5.42.0`,
+ aws: {
+ version: `6.57.0`,
+ },
+ neon: `0.6.3`,
+ },
+ }
+ },
+ async run() {
+ try {
+ const project = neon.getProjectOutput({
+ id: process.env.NEON_PROJECT_ID!,
+ })
+ const base = {
+ projectId: project.id,
+ branchId: project.defaultBranchId,
+ }
+
+ const db = new neon.Database(`yjs-db`, {
+ ...base,
+ ownerName: `neondb_owner`,
+ name: isProduction() ? `yjs` : `yjs-${$app.stage}`,
+ })
+
+ const databaseUri = getNeonDbUri(project, db, false)
+ const pooledUri = getNeonDbUri(project, db, true)
+ databaseUri.apply(applyMigrations)
+
+ const electricInfo = databaseUri.apply((uri) =>
+ addDatabaseToElectric(uri)
+ )
+
+ // const serverless = deployServerlessApp(electricInfo, pooledUri)
+ const website = deployAppServer(electricInfo, databaseUri)
+
+ return {
+ // serverless_url: serverless.url,
+ server_url: website.url,
+ databaseUri,
+ databasePooledUri: pooledUri,
+ }
+ } catch (e) {
+ console.error(e)
+ }
+ },
+})
+
+function applyMigrations(uri: string) {
+ execSync(`pnpm exec pg-migrations apply --directory ./db/migrations`, {
+ env: {
+ ...process.env,
+ DATABASE_URL: uri,
+ },
+ })
+}
+
+function deployAppServer(
+ { id, token }: $util.Output<{ id: string; token: string }>,
+ uri: $util.Output
+) {
+ const vpc = new sst.aws.Vpc(`yjs-vpc-${$app.stage}`, { bastion: true })
+ const cluster = new sst.aws.Cluster(`yjs-cluster-${$app.stage}`, { vpc })
+ const service = cluster.addService(`yjs-service-${$app.stage}`, {
+ loadBalancer: {
+ ports: [{ listen: "443/https", forward: "3000/http" }],
+ domain: {
+ name: `yjs${isProduction() ? `` : `-${$app.stage}`}.examples.electric-sql.com`,
+ dns: sst.cloudflare.dns(),
+ },
+ },
+ environment: {
+ ELECTRIC_URL: process.env.ELECTRIC_API!,
+ DATABASE_URL: uri,
+ DATABASE_ID: id,
+ ELECTRIC_TOKEN: token,
+ },
+ image: {
+ context: "../..",
+ dockerfile: "Dockerfile",
+ },
+ dev: {
+ command: "npm run dev",
+ },
+ })
+
+ return service
+}
+
+function deployServerlessApp(
+ electricInfo: $util.Output<{ id: string; token: string }>,
+ uri: $util.Output
+) {
+ return new sst.aws.Nextjs(`yjs`, {
+ environment: {
+ ELECTRIC_URL: process.env.ELECTRIC_API!,
+ ELECTRIC_TOKEN: electricInfo.token,
+ DATABASE_ID: electricInfo.id,
+ NEON_DATABASE_URL: uri,
+ },
+ domain: {
+ name: `yjs${isProduction() ? `` : `-stage-${$app.stage}`}.examples.electric-sql.com`,
+ dns: sst.cloudflare.dns(),
+ },
+ })
+}
+
+function getNeonDbUri(
+ project: $util.Output,
+ db: neon.Database,
+ pooled: boolean
+) {
+ const passwordOutput = neon.getBranchRolePasswordOutput({
+ projectId: project.id,
+ branchId: project.defaultBranchId,
+ roleName: db.ownerName,
+ })
+
+ const endpoint = neon.getBranchEndpointsOutput({
+ projectId: project.id,
+ branchId: project.defaultBranchId,
+ })
+
+ const databaseHost = pooled
+ ? endpoint.endpoints?.apply((endpoints) =>
+ endpoints![0].host.replace(
+ endpoints![0].id,
+ endpoints![0].id + "-pooler"
+ )
+ )
+ : project.databaseHost
+
+ const url = $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${db.name}?sslmode=require`
+ return url
+}
+
+async function addDatabaseToElectric(
+ uri: string
+): Promise<{ id: string; token: string }> {
+ const adminApi = process.env.ELECTRIC_ADMIN_API
+
+ const result = await fetch(`${adminApi}/v1/databases`, {
+ method: `PUT`,
+ headers: { "Content-Type": `application/json` },
+ body: JSON.stringify({
+ database_url: uri,
+ region: `us-east-1`,
+ }),
+ })
+
+ if (!result.ok) {
+ throw new Error(
+ `Could not add database to Electric (${result.status}): ${await result.text()}`
+ )
+ }
+
+ return await result.json()
+}
diff --git a/examples/yjs/tsconfig.json b/examples/yjs/tsconfig.json
new file mode 100644
index 0000000000..01460806e7
--- /dev/null
+++ b/examples/yjs/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2015",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules", "sst.config.ts"]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1d0f179b37..31fcf9ff79 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -497,7 +497,7 @@ importers:
version: 0.3.4
tsup:
specifier: ^8.0.1
- version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
+ version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
tsx:
specifier: ^4.19.1
version: 4.19.2
@@ -815,6 +815,91 @@ importers:
specifier: ^0.21.0
version: 0.21.0(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0)
+ examples/yjs:
+ dependencies:
+ '@codemirror/lang-javascript':
+ specifier: ^6.2.2
+ version: 6.2.2
+ '@codemirror/state':
+ specifier: ^6.4.1
+ version: 6.5.0
+ '@codemirror/view':
+ specifier: ^6.32.0
+ version: 6.35.2
+ '@electric-sql/client':
+ specifier: workspace:*
+ version: link:../../packages/typescript-client
+ '@electric-sql/react':
+ specifier: workspace:*
+ version: link:../../packages/react-hooks
+ '@neondatabase/serverless':
+ specifier: ^0.10.4
+ version: 0.10.4
+ codemirror:
+ specifier: ^6.0.1
+ version: 6.0.1(@lezer/common@1.2.3)
+ lib0:
+ specifier: ^0.2.96
+ version: 0.2.99
+ next:
+ specifier: ^14.2.9
+ version: 14.2.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ pg:
+ specifier: ^8.13.1
+ version: 8.13.1
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ sst:
+ specifier: ^3.3.35
+ version: 3.3.59
+ y-codemirror.next:
+ specifier: 0.3.5
+ version: 0.3.5(@codemirror/state@6.5.0)(@codemirror/view@6.35.2)(yjs@13.6.20)
+ y-indexeddb:
+ specifier: ^9.0.12
+ version: 9.0.12(yjs@13.6.20)
+ y-protocols:
+ specifier: 1.0.6
+ version: 1.0.6(yjs@13.6.20)
+ yjs:
+ specifier: ^13.6.18
+ version: 13.6.20
+ devDependencies:
+ '@databases/pg-migrations':
+ specifier: ^5.0.3
+ version: 5.0.3(typescript@5.6.3)
+ '@next/eslint-plugin-next':
+ specifier: ^14.2.5
+ version: 14.2.20
+ '@types/pg':
+ specifier: ^8.11.6
+ version: 8.11.10
+ '@types/react':
+ specifier: ^18.3.3
+ version: 18.3.12
+ '@types/react-dom':
+ specifier: ^18.3.0
+ version: 18.3.1
+ '@vitejs/plugin-react':
+ specifier: ^4.3.1
+ version: 4.3.3(vite@5.4.10(@types/node@20.17.6)(terser@5.36.0))
+ dotenv:
+ specifier: ^16.4.5
+ version: 16.4.5
+ eslint:
+ specifier: ^8.57.0
+ version: 8.57.1
+ typescript:
+ specifier: ^5.5.3
+ version: 5.6.3
+ vite:
+ specifier: ^5.3.4
+ version: 5.4.10(@types/node@20.17.6)(terser@5.36.0)
+
packages/elixir-client: {}
packages/experimental:
@@ -865,7 +950,7 @@ importers:
version: 0.3.4
tsup:
specifier: ^8.0.1
- version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
+ version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
typescript:
specifier: ^5.5.2
version: 5.6.3
@@ -941,7 +1026,7 @@ importers:
version: 0.3.4
tsup:
specifier: ^8.0.1
- version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
+ version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
typescript:
specifier: ^5.5.2
version: 5.6.3
@@ -1001,7 +1086,7 @@ importers:
version: 0.3.4
tsup:
specifier: ^8.0.1
- version: 8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
+ version: 8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0)
typescript:
specifier: ^5.5.2
version: 5.6.3
@@ -1727,6 +1812,35 @@ packages:
'@changesets/write@0.3.2':
resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==}
+ '@codemirror/autocomplete@6.18.3':
+ resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==}
+ peerDependencies:
+ '@codemirror/language': ^6.0.0
+ '@codemirror/state': ^6.0.0
+ '@codemirror/view': ^6.0.0
+ '@lezer/common': ^1.0.0
+
+ '@codemirror/commands@6.7.1':
+ resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==}
+
+ '@codemirror/lang-javascript@6.2.2':
+ resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==}
+
+ '@codemirror/language@6.10.6':
+ resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==}
+
+ '@codemirror/lint@6.8.4':
+ resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==}
+
+ '@codemirror/search@6.5.8':
+ resolution: {integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==}
+
+ '@codemirror/state@6.5.0':
+ resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==}
+
+ '@codemirror/view@6.35.2':
+ resolution: {integrity: sha512-u04R04XFCYCNaHoNRr37WUUAfnxKPwPdqV+370NiO6i85qB1J/qCD/WbbMJsyJfRWhXIJXAe2BG/oTzAggqv4A==}
+
'@databases/connection-pool@1.1.0':
resolution: {integrity: sha512-/12/SNgl0V77mJTo5SX3yGPz4c9XGQwAlCfA0vlfs/0HcaErNpYXpmhj0StET07w6TmTJTnaUgX2EPcQK9ez5A==}
@@ -2646,18 +2760,39 @@ packages:
'@jspm/core@2.1.0':
resolution: {integrity: sha512-3sRl+pkyFY/kLmHl0cgHiFp2xEqErA8N3ECjMs7serSUBmoJ70lBa0PG5t0IM6WJgdZNyyI0R8YFfi5wM8+mzg==}
+ '@lezer/common@1.2.3':
+ resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
+
+ '@lezer/highlight@1.2.1':
+ resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
+
+ '@lezer/javascript@1.4.21':
+ resolution: {integrity: sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==}
+
+ '@lezer/lr@1.4.2':
+ resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
+
'@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
+ '@marijn/find-cluster-break@1.0.0':
+ resolution: {integrity: sha512-0YSzy7M9mBiK+h1m33rD8vZOfaO8leG6CY3+Q+1Lig86snkc8OAHQVAdndmnXMWJlVIH6S7fSZVVcjLcq6OH1A==}
+
'@mdx-js/mdx@2.3.0':
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
+ '@neondatabase/serverless@0.10.4':
+ resolution: {integrity: sha512-2nZuh3VUO9voBauuh+IGYRhGU/MskWHt1IuZvHcJw6GLjDgtqj/KViKo7SIrLdGLdot7vFbiRRw+BgEy3wT9HA==}
+
'@next/env@14.2.17':
resolution: {integrity: sha512-MCgO7VHxXo8sYR/0z+sk9fGyJJU636JyRmkjc7ZJY8Hurl8df35qG5hoAh5KMs75FLjhlEo9bb2LGe89Y/scDA==}
+ '@next/eslint-plugin-next@14.2.20':
+ resolution: {integrity: sha512-T0JRi706KLbvR1Uc46t56VtawbhR/igdBagzOrA7G+vv4rvjwnlu/Y4/Iq6X9TDVj5UZjyot4lUdkNd3V2kLhw==}
+
'@next/swc-darwin-arm64@14.2.17':
resolution: {integrity: sha512-WiOf5nElPknrhRMTipXYTJcUz7+8IAjOYw3vXzj3BYRcVY0hRHKWgTgQ5439EvzQyHEko77XK+yN9x9OJ0oOog==}
engines: {node: '>= 10'}
@@ -4110,6 +4245,9 @@ packages:
'@types/pg@8.11.10':
resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==}
+ '@types/pg@8.11.6':
+ resolution: {integrity: sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==}
+
'@types/prop-types@15.7.13':
resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==}
@@ -4829,6 +4967,9 @@ packages:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
+ codemirror@6.0.1:
+ resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -5784,6 +5925,11 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
+ glob@10.3.10:
+ resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==}
+ engines: {node: '>=16 || 14 >=14.17'}
+ hasBin: true
+
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
@@ -6188,6 +6334,13 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ isomorphic.js@0.2.5:
+ resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==}
+
+ jackspeak@2.3.6:
+ resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
+ engines: {node: '>=14'}
+
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
@@ -6327,6 +6480,11 @@ packages:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'}
+ lib0@0.2.99:
+ resolution: {integrity: sha512-vwztYuUf1uf/1zQxfzRfO5yzfNKhTtgOByCruuiQQxWQXnPb8Itaube5ylofcV0oM0aKal9Mv+S1s1Ky0UYP1w==}
+ engines: {node: '>=16'}
+ hasBin: true
+
lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -8108,31 +8266,68 @@ packages:
resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ sst-darwin-arm64@3.3.59:
+ resolution: {integrity: sha512-v8E3pg3JK7LDFOD0ot5N+HMNOA6MDHczx1M1SsJnMcNZPLTFH/Dn1B3roUaTP55+8jLKtqSJhnp9kLtTPHR1GQ==}
+ cpu: [arm64]
+ os: [darwin]
+
sst-darwin-arm64@3.3.7:
resolution: {integrity: sha512-2CQh78YIdvrRpO8enZ/Jx51JsUSFtk564u9w4ldcu5SsMMDY1ocdw5p/XIwBy1eKeRtrXLizd35sYbtSfSy6sw==}
cpu: [arm64]
os: [darwin]
+ sst-darwin-x64@3.3.59:
+ resolution: {integrity: sha512-SOOvHPxdxXwcMEwfdDXFiaMqZTdy1UcTL74omKGAn3i0pdoDN+FK2KPa865gNd1dzyYg0P6QXcUu9hI92g9Cyg==}
+ cpu: [x64]
+ os: [darwin]
+
sst-darwin-x64@3.3.7:
resolution: {integrity: sha512-+hiDT3+am+CBO3xBy8yl3bmFeTjGXUT/+7V6NFOV2yxlRP3A8J65nEjWdzPTU/u7hRl+leE8EBu14j0grt/7/A==}
cpu: [x64]
os: [darwin]
+ sst-linux-arm64@3.3.59:
+ resolution: {integrity: sha512-C7RjmuO0RR6xhmz0EZDcL6FhPCInwj5odehJZkbrvW0SObdoBLZLeDF0Or/7DKKh9VuSes/dpN1UvuUtMVE5Cg==}
+ cpu: [arm64]
+ os: [linux]
+
sst-linux-arm64@3.3.7:
resolution: {integrity: sha512-dYolpXAjq0S8QjL8sTKzcRpPNgZDeMcJ9PHnt/8GpdqxNxEpGlNF9gMl2cB7mleJyJYBNMPvi4YEeCGtcazmeQ==}
cpu: [arm64]
os: [linux]
+ sst-linux-x64@3.3.59:
+ resolution: {integrity: sha512-7XZaYHl7Uun9q6koMEIICvw/+BX18hqiVmBjaC2p6J6x5tw+z7kfpK3x+yD26HLHoFNhpIBLb+6Nqj7XDyCa/Q==}
+ cpu: [x64]
+ os: [linux]
+
sst-linux-x64@3.3.7:
resolution: {integrity: sha512-K2vPOZ5DS8mJmE4QtffgZN5Nem1MIBhoVozNtZ0NoufeKHbFz0Hyw9wbqxYSbs2MOoVNKvG8qwcX99ojVXTFKw==}
cpu: [x64]
os: [linux]
+ sst-linux-x86@3.3.59:
+ resolution: {integrity: sha512-GA8dZw6ty12ZwvpdVXkKBtdNwNuGF8vIcOKSzAQ6eC2UmywHlvQNgz2fiWMPVz94QLnNHCHAZlL6XAR7fz7n4Q==}
+ cpu: [x86]
+ os: [linux]
+
sst-linux-x86@3.3.7:
resolution: {integrity: sha512-4rXj54+UJd+HLmrhCHQ0k9AOkugHZhhh6sCUnkUNChJr5ei62pRscUQ7ge8/jywvfzHZGZw3eXXJWCCsjilXFA==}
cpu: [x86]
os: [linux]
+ sst@3.3.59:
+ resolution: {integrity: sha512-WzKYWMf41n/TMEVp54tEITvmZQ0iZHf2whevpqqS85dAGP6w74NbEkwPNAB5oU2RlA2sj8GgMF76NdctsYQ0JA==}
+ hasBin: true
+ peerDependencies:
+ hono: 4.x
+ valibot: 0.30.x
+ peerDependenciesMeta:
+ hono:
+ optional: true
+ valibot:
+ optional: true
+
sst@3.3.7:
resolution: {integrity: sha512-qIJPQnGeIHarWZoUvphwi6R1nu6Pccd3Q2Qy9ltBLs4Z47TkSdwBNeqCBhgAzWA0eLDwStTXliexyQCcNM6gDQ==}
hasBin: true
@@ -8239,6 +8434,9 @@ packages:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
+ style-mod@4.1.2:
+ resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
+
style-to-object@0.4.4:
resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==}
@@ -9100,6 +9298,25 @@ packages:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
+ y-codemirror.next@0.3.5:
+ resolution: {integrity: sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw==}
+ peerDependencies:
+ '@codemirror/state': ^6.0.0
+ '@codemirror/view': ^6.0.0
+ yjs: ^13.5.6
+
+ y-indexeddb@9.0.12:
+ resolution: {integrity: sha512-9oCFRSPPzBK7/w5vOkJBaVCQZKHXB/v6SIT+WYhnJxlEC61juqG0hBrAf+y3gmSMLFLwICNH9nQ53uscuse6Hg==}
+ engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+ peerDependencies:
+ yjs: ^13.0.0
+
+ y-protocols@1.0.6:
+ resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==}
+ engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+ peerDependencies:
+ yjs: ^13.0.0
+
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -9141,6 +9358,10 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
+ yjs@13.6.20:
+ resolution: {integrity: sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==}
+ engines: {node: '>=16.0.0', npm: '>=8.0.0'}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -10141,6 +10362,61 @@ snapshots:
human-id: 1.0.2
prettier: 2.8.8
+ '@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.2)(@lezer/common@1.2.3)':
+ dependencies:
+ '@codemirror/language': 6.10.6
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ '@lezer/common': 1.2.3
+
+ '@codemirror/commands@6.7.1':
+ dependencies:
+ '@codemirror/language': 6.10.6
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ '@lezer/common': 1.2.3
+
+ '@codemirror/lang-javascript@6.2.2':
+ dependencies:
+ '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.2)(@lezer/common@1.2.3)
+ '@codemirror/language': 6.10.6
+ '@codemirror/lint': 6.8.4
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ '@lezer/common': 1.2.3
+ '@lezer/javascript': 1.4.21
+
+ '@codemirror/language@6.10.6':
+ dependencies:
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+ style-mod: 4.1.2
+
+ '@codemirror/lint@6.8.4':
+ dependencies:
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ crelt: 1.0.6
+
+ '@codemirror/search@6.5.8':
+ dependencies:
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ crelt: 1.0.6
+
+ '@codemirror/state@6.5.0':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.0
+
+ '@codemirror/view@6.35.2':
+ dependencies:
+ '@codemirror/state': 6.5.0
+ style-mod: 4.1.2
+ w3c-keyname: 2.2.8
+
'@databases/connection-pool@1.1.0':
dependencies:
'@databases/queue': 1.0.1
@@ -10766,6 +11042,22 @@ snapshots:
'@jspm/core@2.1.0': {}
+ '@lezer/common@1.2.3': {}
+
+ '@lezer/highlight@1.2.1':
+ dependencies:
+ '@lezer/common': 1.2.3
+
+ '@lezer/javascript@1.4.21':
+ dependencies:
+ '@lezer/common': 1.2.3
+ '@lezer/highlight': 1.2.1
+ '@lezer/lr': 1.4.2
+
+ '@lezer/lr@1.4.2':
+ dependencies:
+ '@lezer/common': 1.2.3
+
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.26.0
@@ -10782,6 +11074,8 @@ snapshots:
globby: 11.1.0
read-yaml-file: 1.1.0
+ '@marijn/find-cluster-break@1.0.0': {}
+
'@mdx-js/mdx@2.3.0':
dependencies:
'@types/estree-jsx': 1.0.5
@@ -10804,8 +11098,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@neondatabase/serverless@0.10.4':
+ dependencies:
+ '@types/pg': 8.11.6
+
'@next/env@14.2.17': {}
+ '@next/eslint-plugin-next@14.2.20':
+ dependencies:
+ glob: 10.3.10
+
'@next/swc-darwin-arm64@14.2.17':
optional: true
@@ -12430,6 +12732,12 @@ snapshots:
pg-protocol: 1.7.0
pg-types: 4.0.2
+ '@types/pg@8.11.6':
+ dependencies:
+ '@types/node': 20.17.6
+ pg-protocol: 1.7.0
+ pg-types: 4.0.2
+
'@types/prop-types@15.7.13': {}
'@types/react-beautiful-dnd@13.1.8':
@@ -13333,6 +13641,18 @@ snapshots:
cluster-key-slot@1.1.2: {}
+ codemirror@6.0.1(@lezer/common@1.2.3):
+ dependencies:
+ '@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.2)(@lezer/common@1.2.3)
+ '@codemirror/commands': 6.7.1
+ '@codemirror/language': 6.10.6
+ '@codemirror/lint': 6.8.4
+ '@codemirror/search': 6.5.8
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ transitivePeerDependencies:
+ - '@lezer/common'
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -14475,6 +14795,14 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob@10.3.10:
+ dependencies:
+ foreground-child: 3.3.0
+ jackspeak: 2.3.6
+ minimatch: 9.0.5
+ minipass: 7.1.2
+ path-scurry: 1.11.1
+
glob@10.4.5:
dependencies:
foreground-child: 3.3.0
@@ -14886,6 +15214,14 @@ snapshots:
isexe@2.0.0: {}
+ isomorphic.js@0.2.5: {}
+
+ jackspeak@2.3.6:
+ dependencies:
+ '@isaacs/cliui': 8.0.2
+ optionalDependencies:
+ '@pkgjs/parseargs': 0.11.0
+
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -15058,6 +15394,10 @@ snapshots:
prelude-ls: 1.2.1
type-check: 0.4.0
+ lib0@0.2.99:
+ dependencies:
+ isomorphic.js: 0.2.5
+
lilconfig@2.1.0: {}
lilconfig@3.1.2: {}
@@ -17115,21 +17455,48 @@ snapshots:
dependencies:
minipass: 7.1.2
+ sst-darwin-arm64@3.3.59:
+ optional: true
+
sst-darwin-arm64@3.3.7:
optional: true
+ sst-darwin-x64@3.3.59:
+ optional: true
+
sst-darwin-x64@3.3.7:
optional: true
+ sst-linux-arm64@3.3.59:
+ optional: true
+
sst-linux-arm64@3.3.7:
optional: true
+ sst-linux-x64@3.3.59:
+ optional: true
+
sst-linux-x64@3.3.7:
optional: true
+ sst-linux-x86@3.3.59:
+ optional: true
+
sst-linux-x86@3.3.7:
optional: true
+ sst@3.3.59:
+ dependencies:
+ aws4fetch: 1.0.20
+ jose: 5.2.3
+ openid-client: 5.6.4
+ optionalDependencies:
+ sst-darwin-arm64: 3.3.59
+ sst-darwin-x64: 3.3.59
+ sst-linux-arm64: 3.3.59
+ sst-linux-x64: 3.3.59
+ sst-linux-x86: 3.3.59
+
sst@3.3.7:
dependencies:
aws4fetch: 1.0.20
@@ -17247,6 +17614,8 @@ snapshots:
strip-json-comments@3.1.1: {}
+ style-mod@4.1.2: {}
+
style-to-object@0.4.4:
dependencies:
inline-style-parser: 0.1.1
@@ -17506,7 +17875,7 @@ snapshots:
tslib@2.8.1: {}
- tsup@8.3.5(@swc/core@1.9.1(@swc/helpers@0.5.5))(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0):
+ tsup@8.3.5(@swc/core@1.9.1)(jiti@1.21.6)(postcss@8.4.47)(tsx@4.19.2)(typescript@5.6.3)(yaml@2.6.0):
dependencies:
bundle-require: 5.0.0(esbuild@0.24.0)
cac: 6.7.14
@@ -18250,6 +18619,23 @@ snapshots:
xtend@4.0.2: {}
+ y-codemirror.next@0.3.5(@codemirror/state@6.5.0)(@codemirror/view@6.35.2)(yjs@13.6.20):
+ dependencies:
+ '@codemirror/state': 6.5.0
+ '@codemirror/view': 6.35.2
+ lib0: 0.2.99
+ yjs: 13.6.20
+
+ y-indexeddb@9.0.12(yjs@13.6.20):
+ dependencies:
+ lib0: 0.2.99
+ yjs: 13.6.20
+
+ y-protocols@1.0.6(yjs@13.6.20):
+ dependencies:
+ lib0: 0.2.99
+ yjs: 13.6.20
+
y18n@5.0.8: {}
yallist@2.1.2: {}
@@ -18288,6 +18674,10 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
+ yjs@13.6.20:
+ dependencies:
+ lib0: 0.2.99
+
yocto-queue@0.1.0: {}
yoctocolors-cjs@2.1.2: {}