refactor: move instance loading into service
This commit is contained in:
parent
478156456e
commit
f5398e7e1e
5 changed files with 255 additions and 127 deletions
|
|
@ -7,6 +7,7 @@ import * as Log from "@opencode-ai/core/util/log"
|
||||||
import { LocalContext } from "@/util/local-context"
|
import { LocalContext } from "@/util/local-context"
|
||||||
import * as Project from "./project"
|
import * as Project from "./project"
|
||||||
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
import { WorkspaceContext } from "@/control-plane/workspace-context"
|
||||||
|
import { Context, Effect, Layer } from "effect"
|
||||||
|
|
||||||
export interface InstanceContext {
|
export interface InstanceContext {
|
||||||
directory: string
|
directory: string
|
||||||
|
|
@ -15,63 +16,160 @@ export interface InstanceContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
const context = LocalContext.create<InstanceContext>("instance")
|
const context = LocalContext.create<InstanceContext>("instance")
|
||||||
const cache = new Map<string, Promise<InstanceContext>>()
|
|
||||||
const project = makeRuntime(Project.Service, Project.defaultLayer)
|
|
||||||
|
|
||||||
const disposal = {
|
export interface LoadInput {
|
||||||
all: undefined as Promise<void> | undefined,
|
directory: string
|
||||||
|
init?: () => Promise<unknown>
|
||||||
|
worktree?: string
|
||||||
|
project?: Project.Info
|
||||||
}
|
}
|
||||||
|
|
||||||
function boot(input: { directory: string; init?: () => Promise<any>; worktree?: string; project?: Project.Info }) {
|
export interface Store {
|
||||||
return iife(async () => {
|
readonly load: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||||
const ctx =
|
readonly reload: (input: LoadInput) => Effect.Effect<InstanceContext>
|
||||||
input.project && input.worktree
|
readonly dispose: (ctx: InstanceContext) => Effect.Effect<void>
|
||||||
? {
|
readonly disposeAll: () => Effect.Effect<void>
|
||||||
directory: input.directory,
|
}
|
||||||
worktree: input.worktree,
|
|
||||||
project: input.project,
|
export class InstanceStore extends Context.Service<InstanceStore, Store>()("@opencode/InstanceStore") {}
|
||||||
}
|
|
||||||
: await project
|
export const instanceStoreLayer: Layer.Layer<InstanceStore, never, Project.Service> = Layer.effect(
|
||||||
.runPromise((svc) => svc.fromDirectory(input.directory))
|
InstanceStore,
|
||||||
.then(({ project, sandbox }) => ({
|
Effect.gen(function* () {
|
||||||
|
const project = yield* Project.Service
|
||||||
|
const cache = new Map<string, Promise<InstanceContext>>()
|
||||||
|
const disposal = {
|
||||||
|
all: undefined as Promise<void> | undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
const boot = Effect.fn("InstanceStore.boot")(function* (input: LoadInput & { directory: string }) {
|
||||||
|
const ctx =
|
||||||
|
input.project && input.worktree
|
||||||
|
? {
|
||||||
directory: input.directory,
|
directory: input.directory,
|
||||||
worktree: sandbox,
|
worktree: input.worktree,
|
||||||
project,
|
project: input.project,
|
||||||
}))
|
}
|
||||||
await context.provide(ctx, async () => {
|
: yield* project.fromDirectory(input.directory).pipe(
|
||||||
await input.init?.()
|
Effect.map((result) => ({
|
||||||
|
directory: input.directory,
|
||||||
|
worktree: result.sandbox,
|
||||||
|
project: result.project,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
const init = input.init
|
||||||
|
if (init) yield* Effect.promise(() => context.provide(ctx, init))
|
||||||
|
return ctx
|
||||||
})
|
})
|
||||||
return ctx
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function track(directory: string, next: Promise<InstanceContext>) {
|
function track(directory: string, next: Promise<InstanceContext>) {
|
||||||
const task = next.catch((error) => {
|
const task = next.catch((error) => {
|
||||||
if (cache.get(directory) === task) cache.delete(directory)
|
if (cache.get(directory) === task) cache.delete(directory)
|
||||||
throw error
|
throw error
|
||||||
})
|
})
|
||||||
cache.set(directory, task)
|
cache.set(directory, task)
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const load = Effect.fn("InstanceStore.load")(function* (input: LoadInput) {
|
||||||
|
const directory = AppFileSystem.resolve(input.directory)
|
||||||
|
const existing = cache.get(directory)
|
||||||
|
if (existing) return yield* Effect.promise(() => existing)
|
||||||
|
|
||||||
|
Log.Default.info("creating instance", { directory })
|
||||||
|
return yield* Effect.promise(() => track(directory, Effect.runPromise(boot({ ...input, directory }))))
|
||||||
|
})
|
||||||
|
|
||||||
|
const reload = Effect.fn("InstanceStore.reload")(function* (input: LoadInput) {
|
||||||
|
const directory = AppFileSystem.resolve(input.directory)
|
||||||
|
Log.Default.info("reloading instance", { directory })
|
||||||
|
yield* Effect.promise(() => disposeInstance(directory))
|
||||||
|
cache.delete(directory)
|
||||||
|
const next = track(directory, Effect.runPromise(boot({ ...input, directory })))
|
||||||
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory,
|
||||||
|
project: input.project?.id,
|
||||||
|
workspace: WorkspaceContext.workspaceID,
|
||||||
|
payload: {
|
||||||
|
type: "server.instance.disposed",
|
||||||
|
properties: {
|
||||||
|
directory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return yield* Effect.promise(() => next)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dispose = Effect.fn("InstanceStore.dispose")(function* (ctx: InstanceContext) {
|
||||||
|
Log.Default.info("disposing instance", { directory: ctx.directory })
|
||||||
|
yield* Effect.promise(() => disposeInstance(ctx.directory))
|
||||||
|
cache.delete(ctx.directory)
|
||||||
|
|
||||||
|
GlobalBus.emit("event", {
|
||||||
|
directory: ctx.directory,
|
||||||
|
project: ctx.project.id,
|
||||||
|
workspace: WorkspaceContext.workspaceID,
|
||||||
|
payload: {
|
||||||
|
type: "server.instance.disposed",
|
||||||
|
properties: {
|
||||||
|
directory: ctx.directory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const disposeAll = Effect.fn("InstanceStore.disposeAll")(function* () {
|
||||||
|
if (disposal.all) return yield* Effect.promise(() => disposal.all!)
|
||||||
|
|
||||||
|
disposal.all = iife(async () => {
|
||||||
|
Log.Default.info("disposing all instances")
|
||||||
|
const entries = [...cache.entries()]
|
||||||
|
for (const [key, value] of entries) {
|
||||||
|
if (cache.get(key) !== value) continue
|
||||||
|
|
||||||
|
const ctx = await value.catch((error) => {
|
||||||
|
Log.Default.warn("instance dispose failed", { key, error })
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
if (cache.get(key) === value) cache.delete(key)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache.get(key) !== value) continue
|
||||||
|
await Effect.runPromise(dispose(ctx))
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
disposal.all = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
return yield* Effect.promise(() => disposal.all!)
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))
|
||||||
|
|
||||||
|
return InstanceStore.of({
|
||||||
|
load,
|
||||||
|
reload,
|
||||||
|
dispose,
|
||||||
|
disposeAll,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const instanceStoreDefaultLayer = instanceStoreLayer.pipe(Layer.provide(Project.defaultLayer))
|
||||||
|
|
||||||
|
const instanceStoreRuntime = makeRuntime(InstanceStore, instanceStoreDefaultLayer)
|
||||||
|
|
||||||
export const Instance = {
|
export const Instance = {
|
||||||
|
load(input: LoadInput): Promise<InstanceContext> {
|
||||||
|
return instanceStoreRuntime.runPromise((store) => store.load(input))
|
||||||
|
},
|
||||||
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
|
||||||
const directory = AppFileSystem.resolve(input.directory)
|
return context.provide(await Instance.load(input), async () => input.fn())
|
||||||
let existing = cache.get(directory)
|
|
||||||
if (!existing) {
|
|
||||||
Log.Default.info("creating instance", { directory })
|
|
||||||
existing = track(
|
|
||||||
directory,
|
|
||||||
boot({
|
|
||||||
directory,
|
|
||||||
init: input.init,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const ctx = await existing
|
|
||||||
return context.provide(ctx, async () => {
|
|
||||||
return input.fn()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
get current() {
|
get current() {
|
||||||
return context.use()
|
return context.use()
|
||||||
|
|
@ -117,74 +215,12 @@ export const Instance = {
|
||||||
return context.provide(ctx, fn)
|
return context.provide(ctx, fn)
|
||||||
},
|
},
|
||||||
async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
|
async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
|
||||||
const directory = AppFileSystem.resolve(input.directory)
|
return instanceStoreRuntime.runPromise((store) => store.reload(input))
|
||||||
Log.Default.info("reloading instance", { directory })
|
|
||||||
await disposeInstance(directory)
|
|
||||||
cache.delete(directory)
|
|
||||||
const next = track(directory, boot({ ...input, directory }))
|
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory,
|
|
||||||
project: input.project?.id,
|
|
||||||
workspace: WorkspaceContext.workspaceID,
|
|
||||||
payload: {
|
|
||||||
type: "server.instance.disposed",
|
|
||||||
properties: {
|
|
||||||
directory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return await next
|
|
||||||
},
|
},
|
||||||
async dispose() {
|
async dispose() {
|
||||||
const directory = Instance.directory
|
return instanceStoreRuntime.runPromise((store) => store.dispose(Instance.current))
|
||||||
const project = Instance.project
|
|
||||||
Log.Default.info("disposing instance", { directory })
|
|
||||||
await disposeInstance(directory)
|
|
||||||
cache.delete(directory)
|
|
||||||
|
|
||||||
GlobalBus.emit("event", {
|
|
||||||
directory,
|
|
||||||
project: project.id,
|
|
||||||
workspace: WorkspaceContext.workspaceID,
|
|
||||||
payload: {
|
|
||||||
type: "server.instance.disposed",
|
|
||||||
properties: {
|
|
||||||
directory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
async disposeAll() {
|
async disposeAll() {
|
||||||
if (disposal.all) return disposal.all
|
return instanceStoreRuntime.runPromise((store) => store.disposeAll())
|
||||||
|
|
||||||
disposal.all = iife(async () => {
|
|
||||||
Log.Default.info("disposing all instances")
|
|
||||||
const entries = [...cache.entries()]
|
|
||||||
for (const [key, value] of entries) {
|
|
||||||
if (cache.get(key) !== value) continue
|
|
||||||
|
|
||||||
const ctx = await value.catch((error) => {
|
|
||||||
Log.Default.warn("instance dispose failed", { key, error })
|
|
||||||
return undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
if (cache.get(key) === value) cache.delete(key)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.get(key) !== value) continue
|
|
||||||
|
|
||||||
await context.provide(ctx, async () => {
|
|
||||||
await Instance.dispose()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
disposal.all = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
return disposal.all
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
import { InstanceRef, WorkspaceRef } from "@/effect/instance-ref"
|
||||||
import { AppRuntime } from "@/effect/app-runtime"
|
import { AppRuntime } from "@/effect/app-runtime"
|
||||||
import { InstanceBootstrap } from "@/project/bootstrap"
|
import { InstanceBootstrap } from "@/project/bootstrap"
|
||||||
import { Instance } from "@/project/instance"
|
import { InstanceStore, type InstanceContext, type Store } from "@/project/instance"
|
||||||
import type { InstanceContext } from "@/project/instance"
|
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
||||||
|
|
@ -24,22 +23,20 @@ function decode(input: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeInstanceContext(directory: string): Effect.Effect<InstanceContext> {
|
function makeInstanceContext(store: Store, directory: string): Effect.Effect<InstanceContext> {
|
||||||
return Effect.promise(() =>
|
return store.load({
|
||||||
Instance.provide({
|
directory: Filesystem.resolve(decode(directory)),
|
||||||
directory: Filesystem.resolve(decode(directory)),
|
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
||||||
init: () => AppRuntime.runPromise(InstanceBootstrap),
|
})
|
||||||
fn: () => Instance.current,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function provideInstanceContext<E>(
|
function provideInstanceContext<E>(
|
||||||
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
|
effect: Effect.Effect<HttpServerResponse.HttpServerResponse, E>,
|
||||||
|
store: Store,
|
||||||
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
|
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, WorkspaceRouteContext> {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
const route = yield* WorkspaceRouteContext
|
const route = yield* WorkspaceRouteContext
|
||||||
const ctx = yield* makeInstanceContext(route.directory)
|
const ctx = yield* makeInstanceContext(store, route.directory)
|
||||||
return yield* effect.pipe(
|
return yield* effect.pipe(
|
||||||
Effect.provideService(InstanceRef, ctx),
|
Effect.provideService(InstanceRef, ctx),
|
||||||
Effect.provideService(WorkspaceRef, route.workspaceID),
|
Effect.provideService(WorkspaceRef, route.workspaceID),
|
||||||
|
|
@ -47,9 +44,17 @@ function provideInstanceContext<E>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const instanceContextLayer = Layer.succeed(
|
export const instanceContextLayer = Layer.effect(
|
||||||
InstanceContextMiddleware,
|
InstanceContextMiddleware,
|
||||||
InstanceContextMiddleware.of((effect) => provideInstanceContext(effect)),
|
Effect.gen(function* () {
|
||||||
|
const store = yield* InstanceStore
|
||||||
|
return InstanceContextMiddleware.of((effect) => provideInstanceContext(effect, store))
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const instanceRouterMiddleware = HttpRouter.middleware()((effect) => provideInstanceContext(effect))
|
export const instanceRouterMiddleware = HttpRouter.middleware()(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const store = yield* InstanceStore
|
||||||
|
return (effect) => provideInstanceContext(effect, store)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { LSP } from "@/lsp/lsp"
|
||||||
import { MCP } from "@/mcp"
|
import { MCP } from "@/mcp"
|
||||||
import { Permission } from "@/permission"
|
import { Permission } from "@/permission"
|
||||||
import { Installation } from "@/installation"
|
import { Installation } from "@/installation"
|
||||||
|
import { instanceStoreDefaultLayer } from "@/project/instance"
|
||||||
import { Project } from "@/project/project"
|
import { Project } from "@/project/project"
|
||||||
import { ProviderAuth } from "@/provider/auth"
|
import { ProviderAuth } from "@/provider/auth"
|
||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
|
|
@ -145,6 +146,7 @@ export function createRoutes(corsOptions?: CorsOptions) {
|
||||||
Format.defaultLayer,
|
Format.defaultLayer,
|
||||||
LSP.defaultLayer,
|
LSP.defaultLayer,
|
||||||
Installation.defaultLayer,
|
Installation.defaultLayer,
|
||||||
|
instanceStoreDefaultLayer,
|
||||||
MCP.defaultLayer,
|
MCP.defaultLayer,
|
||||||
Permission.defaultLayer,
|
Permission.defaultLayer,
|
||||||
Project.defaultLayer,
|
Project.defaultLayer,
|
||||||
|
|
|
||||||
84
packages/opencode/test/project/instance.test.ts
Normal file
84
packages/opencode/test/project/instance.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { afterEach, describe, expect } from "bun:test"
|
||||||
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
|
import { Effect, Layer } from "effect"
|
||||||
|
import { Instance, InstanceStore, instanceStoreDefaultLayer } from "../../src/project/instance"
|
||||||
|
import { tmpdirScoped } from "../fixture/fixture"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
|
|
||||||
|
const it = testEffect(Layer.mergeAll(instanceStoreDefaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Instance.disposeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("InstanceStore", () => {
|
||||||
|
it.live("loads instance context without installing ALS for the caller", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
const store = yield* InstanceStore
|
||||||
|
const ctx = yield* store.load({ directory: dir })
|
||||||
|
|
||||||
|
expect(ctx.directory).toBe(dir)
|
||||||
|
expect(ctx.worktree).toBe(dir)
|
||||||
|
expect(() => Instance.current).toThrow()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("runs load init inside the loaded legacy instance context", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
const store = yield* InstanceStore
|
||||||
|
let initializedDirectory: string | undefined
|
||||||
|
|
||||||
|
yield* store.load({
|
||||||
|
directory: dir,
|
||||||
|
init: async () => {
|
||||||
|
initializedDirectory = Instance.directory
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(initializedDirectory).toBe(dir)
|
||||||
|
expect(() => Instance.current).toThrow()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("caches loaded instance context by directory", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
const store = yield* InstanceStore
|
||||||
|
let initialized = 0
|
||||||
|
|
||||||
|
const first = yield* store.load({
|
||||||
|
directory: dir,
|
||||||
|
init: async () => {
|
||||||
|
initialized++
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const second = yield* store.load({
|
||||||
|
directory: dir,
|
||||||
|
init: async () => {
|
||||||
|
initialized++
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(second).toBe(first)
|
||||||
|
expect(initialized).toBe(1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.live("keeps Instance.provide as the legacy ALS wrapper", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const dir = yield* tmpdirScoped({ git: true })
|
||||||
|
|
||||||
|
const directory = yield* Effect.promise(() =>
|
||||||
|
Instance.provide({
|
||||||
|
directory: dir,
|
||||||
|
fn: () => Instance.directory,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(directory).toBe(dir)
|
||||||
|
expect(() => Instance.current).toThrow()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
@ -11,7 +11,7 @@ import { registerAdapter } from "../../src/control-plane/adapters"
|
||||||
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
import type { WorkspaceAdapter } from "../../src/control-plane/types"
|
||||||
import { Workspace } from "../../src/control-plane/workspace"
|
import { Workspace } from "../../src/control-plane/workspace"
|
||||||
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
|
||||||
import { Instance } from "../../src/project/instance"
|
import { Instance, instanceStoreDefaultLayer } from "../../src/project/instance"
|
||||||
import { Project } from "../../src/project/project"
|
import { Project } from "../../src/project/project"
|
||||||
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
|
import { disposeMiddleware, markInstanceForDisposal } from "../../src/server/routes/instance/httpapi/lifecycle"
|
||||||
import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
|
import { instanceRouterMiddleware } from "../../src/server/routes/instance/httpapi/middleware/instance-context"
|
||||||
|
|
@ -40,6 +40,7 @@ const it = testEffect(
|
||||||
testStateLayer,
|
testStateLayer,
|
||||||
NodeHttpServer.layerTest,
|
NodeHttpServer.layerTest,
|
||||||
NodeServices.layer,
|
NodeServices.layer,
|
||||||
|
instanceStoreDefaultLayer,
|
||||||
Project.defaultLayer,
|
Project.defaultLayer,
|
||||||
Workspace.defaultLayer,
|
Workspace.defaultLayer,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue