Skip to content

Commit 8d2996b

Browse files
authored
Merge pull request #36 from smalruby/fix/issue-79-create-group-heartbeat
fix: include heartbeatIntervalSeconds in createGroup response
2 parents ce400a2 + 9c9453d commit 8d2996b

9 files changed

Lines changed: 94 additions & 33 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ MESH_SECRET_KEY=your-secret-key-here
1010
# Host Heartbeat Settings (Production defaults)
1111
# Host sends heartbeat every 60 seconds
1212
MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=60
13-
# Host group expires after 180 seconds of no heartbeat
14-
MESH_HOST_HEARTBEAT_TTL_SECONDS=180
13+
# Host group expires after 150 seconds of no heartbeat
14+
MESH_HOST_HEARTBEAT_TTL_SECONDS=150
1515

1616
# Member Heartbeat Settings (Production defaults)
1717
# Member sends heartbeat every 120 seconds (2 minutes)

docs/api-reference.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Group {
2626
hostId: ID! # 作成者ノード ID
2727
createdAt: AWSDateTime!
2828
expiresAt: AWSDateTime! # グループの有効期限
29+
heartbeatIntervalSeconds: Int
2930
}
3031
```
3132

@@ -110,6 +111,8 @@ query ListGroupsByDomain($domain: String!) {
110111
name
111112
hostId
112113
createdAt
114+
expiresAt
115+
heartbeatIntervalSeconds
113116
}
114117
}
115118
```
@@ -205,6 +208,8 @@ mutation CreateGroup($name: String!, $hostId: ID!, $domain: String!) {
205208
name
206209
hostId
207210
createdAt
211+
expiresAt
212+
heartbeatIntervalSeconds
208213
}
209214
}
210215
```
@@ -218,10 +223,12 @@ mutation CreateGroup($name: String!, $hostId: ID!, $domain: String!) {
218223
```graphql
219224
mutation JoinGroup($groupId: ID!, $nodeId: ID!, $domain: String!) {
220225
joinGroup(groupId: $groupId, nodeId: $nodeId, domain: $domain) {
221-
nodeId
226+
id
227+
name
222228
groupId
223229
domain
224-
joinedAt
230+
expiresAt
231+
heartbeatIntervalSeconds
225232
}
226233
}
227234
```

docs/architecture.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ sequenceDiagram
115115
DynamoDB-->>Lambda: Success
116116
Lambda-->>AppSync: 新規グループ
117117
end
118-
AppSync-->>Host: Group
118+
AppSync-->>Host: Group (expiresAt, intervalSeconds)
119119
120120
Member->>AppSync: joinGroup(groupId, nodeId, domain)
121121
AppSync->>DynamoDB: Pipeline: checkGroupExists
122122
alt グループ存在
123123
DynamoDB->>DynamoDB: PutItem: ノード登録
124124
DynamoDB-->>AppSync: Node
125-
AppSync-->>Member: Node
125+
AppSync-->>Member: Node (expiresAt, intervalSeconds)
126126
else グループなし
127127
DynamoDB-->>AppSync: GroupNotFound error
128128
AppSync-->>Member: Error
@@ -207,7 +207,7 @@ sequenceDiagram
207207
Resolver->>DynamoDB: UpdateItem: expiresAt更新<br/>(TTL延長)
208208
DynamoDB-->>Resolver: Success
209209
Resolver-->>AppSync: HeartbeatPayload
210-
AppSync-->>Host: expiresAt, intervalSeconds
210+
AppSync-->>Host: expiresAt, heartbeatIntervalSeconds
211211
else 非ホストまたはグループなし
212212
Resolver-->>AppSync: Unauthorized / GroupNotFound
213213
AppSync-->>Host: Error
@@ -220,7 +220,7 @@ sequenceDiagram
220220
Resolver->>DynamoDB: UpdateItem: ノードTTL更新
221221
DynamoDB-->>Resolver: Success
222222
Resolver-->>AppSync: MemberHeartbeatPayload
223-
AppSync-->>Member: expiresAt, intervalSeconds
223+
AppSync-->>Member: expiresAt, heartbeatIntervalSeconds
224224
end
225225
226226
Note over DynamoDB: TTL期限切れ(60-600秒後)
@@ -324,7 +324,6 @@ Mesh v2 は Single Table Design を採用し、1つのテーブルにすべて
324324
"name": "Node 1",
325325
"groupId": "abc123",
326326
"domain": "192.168.1.1",
327-
"heartbeatIntervalSeconds": 120,
328327
"ttl": 1704067200
329328
}
330329
```

docs/deployment.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ cp .env.example .env
7878
| `MESH_HOST_HEARTBEAT_TTL_SECONDS` | `60` | `150` | ホストグループの有効期限(秒) |
7979
| `MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS` | `15` | `120` | メンバーのハートビート送信間隔(秒) |
8080
| `MESH_MEMBER_HEARTBEAT_TTL_SECONDS` | `60` | `600` | メンバーノードの有効期限(秒) |
81-
| `MESH_MAX_CONNECTION_TIME_MINUTES` | `10` | `50` | グループの最大接続時間(|
81+
| `MESH_MAX_CONNECTION_TIME_SECONDS` | `300` | `3000` | グループの最大接続時間(|
8282

8383
### 3.3 開発環境用の設定(stg)
8484

@@ -91,7 +91,7 @@ MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=15
9191
MESH_HOST_HEARTBEAT_TTL_SECONDS=60
9292
MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=15
9393
MESH_MEMBER_HEARTBEAT_TTL_SECONDS=60
94-
MESH_MAX_CONNECTION_TIME_MINUTES=10
94+
MESH_MAX_CONNECTION_TIME_SECONDS=300
9595
```
9696

9797
### 3.4 本番環境用の設定(prod)
@@ -105,7 +105,7 @@ MESH_HOST_HEARTBEAT_INTERVAL_SECONDS=30
105105
MESH_HOST_HEARTBEAT_TTL_SECONDS=150
106106
MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS=120
107107
MESH_MEMBER_HEARTBEAT_TTL_SECONDS=600
108-
MESH_MAX_CONNECTION_TIME_MINUTES=50
108+
MESH_MAX_CONNECTION_TIME_SECONDS=3000
109109
```
110110

111111
**重要**:

docs/development.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,11 @@ Mesh v2 は環境変数を使用して設定を管理し、開発環境と本番
122122
| 変数 | 開発環境 | 本番環境 | 説明 |
123123
|------|---------|---------|------|
124124
| `MESH_SECRET_KEY` | `dev-secret-key-for-testing` | (GitHub Secrets で設定) | ドメイン検証用の秘密鍵 |
125-
| `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` | `15` | `30` | ホストのハートビート間隔(秒) |
126-
| `MESH_HOST_HEARTBEAT_TTL_SECONDS` | `60` | `150` | ホストグループの TTL(秒、間隔の5倍|
125+
| `MESH_HOST_HEARTBEAT_INTERVAL_SECONDS` | `15` | `60` | ホストのハートビート間隔(秒) |
126+
| `MESH_HOST_HEARTBEAT_TTL_SECONDS` | `60` | `150` | ホストグループの TTL(秒) |
127127
| `MESH_MEMBER_HEARTBEAT_INTERVAL_SECONDS` | `15` | `120` | メンバーのハートビート間隔(秒) |
128-
| `MESH_MEMBER_HEARTBEAT_TTL_SECONDS` | `60` | `600` | メンバーノードの TTL(秒、間隔の5倍|
129-
| `MESH_MAX_CONNECTION_TIME_MINUTES` | `10` | `50` | グループの最大接続時間(|
128+
| `MESH_MEMBER_HEARTBEAT_TTL_SECONDS` | `60` | `600` | メンバーノードの TTL(秒) |
129+
| `MESH_MAX_CONNECTION_TIME_SECONDS` | `300` | `1500` | グループの最大接続時間(|
130130

131131
### 設定の根拠
132132

examples/javascript-client/app.js

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const state = {
1515
sessionStartTime: null,
1616
sessionTimerId: null,
1717
heartbeatTimerId: null,
18+
heartbeatIntervalSeconds: 60, // Default 60 seconds
1819
messageSubscriptionId: null,
1920
sensorData: {
2021
temperature: 20,
@@ -257,6 +258,9 @@ async function handleCreateGroup() {
257258

258259
// Join the created group automatically
259260
state.currentGroup = group;
261+
if (group.heartbeatIntervalSeconds) {
262+
state.heartbeatIntervalSeconds = group.heartbeatIntervalSeconds;
263+
}
260264

261265
// Initialize sensor data for this node
262266
// This immediately shares current sensor state with other group members
@@ -403,6 +407,10 @@ async function handleJoinGroup() {
403407
expiresAt: result.expiresAt
404408
};
405409

410+
if (result.heartbeatIntervalSeconds) {
411+
state.heartbeatIntervalSeconds = result.heartbeatIntervalSeconds;
412+
}
413+
406414
// Initialize sensor data for this node
407415
// This immediately shares current sensor state with other group members
408416
const initialData = [
@@ -431,6 +439,7 @@ async function handleJoinGroup() {
431439

432440
// Stop heartbeat if it was running (e.g. from a previously created group)
433441
stopHeartbeat();
442+
startHeartbeat();
434443

435444
showSuccess('groupSuccess', `Joined group: ${state.selectedGroup.name}`);
436445
updateCurrentGroupUI();
@@ -861,15 +870,15 @@ function updateRateStatus() {
861870
}
862871

863872
/**
864-
* Start heartbeat timer (host only)
865-
* Renews the group heartbeat every 15 seconds
873+
* Start heartbeat timer
874+
* Renews the group heartbeat periodically using server-provided interval
866875
*/
867876
function startHeartbeat() {
868877
if (state.heartbeatTimerId) {
869878
clearInterval(state.heartbeatTimerId);
870879
}
871880

872-
console.log('Starting heartbeat timer...');
881+
console.log(`Starting heartbeat timer (${state.heartbeatIntervalSeconds}s)...`);
873882
document.getElementById('heartbeatStatus').style.display = 'block';
874883

875884
state.heartbeatTimerId = setInterval(async () => {
@@ -879,31 +888,45 @@ function startHeartbeat() {
879888
}
880889

881890
const isHost = state.currentGroup.hostId === state.currentNodeId;
882-
if (!isHost) {
883-
stopHeartbeat();
884-
return;
885-
}
886891

887892
try {
888-
const result = await state.client.renewHeartbeat(
889-
state.currentGroup.id,
890-
state.currentNodeId,
891-
state.currentGroup.domain
892-
);
893-
console.log('Heartbeat renewed, expires at:', result.expiresAt);
893+
let result;
894+
if (isHost) {
895+
result = await state.client.renewHeartbeat(
896+
state.currentGroup.id,
897+
state.currentNodeId,
898+
state.currentGroup.domain
899+
);
900+
console.log('Host heartbeat renewed, expires at:', result.expiresAt);
901+
} else {
902+
result = await state.client.sendMemberHeartbeat(
903+
state.currentGroup.id,
904+
state.currentNodeId,
905+
state.currentGroup.domain
906+
);
907+
console.log('Member heartbeat sent, expires at:', result.expiresAt);
908+
}
909+
894910
document.getElementById('lastHeartbeatTime').textContent = new Date().toLocaleTimeString();
895911

896912
// Update session timer with new expiration if possible
897913
if (result.expiresAt) {
898914
state.currentGroup.expiresAt = result.expiresAt;
899915
}
916+
917+
// Check if interval has changed
918+
if (result.heartbeatIntervalSeconds && result.heartbeatIntervalSeconds !== state.heartbeatIntervalSeconds) {
919+
console.log(`Heartbeat interval changed: ${state.heartbeatIntervalSeconds}s -> ${result.heartbeatIntervalSeconds}s`);
920+
state.heartbeatIntervalSeconds = result.heartbeatIntervalSeconds;
921+
startHeartbeat(); // Restart with new interval
922+
}
900923
} catch (error) {
901-
console.error('Heartbeat renewal failed:', error);
924+
console.error('Heartbeat failed:', error);
902925
if (shouldDisconnectOnError(error)) {
903926
handleGroupDissolved({ message: 'Session expired or group lost' });
904927
}
905928
}
906-
}, 15000); // Every 15 seconds
929+
}, state.heartbeatIntervalSeconds * 1000);
907930
}
908931

909932
/**

examples/javascript-client/mesh-client.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class MeshClient {
9797
hostId
9898
createdAt
9999
expiresAt
100+
heartbeatIntervalSeconds
100101
}
101102
}
102103
`;
@@ -118,6 +119,7 @@ class MeshClient {
118119
mutation RenewHeartbeat($groupId: ID!, $hostId: ID!, $domain: String!) {
119120
renewHeartbeat(groupId: $groupId, hostId: $hostId, domain: $domain) {
120121
expiresAt
122+
heartbeatIntervalSeconds
121123
}
122124
}
123125
`;
@@ -243,6 +245,8 @@ class MeshClient {
243245
name
244246
groupId
245247
domain
248+
expiresAt
249+
heartbeatIntervalSeconds
246250
}
247251
}
248252
`;
@@ -256,6 +260,28 @@ class MeshClient {
256260
return data.joinGroup;
257261
}
258262

263+
/**
264+
* Send member heartbeat
265+
*/
266+
async sendMemberHeartbeat(groupId, nodeId, domain) {
267+
const query = `
268+
mutation SendMemberHeartbeat($groupId: ID!, $nodeId: ID!, $domain: String!) {
269+
sendMemberHeartbeat(groupId: $groupId, nodeId: $nodeId, domain: $domain) {
270+
expiresAt
271+
heartbeatIntervalSeconds
272+
}
273+
}
274+
`;
275+
276+
const data = await this.execute(query, {
277+
groupId,
278+
nodeId,
279+
domain: domain || this.domain
280+
});
281+
282+
return data.sendMemberHeartbeat;
283+
}
284+
259285
/**
260286
* Leave a group
261287
*/

graphql/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Group {
2020
hostId: ID!
2121
createdAt: AWSDateTime!
2222
expiresAt: AWSDateTime!
23+
heartbeatIntervalSeconds: Int
2324
}
2425

2526
type Node {

js/functions/createGroupIfNotExists.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export function response(ctx) {
7777
util.error(ctx.error.message, ctx.error.type);
7878
}
7979

80+
// ハートビート間隔を環境変数から取得(ホスト用)
81+
const heartbeatIntervalSeconds = +(ctx.env.MESH_HOST_HEARTBEAT_INTERVAL_SECONDS || '60');
82+
8083
// ctx.stashに既存グループがある場合はそれを返す
8184
if (ctx.stash.existingGroup) {
8285
return {
@@ -86,7 +89,8 @@ export function response(ctx) {
8689
name: ctx.stash.existingGroup.name,
8790
hostId: ctx.stash.existingGroup.hostId,
8891
createdAt: ctx.stash.existingGroup.createdAt,
89-
expiresAt: ctx.stash.existingGroup.expiresAt
92+
expiresAt: ctx.stash.existingGroup.expiresAt,
93+
heartbeatIntervalSeconds: heartbeatIntervalSeconds
9094
};
9195
}
9296

@@ -98,6 +102,7 @@ export function response(ctx) {
98102
name: ctx.result.name,
99103
hostId: ctx.result.hostId,
100104
createdAt: ctx.result.createdAt,
101-
expiresAt: ctx.result.expiresAt
105+
expiresAt: ctx.result.expiresAt,
106+
heartbeatIntervalSeconds: heartbeatIntervalSeconds
102107
};
103108
}

0 commit comments

Comments
 (0)