通讯操作

This commit is contained in:
王一之 2025-01-03 17:09:16 +08:00
parent 9876c1cbcb
commit c4b47d117c
13 changed files with 419 additions and 169 deletions

View File

@ -1,6 +1,6 @@
export function sendMessage(action: string, params?: any): Promise<any> {
export function sendMessage(action: string, data?: any): Promise<any> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ action, data: params }, (res) => {
chrome.runtime.sendMessage({ action, data }, (res) => {
if (res.code) {
console.error(res);
reject(res.message);
@ -11,10 +11,10 @@ export function sendMessage(action: string, params?: any): Promise<any> {
});
}
export function connect(action: string, params?: any): Promise<chrome.runtime.Port> {
export function connect(action: string, data?: any): Promise<chrome.runtime.Port> {
return new Promise((resolve) => {
const port = chrome.runtime.connect();
port.postMessage({ action, data: params });
port.postMessage({ action, data });
resolve(port);
});
}

View File

@ -2,17 +2,20 @@ import EventEmitter from "eventemitter3";
import { connect } from "./client";
import { ApiFunction, Server } from "./server";
export type SubscribeCallback = (message: any) => void;
export class Broker {
constructor() {}
// 订阅
async subscribe(topic: string, handler: (message: any) => void) {
async subscribe(topic: string, handler: SubscribeCallback): Promise<chrome.runtime.Port> {
const con = await connect("messageQueue", { action: "subscribe", topic });
con.onMessage.addListener((msg: { action: string; topic: string; message: any }) => {
if (msg.action === "message") {
handler(msg.message);
}
});
return con;
}
// 发布

View File

@ -0,0 +1,99 @@
import { v4 as uuidv4 } from "uuid";
// 通过 window.postMessage/onmessage 实现通信
import EventEmitter from "eventemitter3";
// 消息体
export type WindowMessageBody = {
messageId: string; // 消息id
type: "sendMessage" | "respMessage" | "connect"; // 消息类型
data: any; // 消息数据
};
export class WindowMessage {
EE: EventEmitter = new EventEmitter();
// source: Window 消息来源
// target: Window 消息目标
constructor(
private source: Window,
private target: Window
) {
// 监听消息
this.source.addEventListener("message", (e) => {
if (e.source === this.target) {
this.messageHandle(e.data);
}
});
}
messageHandle(data: WindowMessageBody) {
// 处理消息
if (data.type === "sendMessage") {
// 接收到消息
this.EE.emit("message", data.data, (resp: any) => {
// 发送响应消息
const body: WindowMessageBody = {
messageId: data.messageId,
type: "respMessage",
data: resp,
};
this.target.postMessage(body, "*");
});
} else if (data.type === "respMessage") {
// 接收到响应消息
this.EE.emit("response:" + data.messageId, data);
} else if (data.type === "connect") {
this.EE.emit("connect", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target));
} else if (data.type === "disconnect") {
this.EE.emit("disconnect", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target));
} else if (data.type === "connectMessage") {
this.EE.emit("connectMessage", data.data, new WindowMessageConnect(data.messageId, this.EE, this.target));
}
}
onConnect(callback: (data: any, con: WindowMessageConnect) => void) {
this.EE.addListener("connect", callback);
}
connect(action: string, data?: any): Promise<WindowMessageConnect> {
return new Promise((resolve) => {
const body: WindowMessageBody = {
messageId: uuidv4(),
type: "connect",
data: { action, data },
};
this.target.postMessage(body, "*");
resolve(new WindowMessageConnect(body.messageId, this.EE, this.target));
});
}
onMessage(callback: (data: any, sendResponse: (data: any) => void) => void) {
this.EE.addListener("message", callback);
}
sendMessage(action: string, data?: any): Promise<any> {
return new Promise((resolve) => {
const body: WindowMessageBody = {
messageId: uuidv4(),
type: "sendMessage",
data: { action, data },
};
const callback = (body: WindowMessageBody) => {
this.EE.removeListener("response:" + body.messageId, callback);
resolve(body.data);
};
this.EE.addListener("response:" + body.messageId, callback);
this.target.postMessage(body, "*");
});
}
}
export class WindowMessageConnect {
constructor(
private messageId: string,
private EE: EventEmitter,
private target: Window
) {}
}

View File

@ -9,20 +9,15 @@ export const SCRIPT_TYPE_NORMAL: SCRIPT_TYPE = 1;
export const SCRIPT_TYPE_CRONTAB: SCRIPT_TYPE = 2;
export const SCRIPT_TYPE_BACKGROUND: SCRIPT_TYPE = 3;
export type SCRIPT_STATUS = 1 | 2 | 3 | 4;
export type SCRIPT_STATUS = 1 | 2;
export const SCRIPT_STATUS_ENABLE: SCRIPT_STATUS = 1;
export const SCRIPT_STATUS_DISABLE: SCRIPT_STATUS = 2;
// 弃用
export const SCRIPT_STATUS_ERROR: SCRIPT_STATUS = 3;
export const SCRIPT_STATUS_DELETE: SCRIPT_STATUS = 4;
export type SCRIPT_RUN_STATUS = "running" | "complete" | "error" | "retry";
export type SCRIPT_RUN_STATUS = "running" | "complete" | "error";
export const SCRIPT_RUN_STATUS_RUNNING: SCRIPT_RUN_STATUS = "running";
export const SCRIPT_RUN_STATUS_COMPLETE: SCRIPT_RUN_STATUS = "complete";
export const SCRIPT_RUN_STATUS_ERROR: SCRIPT_RUN_STATUS = "error";
// 弃用
export const SCRIPT_RUN_STATUS_RETRY: SCRIPT_RUN_STATUS = "retry";
export type Metadata = { [key: string]: string[] };

View File

@ -1,11 +1,17 @@
import { Server } from "@Packages/message/server";
import { ScriptService } from "./script";
import { MessageQueue } from "@Packages/message/message_queue";
// offscreen环境的管理器
export class OffscreenManager {
private api: Server = new Server("offscreen");
private mq: MessageQueue = new MessageQueue(this.api);
initManager() {
// 监听消息
const group = this.api.group("serviceWorker");
const script = new ScriptService(group.group("script"), this.mq);
script.init();
}
}

View File

@ -0,0 +1,20 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { MessageQueue } from "@Packages/message/message_queue";
import { Group } from "@Packages/message/server";
export class ScriptService {
logger: Logger;
constructor(
private group: Group,
private mq: MessageQueue
) {
this.logger = LoggerCore.logger().with({ service: "script" });
}
init() {
// 初始化, 执行所有的后台脚本, 设置定时脚本计时器
}
}

View File

@ -1,6 +1,7 @@
import { Script } from "@App/app/repo/scripts";
import { Client } from "@Packages/message/client";
import { InstallSource } from ".";
import { Broker } from "@Packages/message/message_queue";
export class ScriptClient extends Client {
constructor() {
@ -12,7 +13,26 @@ export class ScriptClient extends Client {
return this.do("getInstallInfo", uuid);
}
installScript(script: Script, upsertBy: InstallSource = "user") {
return this.do("installScript", { script, upsertBy });
install(script: Script, upsertBy: InstallSource = "user") {
return this.do("install", { script, upsertBy });
}
delete(uuid: string) {
return this.do("delete", uuid);
}
enable(uuid: string, enable: boolean) {
return this.do("enable", { uuid, enable });
}
}
export function subscribeScriptInstall(
border: Broker,
callback: (message: { script: Script; update: boolean }) => void
) {
return border.subscribe("installScript", callback);
}
export function subscribeScriptDelete(border: Broker, callback: (message: { uuid: string }) => void) {
return border.subscribe("deleteScript", callback);
}

View File

@ -6,7 +6,14 @@ import LoggerCore from "@App/app/logger/core";
import Cache from "@App/app/cache";
import CacheKey from "@App/app/cache_key";
import { openInCurrentTab } from "@App/pkg/utils/utils";
import { Script, ScriptDAO } from "@App/app/repo/scripts";
import {
Script,
SCRIPT_RUN_STATUS_COMPLETE,
SCRIPT_RUN_STATUS_RUNNING,
SCRIPT_STATUS_DISABLE,
SCRIPT_STATUS_ENABLE,
ScriptDAO,
} from "@App/app/repo/scripts";
import { MessageQueue } from "@Packages/message/message_queue";
import { InstallSource } from ".";
@ -145,11 +152,13 @@ export class ScriptService {
version: script.metadata.version[0],
upsertBy,
});
let update = false;
const dao = new ScriptDAO();
// 判断是否已经安装
const oldScript = await dao.findByUUID(script.uuid);
if (oldScript) {
// 执行更新逻辑
update = true;
script.selfMetadata = oldScript.selfMetadata;
}
return dao
@ -157,7 +166,7 @@ export class ScriptService {
.then(() => {
logger.info("install success");
// 广播一下
this.mq.publish("installScript", script);
this.mq.publish("installScript", { script, update });
return {};
})
.catch((e) => {
@ -166,10 +175,54 @@ export class ScriptService {
});
}
async deleteScript(uuid: string) {
const logger = this.logger.with({ uuid });
const dao = new ScriptDAO();
const script = await dao.findByUUID(uuid);
if (!script) {
logger.error("script not found");
throw new Error("script not found");
}
return dao
.delete(uuid)
.then(() => {
logger.info("delete success");
this.mq.publish("deleteScript", { uuid });
return {};
})
.catch((e) => {
logger.error("delete error", Logger.E(e));
throw e;
});
}
async enableScript(param: { uuid: string; enable: boolean }) {
const logger = this.logger.with({ uuid: param.uuid, enable: param.enable });
const dao = new ScriptDAO();
const script = await dao.findByUUID(param.uuid);
if (!script) {
logger.error("script not found");
throw new Error("script not found");
}
return dao
.update(param.uuid, { status: param.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE })
.then(() => {
logger.info("enable success");
this.mq.publish("enableScript", { uuid: param.uuid, enable: param.enable });
return {};
})
.catch((e) => {
logger.error("enable error", Logger.E(e));
throw e;
});
}
init() {
this.listenerScriptInstall();
this.group.on("getInstallInfo", this.getInstallInfo);
this.group.on("installScript", this.installScript.bind(this));
this.group.on("install", this.installScript.bind(this));
this.group.on("delete", this.deleteScript.bind(this));
this.group.on("enable", this.enableScript.bind(this));
}
}

View File

@ -246,7 +246,7 @@ function App() {
return;
}
new ScriptClient()
.installScript(upsertScript as Script)
.install(upsertScript as Script)
.then(() => {
if (isUpdate) {
Message.success(t("install.update_success")!);

View File

@ -68,10 +68,21 @@ import CloudScriptPlan from "@App/pages/components/CloudScriptPlan";
import { useTranslation } from "react-i18next";
import { nextTime, semTime } from "@App/pkg/utils/utils";
import { i18nName } from "@App/locales/locales";
import { ListHomeRender, ScriptIcons } from "./utils";
import { getValues, ListHomeRender, ScriptIcons } from "./utils";
import { useAppDispatch, useAppSelector } from "@App/store/hooks";
import { fetchScriptList, selectScripts } from "@App/store/features/script";
import {
deleteScript,
requestEnableScript,
fetchAndSortScriptList,
requestDeleteScript,
ScriptLoading,
selectScripts,
sortScript,
upsertScript,
} from "@App/store/features/script";
import { selectScriptListColumnWidth } from "@App/store/features/setting";
import { Broker } from "@Packages/message/message_queue";
import { subscribeScriptDelete, subscribeScriptInstall } from "@App/app/service/service_worker/client";
type ListType = Script & { loading?: boolean };
@ -87,7 +98,7 @@ function ScriptList() {
const scriptListColumnWidth = useAppSelector(selectScriptListColumnWidth);
const inputRef = useRef<RefInputType>(null);
const navigate = useNavigate();
const openUserConfig = parseInt(useSearchParams()[0].get("userConfig") || "", 10);
const openUserConfig = useSearchParams()[0].get("userConfig") || "";
const [showAction, setShowAction] = useState(false);
const [action, setAction] = useState("");
const [select, setSelect] = useState<Script[]>([]);
@ -96,9 +107,24 @@ function ScriptList() {
const { t } = useTranslation();
useEffect(() => {
dispatch(fetchScriptList());
dispatch(fetchAndSortScriptList());
// 监听脚本安装/运行
// Monitor script running status
const border = new Broker();
const subCon: chrome.runtime.Port[] = [];
subscribeScriptInstall(border, (message) => {
dispatch(upsertScript(message.script));
}).then((con) => subCon.push(con));
subscribeScriptDelete(border, (message) => {
dispatch(deleteScript(message.uuid));
}).then((con) => subCon.push(con));
return () => {
subCon.forEach((con) => {
con.disconnect();
});
};
// const channel = runtimeCtrl.watchRunStatus();
// channel.setHandler(([id, status]: any) => {
// setScriptList((list) => {
@ -123,6 +149,9 @@ function ScriptList() {
key: "#",
sorter: (a, b) => a.sort - b.sort,
render(col) {
if (col < 0) {
return "-";
}
return col + 1;
},
},
@ -146,34 +175,14 @@ function ScriptList() {
},
],
onFilter: (value, row) => row.status === value,
render: (col, item: ListType) => {
render: (col, item: ScriptLoading) => {
return (
<Switch
checked={item.status === SCRIPT_STATUS_ENABLE}
loading={item.loading}
disabled={item.loading}
loading={item.enableLoading}
disabled={item.enableLoading}
onChange={(checked) => {
// setScriptList((list) => {
// const index = list.findIndex((script) => script.id === item.id);
// list[index].loading = true;
// let p: Promise<any>;
// if (checked) {
// p = scriptCtrl.enable(item.id).then(() => {
// list[index].status = SCRIPT_STATUS_ENABLE;
// });
// } else {
// p = scriptCtrl.disable(item.id).then(() => {
// list[index].status = SCRIPT_STATUS_DISABLE;
// });
// }
// p.catch((err) => {
// Message.error(err);
// }).finally(() => {
// list[index].loading = false;
// setScriptList([...list]);
// });
// return list;
// });
dispatch(requestEnableScript({ uuid: item.uuid, enable: checked }));
}}
/>
);
@ -466,7 +475,7 @@ function ScriptList() {
dataIndex: "action",
key: "action",
width: 160,
render(col, item: Script) {
render(col, item: ScriptLoading) {
return (
<Button.Group>
<Link to={`/script/editor/${item.uuid}`}>
@ -482,18 +491,13 @@ function ScriptList() {
title={t("confirm_delete_script")}
icon={<RiDeleteBin5Fill />}
onOk={() => {
// setScriptList((list) => {
// return list.filter((i) => i.id !== item.id);
// });
// scriptCtrl.delete(item.id).catch((e) => {
// Message.error(`${t("delete_failed")}: ${e}`);
// });
dispatch(requestDeleteScript(item.uuid));
}}
>
<Button
type="text"
icon={<RiDeleteBin5Fill />}
onClick={() => {}}
loading={item.actionLoading}
style={{
color: "var(--color-text-2)",
}}
@ -504,13 +508,39 @@ function ScriptList() {
type="text"
icon={<RiSettings3Fill />}
onClick={() => {
// Get value
// getValues(item).then((newValues) => {
// setUserConfig({
// userConfig: { ...item.config! },
// script: item,
// values: newValues,
// });
getValues(item).then((newValues) => {
setUserConfig({
userConfig: { ...item.config! },
script: item,
values: newValues,
});
});
}}
style={{
color: "var(--color-text-2)",
}}
/>
)}
{item.type !== SCRIPT_TYPE_NORMAL && (
<Button
type="text"
icon={item.runStatus === SCRIPT_RUN_STATUS_RUNNING ? <RiStopFill /> : <RiPlayFill />}
loading={item.actionLoading}
onClick={async () => {
if (item.runStatus === SCRIPT_RUN_STATUS_RUNNING) {
// Stop script
}
console.log(item.runStatus);
// Stop script
// Message.loading({
// id: "script-stop",
// content: t("stopping_script"),
// });
// await runtimeCtrl.stopScript(item.id);
// Message.success({
// id: "script-stop",
// content: t("script_stopped"),
// duration: 3000,
// });
}}
style={{
@ -518,59 +548,6 @@ function ScriptList() {
}}
/>
)}
{item.type !== SCRIPT_TYPE_NORMAL &&
(item.runStatus === SCRIPT_RUN_STATUS_RUNNING ? (
<Button
type="text"
icon={<RiStopFill />}
onClick={async () => {
// Stop script
// Message.loading({
// id: "script-stop",
// content: t("stopping_script"),
// });
// await runtimeCtrl.stopScript(item.id);
// Message.success({
// id: "script-stop",
// content: t("script_stopped"),
// duration: 3000,
// });
}}
style={{
color: "var(--color-text-2)",
}}
/>
) : (
<Button
type="text"
icon={<RiPlayFill />}
onClick={async () => {
// Start script
// Message.loading({
// id: "script-run",
// content: t("starting_script"),
// });
// await runtimeCtrl.startScript(item.id);
// Message.success({
// id: "script-run",
// content: t("script_started"),
// duration: 3000,
// });
// setScriptList((list) => {
// for (let i = 0; i < list.length; i += 1) {
// if (list[i].id === item.id) {
// list[i].runStatus = SCRIPT_RUN_STATUS_RUNNING;
// break;
// }
// }
// return [...list];
// });
}}
style={{
color: "var(--color-text-2)",
}}
/>
))}
{item.metadata.cloudcat && (
<Button
type="text"
@ -591,28 +568,20 @@ function ScriptList() {
const [newColumns, setNewColumns] = useState<ColumnProps[]>([]);
// 设置列和排序
// 设置列和判断是否打开用户配置
useEffect(() => {
// const dao = new ScriptDAO();
// dao.table
// .orderBy("sort")
// .toArray()
// .then(async (scripts) => {
// // Sort when a new script is added (-1)
// scriptListSort(scripts);
// // Open user config panel
// if (openUserConfig) {
// const script = scripts.find((item) => item.id === openUserConfig);
// if (script && script.config) {
// setUserConfig({
// script,
// userConfig: script.config,
// values: await getValues(script),
// });
// }
// }
// setScriptList(scripts);
// });
if (openUserConfig) {
const script = scriptList.find((item) => item.uuid === openUserConfig);
if (script && script.config) {
getValues(script).then((values) => {
setUserConfig({
script,
userConfig: script.config!,
values: values,
});
});
}
}
setNewColumns(
columns.map((item) => {
item.width = scriptListColumnWidth[item.key!] ?? item.width;
@ -640,24 +609,21 @@ function ScriptList() {
return;
}
if (active.id !== over.id) {
// setScriptList((items) => {
// let oldIndex = 0;
// let newIndex = 0;
// items.forEach((item, index) => {
// if (item.id === active.id) {
// oldIndex = index;
// } else if (item.id === over.id) {
// newIndex = index;
// }
// });
// const newItems = arrayMove(items, oldIndex, newIndex);
// scriptListSort(newItems);
// return newItems;
// });
console.log(active);
let oldIndex = 0;
let newIndex = 0;
scriptList.forEach((item, index) => {
if (item.uuid === active.id) {
oldIndex = index;
} else if (item.uuid === over.id) {
newIndex = index;
}
});
dispatch(sortScript({ uuid: active.id as string, newIndex, oldIndex }));
}
}}
>
<SortableContext items={scriptList} strategy={verticalListSortingStrategy}>
<SortableContext items={scriptList.map((s) => ({ ...s, id: s.uuid }))} strategy={verticalListSortingStrategy}>
<table ref={ref} {...props} />
</SortableContext>
</DndContext>
@ -970,7 +936,7 @@ function ScriptList() {
<Table
className="arco-drag-table-container"
components={components}
rowKey="id"
rowKey="uuid"
tableLayoutFixed
columns={dealColumns}
data={scriptList}

View File

@ -148,7 +148,7 @@ export function ListHomeRender({ script }: { script: Script }) {
}
export function getValues(script: Script) {
return {};
return Promise.resolve({});
}
export type ScriptIconsProps = {

View File

@ -1,3 +1,23 @@
function main() {}
import LoggerCore from "./app/logger/core";
import DBWriter from "./app/logger/db_writer";
import MessageWriter from "./app/logger/message_writer";
import { LoggerDAO } from "./app/repo/logger";
import { OffscreenManager } from "./app/service/offscreen";
function main() {
// 建立与offscreen页面的连接
// 初始化日志组件
const loggerCore = new LoggerCore({
debug: process.env.NODE_ENV === "development",
writer: new MessageWriter(connectSandbox),
labels: { env: "sandbox" },
});
loggerCore.logger().debug("offscreen start");
// 初始化管理器
const manager = new OffscreenManager();
manager.initManager();
}
main();

View File

@ -1,31 +1,99 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { createAppSlice } from "../hooks";
import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { Script, SCRIPT_STATUS_DISABLE, SCRIPT_STATUS_ENABLE, ScriptDAO } from "@App/app/repo/scripts";
import { arrayMove } from "@dnd-kit/sortable";
import { ScriptClient } from "@App/app/service/service_worker/client";
export const fetchScriptList = createAsyncThunk("script/fetchScriptList", () => {
return new ScriptDAO().all();
export const fetchAndSortScriptList = createAsyncThunk("script/fetchScriptList", async () => {
// 排序
const dao = new ScriptDAO();
const scripts = await dao.all();
scripts.sort((a, b) => a.sort - b.sort);
for (let i = 0; i < scripts.length; i += 1) {
if (scripts[i].sort !== i) {
dao.update(scripts[i].uuid, { sort: i });
scripts[i].sort = i;
}
}
return scripts;
});
export const requestEnableScript = createAsyncThunk(
"script/enableScript",
(param: { uuid: string; enable: boolean }) => {
return new ScriptClient().enable(param.uuid, param.enable);
}
);
export const requestDeleteScript = createAsyncThunk("script/deleteScript", async (uuid: string) => {
return new ScriptClient().delete(uuid);
});
export type ScriptLoading = Script & { enableLoading?: boolean; actionLoading?: boolean };
const updateScript = (scripts: ScriptLoading[], uuid: string, update: (s: ScriptLoading) => void) => {
const script = scripts.find((s) => s.uuid === uuid);
if (script) {
update(script);
}
};
export const scriptSlice = createAppSlice({
name: "script",
initialState: {
scripts: [] as Script[],
scripts: [] as ScriptLoading[],
},
reducers: {
upsertScript: (state, action: PayloadAction<Script>) => {
const script = state.scripts.find((s) => s.uuid === action.payload.uuid);
if (script) {
Object.assign(script, action.payload);
} else {
// 放到第一
state.scripts.splice(0, 0, action.payload);
}
},
deleteScript: (state, action: PayloadAction<string>) => {
state.scripts = state.scripts.filter((s) => s.uuid !== action.payload);
},
sortScript: (state, action: PayloadAction<{ uuid: string; newIndex: number; oldIndex: number }>) => {
const dao = new ScriptDAO();
const newItems = arrayMove(state.scripts, action.payload.oldIndex, action.payload.newIndex);
for (let i = 0; i < state.scripts.length; i += 1) {
if (newItems[i].sort !== i) {
dao.update(newItems[i].uuid, { sort: i });
newItems[i].sort = i;
}
}
state.scripts = newItems;
},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchScriptList.fulfilled, (state, action) => {
const newScripts: Script[] = [];
action.payload.forEach((item) => {
newScripts.push(item);
});
state.scripts = newScripts;
});
builder
.addCase(fetchAndSortScriptList.fulfilled, (state, action) => {
state.scripts = action.payload;
})
.addCase(requestEnableScript.fulfilled, (state, action) => {
updateScript(state.scripts, action.meta.arg.uuid, (script) => {
script.enableLoading = false;
script.status = action.meta.arg.enable ? SCRIPT_STATUS_ENABLE : SCRIPT_STATUS_DISABLE;
});
})
.addCase(requestEnableScript.pending, (state, action) =>
updateScript(state.scripts, action.meta.arg.uuid, (s) => (s.enableLoading = true))
)
.addCase(requestDeleteScript.fulfilled, (state, action) => {
state.scripts = state.scripts.filter((s) => s.uuid !== action.meta.arg);
})
.addCase(requestDeleteScript.pending, (state, action) =>
updateScript(state.scripts, action.meta.arg, (s) => (s.actionLoading = true))
);
},
selectors: {
selectScripts: (state) => state.scripts,
},
});
// export const {} = scriptSlice.actions;
export const { sortScript, upsertScript, deleteScript } = scriptSlice.actions;
export const { selectScripts } = scriptSlice.selectors;