@kokimoki/app 0.5.3 → 0.6.1

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.
@@ -6,9 +6,10 @@ import type { KokimokiClientEvents } from "./types/events";
6
6
  import type { Upload } from "./types/upload";
7
7
  import type { Paginated } from "./types/common";
8
8
  declare const KokimokiClient_base: new <T>() => TypedEmitter<KokimokiClientEvents<T>>;
9
- export declare class KokimokiClient<StatelessDataT = any> extends KokimokiClient_base<StatelessDataT> {
9
+ export declare class KokimokiClient<StatelessDataT = any, ClientContextT = any> extends KokimokiClient_base<StatelessDataT> {
10
10
  readonly host: string;
11
11
  readonly appId: string;
12
+ readonly code: string;
12
13
  private _wsUrl;
13
14
  private _apiUrl;
14
15
  private _id?;
@@ -16,13 +17,17 @@ export declare class KokimokiClient<StatelessDataT = any> extends KokimokiClient
16
17
  private _apiHeaders?;
17
18
  private _providers;
18
19
  private _serverTimeOffset;
19
- constructor(host: string, appId: string);
20
+ private _clientContext?;
21
+ private _synced;
22
+ constructor(host: string, appId: string, code?: string);
20
23
  get id(): string;
21
24
  get token(): string;
22
25
  get apiUrl(): string;
23
26
  get apiHeaders(): Headers;
27
+ get clientContext(): NonNullable<ClientContextT>;
24
28
  connect(): Promise<void>;
25
29
  serverTimestamp(): number;
30
+ private checkConnectionState;
26
31
  setProvider<T extends DocTypeDescription>(name: string, store: SyncedStore<T>): Promise<void>;
27
32
  removeProvider(name: string): void;
28
33
  getProvider(name: string): HocuspocusProvider | undefined;
@@ -1,8 +1,10 @@
1
1
  import { HocuspocusProvider } from "@hocuspocus/provider";
2
2
  import EventEmitter from "events";
3
+ import { KOKIMOKI_APP_VERSION } from "./version";
3
4
  export class KokimokiClient extends EventEmitter {
4
5
  host;
5
6
  appId;
7
+ code;
6
8
  _wsUrl;
7
9
  _apiUrl;
8
10
  _id;
@@ -10,10 +12,13 @@ export class KokimokiClient extends EventEmitter {
10
12
  _apiHeaders;
11
13
  _providers = new Map();
12
14
  _serverTimeOffset = 0;
13
- constructor(host, appId) {
15
+ _clientContext;
16
+ _synced = false;
17
+ constructor(host, appId, code = "") {
14
18
  super();
15
19
  this.host = host;
16
20
  this.appId = appId;
21
+ this.code = code;
17
22
  // Set up the URLs
18
23
  const secure = this.host.indexOf(":") === -1;
19
24
  this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
@@ -43,23 +48,30 @@ export class KokimokiClient extends EventEmitter {
43
48
  }
44
49
  return this._apiHeaders;
45
50
  }
51
+ get clientContext() {
52
+ if (!this._clientContext) {
53
+ throw new Error("Client not connected");
54
+ }
55
+ return this._clientContext;
56
+ }
46
57
  async connect() {
47
58
  // Fetch the auth token
48
59
  let clientToken = localStorage.getItem("KM_TOKEN");
49
60
  const startTime = Date.now();
50
- const res = await fetch(`${this.apiUrl}/auth/token?appId=${this.appId}`, {
61
+ const res = await fetch(`${this.apiUrl}/auth/token?appId=${this.appId}&code=${this.code}`, {
51
62
  method: "GET",
52
63
  headers: new Headers({
53
64
  "Content-Type": "application/json",
54
65
  Authorization: `Bearer ${clientToken}`,
55
66
  }),
56
67
  });
57
- const { clientId, appToken, serverTime, token } = await res.json();
68
+ const { clientId, appToken, serverTime, token, clientContext } = await res.json();
58
69
  const endTime = Date.now();
59
70
  const ping = Math.round((endTime - startTime) / 2);
60
71
  this._id = clientId;
61
72
  this._token = appToken;
62
73
  this._serverTimeOffset = Date.now() - serverTime - ping;
74
+ this._clientContext = clientContext;
63
75
  localStorage.setItem("KM_TOKEN", token);
64
76
  // Set up the auth headers
65
77
  this._apiHeaders = new Headers({
@@ -72,10 +84,24 @@ export class KokimokiClient extends EventEmitter {
72
84
  provider.sendStateless("ping");
73
85
  });
74
86
  }, 30000);
87
+ // Initial connected state
88
+ this._synced = true;
89
+ this.emit("connected");
75
90
  }
76
91
  serverTimestamp() {
77
92
  return Date.now() - this._serverTimeOffset;
78
93
  }
94
+ checkConnectionState() {
95
+ const synced = !Array.from(this._providers.values()).some((provider) => provider.status !== "connected" || !provider.synced);
96
+ if (synced && !this._synced) {
97
+ this._synced = true;
98
+ this.emit("connected");
99
+ }
100
+ else if (!synced && this._synced) {
101
+ this._synced = false;
102
+ this.emit("disconnected");
103
+ }
104
+ }
79
105
  // Realtime database
80
106
  async setProvider(name, store) {
81
107
  const provider = new HocuspocusProvider({
@@ -83,10 +109,15 @@ export class KokimokiClient extends EventEmitter {
83
109
  name: `${this.appId}/${name}`,
84
110
  document: store.doc,
85
111
  token: this.token,
112
+ parameters: {
113
+ clientVersion: KOKIMOKI_APP_VERSION,
114
+ },
86
115
  });
87
- this._providers.set(name, provider);
88
116
  // Handle incoming stateless messages
89
117
  provider.on("stateless", (e) => {
118
+ if (e.payload === "pong") {
119
+ return;
120
+ }
90
121
  const payload = JSON.parse(e.payload);
91
122
  this.emit("stateless", name, payload.from, payload.data);
92
123
  });
@@ -98,6 +129,20 @@ export class KokimokiClient extends EventEmitter {
98
129
  };
99
130
  provider.on("synced", handler);
100
131
  });
132
+ this._providers.set(name, provider);
133
+ this.checkConnectionState();
134
+ // Handle connection state changes
135
+ provider.on("disconnect", async () => {
136
+ // KokimokiClient is considered disconnected when any single provider is disconnected
137
+ this.checkConnectionState();
138
+ // Attempt to reconnect immediately with retries
139
+ await provider.connect();
140
+ });
141
+ provider.on("synced", () => {
142
+ if (provider.status === "connected") {
143
+ this.checkConnectionState();
144
+ }
145
+ });
101
146
  }
102
147
  removeProvider(name) {
103
148
  const provider = this._providers.get(name);
@@ -106,6 +151,8 @@ export class KokimokiClient extends EventEmitter {
106
151
  }
107
152
  provider.destroy();
108
153
  this._providers.delete(name);
154
+ // Connection state can change if the removed provider was not connected or synced
155
+ this.checkConnectionState();
109
156
  }
110
157
  getProvider(name) {
111
158
  return this._providers.get(name);
@@ -1,3 +1,5 @@
1
1
  export type KokimokiClientEvents<T> = {
2
2
  stateless: (room: string, from: string, data: T) => void;
3
+ connected: () => void;
4
+ disconnected: () => void;
3
5
  };
@@ -0,0 +1 @@
1
+ export declare const KOKIMOKI_APP_VERSION = "0.6.1";
@@ -0,0 +1 @@
1
+ export const KOKIMOKI_APP_VERSION = "0.6.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokimoki/app",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Kokimoki app",
6
6
  "main": "dist/index.js",
@@ -10,6 +10,7 @@
10
10
  ],
11
11
  "scripts": {
12
12
  "test": "echo \"Error: no test specified\" && exit 1",
13
+ "prebuild": "node -p \"'export const KOKIMOKI_APP_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
13
14
  "build": "tsc",
14
15
  "dev": "tsc -w",
15
16
  "docs": "typedoc src/index.ts"
@@ -1,43 +0,0 @@
1
- export interface Answer<T = any> {
2
- value?: T;
3
- correct: boolean;
4
- }
5
- export interface TaskBase {
6
- id: string;
7
- text: string;
8
- points: number;
9
- metadata?: Record<string, any>;
10
- }
11
- export interface TaskOption {
12
- text: string;
13
- correct: boolean;
14
- }
15
- export interface MultipleChoiceTask extends TaskBase {
16
- type: "multiple-choice";
17
- answers: TaskOption[];
18
- }
19
- export interface SingleChoiceTask extends TaskBase {
20
- type: "single-choice";
21
- answers: TaskOption[];
22
- }
23
- export interface TextTask extends TaskBase {
24
- type: "text";
25
- answer: string;
26
- }
27
- export interface TextSurveyTask extends TaskBase {
28
- type: "text-survey";
29
- }
30
- export interface NumericTask extends TaskBase {
31
- type: "numeric";
32
- answer: number;
33
- }
34
- export interface PhotoTask extends TaskBase {
35
- type: "photo";
36
- }
37
- export interface VideoTask extends TaskBase {
38
- type: "video";
39
- }
40
- export interface InfoTask extends TaskBase {
41
- type: "info";
42
- }
43
- export type Task = MultipleChoiceTask | SingleChoiceTask | TextTask | NumericTask | TextSurveyTask | PhotoTask | VideoTask | InfoTask;
@@ -1 +0,0 @@
1
- export {};