diff --git a/admin/actions/environments/connect.ts b/admin/actions/environments/connect.ts new file mode 100644 index 000000000..c9b98bf16 --- /dev/null +++ b/admin/actions/environments/connect.ts @@ -0,0 +1,96 @@ +import { fjp } from "../../deps.ts"; +import { storage } from "../../fsStorage.ts"; +import { Acked, Commands, Events, State } from "../../types.ts"; + +export interface Props { + /** Environment name to connect to */ + name: string; +} + +const subscribers: WebSocket[] = []; + +export const fetchState = async (): Promise => ({ + decofile: await storage.state({ forceFresh: true }), +}); + +const saveState = ({ decofile }: State): Promise => + storage.update(decofile); + +// Apply patch and save state ATOMICALLY! +// This is easily done on play. On production, however, we probably +// need a distributed queue +let queue = Promise.resolve(); +const patchState = (ops: fjp.Operation[]) => { + queue = queue.catch(() => null).then(async () => + saveState(ops.reduce(fjp.applyReducer, await fetchState())) + ); + + return queue; +}; + +const action = (_props: Props, req: Request) => { + const { socket, response } = Deno.upgradeWebSocket(req); + + const broadcast = (event: Acked) => { + const message = JSON.stringify(event); + subscribers.forEach((s) => s.send(message)); + }; + const send = (event: Acked) => socket.send(JSON.stringify(event)); + const parse = (event: MessageEvent): Acked => + JSON.parse(event.data); + + const open = () => subscribers.push(socket); + const close = () => subscribers.splice(subscribers.indexOf(socket), 1); + const message = async (event: MessageEvent) => { + const data = parse(event); + + const { ack } = data; + + if (data.type === "patch-state") { + try { + const { payload: operations } = data; + + await patchState(operations); + + // Broadcast changes + broadcast({ + type: "state-patched", + payload: operations, + etag: await storage.revision(), + metadata: {}, // TODO: add metadataß + ack, + }); + } catch ({ name, operation }) { + console.error({ name, operation }); + } + } else if (data.type === "fetch-state") { + send({ + type: "state-fetched", + payload: await fetchState(), + etag: await storage.revision(), + ack, + }); + } else { + console.error("UNKNOWN EVENT", event); + } + }; + + /** + * Handles the WebSocket connection on open event. + */ + socket.onopen = open; + /** + * Handles the WebSocket connection on close event. + */ + socket.onclose = close; + + /** + * Handles the WebSocket connection on message event. + * @param {MessageEvent} event - The WebSocket message event. + */ + socket.onmessage = (e) => message(e).catch(() => {}); + + return response; +}; + +export default action; diff --git a/admin/actions/environments/decofile.ts b/admin/actions/environments/decofile.ts new file mode 100644 index 000000000..7a770fc59 --- /dev/null +++ b/admin/actions/environments/decofile.ts @@ -0,0 +1,18 @@ +import { fetchState } from "./connect.ts"; +import { State } from "../../types.ts"; +import { storage } from "../../fsStorage.ts"; + +interface Props { + name: string; +} + +/** TODO(@gimenes): Implement fetching the state from the proper environment name */ +const action = async (_props: Props): Promise => { + const { decofile } = await fetchState(); + + console.log("serving revision", await storage.revision()); + + return decofile; +}; + +export default action; diff --git a/admin/actions/workspaces/connect.ts b/admin/actions/workspaces/connect.ts deleted file mode 100644 index 8102b01b9..000000000 --- a/admin/actions/workspaces/connect.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { Context } from "deco/deco.ts"; -import { Resolvable } from "deco/engine/core/resolver.ts"; -import { lazySchemaFor } from "deco/engine/schema/lazy.ts"; -import { MetaInfo, toManifestBlocks } from "deco/runtime/fresh/routes/_meta.ts"; -import { fjp } from "../../deps.ts"; -import { storage } from "../../fsStorage.ts"; - -export interface Props { - /** Environment name to connect to */ - name: string; -} - -interface PatchState { - type: "patch-state"; - payload: fjp.Operation[]; -} - -interface FetchState { - type: "fetch-state"; -} - -interface StatePatched { - type: "state-patched"; - payload: fjp.Operation[]; - // Maybe add data and user info in here - metadata?: unknown; -} - -interface StateFetched { - type: "state-fetched"; - payload: State; -} - -type Acked = T & { ack: string }; - -interface State { - blocks: Record; - manifest: MetaInfo["manifest"]; - schema: MetaInfo["schema"]; - etag: string; -} - -type Commands = PatchState | FetchState; -type Events = StatePatched | StateFetched; - -const subscribers: WebSocket[] = []; - -const fetchState = async (): Promise => { - const blocks = await storage.state({ forceFresh: true }); - - const active = Context.active(); - const lazySchema = lazySchemaFor(active); - - const [schema, runtime] = await Promise.all([ - lazySchema.value, - active.runtime, - ]); - - return { - blocks, - manifest: toManifestBlocks(runtime!.manifest), - schema, - etag: await storage.revision(), - }; -}; - -const saveState = ({ blocks }: State): Promise => storage.update(blocks); - -// Apply patch and save state ATOMICALLY! -// This is easily done on play. On production, however, we probably -// need a distributed queue -let queue = Promise.resolve([]); -const patchState = (decofileOps: fjp.Operation[]) => { - queue = queue.catch(() => null).then(async () => { - const state = await fetchState(); - const observer = fjp.observe(state); - - try { - await saveState(decofileOps.reduce(fjp.applyReducer, state)); - - // Wait for a while before fetching the state - const newState = await new Promise((resolve) => - setTimeout(() => resolve(fetchState()), 1e3) - ); - - state.manifest = newState.manifest; - state.schema = newState.schema; - state.etag = newState.etag; - - return fjp.generate(observer, true); - } finally { - fjp.unobserve(state, observer); - } - }); - - return queue; -}; - -const action = (_props: Props, req: Request) => { - const { socket, response } = Deno.upgradeWebSocket(req); - - const broadcast = (event: Acked) => { - const message = JSON.stringify(event); - subscribers.forEach((s) => s.send(message)); - }; - const send = (event: Acked) => socket.send(JSON.stringify(event)); - const parse = (event: MessageEvent): Acked => - JSON.parse(event.data); - - const open = () => subscribers.push(socket); - const close = () => subscribers.splice(subscribers.indexOf(socket), 1); - const message = async (event: MessageEvent) => { - const data = parse(event); - - const { ack } = data; - - if (data.type === "patch-state") { - try { - const { payload: decofileOps } = data; - - const allOps = await patchState(decofileOps); - - // Broadcast changes - broadcast({ - type: "state-patched", - payload: allOps, - metadata: {}, // TODO: add metadata - ack, - }); - } catch ({ name, operation }) { - console.error({ name, operation }); - } - } else if (data.type === "fetch-state") { - send({ - type: "state-fetched", - payload: await fetchState(), - ack, - }); - } else { - console.error("UNKNOWN EVENT", event); - } - }; - - /** - * Handles the WebSocket connection on open event. - */ - socket.onopen = open; - /** - * Handles the WebSocket connection on close event. - */ - socket.onclose = close; - - /** - * Handles the WebSocket connection on message event. - * @param {MessageEvent} event - The WebSocket message event. - */ - socket.onmessage = (e) => message(e).catch(() => {}); - - return response; -}; - -export default action; diff --git a/admin/manifest.gen.ts b/admin/manifest.gen.ts index dd6612672..02e1e3e90 100644 --- a/admin/manifest.gen.ts +++ b/admin/manifest.gen.ts @@ -16,15 +16,16 @@ import * as $$$$$$$$$1 from "./actions/blocks/restore.ts"; import * as $$$$$$$$$2 from "./actions/blocks/safeDelete.ts"; import * as $$$$$$$$$3 from "./actions/blocks/newRevision.ts"; import * as $$$$$$$$$4 from "./actions/blocks/delete.ts"; -import * as $$$$$$$$$5 from "./actions/workspaces/connect.ts"; -import * as $$$$$$$$$6 from "./actions/sites/linkRepo.ts"; -import * as $$$$$$$$$7 from "./actions/sites/newDomain.ts"; -import * as $$$$$$$$$8 from "./actions/sites/unlinkRepo.ts"; -import * as $$$$$$$$$9 from "./actions/github/setStatus.ts"; -import * as $$$$$$$$$10 from "./actions/github/webhooks/broker.ts"; -import * as $$$$$$$$$11 from "./actions/pages/publish.ts"; -import * as $$$$$$$$$12 from "./actions/pages/new.ts"; -import * as $$$$$$$$$13 from "./actions/pages/delete.ts"; +import * as $$$$$$$$$5 from "./actions/environments/decofile.ts"; +import * as $$$$$$$$$6 from "./actions/environments/connect.ts"; +import * as $$$$$$$$$7 from "./actions/sites/linkRepo.ts"; +import * as $$$$$$$$$8 from "./actions/sites/newDomain.ts"; +import * as $$$$$$$$$9 from "./actions/sites/unlinkRepo.ts"; +import * as $$$$$$$$$10 from "./actions/github/setStatus.ts"; +import * as $$$$$$$$$11 from "./actions/github/webhooks/broker.ts"; +import * as $$$$$$$$$12 from "./actions/pages/publish.ts"; +import * as $$$$$$$$$13 from "./actions/pages/new.ts"; +import * as $$$$$$$$$14 from "./actions/pages/delete.ts"; const manifest = { "loaders": { @@ -44,15 +45,16 @@ const manifest = { "deco-sites/admin/actions/blocks/publish.ts": $$$$$$$$$0, "deco-sites/admin/actions/blocks/restore.ts": $$$$$$$$$1, "deco-sites/admin/actions/blocks/safeDelete.ts": $$$$$$$$$2, - "deco-sites/admin/actions/github/setStatus.ts": $$$$$$$$$9, - "deco-sites/admin/actions/github/webhooks/broker.ts": $$$$$$$$$10, - "deco-sites/admin/actions/pages/delete.ts": $$$$$$$$$13, - "deco-sites/admin/actions/pages/new.ts": $$$$$$$$$12, - "deco-sites/admin/actions/pages/publish.ts": $$$$$$$$$11, - "deco-sites/admin/actions/sites/linkRepo.ts": $$$$$$$$$6, - "deco-sites/admin/actions/sites/newDomain.ts": $$$$$$$$$7, - "deco-sites/admin/actions/sites/unlinkRepo.ts": $$$$$$$$$8, - "deco-sites/admin/actions/workspaces/connect.ts": $$$$$$$$$5, + "deco-sites/admin/actions/environments/connect.ts": $$$$$$$$$6, + "deco-sites/admin/actions/environments/decofile.ts": $$$$$$$$$5, + "deco-sites/admin/actions/github/setStatus.ts": $$$$$$$$$10, + "deco-sites/admin/actions/github/webhooks/broker.ts": $$$$$$$$$11, + "deco-sites/admin/actions/pages/delete.ts": $$$$$$$$$14, + "deco-sites/admin/actions/pages/new.ts": $$$$$$$$$13, + "deco-sites/admin/actions/pages/publish.ts": $$$$$$$$$12, + "deco-sites/admin/actions/sites/linkRepo.ts": $$$$$$$$$7, + "deco-sites/admin/actions/sites/newDomain.ts": $$$$$$$$$8, + "deco-sites/admin/actions/sites/unlinkRepo.ts": $$$$$$$$$9, }, "name": "deco-sites/admin", "baseUrl": import.meta.url, diff --git a/admin/types.ts b/admin/types.ts index fdfcf7f2b..58ac4357d 100644 --- a/admin/types.ts +++ b/admin/types.ts @@ -1,6 +1,41 @@ +import { type Resolvable } from "deco/engine/core/resolver.ts"; +import { type fjp } from "./deps.ts"; + export interface Pagination { data: T[]; page: number; pageSize: number; total: number; } + +export interface PatchState { + type: "patch-state"; + payload: fjp.Operation[]; +} + +export interface FetchState { + type: "fetch-state"; +} + +export interface StatePatched { + type: "state-patched"; + payload: fjp.Operation[]; + etag: string; + // Maybe add data and user info in here + metadata?: unknown; +} + +export interface StateFetched { + type: "state-fetched"; + payload: State; + etag: string; +} + +export type Acked = T & { ack: string }; + +export interface State { + decofile: Record; +} + +export type Commands = PatchState | FetchState; +export type Events = StatePatched | StateFetched; diff --git a/files/mod.ts b/files/mod.ts index 74a3332ac..426fc1283 100644 --- a/files/mod.ts +++ b/files/mod.ts @@ -36,8 +36,9 @@ const compile = async ( ): Promise<[AppManifest, SourceMap]> => { await initializePromise; const tsModule = await importFromString(content); - const blockPath = join(currdir, blockType, path); - const blockKey = `${manifest.name}/${blockType}/${path}`; + const blockPath = join(currdir, path); + const blockKey = join(manifest.name, path); + return [{ ...manifest, [blockType]: {