create-nexwire 4.2.5 → 4.3.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/README.md +20 -8
- package/express/.env +1 -1
- package/express/package.json +1 -1
- package/express/src/config/database.ts +48 -29
- package/express/src/config/index.ts +83 -83
- package/express/src/config/openapi.ts +6 -24
- package/express/src/config/queue.ts +30 -30
- package/express/src/config/validate.ts +50 -40
- package/express/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/express/src/framework/cache/cache.ts +40 -8
- package/express/src/framework/circuit-breaker/cb.ts +88 -60
- package/express/src/framework/database/connection.ts +52 -27
- package/express/src/framework/events/dispatcher.ts +61 -49
- package/express/src/framework/facade.ts +46 -3
- package/express/src/framework/http/app.ts +28 -22
- package/express/src/framework/http/logger.ts +27 -16
- package/express/src/framework/http/openapi.ts +9 -11
- package/express/src/framework/modules/routes.ts +26 -21
- package/express/src/framework/notification/channels/broadcast.ts +15 -0
- package/express/src/framework/notification/channels/mail.ts +40 -0
- package/express/src/framework/notification/contracts.ts +29 -0
- package/express/src/framework/notification/index.ts +19 -94
- package/express/src/framework/notification/registry.ts +15 -0
- package/express/src/framework/providers/adapter.ts +27 -17
- package/express/src/framework/queue/queue.ts +43 -44
- package/express/src/framework/realtime/broadcast.ts +29 -5
- package/express/src/framework/realtime/socket.ts +149 -119
- package/express/src/framework/redis/client.ts +148 -151
- package/express/src/framework/runtime/server-adapter.ts +39 -34
- package/express/src/framework/scheduler/lock.ts +128 -116
- package/express/src/framework/server.ts +33 -49
- package/express/src/framework/session/session.ts +69 -14
- package/express/src/framework/storage/storage.ts +744 -785
- package/express/src/framework/support/mail.ts +15 -7
- package/express/src/providers/notification.ts +4 -0
- package/express/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/express/src/resources/src/plugins/pulse.ts +157 -147
- package/express/src/resources/vite.config.ts +54 -43
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/express/src/storage/logs/app.log +0 -0
- package/express/src/storage/logs/fatal.log +0 -0
- package/express/src/types/global.d.ts +32 -26
- package/hono/.env +1 -1
- package/hono/package.json +1 -1
- package/hono/src/config/database.ts +48 -29
- package/hono/src/config/index.ts +83 -83
- package/hono/src/config/openapi.ts +6 -24
- package/hono/src/config/queue.ts +30 -30
- package/hono/src/config/validate.ts +50 -40
- package/hono/src/database/migrations/sqlite/0000_init.sql +47 -47
- package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
- package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
- package/hono/src/framework/cache/cache.ts +40 -8
- package/hono/src/framework/circuit-breaker/cb.ts +88 -60
- package/hono/src/framework/database/connection.ts +52 -27
- package/hono/src/framework/events/dispatcher.ts +61 -49
- package/hono/src/framework/facade.ts +38 -1
- package/hono/src/framework/http/openapi.ts +7 -9
- package/hono/src/framework/notification/channels/broadcast.ts +15 -0
- package/hono/src/framework/notification/channels/mail.ts +40 -0
- package/hono/src/framework/notification/contracts.ts +29 -0
- package/hono/src/framework/notification/index.ts +19 -94
- package/hono/src/framework/notification/registry.ts +15 -0
- package/hono/src/framework/providers/adapter.ts +27 -17
- package/hono/src/framework/queue/queue.ts +43 -44
- package/hono/src/framework/realtime/broadcast.ts +29 -5
- package/hono/src/framework/realtime/socket.ts +217 -196
- package/hono/src/framework/redis/client.ts +148 -151
- package/hono/src/framework/scheduler/lock.ts +128 -116
- package/hono/src/framework/server.ts +27 -29
- package/hono/src/framework/session/session.ts +62 -14
- package/hono/src/framework/storage/storage.ts +744 -785
- package/hono/src/framework/support/mail.ts +56 -48
- package/hono/src/providers/notification.ts +4 -0
- package/hono/src/resources/src/layouts/Layout/Header.vue +124 -105
- package/hono/src/resources/src/plugins/pulse.ts +157 -147
- package/hono/src/resources/vite.config.ts +54 -43
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/hono/src/storage/logs/app.log +0 -0
- package/hono/src/storage/logs/fatal.log +0 -0
- package/hono/src/types/global.d.ts +32 -26
- package/package.json +2 -2
- package/src/index.js +139 -14
|
@@ -1,48 +1,56 @@
|
|
|
1
|
-
import nodemailer from "nodemailer";
|
|
2
|
-
import { mailConfig } from "@/config/index.js";
|
|
3
|
-
import {
|
|
4
|
-
import { logger } from "@/framework/support/logger.js";
|
|
5
|
-
|
|
6
|
-
type MailPayload = {
|
|
7
|
-
to: string;
|
|
8
|
-
subject: string;
|
|
9
|
-
html?: string;
|
|
10
|
-
text?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const transport = nodemailer.createTransport({
|
|
14
|
-
host: mailConfig.host,
|
|
15
|
-
port: mailConfig.port,
|
|
16
|
-
secure: mailConfig.encryption === "ssl",
|
|
17
|
-
auth: mailConfig.username ? { user: mailConfig.username, pass: mailConfig.password } : undefined
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const mailBreaker =
|
|
21
|
-
|
|
22
|
-
export const mail = {
|
|
23
|
-
/**
|
|
24
|
-
* Why: Sends transactional email through configured SMTP transport.
|
|
25
|
-
* When: Features need notifications/password reset/signup email.
|
|
26
|
-
* Where: Jobs and event handlers.
|
|
27
|
-
* How: Uses nodemailer transport and respects the fail-silent setting.
|
|
28
|
-
*/
|
|
29
|
-
async sendMail(payload: MailPayload) {
|
|
30
|
-
try {
|
|
31
|
-
return await mailBreaker.exec(() =>
|
|
32
|
-
transport.sendMail({
|
|
33
|
-
from: mailConfig.fromAddress,
|
|
34
|
-
...payload
|
|
35
|
-
})
|
|
36
|
-
);
|
|
37
|
-
} catch (error) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
import nodemailer from "nodemailer";
|
|
2
|
+
import { mailConfig } from "@/config/index.js";
|
|
3
|
+
import { createCircuitBreaker, isCircuitBreakerError } from "@/framework/circuit-breaker/cb.js";
|
|
4
|
+
import { logger } from "@/framework/support/logger.js";
|
|
5
|
+
|
|
6
|
+
type MailPayload = {
|
|
7
|
+
to: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
html?: string;
|
|
10
|
+
text?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const transport = nodemailer.createTransport({
|
|
14
|
+
host: mailConfig.host,
|
|
15
|
+
port: mailConfig.port,
|
|
16
|
+
secure: mailConfig.encryption === "ssl",
|
|
17
|
+
auth: mailConfig.username ? { user: mailConfig.username, pass: mailConfig.password } : undefined
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const mailBreaker = createCircuitBreaker({ name: "SMTP Mail" });
|
|
21
|
+
|
|
22
|
+
export const mail = {
|
|
23
|
+
/**
|
|
24
|
+
* Why: Sends transactional email through configured SMTP transport.
|
|
25
|
+
* When: Features need notifications/password reset/signup email.
|
|
26
|
+
* Where: Jobs and event handlers.
|
|
27
|
+
* How: Uses nodemailer transport and respects the fail-silent setting.
|
|
28
|
+
*/
|
|
29
|
+
async sendMail(payload: MailPayload) {
|
|
30
|
+
try {
|
|
31
|
+
return await mailBreaker.exec(() =>
|
|
32
|
+
transport.sendMail({
|
|
33
|
+
from: mailConfig.fromAddress,
|
|
34
|
+
...payload
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Distinguish between a Circuit Breaker rejection and an actual SMTP failure
|
|
39
|
+
if (isCircuitBreakerError(error)) {
|
|
40
|
+
logger.warn("Mail send skipped: Circuit breaker is open", {
|
|
41
|
+
to: payload.to,
|
|
42
|
+
subject: payload.subject
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
logger.error("Mail send failed", {
|
|
46
|
+
to: payload.to,
|
|
47
|
+
subject: payload.subject,
|
|
48
|
+
error: error instanceof Error ? error.message : error
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!mailConfig.failSilent) throw error;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -1,105 +1,124 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<header :class="{ scrolled: isScrolled }">
|
|
3
|
-
<nav class="navbar bg-transparent p-0">
|
|
4
|
-
<div class="container-fluid p-0 row-gap-2 column-gap-1">
|
|
5
|
-
<button
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
<template>
|
|
2
|
+
<header :class="{ scrolled: isScrolled }">
|
|
3
|
+
<nav class="navbar bg-transparent p-0">
|
|
4
|
+
<div class="container-fluid p-0 row-gap-2 column-gap-1">
|
|
5
|
+
<button id="off-canvas" type="button" class="nav-btn order-0" title="Toggle Button" @click="props.onToggleSidebar">
|
|
6
|
+
<i class="bi bi-layout-text-sidebar-reverse"></i>
|
|
7
|
+
</button>
|
|
8
|
+
<div id="pagebar" class="card card-body nav-card order-5 order-sm-2"></div>
|
|
9
|
+
<template v-for="btn in featureButtons" :key="btn.id">
|
|
10
|
+
<button
|
|
11
|
+
v-if="!btn.render"
|
|
12
|
+
type="button"
|
|
13
|
+
class="nav-btn order-3 order-sm-3"
|
|
14
|
+
:class="btn.class"
|
|
15
|
+
:title="btn.title || undefined"
|
|
16
|
+
:aria-label="btn.label || 'button'"
|
|
17
|
+
v-bind="btn.attrs"
|
|
18
|
+
@click="btn.onClick"
|
|
19
|
+
>
|
|
20
|
+
<i v-if="btn.icon" :class="btn.icon"></i>
|
|
21
|
+
<span v-if="btn.label" class="ms-1">{{ btn.label }}</span>
|
|
22
|
+
</button>
|
|
23
|
+
<template v-else>
|
|
24
|
+
<component :is="featureComponent(btn)" />
|
|
25
|
+
</template>
|
|
26
|
+
</template>
|
|
27
|
+
<div class="btn-group order-4 order-sm-4">
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
class="nav-btn w-100"
|
|
31
|
+
title="user"
|
|
32
|
+
data-bs-toggle="dropdown"
|
|
33
|
+
data-bs-display="static"
|
|
34
|
+
aria-expanded="false"
|
|
35
|
+
aria-label="button"
|
|
36
|
+
>
|
|
37
|
+
<i class="bi bi-person-circle"></i>
|
|
38
|
+
</button>
|
|
39
|
+
<ul class="dropdown-menu dropdown-menu-end">
|
|
40
|
+
<li>
|
|
41
|
+
<a class="dropdown-item" href="#">
|
|
42
|
+
<div class="d-flex align-items-center">
|
|
43
|
+
<div class="flex-shrink-0 me-3">
|
|
44
|
+
<i class="bi bi-person-circle fs-1"></i>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="flex-grow-1 text-capitalize">
|
|
47
|
+
<h6 class="mb-0">{{ authUser?.name }}</h6>
|
|
48
|
+
<small class="text-muted">{{ authUser.role?.name }}</small>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</a>
|
|
52
|
+
</li>
|
|
53
|
+
<li>
|
|
54
|
+
<div class="dropdown-divider my-1"></div>
|
|
55
|
+
</li>
|
|
56
|
+
<li>
|
|
57
|
+
<router-link class="dropdown-item" to="#">
|
|
58
|
+
<i class="bi bi-person-check me-3"></i><span>My Profile</span>
|
|
59
|
+
</router-link>
|
|
60
|
+
</li>
|
|
61
|
+
<li>
|
|
62
|
+
<button type="button" class="dropdown-item" @click="props.onLogout">
|
|
63
|
+
<i class="bi bi-power me-3"></i><span>Log Out</span>
|
|
64
|
+
</button>
|
|
65
|
+
</li>
|
|
66
|
+
</ul>
|
|
67
|
+
</div>
|
|
68
|
+
<button
|
|
69
|
+
id="theme-switch"
|
|
70
|
+
type="button"
|
|
71
|
+
title="theme"
|
|
72
|
+
class="nav-btn order-1 order-sm-5"
|
|
73
|
+
aria-label="button"
|
|
74
|
+
@click="props.onToggleTheme"
|
|
75
|
+
>
|
|
76
|
+
<i :class="themeIconClass"></i>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
</nav>
|
|
80
|
+
</header>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { computed, h, inject } from "vue";
|
|
85
|
+
import { authUser } from "@/composables/useAuth";
|
|
86
|
+
|
|
87
|
+
interface ButtonEntry {
|
|
88
|
+
id: symbol;
|
|
89
|
+
icon?: string;
|
|
90
|
+
label: string;
|
|
91
|
+
title: string;
|
|
92
|
+
attrs?: Record<string, string>;
|
|
93
|
+
class?: any;
|
|
94
|
+
onClick: () => void;
|
|
95
|
+
render?: () => any[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const featureButtons = inject<ButtonEntry[]>("featureButtons")!;
|
|
99
|
+
|
|
100
|
+
function featureComponent(btn: ButtonEntry) {
|
|
101
|
+
return {
|
|
102
|
+
name: "FeatureSlot",
|
|
103
|
+
render: () => h("div", { class: ["order-3 order-sm-4", btn.class] }, btn.render!())
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface HeaderProps {
|
|
108
|
+
isScrolled: boolean;
|
|
109
|
+
themeMode: "light" | "dark" | "auto";
|
|
110
|
+
onLogout: () => void | Promise<void>;
|
|
111
|
+
onToggleSidebar: () => void;
|
|
112
|
+
onToggleTheme: () => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const props = defineProps<HeaderProps>();
|
|
116
|
+
|
|
117
|
+
const themeIconClass = computed(() => {
|
|
118
|
+
if (props.themeMode === "light") return "bi bi-brightness-high-fill";
|
|
119
|
+
if (props.themeMode === "dark") return "bi bi-moon-stars";
|
|
120
|
+
return "bi bi-circle-half";
|
|
121
|
+
});
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<style lang="scss" scoped></style>
|
|
@@ -1,147 +1,157 @@
|
|
|
1
|
-
import { io, type Socket } from "socket.io-client";
|
|
2
|
-
import type { App } from "vue";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
1
|
+
import { io, type Socket } from "socket.io-client";
|
|
2
|
+
import type { App } from "vue";
|
|
3
|
+
|
|
4
|
+
// 1. Read the value Vite inlines into this module. Do not use a bare `define`
|
|
5
|
+
// global here: in dev it is a window global and can survive a server
|
|
6
|
+
// restart or browser cache, leaving Pulse enabled after SOCKET=false.
|
|
7
|
+
const isSocketEnabled = import.meta.env.VITE_SOCKET_ENABLED === true;
|
|
8
|
+
|
|
9
|
+
type EventCallback<T = unknown> = (payload: T) => void;
|
|
10
|
+
|
|
11
|
+
type ChannelBinding = {
|
|
12
|
+
event: string;
|
|
13
|
+
callback: EventCallback;
|
|
14
|
+
wrapped: EventCallback;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PulseChannel = {
|
|
18
|
+
listen: <T = unknown>(event: string, callback: EventCallback<T>) => PulseChannel;
|
|
19
|
+
stopListening: (event: string, callback?: EventCallback) => PulseChannel;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type PulseClient = {
|
|
23
|
+
connect: () => PulseClient;
|
|
24
|
+
disconnect: () => PulseClient;
|
|
25
|
+
channel: (name: string) => PulseChannel;
|
|
26
|
+
private: (room: string) => PulseChannel;
|
|
27
|
+
leave: (room: string) => PulseClient;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function createPulse(): PulseClient {
|
|
31
|
+
// 2. If disabled, return the no-op dummy client immediately
|
|
32
|
+
if (!isSocketEnabled) {
|
|
33
|
+
const dummyChannel = (): PulseChannel => ({
|
|
34
|
+
listen: () => dummyChannel(),
|
|
35
|
+
stopListening: () => dummyChannel()
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const dummyClient: PulseClient = {
|
|
39
|
+
connect: () => dummyClient,
|
|
40
|
+
disconnect: () => dummyClient,
|
|
41
|
+
channel: dummyChannel,
|
|
42
|
+
private: dummyChannel,
|
|
43
|
+
leave: () => dummyClient
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return dummyClient;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let socket: Socket | null = null;
|
|
50
|
+
const channels = new Map<string, ChannelBinding[]>();
|
|
51
|
+
const joinedRooms = new Set<string>();
|
|
52
|
+
|
|
53
|
+
const socketUrl = () => {
|
|
54
|
+
const explicit = import.meta.env.VITE_SOCKET_URL || import.meta.env.VITE_API_URL;
|
|
55
|
+
if (explicit) return explicit;
|
|
56
|
+
return window.location.origin;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const ensureConnected = () => {
|
|
60
|
+
if (socket) return socket;
|
|
61
|
+
|
|
62
|
+
socket = io(socketUrl(), {
|
|
63
|
+
withCredentials: true,
|
|
64
|
+
transports: ["websocket", "polling"]
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
socket.on("connect", () => {
|
|
68
|
+
for (const room of joinedRooms) {
|
|
69
|
+
socket?.emit("join", room);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return socket;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const channel = (name: string): PulseChannel => {
|
|
77
|
+
const room = String(name).trim();
|
|
78
|
+
if (!room) throw new Error("Channel name is required");
|
|
79
|
+
|
|
80
|
+
const currentSocket = ensureConnected();
|
|
81
|
+
joinedRooms.add(room);
|
|
82
|
+
currentSocket.emit("join", room);
|
|
83
|
+
|
|
84
|
+
const api: PulseChannel = {
|
|
85
|
+
listen: <T = unknown>(event: string, callback: EventCallback<T>) => {
|
|
86
|
+
const wrapped = (payload: unknown) => callback(payload as T);
|
|
87
|
+
const bindings = channels.get(room) || [];
|
|
88
|
+
bindings.push({ event, callback: callback as EventCallback, wrapped });
|
|
89
|
+
channels.set(room, bindings);
|
|
90
|
+
currentSocket.on(event, wrapped);
|
|
91
|
+
return api;
|
|
92
|
+
},
|
|
93
|
+
stopListening: (event: string, callback?: EventCallback) => {
|
|
94
|
+
const bindings = channels.get(room) || [];
|
|
95
|
+
const kept: ChannelBinding[] = [];
|
|
96
|
+
|
|
97
|
+
for (const binding of bindings) {
|
|
98
|
+
const matchesEvent = binding.event === event;
|
|
99
|
+
const matchesCallback = !callback || binding.callback === callback;
|
|
100
|
+
if (matchesEvent && matchesCallback) {
|
|
101
|
+
currentSocket.off(binding.event, binding.wrapped);
|
|
102
|
+
} else {
|
|
103
|
+
kept.push(binding);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (kept.length > 0) channels.set(room, kept);
|
|
108
|
+
else channels.delete(room);
|
|
109
|
+
return api;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return api;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const pulse: PulseClient = {
|
|
117
|
+
connect: () => {
|
|
118
|
+
ensureConnected();
|
|
119
|
+
return pulse;
|
|
120
|
+
},
|
|
121
|
+
disconnect: () => {
|
|
122
|
+
socket?.disconnect();
|
|
123
|
+
socket = null;
|
|
124
|
+
return pulse;
|
|
125
|
+
},
|
|
126
|
+
channel,
|
|
127
|
+
private: (room: string) => channel(`private:${room}`),
|
|
128
|
+
leave: (room: string) => {
|
|
129
|
+
if (!socket) return pulse;
|
|
130
|
+
|
|
131
|
+
const bindings = channels.get(room) || [];
|
|
132
|
+
for (const binding of bindings) {
|
|
133
|
+
socket.off(binding.event, binding.wrapped);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
channels.delete(room);
|
|
137
|
+
joinedRooms.delete(room);
|
|
138
|
+
return pulse;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return pulse;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const pulse = createPulse();
|
|
146
|
+
|
|
147
|
+
export const PulsePlugin = {
|
|
148
|
+
install(app: App) {
|
|
149
|
+
app.config.globalProperties.$pulse = pulse;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
declare module "vue" {
|
|
154
|
+
interface ComponentCustomProperties {
|
|
155
|
+
$pulse: PulseClient;
|
|
156
|
+
}
|
|
157
|
+
}
|