Skip to content

Commit

Permalink
WIP: make hydration more compatible with compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
TylorS committed Jun 8, 2024
1 parent 553cce5 commit 1aa8bbb
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 89 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
branches: [development]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -40,6 +40,7 @@ jobs:
fetch-depth: 0
- uses: ./.github/workflows/setup-repo
name: Setup Repo
- run: pnpm run build
- run: pnpm run test

lint:
Expand All @@ -54,6 +55,7 @@ jobs:
fetch-depth: 0
- uses: ./.github/workflows/setup-repo
name: Setup Repo
- run: pnpm run build
- run: pnpm run lint

docs:
Expand All @@ -68,9 +70,9 @@ jobs:
fetch-depth: 0
- uses: ./.github/workflows/setup-repo
name: Setup Repo
- run: pnpm run build
- run: pnpm run docs


publish:
runs-on: ubuntu-latest
strategy:
Expand Down
48 changes: 25 additions & 23 deletions packages/compiler/src/Compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { Service } from "./typescript/Service.js"

// This whole file is a hack-and-a-half, really just prototyping features

export type CompilerTarget = "dom" | "hydrate" | "server" | "static"
export type CompilerTarget = "dom" | "server" | "static"

/**
* Compiler is an all-in-one cass for compile-time optimization and derivations
Expand Down Expand Up @@ -126,8 +126,8 @@ export class Compiler {
if (index > -1) {
const template = templates[index]
const remaining = templates.slice(index + 1)
if (target === "dom" || target === "hydrate") {
return this.replaceDom(template, remaining, importManager, target === "hydrate")
if (target === "dom") {
return this.replaceDom(template, remaining, importManager)
}

// return this.replaceHtml(template, remaining, target === "static")
Expand All @@ -151,50 +151,48 @@ export class Compiler {
private replaceDom(
{ parts, template }: ParsedTemplate,
remaining: Array<ParsedTemplate>,
imports: ImportDeclarationManager,
isHydrating: boolean
imports: ImportDeclarationManager
): ts.Node {
const sink = ts.factory.createParameterDeclaration([], undefined, `sink`)
const ctx = new CreateNodeCtx(
parts,
remaining,
imports,
Chunk.empty(),
isHydrating ? "hydrate" : "dom",
"dom",
Chunk.empty()
)
const setupNodes = createDomSetupStatements(ctx)
const domNodes = Array.from(consumeNestedIterable(createDomTemplateStatements(template, ctx)))
const domEffects = createDomEffectStatements(template, ctx)

// Must come last to avoid mutation affecting behaviors of other nodes above
const domNodesNestedIterable = createDomTemplateStatements(template, ctx)

imports.addImport(`@typed/fx`, "Fx")
imports.addImport(`effect/Effect`, "Effect")

const domNodes: Array<ts.Statement> = Array.from(consumeNestedIterable(domNodesNestedIterable))

return createMethodCall(
"Fx",
"make",
[],
[
ts.factory.createArrowFunction(
ts.factory.createFunctionExpression(
[],
undefined,
`render`,
[],
[sink],
undefined,
undefined,
createMethodCall(`Effect`, `gen`, [], [
ts.factory.createFunctionExpression(
[],
ts.factory.createToken(ts.SyntaxKind.AsteriskToken),
undefined,
[],
[ts.factory.createParameterDeclaration([], undefined, `_`)],
undefined,
ts.factory.createBlock([...setupNodes, ...domNodes, ...domEffects], true)
)
ts.factory.createBlock([
ts.factory.createReturnStatement(createMethodCall(`Effect`, `gen`, [], [
ts.factory.createFunctionExpression(
[],
ts.factory.createToken(ts.SyntaxKind.AsteriskToken),
undefined,
[],
[ts.factory.createParameterDeclaration([], undefined, `_`)],
undefined,
ts.factory.createBlock([...setupNodes, ...domNodes, ...domEffects], true)
)
]))
])
)
]
Expand All @@ -219,6 +217,9 @@ export class Compiler {
...Array.isArray(original.after) ? original.after : original.after ? [original.after] : [],
(ctx) => (sourceFile) => {
const templates = this.templatesByFile.get(sourceFile.fileName) ?? this.parseTemplates(sourceFile)

if (templates.length === 0) return sourceFile

const transformers = this.getTransformersByFileAndTarget(templates, this._compilerTarget)
return transformers.reduce((file, transformer) => transformer(ctx)(file), sourceFile)
}
Expand Down Expand Up @@ -368,6 +369,7 @@ function isHtmlTag(node: ts.Node): node is ts.TaggedTemplateExpression {
return false
}

// Used to maintain stack-safety of recursive functions
function* consumeNestedIterable(
iterable: NestedIterable<ts.Statement>
): Iterable<ts.Statement> {
Expand Down
1 change: 0 additions & 1 deletion packages/template/src/Hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export function hydrate<R, E, T extends RenderEvent | null>(
)
const ctx: HydrateContext = {
where: getHydrationRoot(rootElement),
parentTemplate: null,
hydrate: true
}

Expand Down
50 changes: 50 additions & 0 deletions packages/template/src/compiler-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
* @since 1.0.0
*/

import type { Option } from "effect"
import type * as Cause from "effect/Cause"
import type * as Chunk from "effect/Chunk"
import type * as Effect from "effect/Effect"
import type * as Scope from "effect/Scope"
import type * as internalHydrateContext from "./internal/HydrateContext.js"
import * as utils from "./internal/utils.js"
import * as hydrationTemplate from "./internal/v2/hydration-template.js"
import * as render from "./internal/v2/render.js"
Expand All @@ -24,6 +26,41 @@ import type * as Template from "./Template.js"
*/
export interface TemplateContext extends render.TemplateContext {}

/**
* @since 1.0.0
*/
export interface HydrateContext extends internalHydrateContext.HydrateContext {}

/**
* @since 1.0.0
*/
export interface HydrationTemplate extends hydrationTemplate.HydrationTemplate {}

/**
* @since 1.0.0
*/
export type HydrationNode = hydrationTemplate.HydrationNode

/**
* @since 1.0.0
*/
export interface HydrationHole extends hydrationTemplate.HydrationHole {}

/**
* @since 1.0.0
*/
export interface HydrationMany extends hydrationTemplate.HydrationMany {}

/**
* @since 1.0.0
*/
export interface HydrationElement extends hydrationTemplate.HydrationElement {}

/**
* @since 1.0.0
*/
export interface HydrationLiteral extends hydrationTemplate.HydrationLiteral {}

/**
* @since 1.0.0
*/
Expand Down Expand Up @@ -198,3 +235,16 @@ export const getChildNodes: (node: hydrationTemplate.HydrationNode) => Array<hyd
*/
export const findHydratePath: (node: hydrationTemplate.HydrationNode, path: Chunk.Chunk<number>) => Node =
utils.findHydratePath

/**
* @since 1.0.0
*/
export const attemptHydration: (
ctx: TemplateContext,
hash: string
) => Option.Option<
{
readonly where: HydrationTemplate
readonly hydrateCtx: HydrateContext
}
> = render.attemptHydration
2 changes: 0 additions & 2 deletions packages/template/src/internal/HydrateContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Tagged } from "@typed/context"
import type { Template } from "../Template.js"
import type { HydrationNode } from "./v2/hydration-template.js"

/**
Expand All @@ -8,7 +7,6 @@ import type { HydrationNode } from "./v2/hydration-template.js"
*/
export type HydrateContext = {
readonly where: HydrationNode
readonly parentTemplate: Template | null

// Used to match sibling components using many() to the correct elements
readonly manyKey?: string
Expand Down
116 changes: 55 additions & 61 deletions packages/template/src/internal/v2/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import * as Scope from "effect/Scope"
import { type Directive, isDirective } from "../../Directive.js"
import * as ElementRef from "../../ElementRef.js"
import * as ElementSource from "../../ElementSource.js"
import type { BrowserEntry } from "../../Entry.js"
import * as EventHandler from "../../EventHandler.js"
import type { Placeholder } from "../../Placeholder.js"
import type { ToRendered } from "../../Render.js"
Expand Down Expand Up @@ -97,16 +96,53 @@ export const renderTemplate: (
sink.onFailure
)

const [wire, isHydrating] = yield* setupTemplate(entry, ctx)
const hydration = attemptHydration(ctx, entry.template.hash)

let effects: Array<Effect.Effect<void, any, any>>
let content: DocumentFragment
let wire: Rendered | undefined

if (Option.isSome(hydration)) {
const { hydrateCtx, where } = hydration.value
effects = setupHydrateParts(entry.template.parts, {
...ctx,
where,
manyKey: hydrateCtx.manyKey,
makeHydrateContext: (where: HydrationNode): HydrateContext => ({
where,
hydrate: true
})
})

wire = getWire(where)
} else {
content = ctx.document.importNode(entry.content, true)
effects = setupRenderParts(entry.template.parts, content, ctx)
}

if (effects.length > 0) {
yield* Effect.forEach(effects, flow(Effect.catchAllCause(ctx.onCause), Effect.forkIn(ctx.scope)))
}

// If there's anything to wait on and it's not already done, wait for an initial value
// for all asynchronous sources.
if (ctx.expected > 0 && (yield* ctx.refCounter.expect(ctx.expected))) {
yield* ctx.refCounter.wait
}

// If we're not hydrating, we need to create our wire from our content
if (wire === undefined) {
wire = persistent(ctx.document, content!)
}

// Setup our event listeners for our wire.
// We use the parentScope to allow event listeners to exist
// beyond the lifetime of the current Fiber, but no further than its parent template.
yield* ctx.eventSource.setup(wire, ctx.parentScope)

// If we're hydrating, we need to mark this part of the stack as hydrated
if (isHydrating && Option.isSome(ctx.hydrateContext)) {
ctx.hydrateContext.value.hydrate = false
if (Option.isSome(hydration)) {
hydration.value.hydrateCtx.hydrate = false
}

// Emit our DomRenderEvent
Expand Down Expand Up @@ -168,63 +204,22 @@ export function makeTemplateContext<Values extends ReadonlyArray<Renderable<any,
})
}

function setupTemplate(
entry: BrowserEntry,
ctx: TemplateContext
) {
return Effect.gen(function*() {
if (Option.isSome(ctx.hydrateContext) && ctx.hydrateContext.value.hydrate) {
const hydrateCtx = ctx.hydrateContext.value
const where = findWhere(hydrateCtx, entry.template.hash)
if (where === null) {
hydrateCtx.hydrate = false
} else {
const wire = getWire(where)
const makeHydrateContext = (where: HydrationNode): HydrateContext => ({
where,
parentTemplate: entry.template,
hydrate: true
})

const effects = setupHydrateParts(entry.template.parts, {
...ctx,
where,
manyKey: hydrateCtx.manyKey,
makeHydrateContext
})
if (effects.length > 0) {
yield* Effect.forEach(effects, flow(Effect.catchAllCause(ctx.onCause), Effect.forkIn(ctx.scope)))
}

// If there's anything to wait on and it's not already done, wait for an initial value
// for all asynchronous sources.
if (ctx.expected > 0 && (yield* ctx.refCounter.expect(ctx.expected))) {
yield* ctx.refCounter.wait
}

return [wire, true] as const
}
}

// Standard Render

const content = ctx.document.importNode(entry.content, true)

// Setup all parts
const effects = setupRenderParts(entry.template.parts, content, ctx)
if (effects.length > 0) {
yield* Effect.forEach(effects, flow(Effect.catchAllCause(ctx.onCause), Effect.forkIn(ctx.scope)))
}

// If there's anything to wait on and it's not already done, wait for an initial value
// for all asynchronous sources.
if (ctx.expected > 0 && (yield* ctx.refCounter.expect(ctx.expected))) {
yield* ctx.refCounter.wait
export function attemptHydration(
ctx: TemplateContext,
hash: string
): Option.Option<{ readonly where: HydrationTemplate; readonly hydrateCtx: HydrateContext }> {
if (Option.isSome(ctx.hydrateContext) && ctx.hydrateContext.value.hydrate) {
const hydrateCtx = ctx.hydrateContext.value
const where = findHydrationTemplateByHash(hydrateCtx, hash)
if (where === null) {
hydrateCtx.hydrate = false
return Option.none()
} else {
return Option.some({ where, hydrateCtx })
}
}

// Create a persistent wire from our content
return [persistent(ctx.document, content), false] as const
})
return Option.none()
}

function setupRenderParts(
Expand Down Expand Up @@ -576,7 +571,6 @@ function unwrapRenderable<E, R>(renderable: unknown): Fx.Fx<any, E, R> {
else if (Array.isArray(renderable)) {
return renderable.length === 0
? Fx.succeed(null)
// TODO: We need to ensure the ordering of these values in server environments
: Fx.map(Fx.tuple(renderable.map(unwrapRenderable)), (xs) => xs.flat()) as any
} else if (Fx.FxTypeId in renderable) {
return renderable as any
Expand Down Expand Up @@ -669,7 +663,7 @@ export type HydrateTemplateContext = TemplateContext & {
readonly makeHydrateContext: (where: HydrationNode, index: number) => HydrateContext
}

export function findWhere(hydrateCtx: HydrateContext, hash: string): HydrationTemplate | null {
export function findHydrationTemplateByHash(hydrateCtx: HydrateContext, hash: string): HydrationTemplate | null {
// If there is not a manyKey, we can just find the template by its hash
if (hydrateCtx.manyKey === undefined) {
return findHydrationTemplate(getChildNodes(hydrateCtx.where), hash)
Expand Down

0 comments on commit 1aa8bbb

Please sign in to comment.