官术网_书友最值得收藏!

Implementing the basics

Before we start to write actual code, we need to define what this synchronizing strategy will be like. To keep the implementation from unnecessary distractions, the client will communicate with the server directly through function calls instead of using HTTP requests or Sockets. Also, we'll use in-memory storage, namely variables, to store data on both client and server sides.

Because we are not separating the client and server into two actual applications, and we are not actually using backend technologies, it does not require much Node.js experience to follow this chapter.

However, please keep in mind that even though we are omitting network and database requests, we hope the core logic of the final implementation could be applied to a real environment without being modified too much. So, when it comes to performance concerns, we still need to assume limited network resources, especially for data passing through the server and client, although the implementation is going to be synchronous instead of asynchronous. This is not supposed to happen in practice, but involving asynchronous operations will introduce much more code, as well as many more situations that need to be taken into consideration. But we will have some useful patterns on asynchronous programming in the coming chapters, and it would definitely help if you try to implement an asynchronous version of the synchronizing logic in this chapter.

A client, if without modifying what's been synchronized, stores a copy of all the data available on the server, and what we need to do is to provide a set of APIs that enable the client to keep its copy of data synchronized.

So, it is really simple at the beginning: comparing the last-modified timestamp. If the timestamp on the client is older than what's on the server, then update the copy of data along with new timestamp.

Creating the code base

Firstly, let's create server.ts and client.ts files containing the Server class and Client class respectively:

export class Server { 
    // ... 
} 
 
export class Client { 
    // ... 
} 

I prefer to create an index.ts file as the package entry, which handles what to export internally. In this case, let's export everything:

export * from './server'; 
export * from './client'; 

To import the Server and Client classes from a test file (assuming src/test/test.ts), we can use the following codeto s:

import { Server, Client } from '../'; 

Defining the initial structure of the data to be synchronized

Since we need to compare the timestamps from the client and server, we need to have a timestamp property on the data structure. I would like to have the data to synchronize as a string, so let's add a DataStore interface with a timestamp property to the server.ts file:

export interface DataStore { 
  timestamp: number; 
  data: string; 
} 

Getting data by comparing timestamps

Currently, the synchronizing strategy is one-way, from the server to the client. So what we need to do is simple: we compare the timestamps; if the server has the newer one, it responds with data and the server-side timestamp; otherwise, it responds with undefined:

class Server { 
  store: DataStore = { 
    timestamp: 0, 
    data: '' 
  }; 
 
  getData(clientTimestamp: number): DataStore { 
    if (clientTimestamp < this.store.timestamp) { 
      return this.store; 
    } else { 
      return undefined; 
    } 
  } 
} 

Now we have provided a simple API for the client, and it's time to implement the client:

import { Server, DataStore } from './'; 
 
export class Client { 
  store: DataStore = { 
    timestamp: 0, 
    data: undefined 
  }; 
     
  constructor( 
    public server: Server 
  ) { } 
} 

Tip

Prefixing a constructor parameter with access modifiers (including public, private, and protected) will create a property with the same name and corresponding accessibility. It will also assign the value automatically when the constructor is called.

Now we need to add a synchronize method to the Client class that does the job:

synchronize(): void { 
  let updatedStore = this.server.getData(this.store.timestamp); 
   
  if (updatedStore) { 
    this.store = updatedStore; 
  } 
} 

That's easily done. However, are you already feeling somewhat awkward with what we've written?

Two-way synchronizing

Usually, when we talk about synchronization, we get updates from the server and push changes to the server as well. Now we are going to do the second part, pushing the changes if the client has newer data.

But first, we need to give the client the ability to update its data by adding an update method to the Client class:

update(data: string): void { 
  this.store.data = data; 
  this.store.timestamp = Date.now(); 
} 

And we need the server to have the ability to receive data from the client as well. So we rename the getData method of the Server class as synchronize and make it satisfy the new job:

synchronize(clientDataStore: DataStore): DataStore { 
  if (clientDataStore.timestamp > this.store.timestamp) { 
      this.store = clientDataStore; 
      return undefined; 
  } else if (clientDataStore.timestamp < this.store.timestamp) { 
      return this.store; 
  } else { 
      return undefined; 
  } 
} 

Now we have the basic implementation of our synchronizing service. Later, we'll keep adding new things and make it capable of dealing with a variety of scenarios.

Things that went wrong while implementing the basics

Currently, what we've written is just too simple to be wrong. But there are still some semantic issues.

Passing a data store from the server to the client does not make sense

We used DataStore as the return type of the synchronize method on Server. But what we were actually passing through is not a data store, but information that involves data and its timestamp. The information object just happened to have the same properties as a data store at this point in time.

Also, it will be misleading to people who will later read your code (including yourself in the future). Most of the time, we are trying to eliminate redundancies. But that does not have to mean everything that looks the same. So let's make it two interfaces:

interface DataStore { 
  timestamp: number; 
  data: string; 
} 
 
interface DataSyncingInfo { 
  timestamp: number; 
  data: string; 
} 

I would even prefer to create another instance, instead of directly returning this.store:

return { 
  timestamp: this.store.timestamp, 
  data: this.store.data 
}; 

However, if two pieces of code with different semantic meanings are doing the same thing from the perspective of code itself, you may consider extracting that part as a utility.

Making the relationships clear

Now we have two separated interfaces, DataStore and DataSyncingInfo, in server.ts. Obviously, DataSyncingInfo should be a shared interface between the server and the client, while DataStore happens to be the same on both sides, but it's not actually shared.

So what we are going to do is to create a separate shared.d.ts (it could also be shared.ts if it contains more than typings) that exports DataSyncingInfo and add another DataStore to client.ts.

Note

Do not follow this blindly. Sometimes it is designed for the server and the client to have exactly the same stores. If that's the situation, the interface should be shared.

主站蜘蛛池模板: 习水县| 桃园县| 青川县| 罗源县| 大竹县| 绥江县| 三门县| 晋城| 夏邑县| 老河口市| 哈巴河县| 宕昌县| 玛纳斯县| 康保县| 哈巴河县| 巴塘县| 瓮安县| 万山特区| 南宁市| 邹平县| 祁连县| 吉隆县| 营山县| 常德市| 任丘市| 嘉义市| 五莲县| 高阳县| 凌云县| 新河县| 济源市| 伽师县| 平湖市| 平果县| 渭源县| 百色市| 潞西市| 榆树市| 万宁市| 乌恰县| 阿拉善右旗|