@kokimoki/app 0.2.5 → 0.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/dist/index.d.ts CHANGED
@@ -5,5 +5,3 @@ export * from "./types/upload";
5
5
  export * from "./fields";
6
6
  export * from "./kokimoki-client";
7
7
  export * from "./synced-store";
8
- export * from "./user-media-handler";
9
- export * from "./kokimoki-storage";
package/dist/index.js CHANGED
@@ -5,5 +5,3 @@ export * from "./types/upload";
5
5
  export * from "./fields";
6
6
  export * from "./kokimoki-client";
7
7
  export * from "./synced-store";
8
- export * from "./user-media-handler";
9
- export * from "./kokimoki-storage";
@@ -3,27 +3,38 @@ import type TypedEmitter from "typed-emitter";
3
3
  import type { SyncedStore } from "./synced-store";
4
4
  import type { DocTypeDescription } from "@syncedstore/core/types/doc";
5
5
  import type { KokimokiClientEvents } from "./types/events";
6
+ import type { Upload } from "./types/upload";
7
+ import type { Paginated } from "./types/common";
6
8
  declare const KokimokiClient_base: new <T>() => TypedEmitter<KokimokiClientEvents<T>>;
7
9
  export declare class KokimokiClient<StatelessDataT = any> extends KokimokiClient_base<StatelessDataT> {
8
- serverTimeOffset: number;
9
- private _appId?;
10
- private _wsUrl?;
11
- private _apiUrl?;
10
+ readonly host: string;
11
+ readonly appId: string;
12
+ private _wsUrl;
13
+ private _apiUrl;
12
14
  private _id?;
13
15
  private _token?;
16
+ private _apiHeaders?;
14
17
  private _providers;
15
- constructor();
18
+ private _serverTimeOffset;
19
+ constructor(host: string, appId: string);
16
20
  get id(): string;
17
- get appId(): string;
18
21
  get token(): string;
19
22
  get apiUrl(): string;
20
- connect(host: string, appId: string): Promise<void>;
23
+ get apiHeaders(): Headers;
24
+ connect(): Promise<void>;
25
+ serverTimestamp(): number;
21
26
  setProvider<T extends DocTypeDescription>(name: string, store: SyncedStore<T>): Promise<void>;
22
27
  removeProvider(name: string): void;
23
28
  getProvider(name: string): HocuspocusProvider | undefined;
24
- serverTimestamp(): number;
25
29
  sendStatelessToClient(room: string, clientId: string, data: StatelessDataT): void;
26
30
  sendStatelessToRoom(room: string, data: StatelessDataT, self?: boolean): void;
31
+ private createUpload;
32
+ private uploadChunks;
33
+ private completeUpload;
34
+ upload(name: string, blob: Blob, tags?: string[]): Promise<Upload>;
35
+ listUploads(filter?: {
36
+ clientId?: string;
37
+ mimeTypes?: string[];
38
+ }, skip?: number, limit?: number): Promise<Paginated<Upload>>;
27
39
  }
28
- export declare const kmClient: KokimokiClient<any>;
29
40
  export {};
@@ -1,15 +1,23 @@
1
1
  import { HocuspocusProvider } from "@hocuspocus/provider";
2
2
  import EventEmitter from "events";
3
3
  export class KokimokiClient extends EventEmitter {
4
- serverTimeOffset = 0;
5
- _appId;
4
+ host;
5
+ appId;
6
6
  _wsUrl;
7
7
  _apiUrl;
8
8
  _id;
9
9
  _token;
10
+ _apiHeaders;
10
11
  _providers = new Map();
11
- constructor() {
12
+ _serverTimeOffset = 0;
13
+ constructor(host, appId) {
12
14
  super();
15
+ this.host = host;
16
+ this.appId = appId;
17
+ // Set up the URLs
18
+ const secure = this.host.indexOf(":") === -1;
19
+ this._wsUrl = `ws${secure ? "s" : ""}://${this.host}`;
20
+ this._apiUrl = `http${secure ? "s" : ""}://${this.host}`;
13
21
  }
14
22
  get id() {
15
23
  if (!this._id) {
@@ -17,12 +25,6 @@ export class KokimokiClient extends EventEmitter {
17
25
  }
18
26
  return this._id;
19
27
  }
20
- get appId() {
21
- if (!this._appId) {
22
- throw new Error("Client not connected");
23
- }
24
- return this._appId;
25
- }
26
28
  get token() {
27
29
  if (!this._token) {
28
30
  throw new Error("Client not connected");
@@ -35,16 +37,17 @@ export class KokimokiClient extends EventEmitter {
35
37
  }
36
38
  return this._apiUrl;
37
39
  }
38
- async connect(host, appId) {
39
- this._appId = appId;
40
- // Set up the URLs
41
- const secure = host.indexOf(":") === -1;
42
- this._wsUrl = `ws${secure ? "s" : ""}://${host}`;
43
- this._apiUrl = `http${secure ? "s" : ""}://${host}`;
40
+ get apiHeaders() {
41
+ if (!this._apiHeaders) {
42
+ throw new Error("Client not connected");
43
+ }
44
+ return this._apiHeaders;
45
+ }
46
+ async connect() {
44
47
  // Fetch the auth token
45
48
  let clientToken = localStorage.getItem("KM_TOKEN");
46
49
  const startTime = Date.now();
47
- const res = await fetch(`${this.apiUrl}/auth/token?appId=${appId}`, {
50
+ const res = await fetch(`${this.apiUrl}/auth/token?appId=${this.appId}`, {
48
51
  method: "GET",
49
52
  headers: new Headers({
50
53
  "Content-Type": "application/json",
@@ -56,8 +59,13 @@ export class KokimokiClient extends EventEmitter {
56
59
  const ping = Math.round((endTime - startTime) / 2);
57
60
  this._id = clientId;
58
61
  this._token = appToken;
59
- this.serverTimeOffset = Date.now() - serverTime - ping;
62
+ this._serverTimeOffset = Date.now() - serverTime - ping;
60
63
  localStorage.setItem("KM_TOKEN", token);
64
+ // Set up the auth headers
65
+ this._apiHeaders = new Headers({
66
+ Authorization: `Bearer ${this.token}`,
67
+ "Content-Type": "application/json",
68
+ });
61
69
  // Ping interval
62
70
  setInterval(() => {
63
71
  this._providers.forEach((provider) => {
@@ -65,13 +73,14 @@ export class KokimokiClient extends EventEmitter {
65
73
  });
66
74
  }, 30000);
67
75
  }
76
+ serverTimestamp() {
77
+ return Date.now() - this._serverTimeOffset;
78
+ }
79
+ // Realtime database
68
80
  async setProvider(name, store) {
69
- if (!this._wsUrl || !this._appId) {
70
- throw new Error("Client not connected");
71
- }
72
81
  const provider = new HocuspocusProvider({
73
82
  url: `${this._wsUrl}/connection`,
74
- name: `${this._appId}/${name}`,
83
+ name: `${this.appId}/${name}`,
75
84
  document: store.doc,
76
85
  token: this.token,
77
86
  });
@@ -101,9 +110,6 @@ export class KokimokiClient extends EventEmitter {
101
110
  getProvider(name) {
102
111
  return this._providers.get(name);
103
112
  }
104
- serverTimestamp() {
105
- return Date.now() - this.serverTimeOffset;
106
- }
107
113
  sendStatelessToClient(room, clientId, data) {
108
114
  const provider = this._providers.get(room);
109
115
  if (!provider) {
@@ -118,5 +124,55 @@ export class KokimokiClient extends EventEmitter {
118
124
  }
119
125
  provider.sendStateless(JSON.stringify({ data, self }));
120
126
  }
127
+ // Storage
128
+ async createUpload(name, blob, tags) {
129
+ const res = await fetch(`${this._apiUrl}/uploads`, {
130
+ method: "POST",
131
+ headers: this.apiHeaders,
132
+ body: JSON.stringify({
133
+ name,
134
+ size: blob.size,
135
+ mimeType: blob.type,
136
+ tags,
137
+ }),
138
+ });
139
+ return await res.json();
140
+ }
141
+ async uploadChunks(blob, chunkSize, signedUrls) {
142
+ return await Promise.all(signedUrls.map(async (url, index) => {
143
+ const start = index * chunkSize;
144
+ const end = Math.min(start + chunkSize, blob.size);
145
+ const chunk = blob.slice(start, end);
146
+ const res = await fetch(url, { method: "PUT", body: chunk });
147
+ return JSON.parse(res.headers.get("ETag") || '""');
148
+ }));
149
+ }
150
+ async completeUpload(id, etags) {
151
+ const res = await fetch(`${this._apiUrl}/uploads/${id}`, {
152
+ method: "PUT",
153
+ headers: this.apiHeaders,
154
+ body: JSON.stringify({ etags }),
155
+ });
156
+ return await res.json();
157
+ }
158
+ async upload(name, blob, tags = []) {
159
+ const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
160
+ const etags = await this.uploadChunks(blob, chunkSize, urls);
161
+ return await this.completeUpload(id, etags);
162
+ }
163
+ async listUploads(filter = {}, skip = 0, limit = 100) {
164
+ const url = new URL("/uploads", this._apiUrl);
165
+ url.searchParams.set("skip", skip.toString());
166
+ url.searchParams.set("limit", limit.toString());
167
+ if (filter.clientId) {
168
+ url.searchParams.set("clientId", filter.clientId);
169
+ }
170
+ if (filter.mimeTypes) {
171
+ url.searchParams.set("mimeTypes", filter.mimeTypes.join());
172
+ }
173
+ const res = await fetch(url.href, {
174
+ headers: this.apiHeaders,
175
+ });
176
+ return await res.json();
177
+ }
121
178
  }
122
- export const kmClient = new KokimokiClient();
@@ -1,4 +1,3 @@
1
- export type FacingMode = "user" | "environment";
2
1
  export interface Paginated<T> {
3
2
  items: T[];
4
3
  total: number;
@@ -1,6 +1,3 @@
1
1
  export type KokimokiClientEvents<T> = {
2
2
  stateless: (room: string, from: string, data: T) => void;
3
3
  };
4
- export type UserMediaHandlerEvents = {
5
- stream: (error: string | null, facingMode: "user" | "environment", stream: MediaStream | null) => void;
6
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kokimoki/app",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Kokimoki app",
6
6
  "main": "dist/index.js",
@@ -1,2 +0,0 @@
1
- import type { FetchParams } from "../types/api";
2
- export declare function abortMediaUpload(id: string, { apiUrl, appToken, signal }: FetchParams): Promise<void>;
@@ -1,12 +0,0 @@
1
- import { getHeaders } from "../utils";
2
- export async function abortMediaUpload(id, { apiUrl, appToken, signal }) {
3
- const res = await fetch(`${apiUrl}/upload/${id}`, {
4
- method: "DELETE",
5
- headers: getHeaders(appToken),
6
- signal,
7
- });
8
- // TODO: Improve error handling
9
- if (res.status !== 200) {
10
- throw await res.json();
11
- }
12
- }
@@ -1,2 +0,0 @@
1
- import type { AuthFetchParams, AuthResponse } from "../types/api";
2
- export declare function fetchAuth({ apiUrl, clientToken, appId, }: AuthFetchParams): Promise<AuthResponse>;
@@ -1,12 +0,0 @@
1
- import { getHeaders } from "../utils";
2
- export async function fetchAuth({ apiUrl, clientToken, appId, }) {
3
- const res = await fetch(`${apiUrl}/auth/token?appId=${appId}`, {
4
- method: "GET",
5
- headers: getHeaders(clientToken),
6
- });
7
- // TODO: Improve error handling
8
- if (res.status !== 200) {
9
- throw await res.json();
10
- }
11
- return await res.json();
12
- }
@@ -1,2 +0,0 @@
1
- import type { SendUploadResponse } from "../types/api";
2
- export declare function getETags(blob: Blob, { urls, chunkSize }: SendUploadResponse): Promise<string[]>;
@@ -1,18 +0,0 @@
1
- // TODO: Improve method below
2
- export async function getETags(blob, { urls, chunkSize }) {
3
- const uploadData = [];
4
- for (let index = 0; index < urls.length; ++index) {
5
- const url = urls[index];
6
- uploadData.push({
7
- url,
8
- body: index < urls.length - 1
9
- ? blob.slice(index * chunkSize, (index + 1) * chunkSize)
10
- : blob.slice(index * chunkSize),
11
- });
12
- }
13
- const responses = await Promise.all(uploadData.map(({ url, body }) => fetch(url, {
14
- method: "PUT",
15
- body,
16
- })));
17
- return responses.map((response) => JSON.parse(response.headers.get("ETag") || '""'));
18
- }
@@ -1,2 +0,0 @@
1
- import type { FetchParams, SendUploadResponse } from "../types/api";
2
- export declare function sendMediaUpload(blob: Blob, { apiUrl, appToken, signal }: FetchParams): Promise<SendUploadResponse>;
@@ -1,20 +0,0 @@
1
- import ObjectId from "bson-objectid";
2
- import { getHeaders } from "../utils";
3
- export async function sendMediaUpload(blob, { apiUrl, appToken, signal }) {
4
- const objectId = new ObjectId().toHexString();
5
- const res = await fetch(`${apiUrl}/upload`, {
6
- method: "POST",
7
- headers: getHeaders(appToken),
8
- body: JSON.stringify({
9
- name: objectId,
10
- size: blob.size,
11
- mimeType: blob.type,
12
- }),
13
- signal,
14
- });
15
- // TODO: Improve error handling
16
- if (res.status !== 200) {
17
- throw await res.json();
18
- }
19
- return await res.json();
20
- }
@@ -1,2 +0,0 @@
1
- import type { FetchParams, UploadMediaRequest, UploadMediaResponse } from "../types/api";
2
- export declare function uploadMedia({ id, etags }: UploadMediaRequest, { apiUrl, appToken, signal }: FetchParams): Promise<UploadMediaResponse>;
@@ -1,14 +0,0 @@
1
- import { getHeaders } from "../utils";
2
- export async function uploadMedia({ id, etags }, { apiUrl, appToken, signal }) {
3
- const res = await fetch(`${apiUrl}/upload/${id}`, {
4
- method: "PUT",
5
- headers: getHeaders(appToken),
6
- body: JSON.stringify({ etags }),
7
- signal,
8
- });
9
- // TODO: Improve error handling
10
- if (res.status !== 200) {
11
- throw await res.json();
12
- }
13
- return await res.json();
14
- }
@@ -1,13 +0,0 @@
1
- import type { Paginated } from "./types/common";
2
- import type { Upload } from "./types/upload";
3
- export declare class KokimokiStorage {
4
- private createUpload;
5
- private uploadChunks;
6
- private completeUpload;
7
- upload(name: string, blob: Blob, tags?: string[]): Promise<Upload>;
8
- listUploads(filter?: {
9
- clientId?: string;
10
- mimeTypes?: string[];
11
- }, skip?: number, limit?: number): Promise<Paginated<Upload>>;
12
- }
13
- export declare const kmStorage: KokimokiStorage;
@@ -1,63 +0,0 @@
1
- import { kmClient } from "./kokimoki-client";
2
- export class KokimokiStorage {
3
- async createUpload(name, blob, tags) {
4
- const res = await fetch(`${kmClient.apiUrl}/uploads`, {
5
- method: "POST",
6
- headers: new Headers({
7
- Authorization: `Bearer ${kmClient.token}`,
8
- "Content-Type": "application/json",
9
- }),
10
- body: JSON.stringify({
11
- name,
12
- size: blob.size,
13
- mimeType: blob.type,
14
- tags,
15
- }),
16
- });
17
- return await res.json();
18
- }
19
- async uploadChunks(blob, chunkSize, signedUrls) {
20
- return await Promise.all(signedUrls.map(async (url, index) => {
21
- const start = index * chunkSize;
22
- const end = Math.min(start + chunkSize, blob.size);
23
- const chunk = blob.slice(start, end);
24
- const res = await fetch(url, { method: "PUT", body: chunk });
25
- return JSON.parse(res.headers.get("ETag") || '""');
26
- }));
27
- }
28
- async completeUpload(id, etags) {
29
- const res = await fetch(`${kmClient.apiUrl}/uploads/${id}`, {
30
- method: "PUT",
31
- headers: new Headers({
32
- Authorization: `Bearer ${kmClient.token}`,
33
- "Content-Type": "application/json",
34
- }),
35
- body: JSON.stringify({ etags }),
36
- });
37
- return await res.json();
38
- }
39
- async upload(name, blob, tags = []) {
40
- const { id, chunkSize, urls } = await this.createUpload(name, blob, tags);
41
- const etags = await this.uploadChunks(blob, chunkSize, urls);
42
- return await this.completeUpload(id, etags);
43
- }
44
- async listUploads(filter = {}, skip = 0, limit = 100) {
45
- const url = new URL("/uploads", kmClient.apiUrl);
46
- url.searchParams.set("skip", skip.toString());
47
- url.searchParams.set("limit", limit.toString());
48
- if (filter.clientId) {
49
- url.searchParams.set("clientId", filter.clientId);
50
- }
51
- if (filter.mimeTypes) {
52
- url.searchParams.set("mimeTypes", filter.mimeTypes.join());
53
- }
54
- const res = await fetch(url.href, {
55
- headers: new Headers({
56
- Authorization: `Bearer ${kmClient.token}`,
57
- "Content-Type": "application/json",
58
- }),
59
- });
60
- return await res.json();
61
- }
62
- }
63
- export const kmStorage = new KokimokiStorage();
@@ -1,36 +0,0 @@
1
- export interface SendUploadResponse {
2
- id: string;
3
- urls: string[];
4
- chunkSize: number;
5
- }
6
- export interface UploadMediaResponse {
7
- url: string;
8
- }
9
- export interface AuthResponse {
10
- serverTime: number;
11
- appToken: string;
12
- token: string;
13
- clientId: string;
14
- }
15
- export interface MediaUploadData {
16
- url: string;
17
- body: Blob;
18
- }
19
- export interface FetchParams {
20
- apiUrl: string;
21
- appToken: string;
22
- signal?: AbortSignal;
23
- }
24
- export interface AuthFetchParams {
25
- apiUrl: string;
26
- clientToken: string | null;
27
- appId: string;
28
- }
29
- export interface UploadMediaRequest {
30
- etags: string[];
31
- id: string;
32
- }
33
- export interface UploadedMedia {
34
- id: string;
35
- url: string;
36
- }
package/dist/types/api.js DELETED
@@ -1 +0,0 @@
1
- export {};
@@ -1,16 +0,0 @@
1
- import type TypedEmitter from "typed-emitter";
2
- import type { UserMediaHandlerEvents } from "./types/events";
3
- declare const UserMediaHandler_base: new () => TypedEmitter<UserMediaHandlerEvents>;
4
- export declare class UserMediaHandler extends UserMediaHandler_base {
5
- private _facingMode;
6
- private _stream;
7
- private _mediaRequests;
8
- requestMediaStream(): void;
9
- releaseMediaStream(): void;
10
- private closeMediaStream;
11
- private updateMediaStream;
12
- setFacingMode(facingMode: "user" | "environment"): void;
13
- toggleFacingMode(): void;
14
- }
15
- export declare const userMediaHandler: UserMediaHandler;
16
- export {};
@@ -1,62 +0,0 @@
1
- import EventEmitter from "events";
2
- export class UserMediaHandler extends EventEmitter {
3
- _facingMode = "environment";
4
- _stream = null;
5
- _mediaRequests = 0;
6
- requestMediaStream() {
7
- this._mediaRequests++;
8
- this.updateMediaStream();
9
- }
10
- releaseMediaStream() {
11
- this._mediaRequests--;
12
- if (this._mediaRequests <= 0) {
13
- this.closeMediaStream();
14
- this._mediaRequests = 0;
15
- }
16
- }
17
- closeMediaStream() {
18
- if (!this._stream) {
19
- return;
20
- }
21
- this.emit("stream", null, this._facingMode, null);
22
- this._stream.getTracks().forEach((track) => track.stop());
23
- this._stream = null;
24
- }
25
- async updateMediaStream() {
26
- this.closeMediaStream();
27
- try {
28
- this._stream = await navigator.mediaDevices.getUserMedia({
29
- video: {
30
- facingMode: this._facingMode,
31
- frameRate: { ideal: 24, max: 30 },
32
- },
33
- audio: true,
34
- });
35
- this.emit("stream", null, this._facingMode, this._stream);
36
- }
37
- catch (err) {
38
- this._stream = null;
39
- if ((err + "").match(/^NotReadableError/)) {
40
- await new Promise((resolve) => {
41
- setTimeout(async () => {
42
- await this.updateMediaStream();
43
- resolve();
44
- }, 1000);
45
- });
46
- return;
47
- }
48
- this.emit("stream", err + "", this._facingMode, null);
49
- }
50
- }
51
- setFacingMode(facingMode) {
52
- if (this._facingMode === facingMode) {
53
- return;
54
- }
55
- this._facingMode = facingMode;
56
- this.updateMediaStream();
57
- }
58
- toggleFacingMode() {
59
- this.setFacingMode(this._facingMode === "user" ? "environment" : "user");
60
- }
61
- }
62
- export const userMediaHandler = new UserMediaHandler();
package/dist/utils.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export declare function getHeaders(token: string | null): Headers;
2
- export declare function errorMessage(error: unknown): string;
package/dist/utils.js DELETED
@@ -1,19 +0,0 @@
1
- export function getHeaders(token) {
2
- return token
3
- ? new Headers({
4
- Authorization: `Bearer ${token}`,
5
- "Content-Type": "application/json",
6
- })
7
- : new Headers();
8
- }
9
- export function errorMessage(error) {
10
- if (typeof error === "string")
11
- return error;
12
- if (typeof error !== "object" && typeof error !== "function")
13
- return "Unknown error";
14
- // TODO: Extend errors check
15
- if (error instanceof Error) {
16
- return error.message;
17
- }
18
- return "Unknown error";
19
- }