arcway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +711 -0
- package/client/env.js +55 -0
- package/client/fetcher.js +50 -0
- package/client/graphql.js +35 -0
- package/client/head.js +140 -0
- package/client/hooks/use-api.js +80 -0
- package/client/hooks/use-debounce.js +12 -0
- package/client/hooks/use-form.js +86 -0
- package/client/hooks/use-graphql.js +30 -0
- package/client/hooks/use-interval.js +12 -0
- package/client/hooks/use-mutation.js +27 -0
- package/client/hooks/use-query.js +45 -0
- package/client/hooks/web/use-click-outside.js +22 -0
- package/client/hooks/web/use-local-storage.js +42 -0
- package/client/index.js +62 -0
- package/client/page-loader.js +155 -0
- package/client/provider.js +53 -0
- package/client/query.js +13 -0
- package/client/router.jsx +303 -0
- package/client/ui/accordion.jsx +65 -0
- package/client/ui/accordion.stories.jsx +48 -0
- package/client/ui/alert-dialog.jsx +122 -0
- package/client/ui/alert-dialog.stories.jsx +44 -0
- package/client/ui/alert.jsx +52 -0
- package/client/ui/alert.stories.jsx +31 -0
- package/client/ui/app-shell.jsx +39 -0
- package/client/ui/app-shell.stories.jsx +51 -0
- package/client/ui/aspect-ratio.jsx +6 -0
- package/client/ui/aspect-ratio.stories.jsx +69 -0
- package/client/ui/avatar.jsx +78 -0
- package/client/ui/avatar.stories.jsx +62 -0
- package/client/ui/badge.jsx +34 -0
- package/client/ui/badge.stories.js +32 -0
- package/client/ui/breadcrumb.jsx +86 -0
- package/client/ui/breadcrumb.stories.jsx +43 -0
- package/client/ui/button-group.jsx +58 -0
- package/client/ui/button-group.stories.jsx +67 -0
- package/client/ui/button.jsx +46 -0
- package/client/ui/button.stories.js +72 -0
- package/client/ui/calendar.jsx +172 -0
- package/client/ui/card.jsx +57 -0
- package/client/ui/card.stories.jsx +33 -0
- package/client/ui/carousel.jsx +167 -0
- package/client/ui/chart.jsx +244 -0
- package/client/ui/checkbox.jsx +24 -0
- package/client/ui/checkbox.stories.js +33 -0
- package/client/ui/collapsible.jsx +12 -0
- package/client/ui/collapsible.stories.jsx +42 -0
- package/client/ui/combobox.jsx +223 -0
- package/client/ui/command.jsx +128 -0
- package/client/ui/context-menu.jsx +170 -0
- package/client/ui/context-menu.stories.jsx +35 -0
- package/client/ui/dialog.jsx +109 -0
- package/client/ui/dialog.stories.jsx +37 -0
- package/client/ui/direction.jsx +9 -0
- package/client/ui/drawer.jsx +87 -0
- package/client/ui/dropdown-menu.jsx +172 -0
- package/client/ui/dropdown-menu.stories.jsx +34 -0
- package/client/ui/empty.jsx +76 -0
- package/client/ui/empty.stories.jsx +64 -0
- package/client/ui/field.jsx +174 -0
- package/client/ui/field.stories.jsx +118 -0
- package/client/ui/form.jsx +17 -0
- package/client/ui/hooks/use-mobile.js +16 -0
- package/client/ui/hover-card.jsx +26 -0
- package/client/ui/hover-card.stories.jsx +28 -0
- package/client/ui/index.js +649 -0
- package/client/ui/input-group.jsx +116 -0
- package/client/ui/input-group.stories.jsx +65 -0
- package/client/ui/input-otp.jsx +62 -0
- package/client/ui/input.jsx +16 -0
- package/client/ui/input.stories.js +27 -0
- package/client/ui/item.jsx +155 -0
- package/client/ui/item.stories.jsx +118 -0
- package/client/ui/kbd.jsx +24 -0
- package/client/ui/kbd.stories.jsx +32 -0
- package/client/ui/label.jsx +16 -0
- package/client/ui/label.stories.js +25 -0
- package/client/ui/lib/utils.js +6 -0
- package/client/ui/main-content.jsx +30 -0
- package/client/ui/menubar.jsx +189 -0
- package/client/ui/menubar.stories.jsx +43 -0
- package/client/ui/native-select.jsx +34 -0
- package/client/ui/native-select.stories.jsx +67 -0
- package/client/ui/navigation-menu.jsx +120 -0
- package/client/ui/navigation-menu.stories.jsx +45 -0
- package/client/ui/pagination.jsx +92 -0
- package/client/ui/pagination.stories.jsx +52 -0
- package/client/ui/panel.jsx +66 -0
- package/client/ui/popover.jsx +54 -0
- package/client/ui/popover.stories.jsx +27 -0
- package/client/ui/progress.jsx +19 -0
- package/client/ui/progress.stories.js +34 -0
- package/client/ui/radio-group.jsx +33 -0
- package/client/ui/radio-group.stories.jsx +49 -0
- package/client/ui/resizable.jsx +33 -0
- package/client/ui/scroll-area.jsx +41 -0
- package/client/ui/scroll-area.stories.jsx +43 -0
- package/client/ui/select.jsx +145 -0
- package/client/ui/select.stories.jsx +80 -0
- package/client/ui/separator.jsx +18 -0
- package/client/ui/separator.stories.jsx +37 -0
- package/client/ui/sheet.jsx +95 -0
- package/client/ui/sheet.stories.jsx +56 -0
- package/client/ui/sidebar.jsx +544 -0
- package/client/ui/skeleton.jsx +8 -0
- package/client/ui/skeleton.stories.js +23 -0
- package/client/ui/slider.jsx +41 -0
- package/client/ui/slider.stories.js +31 -0
- package/client/ui/sonner.jsx +37 -0
- package/client/ui/spinner.jsx +14 -0
- package/client/ui/spinner.stories.js +16 -0
- package/client/ui/style-mira.css +1316 -0
- package/client/ui/switch.jsx +22 -0
- package/client/ui/switch.stories.js +44 -0
- package/client/ui/table.jsx +33 -0
- package/client/ui/table.stories.jsx +42 -0
- package/client/ui/tabs.jsx +63 -0
- package/client/ui/tabs.stories.jsx +45 -0
- package/client/ui/textarea.jsx +15 -0
- package/client/ui/textarea.stories.js +33 -0
- package/client/ui/theme.css +459 -0
- package/client/ui/toggle-group.jsx +62 -0
- package/client/ui/toggle-group.stories.jsx +68 -0
- package/client/ui/toggle.jsx +34 -0
- package/client/ui/toggle.stories.js +46 -0
- package/client/ui/tooltip.jsx +37 -0
- package/client/ui/tooltip.stories.jsx +32 -0
- package/client/ui/use-transition.js +35 -0
- package/client/ws.js +132 -0
- package/package.json +134 -0
- package/server/bin/cli.js +42 -0
- package/server/bin/commands/build.js +23 -0
- package/server/bin/commands/dev.js +57 -0
- package/server/bin/commands/docs.js +30 -0
- package/server/bin/commands/graphql-schema.js +32 -0
- package/server/bin/commands/lint.js +35 -0
- package/server/bin/commands/mcp.js +26 -0
- package/server/bin/commands/migrate.js +82 -0
- package/server/bin/commands/schema.js +41 -0
- package/server/bin/commands/seed.js +36 -0
- package/server/bin/commands/start.js +31 -0
- package/server/bin/commands/test.js +20 -0
- package/server/bin/solo.js +4 -0
- package/server/boot/index.js +150 -0
- package/server/boot.js +2 -0
- package/server/build.js +23 -0
- package/server/cache/drivers/memory.js +23 -0
- package/server/cache/drivers/redis.js +28 -0
- package/server/cache/index.js +69 -0
- package/server/config/loader.js +89 -0
- package/server/config/modules/api.js +17 -0
- package/server/config/modules/build.js +9 -0
- package/server/config/modules/cache.js +10 -0
- package/server/config/modules/database.js +29 -0
- package/server/config/modules/events.js +15 -0
- package/server/config/modules/files.js +15 -0
- package/server/config/modules/jobs.js +20 -0
- package/server/config/modules/logger.js +9 -0
- package/server/config/modules/mail.js +11 -0
- package/server/config/modules/mcp.js +9 -0
- package/server/config/modules/pages.js +20 -0
- package/server/config/modules/queue.js +10 -0
- package/server/config/modules/redis.js +9 -0
- package/server/config/modules/server.js +30 -0
- package/server/config/modules/session.js +9 -0
- package/server/config/modules/websocket.js +11 -0
- package/server/constants.js +67 -0
- package/server/context.js +15 -0
- package/server/db/index.js +87 -0
- package/server/db/schema/drivers/mysql.js +28 -0
- package/server/db/schema/drivers/pg.js +34 -0
- package/server/db/schema/drivers/sqlite.js +22 -0
- package/server/db/schema/index.js +78 -0
- package/server/db/seeds.js +22 -0
- package/server/discovery.js +67 -0
- package/server/docs/openapi.js +153 -0
- package/server/env.js +17 -0
- package/server/events/drivers/memory.js +45 -0
- package/server/events/drivers/redis.js +64 -0
- package/server/events/handler.js +67 -0
- package/server/events/index.js +35 -0
- package/server/events/pattern.js +5 -0
- package/server/files/drivers/local.js +83 -0
- package/server/files/drivers/s3.js +113 -0
- package/server/files/index.js +57 -0
- package/server/filewatcher/index.js +156 -0
- package/server/glob.js +6 -0
- package/server/graphql/discovery.js +70 -0
- package/server/graphql/handler.js +41 -0
- package/server/graphql/index.js +13 -0
- package/server/graphql/loaders.js +19 -0
- package/server/graphql/merge.js +48 -0
- package/server/graphql/subscriptions.js +43 -0
- package/server/health.js +34 -0
- package/server/helpers.js +9 -0
- package/server/index.js +55 -0
- package/server/internals.js +139 -0
- package/server/jobs/cron.js +10 -0
- package/server/jobs/drivers/knex-queue.js +207 -0
- package/server/jobs/drivers/lease.js +148 -0
- package/server/jobs/drivers/memory-queue.js +134 -0
- package/server/jobs/queue.js +27 -0
- package/server/jobs/runner.js +197 -0
- package/server/jobs/throughput.js +63 -0
- package/server/lib/vault/encrypt.js +40 -0
- package/server/lib/vault/ids.js +9 -0
- package/server/lib/vault/index.js +14 -0
- package/server/lib/vault/jwt.js +55 -0
- package/server/lib/vault/password.js +10 -0
- package/server/lint/boundaries.js +77 -0
- package/server/logger/index.js +130 -0
- package/server/mail/drivers/console.js +31 -0
- package/server/mail/drivers/smtp.js +34 -0
- package/server/mail/imap.js +105 -0
- package/server/mail/inbound-store.js +58 -0
- package/server/mail/inbound.js +79 -0
- package/server/mail/index.js +112 -0
- package/server/mcp/debug-api.js +137 -0
- package/server/mcp/helpers.js +30 -0
- package/server/mcp/index.js +77 -0
- package/server/mcp/runtime.js +7 -0
- package/server/mcp/server.js +19 -0
- package/server/mcp/tools/debugging.js +133 -0
- package/server/mcp/tools/introspection.js +87 -0
- package/server/middlewares/cors.js +30 -0
- package/server/middlewares/index.js +3 -0
- package/server/middlewares/require-session.js +15 -0
- package/server/module-loader.js +9 -0
- package/server/pages/build-client.js +187 -0
- package/server/pages/build-css.js +47 -0
- package/server/pages/build-manifest.js +55 -0
- package/server/pages/build-plugins.js +75 -0
- package/server/pages/build-server.js +115 -0
- package/server/pages/build.js +116 -0
- package/server/pages/discovery.js +120 -0
- package/server/pages/fonts.js +128 -0
- package/server/pages/handler.js +276 -0
- package/server/pages/hmr.js +176 -0
- package/server/pages/pages-router.js +78 -0
- package/server/pages/ssr.js +276 -0
- package/server/pages/static.js +92 -0
- package/server/pages/watcher.js +90 -0
- package/server/queue/drivers/knex.js +67 -0
- package/server/queue/drivers/redis.js +91 -0
- package/server/queue/index.js +61 -0
- package/server/rate-limit/consume.js +21 -0
- package/server/rate-limit/drivers/memory.js +24 -0
- package/server/rate-limit/drivers/redis.js +32 -0
- package/server/rate-limit/index.js +33 -0
- package/server/redis/index.js +67 -0
- package/server/ring-buffer.js +44 -0
- package/server/route.js +4 -0
- package/server/router/api-router.js +317 -0
- package/server/router/cors.js +31 -0
- package/server/router/middleware.js +91 -0
- package/server/router/routes.js +132 -0
- package/server/server.js +35 -0
- package/server/session/helpers.js +21 -0
- package/server/session/index.js +89 -0
- package/server/static/index.js +36 -0
- package/server/system-jobs/index.js +50 -0
- package/server/system-routes/index.js +84 -0
- package/server/testing/index.js +263 -0
- package/server/validation.js +41 -0
- package/server/watcher.js +34 -0
- package/server/web-server.js +231 -0
- package/server/ws/discovery.js +54 -0
- package/server/ws/index.js +14 -0
- package/server/ws/realtime.js +318 -0
- package/server/ws/registry.js +17 -0
- package/server/ws/server.js +152 -0
- package/server/ws/ws-router.js +335 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { buildPages } from './build.js';
|
|
2
|
+
import { createPagesHandler } from './handler.js';
|
|
3
|
+
import { createPagesWatcher } from './watcher.js';
|
|
4
|
+
|
|
5
|
+
class PagesRouter {
|
|
6
|
+
config;
|
|
7
|
+
rootDir;
|
|
8
|
+
log;
|
|
9
|
+
fileWatcher;
|
|
10
|
+
handler = null;
|
|
11
|
+
watcher = null;
|
|
12
|
+
|
|
13
|
+
constructor(config, { rootDir, log, fileWatcher, appContext }) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.rootDir = rootDir;
|
|
16
|
+
this.log = log;
|
|
17
|
+
this.fileWatcher = fileWatcher ?? null;
|
|
18
|
+
this.appContext = appContext ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init() {
|
|
22
|
+
const { config, rootDir, log, fileWatcher } = this;
|
|
23
|
+
const isDev = fileWatcher !== null;
|
|
24
|
+
|
|
25
|
+
let initialClientMetafile;
|
|
26
|
+
if (isDev) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await buildPages({
|
|
29
|
+
rootDir,
|
|
30
|
+
pagesDir: config.pages.dir,
|
|
31
|
+
fonts: config.pages?.fonts,
|
|
32
|
+
minify: false,
|
|
33
|
+
devMode: true,
|
|
34
|
+
});
|
|
35
|
+
initialClientMetafile = result.clientMetafile;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
log.error('Pages build failed', { error: String(err) });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.handler = createPagesHandler({
|
|
42
|
+
rootDir,
|
|
43
|
+
session: config.session,
|
|
44
|
+
appContext: this.appContext,
|
|
45
|
+
devMode: isDev,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!this.handler) return;
|
|
49
|
+
|
|
50
|
+
log.info('Pages: SSR enabled');
|
|
51
|
+
|
|
52
|
+
if (isDev) {
|
|
53
|
+
this.watcher = createPagesWatcher({
|
|
54
|
+
rootDir,
|
|
55
|
+
handler: this.handler,
|
|
56
|
+
log,
|
|
57
|
+
fonts: config.pages?.fonts,
|
|
58
|
+
initialClientMetafile,
|
|
59
|
+
fileWatcher,
|
|
60
|
+
});
|
|
61
|
+
log.info('Pages: watching for changes (HMR enabled)');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async handle(req, res) {
|
|
66
|
+
if (!this.handler) return false;
|
|
67
|
+
return this.handler.handle(req, res);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async close() {
|
|
71
|
+
if (this.watcher) {
|
|
72
|
+
await this.watcher.close();
|
|
73
|
+
this.watcher = null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { PagesRouter };
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { setSSRHeadData, clearSSRHeadData, renderHeadToString } from '#client/head.js';
|
|
5
|
+
import { collectPublicEnv, buildEnvScriptTag } from '#client/env.js';
|
|
6
|
+
import { buildHmrScript } from './hmr.js';
|
|
7
|
+
const REACT_INTERNALS_KEY = '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE';
|
|
8
|
+
function syncReactInternals(projectReactModule) {
|
|
9
|
+
try {
|
|
10
|
+
const fwRequire = createRequire(import.meta.url);
|
|
11
|
+
const fwReact = fwRequire('react');
|
|
12
|
+
const projectInternals = projectReactModule[REACT_INTERNALS_KEY];
|
|
13
|
+
const fwInternals = fwReact[REACT_INTERNALS_KEY];
|
|
14
|
+
if (projectInternals && fwInternals && projectInternals !== fwInternals) {
|
|
15
|
+
const keys = new Set([
|
|
16
|
+
...Object.keys(projectInternals),
|
|
17
|
+
...Object.keys(fwInternals),
|
|
18
|
+
]);
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
Object.defineProperty(fwInternals, key, {
|
|
21
|
+
get() {
|
|
22
|
+
return projectInternals[key];
|
|
23
|
+
},
|
|
24
|
+
set(v) {
|
|
25
|
+
projectInternals[key] = v;
|
|
26
|
+
},
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// React internals sync is best-effort — mismatch is non-fatal
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function loadComponent(bundlePath, cacheKey, componentCache, cacheVersion) {
|
|
36
|
+
if (componentCache.has(cacheKey)) {
|
|
37
|
+
return componentCache.get(cacheKey);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const fileUrl = pathToFileURL(bundlePath).href;
|
|
41
|
+
const specifier = cacheVersion > 0 ? `${fileUrl}?v=${cacheVersion}` : fileUrl;
|
|
42
|
+
const mod = await import(specifier);
|
|
43
|
+
const Component = mod.default;
|
|
44
|
+
componentCache.set(cacheKey, Component);
|
|
45
|
+
return Component;
|
|
46
|
+
} catch {
|
|
47
|
+
// Component failed to load (missing file, syntax error) — caller handles null
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const ROUTER_CTX_KEY = '__router_context__';
|
|
52
|
+
const PROVIDER_CTX_KEY = '__provider_context__';
|
|
53
|
+
function wrapWithProviders(createElement, element, pathname, params) {
|
|
54
|
+
const routerCtx = globalThis[ROUTER_CTX_KEY];
|
|
55
|
+
if (routerCtx) {
|
|
56
|
+
const routerValue = {
|
|
57
|
+
pathname,
|
|
58
|
+
params,
|
|
59
|
+
navigate: () => {},
|
|
60
|
+
// No-op during SSR
|
|
61
|
+
};
|
|
62
|
+
element = createElement(routerCtx.Provider, { value: routerValue }, element);
|
|
63
|
+
}
|
|
64
|
+
const providerCtx = globalThis[PROVIDER_CTX_KEY];
|
|
65
|
+
if (providerCtx) {
|
|
66
|
+
const apiValue = { pathPrefix: '' };
|
|
67
|
+
element = createElement(providerCtx.Provider, { value: apiValue }, element);
|
|
68
|
+
}
|
|
69
|
+
return element;
|
|
70
|
+
}
|
|
71
|
+
function buildCssLinkTag(manifest) {
|
|
72
|
+
if (!manifest.cssBundle) return '';
|
|
73
|
+
return `<link rel="stylesheet" href="/static/${manifest.cssBundle}" />`;
|
|
74
|
+
}
|
|
75
|
+
function buildFontPreloadTags(manifest) {
|
|
76
|
+
return manifest.fontPreloadHtml ?? '';
|
|
77
|
+
}
|
|
78
|
+
function buildLiveReloadScript() {
|
|
79
|
+
return `<script>
|
|
80
|
+
(function(){var es=new EventSource("/__livereload");es.onmessage=function(e){if(e.data==="reload")window.location.reload()};es.onerror=function(){es.close()}})();
|
|
81
|
+
</script>`;
|
|
82
|
+
}
|
|
83
|
+
function buildScriptTags(route, manifest) {
|
|
84
|
+
const tags = [];
|
|
85
|
+
for (const chunk of manifest.sharedChunks) {
|
|
86
|
+
tags.push(`<script type="module" src="/static/${chunk}"></script>`);
|
|
87
|
+
}
|
|
88
|
+
tags.push(`<script type="module" src="/static/${route.clientBundle}"></script>`);
|
|
89
|
+
return tags.join('\n');
|
|
90
|
+
}
|
|
91
|
+
function buildHtmlShell({ headHtml, fontPreloadTags, cssLinkTag, bodysuffix }) {
|
|
92
|
+
return {
|
|
93
|
+
head: `<!DOCTYPE html>
|
|
94
|
+
<html>
|
|
95
|
+
<head>
|
|
96
|
+
<meta charset="utf-8" />
|
|
97
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
98
|
+
${fontPreloadTags}
|
|
99
|
+
${cssLinkTag}
|
|
100
|
+
${headHtml}
|
|
101
|
+
</head>
|
|
102
|
+
<body>
|
|
103
|
+
<div id="__app">`,
|
|
104
|
+
tail: `</div>
|
|
105
|
+
${bodysuffix ?? ''}
|
|
106
|
+
</body>
|
|
107
|
+
</html>`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function renderPage(
|
|
111
|
+
route,
|
|
112
|
+
params,
|
|
113
|
+
pathname,
|
|
114
|
+
manifest,
|
|
115
|
+
clientManifestJson,
|
|
116
|
+
outDir,
|
|
117
|
+
res,
|
|
118
|
+
componentCache,
|
|
119
|
+
cacheVersion,
|
|
120
|
+
react,
|
|
121
|
+
devMode,
|
|
122
|
+
) {
|
|
123
|
+
const { createElement, renderToPipeableStream } = react;
|
|
124
|
+
const bundlePath = path.join(outDir, route.serverBundle);
|
|
125
|
+
const Component = await loadComponent(
|
|
126
|
+
bundlePath,
|
|
127
|
+
`page:${route.pattern}`,
|
|
128
|
+
componentCache,
|
|
129
|
+
cacheVersion,
|
|
130
|
+
);
|
|
131
|
+
if (!Component || typeof Component !== 'function') {
|
|
132
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
133
|
+
res.end('<h1>500 - Page component is not a valid React component</h1>');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const layoutComponents = [];
|
|
137
|
+
for (const layoutBundle of route.layoutServerBundles) {
|
|
138
|
+
const layoutPath = path.join(outDir, layoutBundle);
|
|
139
|
+
const Layout = await loadComponent(
|
|
140
|
+
layoutPath,
|
|
141
|
+
`layout:${layoutBundle}`,
|
|
142
|
+
componentCache,
|
|
143
|
+
cacheVersion,
|
|
144
|
+
);
|
|
145
|
+
if (Layout && typeof Layout === 'function') {
|
|
146
|
+
layoutComponents.push(Layout);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const scriptTags = buildScriptTags(route, manifest);
|
|
150
|
+
const cssLinkTag = buildCssLinkTag(manifest);
|
|
151
|
+
const fontPreloadTags = buildFontPreloadTags(manifest);
|
|
152
|
+
const envScriptTag = buildEnvScriptTag(collectPublicEnv());
|
|
153
|
+
const hmrTag = devMode ? buildHmrScript() : '';
|
|
154
|
+
const liveReloadTag = devMode && !hmrTag ? buildLiveReloadScript() : '';
|
|
155
|
+
const propsJson = JSON.stringify(params).replace(/</g, '\\u003c');
|
|
156
|
+
const headData = { meta: [], links: [] };
|
|
157
|
+
setSSRHeadData(headData);
|
|
158
|
+
let element = createElement(Component, params);
|
|
159
|
+
for (let i = layoutComponents.length - 1; i >= 0; i--) {
|
|
160
|
+
element = createElement(layoutComponents[i], null, element);
|
|
161
|
+
}
|
|
162
|
+
element = wrapWithProviders(createElement, element, pathname, params);
|
|
163
|
+
const { PassThrough } = await import('node:stream');
|
|
164
|
+
const { pipe } = renderToPipeableStream(element, {
|
|
165
|
+
onAllReady() {
|
|
166
|
+
if (res.headersSent) return;
|
|
167
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
168
|
+
const headHtml = renderHeadToString(headData);
|
|
169
|
+
const shell = buildHtmlShell({
|
|
170
|
+
headHtml,
|
|
171
|
+
fontPreloadTags,
|
|
172
|
+
cssLinkTag,
|
|
173
|
+
bodysuffix: `${envScriptTag}
|
|
174
|
+
<script id="__app_props" type="application/json">${propsJson}</script>
|
|
175
|
+
<script id="__app_manifest" type="application/json">${clientManifestJson}</script>
|
|
176
|
+
${hmrTag}
|
|
177
|
+
${scriptTags}
|
|
178
|
+
${liveReloadTag}`,
|
|
179
|
+
});
|
|
180
|
+
res.write(shell.head);
|
|
181
|
+
const pass = new PassThrough();
|
|
182
|
+
pass.on('data', (chunk) => res.write(chunk));
|
|
183
|
+
pass.on('end', () => {
|
|
184
|
+
res.write(shell.tail);
|
|
185
|
+
clearSSRHeadData();
|
|
186
|
+
res.end();
|
|
187
|
+
});
|
|
188
|
+
pipe(pass);
|
|
189
|
+
},
|
|
190
|
+
onError(err) {
|
|
191
|
+
clearSSRHeadData();
|
|
192
|
+
console.error('SSR render error:', err);
|
|
193
|
+
if (!res.headersSent) {
|
|
194
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
195
|
+
res.end('<h1>500 - Render Error</h1>');
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
async function renderErrorPage(
|
|
201
|
+
bundlePath,
|
|
202
|
+
statusCode,
|
|
203
|
+
props,
|
|
204
|
+
outDir,
|
|
205
|
+
res,
|
|
206
|
+
componentCache,
|
|
207
|
+
manifest,
|
|
208
|
+
cacheVersion,
|
|
209
|
+
react,
|
|
210
|
+
) {
|
|
211
|
+
const { createElement, renderToPipeableStream } = react;
|
|
212
|
+
const fullPath = path.join(outDir, bundlePath);
|
|
213
|
+
const Component = await loadComponent(
|
|
214
|
+
fullPath,
|
|
215
|
+
`error:${bundlePath}`,
|
|
216
|
+
componentCache,
|
|
217
|
+
cacheVersion,
|
|
218
|
+
);
|
|
219
|
+
if (!Component || typeof Component !== 'function') {
|
|
220
|
+
res.writeHead(statusCode, { 'Content-Type': 'text/html' });
|
|
221
|
+
res.end(
|
|
222
|
+
`<h1>${statusCode} - ${statusCode === 404 ? 'Not Found' : 'Internal Server Error'}</h1>`,
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const headData = { meta: [], links: [] };
|
|
227
|
+
setSSRHeadData(headData);
|
|
228
|
+
const cssLinkTag = buildCssLinkTag(manifest);
|
|
229
|
+
const fontPreloadTags = buildFontPreloadTags(manifest);
|
|
230
|
+
const errorPath = props.pathname ?? '/';
|
|
231
|
+
const element = wrapWithProviders(
|
|
232
|
+
createElement,
|
|
233
|
+
createElement(Component, { statusCode, ...props }),
|
|
234
|
+
errorPath,
|
|
235
|
+
{},
|
|
236
|
+
);
|
|
237
|
+
const { PassThrough } = await import('node:stream');
|
|
238
|
+
const { pipe } = renderToPipeableStream(element, {
|
|
239
|
+
onAllReady() {
|
|
240
|
+
if (res.headersSent) return;
|
|
241
|
+
const headHtml = renderHeadToString(headData);
|
|
242
|
+
res.writeHead(statusCode, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
243
|
+
const shell = buildHtmlShell({ headHtml, fontPreloadTags, cssLinkTag });
|
|
244
|
+
res.write(shell.head);
|
|
245
|
+
const pass = new PassThrough();
|
|
246
|
+
pass.on('data', (chunk) => res.write(chunk));
|
|
247
|
+
pass.on('end', () => {
|
|
248
|
+
res.write(shell.tail);
|
|
249
|
+
clearSSRHeadData();
|
|
250
|
+
res.end();
|
|
251
|
+
});
|
|
252
|
+
pipe(pass);
|
|
253
|
+
},
|
|
254
|
+
onError(err) {
|
|
255
|
+
clearSSRHeadData();
|
|
256
|
+
console.error('Error page render error:', err);
|
|
257
|
+
if (!res.headersSent) {
|
|
258
|
+
res.writeHead(statusCode, { 'Content-Type': 'text/html' });
|
|
259
|
+
res.end(
|
|
260
|
+
`<h1>${statusCode} - ${statusCode === 404 ? 'Not Found' : 'Internal Server Error'}</h1>`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
buildCssLinkTag,
|
|
268
|
+
buildFontPreloadTags,
|
|
269
|
+
buildLiveReloadScript,
|
|
270
|
+
buildScriptTags,
|
|
271
|
+
loadComponent,
|
|
272
|
+
renderErrorPage,
|
|
273
|
+
renderPage,
|
|
274
|
+
syncReactInternals,
|
|
275
|
+
wrapWithProviders,
|
|
276
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
const MIME_TYPES = {
|
|
4
|
+
'.html': 'text/html',
|
|
5
|
+
'.css': 'text/css',
|
|
6
|
+
'.js': 'application/javascript',
|
|
7
|
+
'.mjs': 'application/javascript',
|
|
8
|
+
'.json': 'application/json',
|
|
9
|
+
'.map': 'application/json',
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.jpg': 'image/jpeg',
|
|
12
|
+
'.jpeg': 'image/jpeg',
|
|
13
|
+
'.gif': 'image/gif',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.ico': 'image/x-icon',
|
|
16
|
+
'.woff': 'font/woff',
|
|
17
|
+
'.woff2': 'font/woff2',
|
|
18
|
+
'.ttf': 'font/ttf',
|
|
19
|
+
'.txt': 'text/plain',
|
|
20
|
+
'.xml': 'application/xml',
|
|
21
|
+
'.webp': 'image/webp',
|
|
22
|
+
'.mp4': 'video/mp4',
|
|
23
|
+
'.webm': 'video/webm',
|
|
24
|
+
'.pdf': 'application/pdf',
|
|
25
|
+
};
|
|
26
|
+
function serveStaticAsset(pathname, res, outDir) {
|
|
27
|
+
const relativePath = pathname.slice('/static/'.length);
|
|
28
|
+
const normalized = path.normalize(relativePath);
|
|
29
|
+
if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
|
|
30
|
+
res.writeHead(403);
|
|
31
|
+
res.end();
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (!normalized.startsWith('client')) {
|
|
35
|
+
res.writeHead(404);
|
|
36
|
+
res.end();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const filePath = path.resolve(outDir, normalized);
|
|
40
|
+
const resolvedOutDir = path.resolve(outDir);
|
|
41
|
+
if (!filePath.startsWith(resolvedOutDir + path.sep) && filePath !== resolvedOutDir) {
|
|
42
|
+
res.writeHead(403);
|
|
43
|
+
res.end();
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (!fs.existsSync(filePath)) {
|
|
47
|
+
res.writeHead(404);
|
|
48
|
+
res.end();
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
52
|
+
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
53
|
+
const basename = path.basename(normalized);
|
|
54
|
+
const hasContentHash = /.-[a-zA-Z0-9]+\.\w+$/.test(basename);
|
|
55
|
+
const cacheControl = hasContentHash
|
|
56
|
+
? 'public, max-age=31536000, immutable'
|
|
57
|
+
: 'public, max-age=3600';
|
|
58
|
+
res.writeHead(200, {
|
|
59
|
+
'Content-Type': contentType,
|
|
60
|
+
'Cache-Control': cacheControl,
|
|
61
|
+
});
|
|
62
|
+
const stream = fs.createReadStream(filePath);
|
|
63
|
+
stream.pipe(res);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
function servePublicFile(pathname, res, publicDir) {
|
|
67
|
+
const normalized = path.normalize(pathname.slice(1));
|
|
68
|
+
if (normalized.startsWith('..') || path.isAbsolute(normalized)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const filePath = path.resolve(publicDir, normalized);
|
|
72
|
+
const resolvedPublicDir = path.resolve(publicDir);
|
|
73
|
+
if (!filePath.startsWith(resolvedPublicDir + path.sep) && filePath !== resolvedPublicDir) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const stat = fs.statSync(filePath);
|
|
78
|
+
if (!stat.isFile()) return false;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
83
|
+
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
84
|
+
res.writeHead(200, {
|
|
85
|
+
'Content-Type': contentType,
|
|
86
|
+
'Cache-Control': 'public, max-age=3600',
|
|
87
|
+
});
|
|
88
|
+
const stream = fs.createReadStream(filePath);
|
|
89
|
+
stream.pipe(res);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
export { MIME_TYPES, servePublicFile, serveStaticAsset };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { buildPages } from './build.js';
|
|
3
|
+
import { diffClientMetafiles } from './hmr.js';
|
|
4
|
+
|
|
5
|
+
function createPagesWatcher(options) {
|
|
6
|
+
const { rootDir, handler, log, fonts, fileWatcher } = options;
|
|
7
|
+
let building = false;
|
|
8
|
+
let pendingRebuild = false;
|
|
9
|
+
let pendingStructuralChange = false;
|
|
10
|
+
let prevClientMetafile = options.initialClientMetafile;
|
|
11
|
+
const outDir = path.resolve(rootDir, '.build/pages');
|
|
12
|
+
|
|
13
|
+
async function rebuild(isStructuralChange) {
|
|
14
|
+
if (building) {
|
|
15
|
+
pendingRebuild = true;
|
|
16
|
+
if (isStructuralChange) pendingStructuralChange = true;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
building = true;
|
|
20
|
+
try {
|
|
21
|
+
const start = Date.now();
|
|
22
|
+
const result = await buildPages({ rootDir, fonts, minify: false, devMode: true });
|
|
23
|
+
handler.reload();
|
|
24
|
+
const elapsed = Date.now() - start;
|
|
25
|
+
if (handler.emitEvent && prevClientMetafile && result.clientMetafile && !isStructuralChange) {
|
|
26
|
+
const diff = diffClientMetafiles(prevClientMetafile, result.clientMetafile, outDir);
|
|
27
|
+
if (diff.needsFullReload) {
|
|
28
|
+
handler.emitEvent({ type: 'reload' });
|
|
29
|
+
log.info(`Pages rebuilt in ${elapsed}ms (full reload)`);
|
|
30
|
+
} else if (diff.changedNavBundles.length > 0) {
|
|
31
|
+
const manifestJson = handler.getClientManifestJson?.();
|
|
32
|
+
handler.emitEvent({
|
|
33
|
+
type: 'hmr-update',
|
|
34
|
+
modules: diff.changedNavBundles,
|
|
35
|
+
timestamp: Date.now(),
|
|
36
|
+
...(manifestJson ? { manifest: manifestJson } : {}),
|
|
37
|
+
});
|
|
38
|
+
log.info(
|
|
39
|
+
`Pages rebuilt in ${elapsed}ms (HMR: ${diff.changedNavBundles.length} module(s))`,
|
|
40
|
+
);
|
|
41
|
+
} else if (diff.changedCssBundle) {
|
|
42
|
+
handler.emitEvent({
|
|
43
|
+
type: 'css-update',
|
|
44
|
+
cssBundle: diff.changedCssBundle,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
log.info(`Pages rebuilt in ${elapsed}ms (CSS update)`);
|
|
48
|
+
} else {
|
|
49
|
+
log.info(`Pages rebuilt in ${elapsed}ms (no client changes)`);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if (handler.emitEvent) {
|
|
53
|
+
handler.emitEvent({ type: 'reload' });
|
|
54
|
+
}
|
|
55
|
+
log.info(`Pages rebuilt in ${elapsed}ms`);
|
|
56
|
+
}
|
|
57
|
+
prevClientMetafile = result.clientMetafile;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
log.error('Pages rebuild failed', { error: String(err) });
|
|
60
|
+
} finally {
|
|
61
|
+
building = false;
|
|
62
|
+
if (pendingRebuild) {
|
|
63
|
+
pendingRebuild = false;
|
|
64
|
+
const structural = pendingStructuralChange;
|
|
65
|
+
pendingStructuralChange = false;
|
|
66
|
+
await rebuild(structural);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fileWatcher.subscribe('pages', {
|
|
72
|
+
extensions: ['.tsx', '.jsx', '.ts', '.js', '.css'],
|
|
73
|
+
debounceMs: 150,
|
|
74
|
+
onChange(events) {
|
|
75
|
+
const isStructural = events.some((e) => e.event === 'add' || e.event === 'unlink');
|
|
76
|
+
for (const e of events) {
|
|
77
|
+
log.info(`Page ${e.event === 'unlink' ? 'removed' : e.event === 'add' ? 'added' : 'changed'}: ${e.relativePath}`);
|
|
78
|
+
}
|
|
79
|
+
return rebuild(isStructural);
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
async close() {
|
|
85
|
+
fileWatcher.unsubscribe('pages');
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { createPagesWatcher };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class KnexQueueDriver {
|
|
2
|
+
tableName;
|
|
3
|
+
constructor(db, tableName = 'arcway_queue') {
|
|
4
|
+
this.db = db;
|
|
5
|
+
this.tableName = tableName;
|
|
6
|
+
}
|
|
7
|
+
db;
|
|
8
|
+
async init() {
|
|
9
|
+
const exists = await this.db.schema.hasTable(this.tableName);
|
|
10
|
+
if (!exists) {
|
|
11
|
+
await this.db.schema.createTable(this.tableName, (table) => {
|
|
12
|
+
table.increments('id').primary();
|
|
13
|
+
table.string('namespace').notNullable().index();
|
|
14
|
+
table.string('topic').notNullable().index();
|
|
15
|
+
table.text('payload').notNullable();
|
|
16
|
+
table.string('status').notNullable().defaultTo('available');
|
|
17
|
+
table.timestamp('locked_at').nullable();
|
|
18
|
+
table.timestamp('created_at').defaultTo(this.db.fn.now());
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async push(namespace, topic, payload) {
|
|
23
|
+
await this.db(this.tableName).insert({
|
|
24
|
+
namespace,
|
|
25
|
+
topic,
|
|
26
|
+
payload,
|
|
27
|
+
status: 'available',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async pop(namespace, topic, count, lockCooldownMs) {
|
|
31
|
+
const cutoff = new Date(Date.now() - lockCooldownMs).toISOString();
|
|
32
|
+
const now = new Date().toISOString();
|
|
33
|
+
return this.db.transaction(async (trx) => {
|
|
34
|
+
const subquery = trx(this.tableName)
|
|
35
|
+
.where('namespace', namespace)
|
|
36
|
+
.andWhere('topic', topic)
|
|
37
|
+
.andWhere(function () {
|
|
38
|
+
this.where('status', 'available').orWhere(function () {
|
|
39
|
+
this.where('status', 'locked').andWhere('locked_at', '<', cutoff);
|
|
40
|
+
});
|
|
41
|
+
})
|
|
42
|
+
.orderBy('id', 'asc')
|
|
43
|
+
.limit(count)
|
|
44
|
+
.select('id');
|
|
45
|
+
const updatedCount = await trx(this.tableName)
|
|
46
|
+
.whereIn('id', subquery)
|
|
47
|
+
.update({ status: 'locked', locked_at: now });
|
|
48
|
+
if (updatedCount === 0) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const rows = await trx(this.tableName)
|
|
52
|
+
.where('namespace', namespace)
|
|
53
|
+
.andWhere('topic', topic)
|
|
54
|
+
.andWhere('status', 'locked')
|
|
55
|
+
.andWhere('locked_at', now)
|
|
56
|
+
.orderBy('id', 'asc')
|
|
57
|
+
.limit(count)
|
|
58
|
+
.select('id', 'payload');
|
|
59
|
+
return rows.map((r) => ({ id: r.id, payload: r.payload }));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async remove(namespace, ids) {
|
|
63
|
+
if (ids.length === 0) return;
|
|
64
|
+
await this.db(this.tableName).where('namespace', namespace).whereIn('id', ids).delete();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export default KnexQueueDriver;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { REDIS_SCAN_COUNT } from '../../constants.js';
|
|
2
|
+
const REQUEUE_EXPIRED_LUA = `
|
|
3
|
+
local expired = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
|
|
4
|
+
for i, member in ipairs(expired) do
|
|
5
|
+
redis.call('LPUSH', KEYS[2], member)
|
|
6
|
+
redis.call('ZREM', KEYS[1], member)
|
|
7
|
+
end
|
|
8
|
+
return #expired
|
|
9
|
+
`;
|
|
10
|
+
const POP_AND_LOCK_LUA = `
|
|
11
|
+
local raw = redis.call('LPOP', KEYS[1])
|
|
12
|
+
if raw then
|
|
13
|
+
redis.call('ZADD', KEYS[2], ARGV[1], raw)
|
|
14
|
+
end
|
|
15
|
+
return raw
|
|
16
|
+
`;
|
|
17
|
+
class RedisQueueDriver {
|
|
18
|
+
client;
|
|
19
|
+
prefix;
|
|
20
|
+
constructor(client, keyPrefix = 'arcway:') {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.prefix = `${keyPrefix}queue:`;
|
|
23
|
+
}
|
|
24
|
+
listKey(namespace, topic) {
|
|
25
|
+
return `${this.prefix}${namespace}:${topic}`;
|
|
26
|
+
}
|
|
27
|
+
lockedKey(namespace, topic) {
|
|
28
|
+
return `${this.prefix}${namespace}:${topic}:locked`;
|
|
29
|
+
}
|
|
30
|
+
idKey() {
|
|
31
|
+
return `${this.prefix}id`;
|
|
32
|
+
}
|
|
33
|
+
async init() {}
|
|
34
|
+
|
|
35
|
+
async push(namespace, topic, payload) {
|
|
36
|
+
const id = await this.client.incr(this.idKey());
|
|
37
|
+
const item = JSON.stringify({ id, payload });
|
|
38
|
+
await this.client.rpush(this.listKey(namespace, topic), item);
|
|
39
|
+
}
|
|
40
|
+
async pop(namespace, topic, count, lockCooldownMs) {
|
|
41
|
+
const results = [];
|
|
42
|
+
const listKey = this.listKey(namespace, topic);
|
|
43
|
+
const lockedKey = this.lockedKey(namespace, topic);
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
const cutoff = now - lockCooldownMs;
|
|
46
|
+
await this.client.eval(REQUEUE_EXPIRED_LUA, 2, lockedKey, listKey, cutoff.toString());
|
|
47
|
+
for (let i = 0; i < count; i++) {
|
|
48
|
+
const raw = await this.client.eval(POP_AND_LOCK_LUA, 2, listKey, lockedKey, now.toString());
|
|
49
|
+
if (!raw) break;
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
results.push({ id: parsed.id, payload: parsed.payload });
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
async remove(namespace, ids) {
|
|
56
|
+
if (ids.length === 0) return;
|
|
57
|
+
const idSet = new Set(ids);
|
|
58
|
+
const pattern = `${this.prefix}${namespace}:*:locked`;
|
|
59
|
+
const keys = await this.scanKeys(pattern);
|
|
60
|
+
for (const lockedKey of keys) {
|
|
61
|
+
const members = await this.client.zrange(lockedKey, 0, -1);
|
|
62
|
+
for (const member of members) {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(member);
|
|
65
|
+
if (idSet.has(parsed.id)) {
|
|
66
|
+
await this.client.zrem(lockedKey, member);
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// Malformed locked member — skip removal
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async scanKeys(pattern) {
|
|
75
|
+
const keys = [];
|
|
76
|
+
let cursor = '0';
|
|
77
|
+
do {
|
|
78
|
+
const [next, found] = await this.client.scan(
|
|
79
|
+
cursor,
|
|
80
|
+
'MATCH',
|
|
81
|
+
pattern,
|
|
82
|
+
'COUNT',
|
|
83
|
+
REDIS_SCAN_COUNT,
|
|
84
|
+
);
|
|
85
|
+
cursor = next;
|
|
86
|
+
keys.push(...found);
|
|
87
|
+
} while (cursor !== '0');
|
|
88
|
+
return keys;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export default RedisQueueDriver;
|